diff --git a/src/codemirror/ecs.js b/src/codemirror/ecs.js index eb149db1..947b0806 100644 --- a/src/codemirror/ecs.js +++ b/src/codemirror/ecs.js @@ -19,8 +19,8 @@ var directives_list = [ 'component', 'system', 'entity', 'scope', 'end', 'const', 'init', 'locals', - 'on', 'do', 'emit', - 'once', 'foreach', 'source', + 'on', 'do', 'emit', 'limit', + 'once', 'foreach', 'source', 'join' ]; var keywords_list = [ 'processor', diff --git a/src/common/ecs/compiler.ts b/src/common/ecs/compiler.ts index ba3677c7..1e06aa46 100644 --- a/src/common/ecs/compiler.ts +++ b/src/common/ecs/compiler.ts @@ -152,9 +152,15 @@ export class ECSCompiler extends Tokenizer { parseAction(): Action { let event = this.expectIdent().str; this.expectToken('do'); - let select = this.expectTokens(['once', 'foreach', 'source']).str as SelectType; // TODO: type check? + let select = this.expectTokens(['once', 'foreach', 'source', 'join']).str as SelectType; // TODO: type check? let query = this.parseQuery(); + let join = select == 'join' && this.parseQuery(); let emits; + let limit; + if (this.peekToken().str == 'limit') { + this.consumeToken(); + limit = this.expectInteger(); + } if (this.peekToken().str == 'emit') { this.consumeToken(); this.expectToken('('); @@ -162,7 +168,8 @@ export class ECSCompiler extends Tokenizer { this.expectToken(')'); } let text = this.parseCode(); - return { text, event, query, select }; + let action = { text, event, query, join, select, limit }; + return action; } parseQuery() { @@ -222,12 +229,12 @@ export class ECSCompiler extends Tokenizer { // TODO: check data types if (cmd == 'const' || cmd == 'init') { let name = this.expectIdent().str; - this.expectToken('='); let comps = this.em.componentsWithFieldName([{etype: e.etype, cmatch:e.etype.components}], name); if (comps.length == 0) this.compileError(`I couldn't find a field named "${name}" for this entity.`) if (comps.length > 1) this.compileError(`I found more than one field named "${name}" for this entity.`) let field = comps[0].fields.find(f => f.name == name); if (!field) this.internalError(); + this.expectToken('='); let value = this.parseDataValue(field); if (cmd == 'const') this.currentScope.setConstValue(e, comps[0], name, value); if (cmd == 'init') this.currentScope.setInitValue(e, comps[0], name, value); diff --git a/src/common/ecs/ecs.ts b/src/common/ecs/ecs.ts index 019452c2..caeee962 100644 --- a/src/common/ecs/ecs.ts +++ b/src/common/ecs/ecs.ts @@ -33,6 +33,7 @@ // 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 import { SourceLocated, SourceLocation } from "../workertypes"; @@ -92,12 +93,14 @@ export interface System { export interface Action { text: string; event: string; - query: Query; select: SelectType + query: Query; + join?: Query; + limit?: number; emits?: string[]; } -export type SelectType = 'once' | 'foreach' | 'source'; +export type SelectType = 'once' | 'foreach' | 'source' | 'join'; export type DataValue = number | boolean | Uint8Array | Uint16Array; @@ -160,6 +163,17 @@ export class Dialect_CA65 { cpx #{{ecount}} bne @__each `; + +readonly ASM_ITERATE_JOIN = ` + ldy #0 +@__each: + ldx {{joinfield}},y + {{code}} + iny + cpy #{{ecount}} + bne @__each +`; + readonly INIT_FROM_ARRAY = ` ldy #{{nbytes}} : lda {{src}}-1,y @@ -345,18 +359,19 @@ export class EntityScope { } newEntity(etype: EntityArchetype): Entity { // TODO: add parent ID? lock parent scope? + // TODO: name identical check? let id = this.entities.length; + etype = this.em.addArchetype(etype); let entity: Entity = { id, etype, consts: {}, inits: {} }; - this.em.archtypes.add(etype); for (let c of etype.components) { this.componentsInScope.add(c.name); } this.entities.push(entity); return entity; } - *iterateFields() { - for (let i = 0; i < this.entities.length; i++) { - let e = this.entities[i]; + *iterateEntityFields(entities: Entity[]) { + for (let i = 0; i < entities.length; i++) { + let e = entities[i]; for (let c of e.etype.components) { for (let f of c.fields) { yield { i, e, c, f, v: e.consts[mksymbol(c, f.name)] }; @@ -364,8 +379,77 @@ export class EntityScope { } } } + *iterateArchetypeFields(arch: ArchetypeMatch[], filter?: (c:ComponentType,f:DataField) => boolean) { + for (let i = 0; i < arch.length; i++) { + let a = arch[i]; + for (let c of a.etype.components) { + for (let f of c.fields) { + if (!filter || filter(c,f)) + yield { i, c, f }; + } + } + } + } + entitiesMatching(atypes: ArchetypeMatch[]) { + let result : Entity[] = []; + for (let e of this.entities) { + for (let a of atypes) { + // TODO: what about subclasses? + // TODO: very scary identity ocmpare + if (e.etype === a.etype) { + result.push(e); + break; + } + } + } + return result; + } + getSystems(events: string[]) { + let result : System[] = []; + for (let sys of Object.values(this.em.systems)) { + if (this.systemListensTo(sys, events)) { + result.push(sys); + } + } + return result; + } + systemListensTo(sys: System, events: string[]) { + for (let action of sys.actions) { + if (action.event != null && events.includes(action.event)) { + let archs = this.em.archetypesMatching(action.query); + for (let arch of archs) { + for (let ctype of arch.cmatch) { + if (this.hasComponent(ctype)) { + return true; + } + } + } + } + } + } + hasComponent(ctype: ComponentType) { + return this.componentsInScope.has(ctype.name); + } + getJoinField(atypes: ArchetypeMatch[], jtypes: ArchetypeMatch[]) : ComponentFieldPair { + let refs = Array.from(this.iterateArchetypeFields(atypes, (c,f) => f.dtype == 'ref')); + if (refs.length == 0) throw new ECSError(`cannot find join fields`); + if (refs.length > 1) throw new ECSError(`cannot join multiple fields`); + // TODO: check to make sure join works + return refs[0]; // TODO + /* TODO + let match = refs.map(ref => this.em.archetypesMatching((ref.f as RefType).query)); + for (let ref of refs) { + let m = this.em.archetypesMatching((ref.f as RefType).query); + for (let a of m) { + if (jtypes.includes(a.etype)) { + console.log(a,m); + } + } + } + */ + } buildSegments() { - let iter = this.iterateFields(); + let iter = this.iterateEntityFields(this.entities); for (var o = iter.next(); o.value; o = iter.next()) { let { i, e, c, f, v } = o.value; let segment = v === undefined ? this.bss : this.rodata; @@ -404,7 +488,7 @@ export class EntityScope { } } allocateROData(segment: Segment) { - let iter = this.iterateFields(); + let iter = this.iterateEntityFields(this.entities); for (var o = iter.next(); o.value; o = iter.next()) { let { i, e, c, f, v } = o.value; let cfname = mksymbol(c, f.name); @@ -441,7 +525,7 @@ export class EntityScope { } allocateInitData(segment: Segment) { let initbytes = new Uint8Array(segment.size); - let iter = this.iterateFields(); + let iter = this.iterateEntityFields(this.entities); for (var o = iter.next(); o.value; o = iter.next()) { let { i, e, c, f, v } = o.value; let scfname = mkscopesymbol(this, c, f.name); @@ -526,7 +610,7 @@ export class EntityScope { if (n < 0) this.tempOffset = this.tempSize; } replaceCode(code: string, sys: System, action: Action): string { - const re = /\{\{(.+?)\}\}/g; + const tag_re = /\{\{(.+?)\}\}/g; let label = `${sys.name}__${action.event}`; let atypes = this.em.archetypesMatching(action.query); let entities = this.entitiesMatching(atypes); @@ -534,14 +618,22 @@ export class EntityScope { // TODO: "source"? // TODO: what if only 1 item? if (action.select == 'foreach') { - code = this.wrapCodeInLoop(code, sys, action, entities); - //console.log(sys.name, action.event, ents); - //frag = this.iterateCode(frag); + code = this.wrapCodeInLoop(code, action, entities); } + if (action.select == 'join' && action.join) { + let jtypes = this.em.archetypesMatching(action.join); + let jentities = this.entitiesMatching(jtypes); + let joinfield = this.getJoinField(atypes, jtypes); + // TODO: what if only 1 item? + code = this.wrapCodeInLoop(code, action, entities, joinfield); + atypes = jtypes; + entities = jentities; + } + if (entities.length == 0) throw new ECSError(`action ${label} doesn't match any entities`); // replace @labels code = code.replace(/@(\w+)\b/g, (s: string, a: string) => `${label}__${a}`); // replace {{...}} tags - return code.replace(re, (entire, group: string) => { + return code.replace(tag_re, (entire, group: string) => { let cmd = group.charAt(0); let rest = group.substring(1); switch (cmd) { @@ -569,14 +661,22 @@ export class EntityScope { this.subroutines.add(symbol); return symbol; } - wrapCodeInLoop(code: string, sys: System, action: Action, ents: Entity[]): string { + wrapCodeInLoop(code: string, action: Action, ents: Entity[], joinfield?: ComponentFieldPair): string { // TODO: check ents // TODO: check segment bounds + // TODO: what if 0 or 1 entitites? let s = this.dialect.ASM_ITERATE_EACH; - s = s.replace('{{elo}}', ents[0].id.toString()); - s = s.replace('{{ehi}}', ents[ents.length - 1].id.toString()); - s = s.replace('{{ecount}}', ents.length.toString()); + if (joinfield) s = this.dialect.ASM_ITERATE_JOIN; + if (action.limit) { + ents = ents.slice(0, action.limit); + } + s = s.replace('{{elo}}', () => ents[0].id.toString()); + s = s.replace('{{ehi}}', () => ents[ents.length - 1].id.toString()); + s = s.replace('{{ecount}}', () => ents.length.toString()); s = s.replace('{{code}}', code); + if (joinfield) { + s = s.replace('{{joinfield}}', () => this.dialect.fieldsymbol(joinfield.c, joinfield.f, 0)); + } return s; } generateCodeForField(sys: System, action: Action, @@ -586,8 +686,8 @@ export class EntityScope { var component : ComponentType; var qualified = false; // is qualified field? - if (fieldName.indexOf('.') > 0) { - let [cname,fname] = fieldName.split('.'); + if (fieldName.indexOf(':') > 0) { + let [cname,fname] = fieldName.split(':'); component = this.em.getComponentByName(cname); fieldName = fname; qualified = true; @@ -638,45 +738,6 @@ export class EntityScope { return this.dialect.indexed_x(ident); } } - entitiesMatching(atypes: ArchetypeMatch[]) { - let result : Entity[] = []; - for (let e of this.entities) { - for (let a of atypes) { - // TODO: what about subclasses? - if (e.etype == a.etype) { - result.push(e); - break; - } - } - } - return result; - } - getSystems(events: string[]) { - let result : System[] = []; - for (let sys of Object.values(this.em.systems)) { - if (this.systemListensTo(sys, events)) { - result.push(sys); - } - } - return result; - } - systemListensTo(sys: System, events: string[]) { - for (let action of sys.actions) { - if (action.event != null && events.includes(action.event)) { - let archs = this.em.archetypesMatching(action.query); - for (let arch of archs) { - for (let ctype of arch.cmatch) { - if (this.hasComponent(ctype)) { - return true; - } - } - } - } - } - } - hasComponent(ctype: ComponentType) { - return this.componentsInScope.has(ctype.name); - } analyzeEntities() { this.buildSegments(); this.allocateSegment(this.bss, false); @@ -708,7 +769,7 @@ export class EntityScope { } export class EntityManager { - archtypes = new Set(); + archetypes: { [key: string]: EntityArchetype } = {}; components: { [name: string]: ComponentType } = {}; systems: { [name: string]: System } = {}; scopes: { [name: string]: EntityScope } = {}; @@ -730,6 +791,13 @@ export class EntityManager { if (this.systems[system.name]) throw new ECSError(`system ${system.name} already defined`); return this.systems[system.name] = system; } + addArchetype(atype: EntityArchetype) : EntityArchetype { + let key = atype.components.map(c => c.name).join(','); + if (this.archetypes[key]) + return this.archetypes[key]; + else + return this.archetypes[key] = atype; + } componentsMatching(q: Query, etype: EntityArchetype) { let list = []; for (let c of etype.components) { @@ -746,12 +814,12 @@ export class EntityManager { } archetypesMatching(q: Query) { let result: ArchetypeMatch[] = []; - this.archtypes.forEach(etype => { + for (let etype of Object.values(this.archetypes)) { let cmatch = this.componentsMatching(q, etype); if (cmatch.length > 0) { result.push({ etype, cmatch }); } - }); + } return result; } componentsWithFieldName(atypes: ArchetypeMatch[], fieldName: string) { diff --git a/src/ide/views/editors.ts b/src/ide/views/editors.ts index d594ac10..b9b5dd38 100644 --- a/src/ide/views/editors.ts +++ b/src/ide/views/editors.ts @@ -36,7 +36,7 @@ const MODEDEFS = { markdown: { lineWrap: true }, fastbasic: { noGutters: true }, basic: { noLineNumbers: true, noGutters: true }, // TODO: not used? - ecs: { theme: 'mbo', isAsm: false }, + ecs: { theme: 'mbo', isAsm: true }, } export var textMapFunctions = { diff --git a/src/worker/tools/cc65.ts b/src/worker/tools/cc65.ts index 5f1f2a71..77b40837 100644 --- a/src/worker/tools/cc65.ts +++ b/src/worker/tools/cc65.ts @@ -216,7 +216,7 @@ export function linkLD65(step: BuildStep): BuildStepResult { if (fn.endsWith('.lst')) { var lstout = FS.readFile(fn, { encoding: 'utf8' }); lstout = lstout.split('\n\n')[1] || lstout; // remove header - var asmlines = parseCA65Listing(lstout, symbolmap, params, false); + var asmlines = []; // TODO: parseCA65Listing(lstout, symbolmap, params, false); var srclines = parseCA65Listing(lstout, symbolmap, params, true); putWorkFile(fn, lstout); // TODO: you have to get rid of all source lines to get asm listing diff --git a/src/worker/tools/ecs.ts b/src/worker/tools/ecs.ts index cfb01d80..c130d8a5 100644 --- a/src/worker/tools/ecs.ts +++ b/src/worker/tools/ecs.ts @@ -18,13 +18,16 @@ export function assembleECS(step: BuildStep): BuildStepResult { throw e; } } - //var listings: CodeListingMap = {}; - putWorkFile(destpath, compiler.export().toString()); + let outtext = compiler.export().toString(); + putWorkFile(destpath, outtext); + var listings: CodeListingMap = {}; + listings[destpath] = {lines:[], text:outtext} // TODO } return { nexttool: "ca65", path: destpath, args: [destpath], files: [destpath, 'vcs-ca65.h'], //TODO + listings }; }