From 32689256380ee8895118a57c53cb320769e3725e Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 15 Feb 2022 11:25:00 -0600 Subject: [PATCH] ecs: tests, refactor, event args, fixed c.f --- src/common/ecs/README.md | 146 ++++++++++++++++++++++++++++++++++ src/common/ecs/binpack.ts | 2 +- src/common/ecs/compiler.ts | 16 ++-- src/common/ecs/decoder.ts | 36 ++++++--- src/common/ecs/ecs.ts | 159 ++++++++++++++++++++++++++----------- src/test/testecs.ts | 26 +++--- test/ecs/narrow1.ecs | 28 +++++++ test/ecs/narrow1.txt | 27 +++++++ test/ecs/noconstvalues.ecs | 42 ++++++++++ test/ecs/noconstvalues.txt | 25 ++++++ test/ecs/sprites1.txt | 20 ++--- 11 files changed, 442 insertions(+), 85 deletions(-) create mode 100644 src/common/ecs/README.md create mode 100644 test/ecs/narrow1.ecs create mode 100644 test/ecs/narrow1.txt create mode 100644 test/ecs/noconstvalues.ecs create mode 100644 test/ecs/noconstvalues.txt diff --git a/src/common/ecs/README.md b/src/common/ecs/README.md new file mode 100644 index 00000000..04800ec4 --- /dev/null +++ b/src/common/ecs/README.md @@ -0,0 +1,146 @@ + +# NOTES + +entity scopes contain entities, and are nested +also contain segments (code, bss, rodata) +components and systems are global +component fields are stored in arrays, range of entities, can be bit-packed +some values can be constant, are stored in rodata (or loaded immediate) +optional components? on or off +union components? either X or Y or Z... + +systems receive and send events, execute code on entities +systems are generated on a per-scope basis +system queries can only contain entities from self and parent scopes +starting from the 'init' event walk the event tree +include systems that have at least 1 entity in scope (except init?) + +when entering scope, entities are initialized (zero or init w/ data) +to change scope, fire event w/ scope name +- how to handle bank-switching? + +helps with: +- rapid prototyping w/ reasonable defaults +- deconstructing objects into arrays +- packing/unpacking bitfields +- initializing objects +- building lookup tables +- selecting and iterating objects +- managing events +- managing memory and scope +- converting assets to native formats? +- removing unused data + +it's more convenient to have loops be zero-indexed +for page cross, temp storage, etc +should references be zero-indexed to a field, or global? +should we limit # of entities passed to systems? min-max +join thru a reference? load both x and y + +code fragments can be parameterized like macros +if two fragments are identical, do a JSR +(do we have to look for labels?) +should events have parameters? e.g. addscore X Y Z +how are Z80 arrays working? +https://forums.nesdev.org/viewtopic.php?f=20&t=14691 +https://www.cpcwiki.eu/forum/programming/trying-not-to-use-ix/msg133416/#msg133416 + +how to select two between two entities with once? like scoreboard +maybe stack-based interpreter? + +can you query specific entities? merge with existing queries? +bigints? +source/if query? + +only worry about intersection when non-contiguous ranges? + +crazy idea -- full expansion, then relooper + +how to avoid cycle crossing for critical code and data? bin packing + +system define order, action order, entity order, using order? +what happens when a system must be nested inside another? like display kernels + +constants? (NTSC vs PAL) + +set operations: + +E = select entities from scope +A intersect B +A join B +loop over E limit N asc/desc +select Nth from E +run if A intersects B (if) + + +virtual machine +- entityset stack +- register states + + +entity Foo[Bar] { } + +system FrameLoop + +end + +class StaticKernel + locals 12 + func init --- + lda {{$0}} + sta {{$1}} + --- + func display --- + lda {{$0}} + sta {{$1}} + --- +end + +Game { + superman: [Sprite, ...] { + var xpos=12 + } + FrameLoop { + a: StaticKernel(lines=30,bgcolor=$30) + b: Kernel48(...) + c: StaticKernel(...) + + on preframe { + } + on display { + } + on postframe { + } + } +} + + +systems and scope same thing? +nested systems? +systems allocated in blocks +entities allocated in arrays, take up 1 or more blocks +mid-level abstraction for scopes/macros/(banks?) + + + +Init +with FrameLoop do + with StaticKernel do + end + ResetSwitch: + on reset do ResetConsole + end + StaticKernel: + end + JoyButton: + end + end +end + + +scopes are banks! +banks need to duplicate code and/or rodata +- don't split critical code across banks +need bank trampoline macro + +events might need args diff --git a/src/common/ecs/binpack.ts b/src/common/ecs/binpack.ts index 8b92517c..61a576cb 100644 --- a/src/common/ecs/binpack.ts +++ b/src/common/ecs/binpack.ts @@ -1,5 +1,5 @@ -var debug = true; +var debug = false; export interface BoxConstraints { left?: number; diff --git a/src/common/ecs/compiler.ts b/src/common/ecs/compiler.ts index ae522ba1..d3ddbd98 100644 --- a/src/common/ecs/compiler.ts +++ b/src/common/ecs/compiler.ts @@ -129,7 +129,7 @@ export class ECSCompiler extends Tokenizer { let name = this.expectIdent(); this.expectToken(':', 'I expected either a ":" or "end" here.'); // TODO let type = this.parseDataType(); - return { name: name.str, ...type }; + return { name: name.str, $loc: name.$loc, ...type }; } parseDataType(): DataType { @@ -369,7 +369,7 @@ export class ECSCompiler extends Tokenizer { // TODO: remove init? while ((cmd2 = this.expectTokens(['const', 'init', 'var', 'decode', 'end']).str) != 'end') { let cmd = cmd2; // put in scope - if (cmd == 'var') cmd = 'init'; + if (cmd == 'var') cmd = 'init'; // TODO: remove? if (cmd == 'init' || cmd == 'const') { // TODO: check data types let name = this.expectIdent().str; @@ -391,14 +391,20 @@ export class ECSCompiler extends Tokenizer { } } else if (cmd == 'decode') { let decoderid = this.expectIdent().str; - let code = this.expectTokenTypes([ECSTokenType.CodeFragment]).str; + let codetok = this.expectTokenTypes([ECSTokenType.CodeFragment]); + let code = codetok.str; code = code.substring(3, code.length-3); let decoder = newDecoder(decoderid, code); if (!decoder) { this.compileError(`I can't find a "${decoderid}" decoder.`); throw new Error() } - let result = decoder.parse(); + let result; + try { + result = decoder.parse(); + } catch (e) { + throw new ECSError(e.message, decoder.getErrorLocation(codetok.$loc)); + } for (let entry of Object.entries(result.properties)) { let { c, f } = this.getEntityField(entity, entry[0]); - scope.setConstValue(entity, c, f.name, entry[1]); + scope.setConstValue(entity, c, f.name, entry[1] as DataValue); } } } diff --git a/src/common/ecs/decoder.ts b/src/common/ecs/decoder.ts index fa2cfcd5..628a193f 100644 --- a/src/common/ecs/decoder.ts +++ b/src/common/ecs/decoder.ts @@ -1,4 +1,5 @@ -import { threadId } from "worker_threads"; + +import { SourceLocation } from "../workertypes"; import { DataValue, ECSError } from "./ecs"; export interface DecoderResult { @@ -6,7 +7,8 @@ export interface DecoderResult { } abstract class LineDecoder { - lines : string[][]; + curline: number = 0; // for debugging, zero-indexed + lines : string[][]; // array of token arrays constructor( text: string @@ -41,6 +43,12 @@ abstract class LineDecoder { return v; } + getErrorLocation($loc: SourceLocation): SourceLocation { + // TODO: blank lines mess this up + $loc.line += this.curline + 1; + return $loc; + } + abstract parse() : DecoderResult; } @@ -50,7 +58,8 @@ export class VCSSpriteDecoder extends LineDecoder { let bitmapdata = new Uint8Array(height); let colormapdata = new Uint8Array(height); for (let i=0; i range.ehi) throw new ECSError(`entity ID ${entityID} too high for ${access.symbol}`); let ofs = this.symbols[access.symbol]; if (ofs !== undefined) { return ofs + entityID - range.elo; @@ -425,21 +428,6 @@ class DataSegment { // TODO: show entity name? throw new ECSError(`cannot find field access for ${access.symbol}`); } - getSegmentByteOffset(component: ComponentType, fieldName: string, entityID: number, bitofs: number, width: number) { - let range = this.getFieldRange(component, fieldName); - if (range && range.access) { - for (let a of range.access) { - if (a.bit == bitofs && a.width == width) { - let ofs = this.symbols[a.symbol]; - if (ofs !== undefined) { - return ofs + entityID - range.elo; - } - } - } - } - // TODO: show entity name? - throw new ECSError(`cannot find field offset for ${component.name}:${fieldName} entity #${entityID} bits ${bitofs} ${width}`) - } getOriginSymbol() { let a = this.ofs2sym.get(0); if (!a) throw new ECSError('getOriginSymbol(): no symbol at offset 0'); // TODO @@ -516,13 +504,59 @@ class EntitySet { } } +class IndexRegister { + lo: number | null; + hi: number | null; + offset = 0; + elo: number; + ehi: number; + eset: EntitySet | undefined; + + constructor( + public readonly scope: EntityScope, + eset?: EntitySet + ) { + this.elo = 0; + this.ehi = scope.entities.length - 1; + this.lo = null; + this.hi = null; + if (eset) { this.narrowInPlace(eset); } + } + entityCount() { + return this.ehi - this.elo + 1; + } + narrow(eset: EntitySet): IndexRegister { + let i = new IndexRegister(this.scope); + i.narrowInPlace(eset); + return i; + } + narrowInPlace(eset: EntitySet) { + if (this.scope != eset.scope) throw new ECSError(`scope mismatch`); + if (!eset.isContiguous()) throw new ECSError(`entities are not contiguous`); + let newelo = eset.entities[0].id; + let newehi = eset.entities[eset.entities.length - 1].id; + if (this.elo > newelo) throw new ECSError(`cannot narrow to new range, elo`); + if (this.ehi < newehi) throw new ECSError(`cannot narrow to new range, ehi`); + if (this.lo == null || this.hi == null) { + this.lo = 0; + this.hi = newehi - newelo + 1; + } else { + this.offset += newelo - this.elo; + } + this.elo = newelo; + this.ehi = newehi; + this.eset = eset; + } +} + // todo: generalize class ActionCPUState { - loops: EntitySet[] = []; x: EntitySet | null = null; y: EntitySet | null = null; xofs: number = 0; yofs: number = 0; + xreg: IndexRegister | null = null; + yreg: IndexRegister | null = null; } class ActionEval { @@ -537,7 +571,8 @@ class ActionEval { constructor( readonly scope: EntityScope, readonly instance: SystemInstance, - readonly action: Action) + readonly action: Action, + readonly eventargs: string[]) { this.em = scope.em; this.dialect = scope.em.dialect; @@ -570,14 +605,12 @@ class ActionEval { if (state.x && state.y) throw new ECSError('no more index registers', this.action); if (state.x) state.y = this.qr; else state.x = this.qr; - state.loops = state.loops.concat([this.qr]); break; case 'join': if (state.x || state.y) throw new ECSError('no free index registers for join', this.action); this.jr = new EntitySet(this.scope, (this.action as ActionWithJoin).join); state.y = this.qr; state.x = this.jr; - state.loops = state.loops.concat([this.qr]); break; case 'if': case 'with': @@ -639,8 +672,6 @@ class ActionEval { // select subset of entities let fullEntityCount = this.qr.entities.length; //entities.length.toString(); let entities = this.entities; - let loops = this.scope.state.loops; - let loopents = loops[loops.length-1]?.entities; // TODO: let loopreduce = !loopents || entities.length < loopents.length; //console.log(action.event, entities.length, loopents.length); // filter entities from loop? @@ -652,7 +683,7 @@ class ActionEval { let rf = this.instance.params.refField; code = this.wrapCodeInRefLookup(code); // TODO: only fetches 1st entity in list, need offset - let range = this.scope.bss.getFieldRange(rf.c, rf.f.name) || this.scope.rodata.getFieldRange(rf.c, rf.f.name); + let range = this.scope.getFieldRange(rf.c, rf.f.name); let eidofs = re.id - range.elo; props['%reffield'] = `${this.dialect.fieldsymbol(rf.c, rf.f, 0)}+${eidofs}`; } else { @@ -680,13 +711,15 @@ class ActionEval { let toks = group.split(/\s+/); if (toks.length == 0) throw new ECSError(`empty command`, action); let cmd = group.charAt(0); - let rest = group.substring(1).trim(); + let arg0 = toks[0].substring(1).trim(); + let args = [arg0].concat(toks.slice(1)); switch (cmd) { - case '!': return this.__emit([rest]); - case '$': return this.__local([rest]); - case '^': return this.__use([rest]); - case '<': return this.__get([rest, '0']); - case '>': return this.__get([rest, '8']); + case '!': return this.__emit(args); + case '$': return this.__local(args); + case '^': return this.__use(args); + case '#': return this.__arg(args); + case '<': return this.__get([arg0, '0']); + case '>': return this.__get([arg0, '8']); default: let value = props[toks[0]]; if (value) return value; @@ -740,7 +773,8 @@ class ActionEval { } __emit(args: string[]) { let event = args[0]; - return this.scope.generateCodeForEvent(event); + let eventargs = args.slice(1); + return this.scope.generateCodeForEvent(event, eventargs); } __local(args: string[]) { let tempinc = parseInt(args[0]); @@ -751,6 +785,10 @@ class ActionEval { this.scope.updateTempLiveness(this.instance); return `${this.tmplabel}+${tempinc}`; } + __arg(args: string[]) { + let index = parseInt(args[0] || '0'); + return this.eventargs[index] || ''; + } __bss_init(args: string[]) { return this.scope.allocateInitData(this.scope.bss); } @@ -791,15 +829,17 @@ class ActionEval { var component: ComponentType; var baseLookup = false; + var entityLookup = false; let entities: Entity[]; // is qualified field? if (fieldName.indexOf('.') > 0) { let [entname, fname] = fieldName.split('.'); let ent = this.scope.getEntityByName(entname); if (ent == null) throw new ECSError(`no entity named "${entname}" in this scope`, action); - component = this.em.singleComponentWithFieldName(this.qr.atypes, fname, action); + component = this.em.singleComponentWithFieldName([ent.etype], fname, action); fieldName = fname; entities = [ent]; + entityLookup = true; } else if (fieldName.indexOf(':') > 0) { let [cname, fname] = fieldName.split(':'); component = this.em.getComponentByName(cname); @@ -838,10 +878,13 @@ class ActionEval { } // TODO: offset > 0? // TODO: don't mix const and init data - let range = this.scope.bss.getFieldRange(component, fieldName) || this.scope.rodata.getFieldRange(component, fieldName); + let range = this.scope.getFieldRange(component, field.name); if (!range) throw new ECSError(`couldn't find field for ${component.name}:${fieldName}, maybe no entities?`); // TODO // TODO: dialect + // TODO: doesnt work for entity.field let eidofs = qr.entities.length && qr.entities[0].id - range.elo; // TODO: negative? + if (entityLookup) + eidofs = entities[0].id - range.elo; // TODO: array field baseoffset? if (baseLookup) { return this.dialect.absolute(ident); @@ -982,8 +1025,11 @@ export class EntityScope implements SourceLocated { for (var o = iter.next(); o.value; o = iter.next()) { let { i, e, c, f, v } = o.value; // constants and array pointers go into rodata - let segment = v === undefined && f.dtype != 'array' ? this.bss : this.rodata; let cfname = mksymbol(c, f.name); + let ftype = this.fieldtypes[cfname]; + let segment = ftype == 'const' ? this.rodata : this.bss; + if (v === undefined && ftype == 'const') + throw new ECSError(`no value for const field ${cfname}`, e); // determine range of indices for entities let array = segment.fieldranges[cfname]; if (!array) { @@ -991,7 +1037,7 @@ export class EntityScope implements SourceLocated { } else { array.ehi = i; } - //console.log(i,array,cfname); + //console.log(i,e.name,array,cfname); } } // TODO: cull unused entity fields @@ -1032,18 +1078,20 @@ export class EntityScope implements SourceLocated { let entcount = range ? range.ehi - range.elo + 1 : 0; if (v == null && f.dtype == 'int') v = 0; if (v == null && f.dtype == 'ref') v = 0; - if (v == null && f.dtype == 'array') throw new ECSError(`no default value for array ${cfname}`) + if (v == null && f.dtype == 'array') + throw new ECSError(`no default value for array ${cfname}`, e); //console.log(c.name, f.name, '#'+e.id, '=', v); // this is a constant // is it a byte array? //TODO? if (ArrayBuffer.isView(v) && f.dtype == 'array') { if (v instanceof Uint8Array && f.dtype == 'array') { - let datasym = this.dialect.datasymbol(c, f, e.id, 0); - segment.allocateInitData(datasym, v); let ptrlosym = this.dialect.fieldsymbol(c, f, 0); let ptrhisym = this.dialect.fieldsymbol(c, f, 8); let loofs = segment.allocateBytes(ptrlosym, entcount); let hiofs = segment.allocateBytes(ptrhisym, entcount); + let datasym = this.dialect.datasymbol(c, f, e.id, 0); + // TODO: share shared data + segment.allocateInitData(datasym, v); if (f.baseoffset) datasym = `(${datasym}+${f.baseoffset})`; segment.initdata[loofs + e.id - range.elo] = { symbol: datasym, bitofs: 0 }; segment.initdata[hiofs + e.id - range.elo] = { symbol: datasym, bitofs: 8 }; @@ -1056,8 +1104,8 @@ export class EntityScope implements SourceLocated { segment.allocateBytes(a.symbol, entcount); let ofs = segment.getByteOffset(range, a, e.id); // TODO: this happens if you forget a const field on an object? - if (e.id < range.elo) throw new ECSError(c.name + ' ' + f.name); - if (typeof segment.initdata[ofs] !== 'undefined') throw new ECSError(ofs+""); + if (e.id < range.elo) throw new ECSError('entity out of range ' + c.name + ' ' + f.name, e); + if (segment.initdata[ofs] !== undefined) throw new ECSError('initdata already set ' + ofs), e; segment.initdata[ofs] = (v >> a.bit) & 0xff; } } @@ -1122,21 +1170,31 @@ export class EntityScope implements SourceLocated { code = code.replace('{{%dest}}', segment.getOriginSymbol()); return code; } + getFieldRange(c: ComponentType, fn: string) { + return this.bss.getFieldRange(c, fn) || this.rodata.getFieldRange(c, fn); + } // TODO: check type/range of value setConstValue(e: Entity, component: ComponentType, fieldName: string, value: DataValue) { - let c = this.em.singleComponentWithFieldName([e.etype], fieldName, e); - e.consts[mksymbol(component, fieldName)] = value; - this.fieldtypes[mksymbol(component, fieldName)] = 'const'; + this.setConstInitValue(e, component, fieldName, value, 'const'); } setInitValue(e: Entity, component: ComponentType, fieldName: string, value: DataValue) { - let c = this.em.singleComponentWithFieldName([e.etype], fieldName, e); - e.inits[mkscopesymbol(this, component, fieldName)] = value; - this.fieldtypes[mksymbol(component, fieldName)] = 'init'; + this.setConstInitValue(e, component, fieldName, value, 'init'); + } + setConstInitValue(e: Entity, component: ComponentType, fieldName: string, value: DataValue, + type: 'const'|'init') { + this.em.singleComponentWithFieldName([e.etype], fieldName, e); + let cfname = mksymbol(component, fieldName); + let ecfname = mkscopesymbol(this, component, fieldName); + if (e.consts[cfname] !== undefined) throw new ECSError(`"${fieldName}" is already defined as a constant`, e); + if (e.inits[ecfname] !== undefined) throw new ECSError(`"${fieldName}" is already defined as a`, e); + if (type == 'const') e.consts[cfname] = value; + if (type == 'init') e.inits[ecfname] = value; + this.fieldtypes[cfname] = type; } isConstOrInit(component: ComponentType, fieldName: string) : 'const' | 'init' { return this.fieldtypes[mksymbol(component, fieldName)]; } - generateCodeForEvent(event: string): string { + generateCodeForEvent(event: string, args?: string[]): string { // find systems that respond to event // and have entities in this scope let systems = this.em.event2systems[event]; @@ -1150,14 +1208,16 @@ export class EntityScope implements SourceLocated { // generate code let s = this.dialect.code(); //s += `\n; event ${event}\n`; + let eventCount = 0; let instances = this.instances.filter(inst => systems.includes(inst.system)); for (let inst of instances) { let sys = inst.system; for (let action of sys.actions) { if (action.event == event) { + eventCount++; // TODO: use Tokenizer so error msgs are better // TODO: keep event tree - let codeeval = new ActionEval(this, inst, action); + let codeeval = new ActionEval(this, inst, action, args || []); codeeval.begin(); s += this.dialect.comment(`start action ${sys.name} ${inst.id} ${event}`); // TODO s += codeeval.codeToString(); @@ -1167,6 +1227,9 @@ export class EntityScope implements SourceLocated { } } } + if (eventCount == 0) { + console.log(`warning: event ${event} not handled`); + } return s; } getSystemStats(inst: SystemInstance) : SystemStats { @@ -1210,7 +1273,7 @@ export class EntityScope implements SourceLocated { } } if (!pack.pack()) console.log('cannot pack temporary local vars'); // TODO - console.log('tempvars', pack); + //console.log('tempvars', pack); if (bssbin.extents.right > 0) { this.bss.allocateBytes('TEMP', bssbin.extents.right); for (let b of pack.boxes) { diff --git a/src/test/testecs.ts b/src/test/testecs.ts index aac04e1a..4b07d0cf 100644 --- a/src/test/testecs.ts +++ b/src/test/testecs.ts @@ -1,6 +1,6 @@ import { spawnSync } from "child_process"; -import { readdirSync, readFileSync, writeFileSync } from "fs"; +import { existsSync, readdirSync, readFileSync, writeFileSync } from "fs"; import { describe } from "mocha"; import { Bin, BoxConstraints, Packer } from "../common/ecs/binpack"; import { ECSCompiler } from "../common/ecs/compiler"; @@ -19,11 +19,9 @@ component Kernel lines: 0..255 bgcolor: 0..255 end - component Bitmap data: array of 0..255 end - component HasBitmap bitmap: [Bitmap] end @@ -42,16 +40,19 @@ comment --- --- scope Root - entity kernel [Kernel] const lines = 0xc0 - const lines = $c0 + //const lines = $c0 + end + entity nobmp [Bitmap] + const data = [4] + end + entity bmp [Bitmap] + const data = [1,2,3] end - entity player1 [HasBitmap] - init bitmap = 1 + init bitmap = #bmp end - end `, 'foo.txt'); @@ -59,7 +60,7 @@ end let src = new SourceFileExport(); c.exportToFile(src); // TODO: test? - //console.log(src.toString()); + console.log(src.toString()); return em; } catch (e) { console.log(e); @@ -97,10 +98,11 @@ describe('Compiler', function() { } let code = readFileSync(ecspath, 'utf-8'); compiler.parseFile(code, ecspath); + // TODO: errors let out = new SourceFileExport(); em.exportToFile(out); let outtxt = out.toString(); - let goodtxt = readFileSync(goodpath, 'utf-8'); + let goodtxt = existsSync(goodpath) ? readFileSync(goodpath, 'utf-8') : ''; if (outtxt.trim() != goodtxt.trim()) { let asmpath = '/tmp/' + asmfn; writeFileSync(asmpath, outtxt, 'utf-8'); @@ -115,8 +117,8 @@ function testPack(bins: Bin[], boxes: BoxConstraints[]) { for (let bin of bins) packer.bins.push(bin); for (let bc of boxes) packer.boxes.push(bc); if (!packer.pack()) throw new Error('cannot pack') - console.log(packer.boxes); - console.log(packer.bins[0].free) + //console.log(packer.boxes); + //console.log(packer.bins[0].free) } describe('Box Packer', function() { diff --git a/test/ecs/narrow1.ecs b/test/ecs/narrow1.ecs new file mode 100644 index 00000000..164c9bde --- /dev/null +++ b/test/ecs/narrow1.ecs @@ -0,0 +1,28 @@ + +component Xpos + x: 0..255 +end +component Player +end +component Enemy +end + +scope Main + entity foo [Xpos] + end + entity p [Xpos,Player] + init x = 50 + end + entity e1 [Xpos,Enemy] + init x = 100 + end + entity e2 [Xpos,Enemy] + init x = 150 + end + system move + on start do foreach [Enemy] + --- + lda {{(Bitmap_bitmapdata_e1_b0+31) +.byte >(Bitmap_bitmapdata_e2_b0+31) Bitmap_bitmapdata_e1_b0: .byte 1 .byte 1 @@ -57,12 +63,6 @@ Bitmap_bitmapdata_e1_b0: .byte 31 .byte 63 .byte 127 -Bitmap_bitmapdata_b0: -.byte <(Bitmap_bitmapdata_e1_b0+31) -.byte <(Bitmap_bitmapdata_e2_b0+31) -Bitmap_bitmapdata_b8: -.byte >(Bitmap_bitmapdata_e1_b0+31) -.byte >(Bitmap_bitmapdata_e2_b0+31) Bitmap_height_b0: .byte 8 .byte 8 @@ -75,6 +75,10 @@ Bitmap_bitmapdata_e2_b0: .byte 255 .byte 62 .byte 24 +Colormap_colormapdata_b0: +.byte <(Colormap_colormapdata_e3_b0+31) +Colormap_colormapdata_b8: +.byte >(Colormap_colormapdata_e3_b0+31) Colormap_colormapdata_e3_b0: .byte 6 .byte 3 @@ -84,10 +88,6 @@ Colormap_colormapdata_e3_b0: .byte 14 .byte 31 .byte 63 -Colormap_colormapdata_b0: -.byte <(Colormap_colormapdata_e3_b0+31) -Colormap_colormapdata_b8: -.byte >(Colormap_colormapdata_e3_b0+31) Sprite_plyrflags_b0: .byte 0 .byte 3