//! By convention, main.zig is where your main function lives in the case that //! you are building an executable. If you are making a library, the convention //! is to delete this file and start with root.zig instead. const std = @import("std"); const Allocator = std.mem.Allocator; const vec = std.ArrayList; const map = std.AutoArrayHashMap; const hash = std.StringArrayHashMap; const blk = std.heap.ArenaAllocator; const ParseError = error{ Invalid, Outofrange, }; const File = std.fs.File; const Utf8View = std.unicode.Utf8View; const ParseState = enum { WaitLock, ReadLock, WaitHole, ReadHole, ReadKey, WaitPart, NeedPart, }; const Passhole = [2][]const u8{ "text", "off" }; const Hex = "0123456789abcdefABCDEF"; const Module = vec(u8); const LockKey = vec(Module); const LockHole = struct { name: vec(u8), keys: vec(LockKey) }; const Lock = struct { name: vec(u8), holes: vec(LockHole) }; const EntData = union(enum) { strs: *vec(*Module), keys: *vec(LockKey) }; const Entry = struct { id: u32, data: EntData }; fn cmpEntry(ctx: void, a: Entry, b: Entry) bool { _ = ctx; return a.id < b.id; } const CheatMap = struct { order: []u8, serial: []u8 }; // data from json const CheatSet = struct { code: u32, sets: Module }; // cht单文件产出 const CheatGrp = struct { serial: []u8, grp: vec(CheatSet) }; // cht组文件产出 const ParserCtx = struct { state: ParseState, token: vec(u8), line: u32, file: []u8, code: u32, farm: Allocator, shop: Allocator, locks: vec(Lock), curlk: ?Lock, curhl: ?LockHole, strcache: ?hash(u16), output: *vec(CheatSet), }; var runs: i64 = 0; pub fn main() !void { const now = std.time.microTimestamp(); // 追踪内存分配,确定没问题后就不用gpa而是arena了 // var gpa = std.heap.GeneralPurposeAllocator(.{}){}; // defer std.debug.assert(gpa.deinit() == std.heap.Check.ok); var gpa = blk.init(std.heap.page_allocator); defer gpa.deinit(); const list = try loadList(gpa.allocator()); defer list.deinit(); const roms = transform(gpa.allocator(), list) catch vec(CheatGrp).init(gpa.allocator()); defer roms.deinit(); std.debug.print("all rom has {d} and time {d}\n", .{ roms.items.len, std.time.microTimestamp() - now }); const content = calcfile: { var alloc = blk.init(gpa.allocator()); defer alloc.deinit(); var res = vec(*CheatGrp).init(alloc.allocator()); var idx = hash(*vec(CheatSet)).init(alloc.allocator()); for (roms.items) |*rom| { if (rom.grp.items.len == 0) continue; if (!idx.contains(rom.serial)) { var grp = try alloc.allocator().create(CheatGrp); grp.serial = rom.serial; grp.grp = vec(CheatSet).init(alloc.allocator()); try res.append(grp); var cheats = &grp.grp; try cheats.appendSlice(rom.grp.items); try idx.put(rom.serial, cheats); } else { const pcheats = idx.get(rom.serial).?; try pcheats.appendSlice(rom.grp.items); } } const filedata = try format(alloc.allocator(), gpa.allocator(), &res); break :calcfile filedata; }; defer gpa.allocator().free(content); try std.fs.cwd().writeFile(.{ .sub_path = "gba.acl", .data = content, .flags = .{ .truncate = true } }); // std.debug.print("check runs {d}\n", .{std.time.microTimestamp() - now}); // 如果用gpa才需要手动释放这几个没有释放的内存 // for (roms.items) |*grp| { // gpa.allocator().free(grp.serial); // for (grp.grp.items) |*set| { // set.sets.deinit(); // } // grp.grp.deinit(); // } } fn readAll(allocator: Allocator, file: File) ![]u8 { const file_size: usize = @intCast(try file.getEndPos()); const file_contents = try allocator.alloc(u8, file_size); const readed = try file.readAll(file_contents); if (readed != file_size) { return error.EndOfStream; } return file_contents; } fn loadList(allocator: Allocator) !vec(CheatMap) { const file = try std.fs.cwd().openFile("./serial.json", .{}); defer file.close(); const file_contents = try readAll(allocator, file); defer allocator.free(file_contents); var entries = try std.json.parseFromSlice(std.json.Value, allocator, file_contents, .{}); defer entries.deinit(); var iter = entries.value.object.iterator(); var list = try vec(CheatMap).initCapacity(allocator, entries.value.object.count()); while (iter.next()) |*entry| { const key = entry.key_ptr; const value = entry.value_ptr; if (!std.mem.eql(u8, value.string, "????")) { const nkey = try allocator.alloc(u8, key.len); const nval = try allocator.alloc(u8, value.string.len); std.mem.copyForwards(u8, nkey, key.*); std.mem.copyForwards(u8, nval, value.string); try list.append(CheatMap{ .order = nkey, .serial = nval }); } } return list; } fn transform(allocator: Allocator, list: vec(CheatMap)) !vec(CheatGrp) { var alloc = blk.init(allocator); defer alloc.deinit(); const innalloc = alloc.allocator(); var retval = vec(CheatGrp).init(allocator); for (list.items) |*chtmap| { const order = chtmap.order; const serial = chtmap.serial; defer allocator.free(order); const file = try std.fmt.allocPrint(innalloc, "./gba/{s}.u8", .{order}); var cheats = vec(CheatSet).init(allocator); const fd = std.fs.cwd().openFile(file, .{}) catch null; if (fd) |chtfile| { defer chtfile.close(); const chtdata = try readAll(innalloc, chtfile); if (chtdata.len > 0) { parse(innalloc, allocator, &cheats, chtdata, serial, order) catch |err| { std.debug.print("got error {any} in {s}!\n", .{ err, file }); }; try retval.append(CheatGrp{ .serial = serial, .grp = cheats }); } else try retval.append(CheatGrp{ .serial = serial, .grp = cheats }); } else try retval.append(CheatGrp{ .serial = serial, .grp = cheats }); } return retval; } fn parse(innalloc: Allocator, outeralloc: Allocator, output: *vec(CheatSet), chtdata: []u8, serial: []u8, order: []u8) !void { var context = ParserCtx{ .state = ParseState.WaitLock, .token = vec(u8).init(innalloc), .line = 1, .file = order, .farm = innalloc, .shop = outeralloc, .code = calc_code: { var r: u32 = 0; for (serial) |v| r = v | r << 8; break :calc_code r; }, .locks = vec(Lock).init(innalloc), .curlk = null, .curhl = null, .strcache = null, .output = output, }; defer context.locks.deinit(); defer context.token.deinit(); var data = (try Utf8View.init(chtdata)).iterator(); while (data.nextCodepointSlice()) |codepoint| { try incr(codepoint, &context); } done(&context); } fn done(ctx: *ParserCtx) void { switch (ctx.state) { ParseState.WaitPart, ParseState.NeedPart => {}, else => unreachable, } } inline fn parserr(str: []u8, ctx: *ParserCtx) !void { std.debug.print("Cheat Invad: {s} at line {d} for file {s}.u8\n", .{ str, ctx.line, ctx.file }); return ParseError.Invalid; } inline fn tokenStr(ctx: *ParserCtx) vec(u8) { const s = ctx.token; ctx.token = vec(u8).init(ctx.farm); return s; } fn incr(ch: []const u8, ctx: *ParserCtx) !void { const state = ctx.state; const allocator = ctx.farm; switch (ch[0]) { '[' => { if (state == ParseState.WaitLock or state == ParseState.ReadHole or state == ParseState.NeedPart) { ctx.state = ParseState.ReadLock; if (ctx.curlk) |lock| { try ctx.locks.append(lock); } ctx.curlk = Lock{ .name = vec(u8).init(allocator), .holes = vec(LockHole).init(allocator) }; } else if (state == ParseState.WaitPart) {} else try parserr(try std.fmt.allocPrint(allocator, "error occur [ on {s}", .{@tagName(state)}), ctx); }, ']' => { if (state == ParseState.ReadLock) { ctx.state = ParseState.WaitHole; const name = tokenStr(ctx); if (std.ascii.eqlIgnoreCase(name.items, "gameinfo")) { try lade(ctx); ctx.state = ParseState.WaitPart; ctx.locks = vec(Lock).init(allocator); ctx.curhl = null; ctx.curlk = null; } else if (name.items.len > 0) { ctx.curlk = Lock{ .name = name, .holes = vec(LockHole).init(allocator), }; ctx.curhl = null; } else try parserr(try std.fmt.allocPrint(allocator, "empty lock name", .{}), ctx); } else if (state == ParseState.WaitPart) {} else if (state == ParseState.NeedPart) { ctx.state = ParseState.WaitPart; } else try parserr(try std.fmt.allocPrint(allocator, "error occur ] on {s}", .{@tagName(state)}), ctx); }, '\r', '\n' => { if (state == ParseState.WaitHole) { ctx.state = ParseState.ReadHole; } else if (state == ParseState.ReadKey) { const token = tokenStr(ctx); var pass = false; if (ctx.curhl) |*hole| { if (token.items.len > 0) { var key = &hole.keys.items[hole.keys.items.len - 1]; try key.append(token); } pass = for (Passhole) |keyword| { if (std.ascii.eqlIgnoreCase(keyword, hole.name.items)) { break true; } } else false; } if (!pass) { if (ctx.curlk != null) { if (ctx.curhl) |hole| { try ctx.curlk.?.holes.append(hole); } else unreachable; } ctx.curhl = null; } ctx.state = ParseState.ReadHole; } else if (state == ParseState.ReadLock) { unreachable; } else if (state == ParseState.ReadHole) { const token = tokenStr(ctx); if (token.items.len > 0) { const onlyspace = for (token.items) |c| { if (c != ' ' and c != '\t') { break false; } } else true; if (onlyspace == false) { try parserr(try std.fmt.allocPrint(allocator, "error occur newline on {s}", .{@tagName(state)}), ctx); } } } else if (state == ParseState.WaitPart) { ctx.state = ParseState.NeedPart; } else {} if (ch[0] == '\n') ctx.line = ctx.line + 1; }, '=' => { if (state == ParseState.ReadHole) { ctx.state = ParseState.ReadKey; ctx.curhl = LockHole{ .name = tokenStr(ctx), .keys = initkey: { var res = vec(LockKey).init(allocator); try res.append(vec(Module).init(allocator)); break :initkey res; } }; } else if (state == ParseState.ReadLock) { try ctx.token.appendSlice(ch); } else if (state == ParseState.WaitPart) {} else if (state == ParseState.NeedPart) { ctx.state = ParseState.WaitPart; } else try parserr(try std.fmt.allocPrint(allocator, "error occur = on {s}", .{@tagName(state)}), ctx); }, ',' => { if (state == ParseState.ReadKey) { const token = tokenStr(ctx); if (token.items.len > 0) { if (ctx.curhl) |*hole| { var key = &hole.keys.items[hole.keys.items.len - 1]; try key.append(token); } else unreachable; } } else if (state == ParseState.ReadHole) { const token = tokenStr(ctx); var holes = &ctx.curlk.?.holes; const hole = holes.pop(); if (token.items.len > 0) { var key = &hole.keys.items[hole.keys.items.len - 1]; try key.append(token); } ctx.curhl = hole; ctx.state = ParseState.ReadKey; } else if (state == ParseState.ReadLock) { try ctx.token.appendSlice(ch); } else if (state == ParseState.WaitPart) {} else if (state == ParseState.NeedPart) { ctx.state = ParseState.WaitPart; } else try parserr(try std.fmt.allocPrint(allocator, "error occur , on {s}", .{@tagName(state)}), ctx); }, ';' => { if (state == ParseState.ReadKey) { const token = tokenStr(ctx); if (token.items.len > 0) { const keys = ctx.curhl.?.keys; var key = &keys.items[keys.items.len - 1]; try key.append(token); } try ctx.curhl.?.keys.append(vec(Module).init(allocator)); } else if (state == ParseState.ReadLock) { try ctx.token.appendSlice(ch); } else if (state == ParseState.ReadHole) { var holes = &ctx.curlk.?.holes; var hole = holes.pop(); const token = tokenStr(ctx); if (token.items.len > 0) { var cmd = &hole.keys.items[hole.keys.items.len - 1]; try cmd.append(token); } try hole.keys.append(vec(Module).init(allocator)); ctx.curhl = hole; ctx.state = ParseState.ReadKey; } else if (state == ParseState.WaitPart) {} else if (state == ParseState.NeedPart) { ctx.state = ParseState.WaitPart; } else try parserr(try std.fmt.allocPrint(allocator, "error occur ; on {s}", .{@tagName(state)}), ctx); }, ' ' => { if (state == ParseState.ReadLock or state == ParseState.ReadHole) { try ctx.token.appendSlice(ch); } else if (state == ParseState.NeedPart) { ctx.state = ParseState.WaitPart; } else {} }, else => { if (state == ParseState.ReadLock or state == ParseState.ReadHole) { try ctx.token.appendSlice(ch); } else if (state == ParseState.ReadKey) { const ishex = for (Hex) |c| { if (c == ch[0]) { break true; } } else false; if (ishex) { try ctx.token.appendSlice(ch); } else if (ctx.curhl) |*hole| { if (std.ascii.eqlIgnoreCase(hole.name.items, "text")) { try ctx.token.appendSlice(ch); } } else try parserr(try std.fmt.allocPrint(allocator, "error occur {s} on {s}", .{ ch, @tagName(state) }), ctx); } else if (state == ParseState.WaitPart) {} else if (state == ParseState.NeedPart) { ctx.state = ParseState.WaitPart; } else try parserr(try std.fmt.allocPrint(allocator, "error occur {s} on {s}", .{ ch, @tagName(state) }), ctx); }, } } inline fn makeId(a: usize, b: usize) u32 { return @intCast(a | (b << 16)); } inline fn getalign(n: usize, basic: usize) usize { const ext = n % basic; return if (ext == 0) n else n + basic - ext; } fn findIndex(t: []u8, tr: []u8) i32 { const rlen: usize = tr.len; const tlen: usize = t.len; if (tlen < rlen) return -1; const limit = tlen - rlen; for (0..limit) |i| { if (t[i + rlen] != 0) continue; const eq = for (0..rlen) |j| { if (t[i + j] != tr[j]) { break false; } } else true; if (eq) return @intCast(i); } return -1; } fn fromTaddr(taddr: vec(u8)) !u32 { const n = try std.fmt.parseInt(u32, taddr.items, 16); if (n == 0 or n > 0x41f_ffff) { return error.Outofrange; } return if (n < 0x3_ffff) n | 0x200_0000 else if (0 == (n & 0xf00_0000)) (n & 0xffff) | 0x300_0000 else n; } fn assembleCheat(ctx: *ParserCtx, list: *vec(LockKey), dup: *map(usize, u16), hole: u16) !vec(u32) { const alloc = ctx.farm; var addrval = map(u32, u32).init(alloc); for (list.items) |*command| { if (command.items.len == 0) { continue; } const addr = try fromTaddr(command.items[0]); const len = command.items.len - 1; for (0..len) |pos| { const target = command.items[pos + 1]; try addrval.put(@intCast(addr + pos), try std.fmt.parseInt(u32, target.items, 16)); } } const ordered = calcordered: { var res = vec([2]u32).init(alloc); var iter = addrval.iterator(); while (iter.next()) |kvp| { try res.append(.{ kvp.key_ptr.*, kvp.value_ptr.* }); } std.sort.block([2]u32, res.items, {}, cmpOrdered); break :calcordered res; }; var blocks = vec(u32).init(alloc); var curr: [3]u32 = .{ 0, 0, 0 }; for (ordered.items) |cmd| { const addr = cmd[0]; const value = cmd[1]; try dup.put(addr, hole); if (curr[2] == 0) { curr = .{ addr, value, 1 }; } else { if (curr[1] == value and curr[0] + curr[2] == addr) { curr[2] = curr[2] + 1; } else { try blocks.append(curr[0]); try blocks.append((curr[2] << 8) | curr[1]); curr = .{ addr, value, 1 }; } } } if (curr[2] != 0) { try blocks.append(curr[0]); try blocks.append((curr[2] << 8) | curr[1]); } return blocks; } fn cmpNames(ctx: void, a: *Module, b: *Module) bool { _ = ctx; if (a.items.len == b.items.len) { return std.mem.lessThan(u8, a.items, b.items); } else return b.items.len < a.items.len; } fn cmpOrdered(ctx: void, a: [2]u32, b: [2]u32) bool { _ = ctx; return a[0] < b[0]; } fn utpush(str: []const u8, table: *vec(u8)) !u16 { const off = table.items.len; try table.ensureTotalCapacity(off + str.len + 1); try table.appendSlice(str); try table.append(0); return @intCast(off); } fn utaddr(ctx: *ParserCtx, str: []u8, table: *vec(u8)) !u16 { var strcache = &ctx.strcache.?; if (strcache.contains(str)) { const res = strcache.get(str); return res.?; } else { const idx = findIndex(table.items, str); const off: u16 = if (idx < 0) try utpush(str, table) else @intCast(idx); try strcache.put(str, off); return off; } } fn addString(ctx: *ParserCtx, list: *const vec(*Module), table: *vec(u8)) !vec(u16) { var retval = try vec(u16).initCapacity(ctx.farm, list.items.len); for (list.items) |str| { try retval.append(try utaddr(ctx, str.items, table)); } return retval; } fn addStrIndex(list: *const vec(u16), table: *vec(u16)) !u32 { if (list.items.len == 0) return 0; const ret = table.items.len; try table.ensureTotalCapacity(ret + list.items.len); try table.appendSlice(list.items); return @intCast(ret * 2); } fn addCommand(list: vec(u32), table: *vec(u32)) !u32 { const ret = table.items.len; try table.ensureTotalCapacity(ret + list.items.len); try table.appendSlice(list.items); return @intCast(ret * 4); } fn lade(ctx: *ParserCtx) !void { // const st = std.time.microTimestamp(); // defer runs = runs - st + std.time.microTimestamp(); const alloc = ctx.farm; var rootstr = vec(*Module).init(alloc); var names = vec(*Module).init(alloc); var enttable = vec(Entry).init(alloc); const locks = ctx.locks; for (locks.items, 0..) |*l, i| { try rootstr.append(&l.name); try names.append(&l.name); var holestr = try ctx.farm.create(vec(*Module)); holestr.* = vec(*Module).init(ctx.farm); const colname = l.holes.items.len > 1; for (l.holes.items, 0..) |*hole, index| { try holestr.append(&hole.name); if (colname) { try names.append(&hole.name); } const id = makeId(i + 1, index + 1); try enttable.append(Entry{ .id = id, .data = EntData{ .keys = &hole.keys } }); } const id = makeId(i + 1, 0); try enttable.append(Entry{ .id = id, .data = EntData{ .strs = holestr } }); } try enttable.append(Entry{ .id = makeId(0, 0), .data = EntData{ .strs = &rootstr } }); if (enttable.items.len > 0xffff) { try parserr(try std.fmt.allocPrint(alloc, "too many entries in file {s}", .{ctx.file}), ctx); } std.sort.block(Entry, enttable.items, {}, cmpEntry); std.sort.block(*Module, names.items, {}, cmpNames); var strtable = vec(u8).init(alloc); var idxtable = vec(u16).init(alloc); var cmdtable = vec(u32).init(alloc); ctx.strcache = hash(u16).init(alloc); _ = try addString(ctx, &names, &strtable); var entbytelist = vec([3]u32).init(alloc); const emptylist = vec(*Module).init(alloc); var holedup = map(usize, u16).init(alloc); for (enttable.items) |*entry| { const id = entry.id; if (id < 0x1_0000) { const labels = if (id == 0) entry.data.strs else if (entry.data.strs.items.len > 1) entry.data.strs else &emptylist; const idxlist = try addString(ctx, labels, &strtable); const location = try addStrIndex(&idxlist, &idxtable); try entbytelist.append([3]u32{ id, location, @intCast(idxlist.items.len) }); } else { const armbytecode = try assembleCheat(ctx, entry.data.keys, &holedup, @intCast(id & 0xffff)); const location = try addCommand(armbytecode, &cmdtable); try entbytelist.append([3]u32{ id, location, @intCast(armbytecode.items.len) }); } } const code = calccode: { var res = ctx.code; for (cmdtable.items) |cmd| res = res ^ cmd; break :calccode res; }; const sets = try pack(ctx, &entbytelist, &strtable, &idxtable, &cmdtable); try ctx.output.append(CheatSet{ .code = code, .sets = sets }); } fn pack(ctx: *ParserCtx, entlist: *vec([3]u32), strlist: *vec(u8), idxlist: *vec(u16), cmdlist: *vec(u32)) !vec(u8) { const external = ctx.shop; const entrysize = @sizeOf([3]u32); const strbase = 2 + entlist.items.len * entrysize; const idxbase = strbase + strlist.items.len; const cmdbase = idxbase + idxlist.items.len * 2; const noalign_size = cmdbase + cmdlist.items.len * 4; const size = getalign(noalign_size, 32); const endian = std.builtin.Endian.little; var result = try vec(u8).initCapacity(external, size); try result.writer().writeInt(u16, @intCast(entlist.items.len), endian); for (entlist.items) |entry| { const locbase = if (entry[0] > 0xffff) cmdbase else idxbase; try result.writer().writeInt(u32, entry[0], endian); try result.writer().writeInt(u32, @intCast(locbase + entry[1]), endian); try result.writer().writeInt(u32, entry[2], endian); } try result.writer().writeAll(strlist.items); try result.writer().writeAll(std.mem.sliceAsBytes(idxlist.items)); try result.writer().writeAll(std.mem.sliceAsBytes(cmdlist.items)); if (noalign_size < size) { try result.writer().writeByteNTimes(0, size - noalign_size); } return result; } fn cmpCheatGrp(ctx: void, a: *CheatGrp, b: *CheatGrp) bool { _ = ctx; return std.mem.lessThan(u8, a.serial, b.serial); } fn format(alloc: Allocator, global: Allocator, cheats: *vec(*CheatGrp)) ![]const u8 { std.sort.block(*CheatGrp, cheats.items, {}, cmpCheatGrp); std.debug.print("valid rom has {d}\n", .{cheats.items.len}); var sers: ?vec(u8) = null; var offs: ?vec(u16) = null; var chtc: ?usize = null; var maxl: ?usize = null; { var item1 = vec(u8).init(alloc); var item2 = vec(u16).init(alloc); var item3: usize = 0; var item4: usize = 0; var item5: []const u8 = ""; var last: usize = 0; for (cheats.items) |cheat| { const val = cheat.serial[0..3]; if (!std.mem.eql(u8, val, item5)) { if (item2.items.len > 0 and item3 - last > item4) { item4 = item3 - last; } try item1.appendSlice(val); try item2.append(@intCast(item3)); last = item3; item5 = val; } item3 = item3 + cheat.grp.items.len; } if (item2.items.len > 0) { if (item3 - last > item4) { item4 = item3 - last; } try item2.append(@intCast(item3)); } sers = item1; offs = item2; chtc = item3; maxl = item4; } var chts: ?vec(u32) = null; var expanded: ?vec(u8) = null; { var item1 = vec(u32).init(alloc); var item2 = vec(u8).init(alloc); const item3 = getalign(8 + sers.?.items.len + offs.?.items.len * 2 + chtc.? * 8, 32); for (cheats.items) |cheat| { const val = cheat.serial[3]; for (cheat.grp.items) |*set| { const off = item3 + item2.items.len; try item1.append(@intCast(val | (off << 3))); try item1.append(set.code); try item2.ensureTotalCapacity(item2.items.len + set.sets.items.len); try item2.appendSlice(set.sets.items); } } chts = item1; expanded = item2; } std.debug.print("name: {d} cheats: {d} maxl: {d}\n", .{ sers.?.items.len, chtc.?, maxl.? }); const serialbase = 8; const offsetbase = serialbase + sers.?.items.len; const cheatbase = offsetbase + offs.?.items.len * 2; const cheatend = cheatbase + chts.?.items.len * 4; const expandbase = getalign(cheatend, 32); const total = expandbase + expanded.?.items.len; const endian = std.builtin.Endian.little; var retval = try global.alloc(u8, total); retval[0] = 'A'; retval[1] = 'C'; retval[2] = 'L'; retval[3] = 1; std.mem.writeInt(u16, retval[4..6], @intCast(sers.?.items.len / 3), endian); std.mem.writeInt(u16, retval[6..8], @intCast(maxl.?), endian); std.mem.copyForwards(u8, retval[serialbase..offsetbase], sers.?.items); std.mem.copyForwards(u8, retval[offsetbase..cheatbase], std.mem.sliceAsBytes(offs.?.items)); std.mem.copyForwards(u8, retval[cheatbase..cheatend], std.mem.sliceAsBytes(chts.?.items)); if (cheatend < expandbase) { @memset(retval[cheatend..expandbase], 0); } std.mem.copyForwards(u8, retval[expandbase..], expanded.?.items); return retval; }