1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2024-06-01 20:41:36 +00:00
8bitworkshop/src/common/ecs/ecs.ts

1271 lines
46 KiB
TypeScript
Raw Normal View History

2022-01-31 19:28:55 +00:00
/*
2022-02-02 17:34:55 +00:00
2022-01-31 19:28:55 +00:00
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
2022-02-01 14:07:33 +00:00
code fragments can be parameterized like macros
if two fragments are identical, do a JSR
(do we have to look for labels?)
2022-02-01 16:38:54 +00:00
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?
2022-01-31 19:28:55 +00:00
2022-02-02 17:34:55 +00:00
can you query specific entities? merge with existing queries?
bigints?
source/if query?
crazy idea -- full expansion, then relooper
2022-02-04 20:51:53 +00:00
how to avoid cycle crossing for critical code and data?
2022-01-31 19:28:55 +00:00
*/
2022-01-26 16:54:57 +00:00
2022-02-05 01:48:08 +00:00
import { data } from "jquery";
2022-01-29 19:07:21 +00:00
import { SourceLocated, SourceLocation } from "../workertypes";
export class ECSError extends Error {
$loc: SourceLocation;
constructor(msg: string, obj?: SourceLocation | SourceLocated) {
super(msg);
Object.setPrototypeOf(this, ECSError.prototype);
if (obj) this.$loc = (obj as SourceLocated).$loc || (obj as SourceLocation);
}
}
2022-01-27 01:12:49 +00:00
function mksymbol(c: ComponentType, fieldName: string) {
return c.name + '_' + fieldName;
}
2022-01-27 03:12:11 +00:00
function mkscopesymbol(s: EntityScope, c: ComponentType, fieldName: string) {
return s.name + '_' + c.name + '_' + fieldName;
}
2022-01-27 01:12:49 +00:00
2022-01-30 16:48:56 +00:00
export interface Entity extends SourceLocated {
2022-01-26 16:54:57 +00:00
id: number;
name?: string;
2022-01-26 16:54:57 +00:00
etype: EntityArchetype;
2022-01-27 18:43:27 +00:00
consts: { [component_field: string]: DataValue };
inits: { [scope_component_field: string]: DataValue };
2022-01-26 16:54:57 +00:00
}
export interface EntityConst {
component: ComponentType;
name: string;
value: DataValue;
}
export interface EntityArchetype {
components: ComponentType[];
}
2022-01-30 16:48:56 +00:00
export interface ComponentType extends SourceLocated {
2022-01-26 16:54:57 +00:00
name: string;
fields: DataField[];
optional?: boolean;
}
2022-01-31 15:17:40 +00:00
export interface Query extends SourceLocated {
2022-02-02 17:34:55 +00:00
include: ComponentType[]; // TODO: make ComponentType
exclude?: ComponentType[];
limit?: number;
2022-01-26 16:54:57 +00:00
}
2022-01-30 16:48:56 +00:00
export interface System extends SourceLocated {
2022-01-26 16:54:57 +00:00
name: string;
2022-01-27 18:43:27 +00:00
actions: Action[];
2022-01-26 16:54:57 +00:00
tempbytes?: number;
}
2022-02-03 15:24:00 +00:00
export type SelectType = 'once' | 'foreach' | 'join' | 'with' | 'if' | 'select';
2022-02-03 13:44:47 +00:00
export interface ActionBase extends SourceLocated {
select: SelectType;
2022-01-26 16:54:57 +00:00
event: string;
2022-02-03 13:44:47 +00:00
text: string;
2022-01-29 03:13:33 +00:00
emits?: string[];
2022-01-26 16:54:57 +00:00
}
2022-02-03 13:44:47 +00:00
export interface ActionOnce extends ActionBase {
select: 'once'
}
export interface ActionWithQuery extends ActionBase {
2022-02-03 15:24:00 +00:00
select: 'foreach' | 'join' | 'with' | 'if' | 'select'
2022-02-03 13:44:47 +00:00
query: Query
}
export interface ActionWithJoin extends ActionWithQuery {
select: 'join'
join?: Query
}
export type Action = ActionWithQuery | ActionWithJoin | ActionOnce;
2022-01-27 20:39:37 +00:00
export type DataValue = number | boolean | Uint8Array | Uint16Array;
2022-01-26 16:54:57 +00:00
export type DataField = { name: string } & DataType;
export type DataType = IntType | ArrayType | RefType;
export interface IntType {
dtype: 'int'
lo: number
hi: number
}
export interface ArrayType {
dtype: 'array'
elem: DataType
index?: DataType
2022-02-05 01:48:08 +00:00
baseoffset?: number
2022-01-26 16:54:57 +00:00
}
export interface RefType {
dtype: 'ref'
query: Query
}
interface FieldArray {
component: ComponentType;
field: DataField;
elo: number;
ehi: number;
access?: FieldAccess[];
}
interface FieldAccess {
symbol: string;
bit: number;
width: number;
}
interface ConstByte {
symbol: string;
bitofs: number;
}
2022-01-27 01:12:49 +00:00
interface ArchetypeMatch {
etype: EntityArchetype;
cmatch: ComponentType[];
}
2022-01-29 03:13:33 +00:00
interface ComponentFieldPair {
c: ComponentType;
f: DataField;
}
export class Dialect_CA65 {
2022-01-31 18:11:50 +00:00
2022-01-28 13:20:02 +00:00
readonly ASM_ITERATE_EACH = `
ldx #0
2022-01-30 00:21:38 +00:00
@__each:
2022-01-30 16:48:56 +00:00
{{%code}}
2022-01-28 13:20:02 +00:00
inx
2022-01-30 16:48:56 +00:00
cpx #{{%ecount}}
2022-01-30 00:21:38 +00:00
bne @__each
2022-01-30 16:48:56 +00:00
@__exit:
2022-01-28 13:20:02 +00:00
`;
2022-02-01 14:16:53 +00:00
readonly ASM_ITERATE_JOIN = `
ldy #0
@__each:
2022-01-30 16:48:56 +00:00
ldx {{%joinfield}},y
{{%code}}
iny
2022-01-30 16:48:56 +00:00
cpy #{{%ecount}}
bne @__each
2022-01-30 16:48:56 +00:00
@__exit:
`;
2022-02-03 14:51:28 +00:00
readonly ASM_FILTER_RANGE_LO_X = `
2022-02-02 17:34:55 +00:00
cpx #{{%xofs}}
2022-02-03 14:51:28 +00:00
bcc @__skipxlo
{{%code}}
@__skipxlo:
`
readonly ASM_FILTER_RANGE_HI_X = `
2022-02-02 17:34:55 +00:00
cpx #{{%xofs}}+{{%ecount}}
2022-02-03 14:51:28 +00:00
bcs @__skipxhi
2022-02-02 17:34:55 +00:00
{{%code}}
2022-02-03 14:51:28 +00:00
@__skipxhi:
2022-02-02 17:34:55 +00:00
`
2022-02-03 16:44:29 +00:00
// TODO
2022-02-02 17:34:55 +00:00
readonly ASM_MAP_RANGES = `
txa
pha
lda {{%mapping}},x
bmi @__mapskip
tax
{{%code}}
@__mapskip:
pla
tax
`;
2022-01-28 13:20:02 +00:00
readonly INIT_FROM_ARRAY = `
2022-01-30 16:48:56 +00:00
ldy #{{%nbytes}}
: lda {{%src}}-1,y
sta {{%dest}}-1,y
2022-01-28 13:20:02 +00:00
dey
bne :-
2022-01-28 13:20:02 +00:00
`
readonly HEADER = `
.include "vcs-ca65.h"
.define PAL 0
.code
2022-01-28 13:20:02 +00:00
`
readonly FOOTER = `
.segment "VECTORS"
2022-02-04 04:38:35 +00:00
Return: .word $6060
VecNMI:
VecReset: .word Main::__Reset
VecBRK: .word Main::__BRK
2022-01-28 13:20:02 +00:00
`
readonly TEMPLATE_INIT_MAIN = `
__NMI:
__Reset:
__BRK:
2022-01-28 13:20:02 +00:00
CLEAN_START
`
comment(s: string) {
return `\n;;; ${s}\n`;
}
2022-02-03 22:44:43 +00:00
absolute(ident: string, offset?: number) {
return this.addOffset(ident, offset || 0);
}
2022-02-02 17:34:55 +00:00
addOffset(ident: string, offset: number) {
if (offset > 0) return `${ident}+${offset}`;
if (offset < 0) return `${ident}-${-offset}`;
return ident;
}
indexed_x(ident: string, offset: number) {
return this.addOffset(ident, offset) + ',x';
}
indexed_y(ident: string, offset: number) {
return this.addOffset(ident, offset) + ',y';
}
2022-01-29 15:15:44 +00:00
fieldsymbol(component: ComponentType, field: DataField, bitofs: number) {
return `${component.name}_${field.name}_b${bitofs}`;
}
datasymbol(component: ComponentType, field: DataField, eid: number) {
return `${component.name}_${field.name}_e${eid}`;
}
2022-01-31 18:11:50 +00:00
code() {
return `.code\n`;
}
bss() {
return `.bss\n`;
}
2022-02-04 17:45:14 +00:00
debug_file(path: string) {
return `.dbg file, "${path}", 0, 0`
}
debug_line(path: string, line: number) {
return `.dbg line, "${path}", ${line}`
}
startScope(name: string) {
return `.scope ${name}`
}
endScope(name: string) {
return `.endscope\n${name}__Start = ${name}::__Start`
// TODO: scope__start = scope::start
2022-01-26 16:54:57 +00:00
}
2022-02-01 14:07:33 +00:00
segment(seg: string, segtype: 'rodata' | 'bss' | 'code') {
2022-01-27 01:12:49 +00:00
if (segtype == 'bss') {
2022-02-04 17:45:14 +00:00
return `.zeropage`; // TODO
2022-01-27 01:12:49 +00:00
} else {
2022-02-04 17:45:14 +00:00
return `.code`;
2022-01-27 01:12:49 +00:00
}
2022-01-26 16:54:57 +00:00
}
label(sym: string) {
2022-02-04 17:45:14 +00:00
return `${sym}:`;
2022-01-26 16:54:57 +00:00
}
byte(b: number | ConstByte | undefined) {
if (b === undefined) {
2022-02-04 17:45:14 +00:00
return `.res 1`
2022-01-26 16:54:57 +00:00
} else if (typeof b === 'number') {
2022-01-29 19:07:21 +00:00
if (b < 0 || b > 255) throw new ECSError(`out of range byte ${b}`);
2022-02-04 17:45:14 +00:00
return `.byte ${b}`
2022-01-26 16:54:57 +00:00
} else {
2022-02-04 17:45:14 +00:00
if (b.bitofs == 0) return `.byte <${b.symbol}`
else if (b.bitofs == 8) return `.byte >${b.symbol}`
else return `.byte (${b.symbol} >> ${b.bitofs})` // TODO?
2022-01-26 16:54:57 +00:00
}
}
2022-02-04 17:45:14 +00:00
}
// TODO: merge with Dialect?
export class SourceFileExport {
lines: string[] = [];
line(s: string) {
this.text(s);
}
2022-01-27 01:12:49 +00:00
text(s: string) {
for (let l of s.split('\n'))
this.lines.push(l);
}
2022-01-26 16:54:57 +00:00
toString() {
return this.lines.join('\n');
}
}
2022-02-01 14:07:33 +00:00
class CodeSegment {
codefrags: string[] = [];
addCodeFragment(code: string) {
this.codefrags.push(code);
}
dump(file: SourceFileExport) {
for (let code of this.codefrags) {
file.text(code);
}
}
}
class DataSegment {
2022-01-27 18:43:27 +00:00
symbols: { [sym: string]: number } = {};
ofs2sym = new Map<number, string[]>();
fieldranges: { [cfname: string]: FieldArray } = {};
2022-01-26 16:54:57 +00:00
size: number = 0;
initdata: (number | ConstByte | undefined)[] = [];
allocateBytes(name: string, bytes: number) {
2022-01-30 00:21:38 +00:00
let ofs = this.symbols[name];
if (ofs == null) {
ofs = this.size;
this.symbols[name] = ofs;
if (!this.ofs2sym.has(ofs))
this.ofs2sym.set(ofs, []);
this.ofs2sym.get(ofs)?.push(name);
this.size += bytes;
}
2022-01-26 16:54:57 +00:00
return ofs;
}
// TODO: optimize shared data
allocateInitData(name: string, bytes: Uint8Array) {
let ofs = this.allocateBytes(name, bytes.length);
2022-01-27 18:43:27 +00:00
for (let i = 0; i < bytes.length; i++) {
2022-01-26 16:54:57 +00:00
this.initdata[ofs + i] = bytes[i];
}
}
2022-02-04 17:45:14 +00:00
dump(file: SourceFileExport, dialect: Dialect_CA65) {
2022-01-27 18:43:27 +00:00
for (let i = 0; i < this.size; i++) {
2022-01-26 16:54:57 +00:00
let syms = this.ofs2sym.get(i);
if (syms) {
2022-02-04 17:45:14 +00:00
for (let sym of syms)
file.line(dialect.label(sym));
2022-01-26 16:54:57 +00:00
}
2022-02-04 17:45:14 +00:00
file.line(dialect.byte(this.initdata[i]));
2022-01-26 16:54:57 +00:00
}
}
2022-01-27 01:12:49 +00:00
// TODO: move cfname functions in here too
getFieldRange(component: ComponentType, fieldName: string) {
return this.fieldranges[mksymbol(component, fieldName)];
}
2022-02-01 03:16:40 +00:00
getByteOffset(range: FieldArray, access: FieldAccess, entityID: number) {
let ofs = this.symbols[access.symbol];
if (ofs !== undefined) {
return ofs + entityID - range.elo;
}
// 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) {
2022-01-27 03:12:11 +00:00
let range = this.getFieldRange(component, fieldName);
if (range && range.access) {
2022-02-01 03:16:40 +00:00
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;
}
}
2022-01-27 03:12:11 +00:00
}
}
2022-02-01 03:16:40 +00:00
// TODO: show entity name?
throw new ECSError(`cannot find field offset for ${component.name}:${fieldName} entity #${entityID} bits ${bitofs} ${width}`)
2022-01-27 03:12:11 +00:00
}
getOriginSymbol() {
let a = this.ofs2sym.get(0);
2022-01-29 19:07:21 +00:00
if (!a) throw new ECSError('getOriginSymbol(): no symbol at offset 0'); // TODO
2022-01-27 03:12:11 +00:00
return a[0];
}
2022-01-26 16:54:57 +00:00
}
function getFieldBits(f: IntType) {
let n = f.hi - f.lo + 1;
return Math.ceil(Math.log2(n));
}
2022-01-31 19:28:55 +00:00
function getFieldLength(f: DataType) {
if (f.dtype == 'int') {
return f.hi - f.lo + 1;
} else {
return 1; //TODO?
}
}
2022-01-26 16:54:57 +00:00
function getPackedFieldSize(f: DataType, constValue?: DataValue): number {
if (f.dtype == 'int') {
return getFieldBits(f);
} if (f.dtype == 'array' && f.index) {
2022-01-31 19:28:55 +00:00
return 0; // TODO? getFieldLength(f.index) * getPackedFieldSize(f.elem);
2022-01-26 16:54:57 +00:00
} if (f.dtype == 'array' && constValue != null && Array.isArray(constValue)) {
return constValue.length * getPackedFieldSize(f.elem);
} if (f.dtype == 'ref') {
return 8; // TODO: > 256 entities?
}
return 0;
}
class EntitySet {
2022-02-02 17:34:55 +00:00
atypes: ArchetypeMatch[];
entities: Entity[];
scope;
constructor(scope: EntityScope, query?: Query, a?: ArchetypeMatch[], e?: Entity[]) {
this.scope = scope;
if (query) {
this.atypes = scope.em.archetypesMatching(query);
this.entities = scope.entitiesMatching(this.atypes);
if (query.limit) {
this.entities = this.entities.slice(0, query.limit);
}
2022-02-02 17:34:55 +00:00
} else if (a && e) {
this.atypes = a;
this.entities = e;
}
}
contains(c: ComponentType, f: DataField, where: SourceLocated) {
// TODO: action for error msg
return this.scope.em.singleComponentWithFieldName(this.atypes, f.name, where);
}
intersection(qr: EntitySet) {
2022-02-02 17:34:55 +00:00
let ents = this.entities.filter(e => qr.entities.includes(e));
let atypes = this.atypes.filter(a1 => qr.atypes.find(a2 => a2.etype == a1.etype));
return new EntitySet(this.scope, undefined, atypes, ents);
2022-02-02 17:34:55 +00:00
}
isContiguous() {
if (this.entities.length == 0) return true;
let id = this.entities[0].id;
2022-02-03 16:44:29 +00:00
for (let i = 1; i < this.entities.length; i++) {
2022-02-02 17:34:55 +00:00
if (this.entities[i].id != ++id) return false;
}
return true;
}
}
// todo: generalize
class ActionCPUState {
x: EntitySet | null = null;
y: EntitySet | null = null;
2022-02-02 17:34:55 +00:00
xofs: number = 0;
yofs: number = 0;
}
2022-02-01 14:16:53 +00:00
class ActionEval {
em;
dialect;
qr: EntitySet;
jr: EntitySet | undefined;
oldState : ActionCPUState;
entities : Entity[];
2022-02-04 04:38:35 +00:00
tmplabel = '';
2022-02-02 17:34:55 +00:00
2022-02-01 14:16:53 +00:00
constructor(
readonly scope: EntityScope,
readonly sys: System,
2022-02-03 16:44:29 +00:00
readonly action: Action)
{
2022-02-01 14:16:53 +00:00
this.em = scope.em;
this.dialect = scope.em.dialect;
2022-02-02 17:34:55 +00:00
this.oldState = scope.state;
2022-02-03 13:44:47 +00:00
let q = (action as ActionWithQuery).query;
if (q) this.qr = new EntitySet(scope, q);
else this.qr = new EntitySet(scope, undefined, [], []);
this.entities = this.qr.entities;
2022-02-04 01:38:35 +00:00
//let query = (this.action as ActionWithQuery).query;
//TODO? if (query && this.entities.length == 0)
//throw new ECSError(`query doesn't match any entities`, query); // TODO
2022-02-02 17:34:55 +00:00
}
begin() {
let state = this.scope.state = Object.assign({}, this.scope.state);
// TODO: generalize to other cpus/langs
switch (this.action.select) {
case 'foreach':
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;
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);
2022-02-02 17:34:55 +00:00
state.y = this.qr;
2022-02-03 13:44:47 +00:00
state.x = this.jr;
2022-02-02 17:34:55 +00:00
break;
2022-02-03 15:24:00 +00:00
case 'if':
2022-02-03 13:44:47 +00:00
case 'with':
// TODO: what if not in X because 1 element?
2022-02-03 13:44:47 +00:00
if (state.x) {
let int = state.x.intersection(this.qr);
2022-02-03 15:24:00 +00:00
if (int.entities.length == 0) {
if (this.action.select == 'with')
2022-02-03 16:44:29 +00:00
throw new ECSError('no entities match this query', this.action);
2022-02-03 15:24:00 +00:00
else
break;
} else {
let indofs = int.entities[0].id - state.x.entities[0].id;
state.xofs += indofs; // TODO: should merge with filter
state.x = int;
this.entities = int.entities; // TODO?
2022-02-03 15:24:00 +00:00
}
2022-02-04 20:51:53 +00:00
} else if (this.action.select == 'with') {
2022-02-03 13:44:47 +00:00
if (this.qr.entities.length != 1)
throw new ECSError(`query outside of loop must match exactly one entity`, this.action);
}
2022-02-02 17:34:55 +00:00
break;
}
}
end() {
this.scope.state = this.oldState;
2022-02-01 14:16:53 +00:00
}
codeToString(): string {
const tag_re = /\{\{(.+?)\}\}/g;
const label_re = /@(\w+)\b/g;
2022-02-04 01:38:35 +00:00
const allowEmpty = ['if','foreach','join'];
if (this.entities.length == 0 && allowEmpty.includes(this.action.select))
return '';
2022-02-01 14:16:53 +00:00
let action = this.action;
let sys = this.sys;
let code = action.text;
2022-02-03 13:44:47 +00:00
let label = `${sys.name}__${action.event}__${this.em.seq++}`; // TODO: better label that won't conflict (seq?)
2022-02-01 14:16:53 +00:00
let props: { [name: string]: string } = {};
2022-02-03 13:44:47 +00:00
if (action.select != 'once') {
// TODO: detect cycles
// TODO: "source"?
2022-02-01 14:16:53 +00:00
// TODO: what if only 1 item?
2022-02-04 01:38:35 +00:00
// TODO: what if join is subset of items?
2022-02-03 13:44:47 +00:00
if (action.select == 'join' && this.jr) {
let jentities = this.jr.entities;
2022-02-04 01:38:35 +00:00
if (jentities.length == 0) return '';
// TODO? throw new ECSError(`join query doesn't match any entities`, (action as ActionWithJoin).join); // TODO
2022-02-03 13:44:47 +00:00
let joinfield = this.getJoinField(action, this.qr.atypes, this.jr.atypes);
// TODO: what if only 1 item?
// TODO: should be able to access fields via Y reg
code = this.wrapCodeInLoop(code, action, this.qr.entities, joinfield);
props['%joinfield'] = this.dialect.fieldsymbol(joinfield.c, joinfield.f, 0); //TODO?
}
2022-02-03 14:51:28 +00:00
// select subset of entities
let fullEntityCount = this.qr.entities.length; //entities.length.toString();
let entities = this.entities;
2022-02-03 13:44:47 +00:00
// filter entities from loop?
if (action.select == 'with' && entities.length > 1) {
code = this.wrapCodeInFilter(code);
}
2022-02-03 15:24:00 +00:00
if (action.select == 'if' && entities.length > 1) {
code = this.wrapCodeInFilter(code);
}
2022-02-03 16:44:29 +00:00
if (action.select == 'foreach' && entities.length > 1) {
code = this.wrapCodeInLoop(code, action, this.qr.entities);
}
2022-02-03 13:44:47 +00:00
// define properties
props['%elo'] = entities[0].id.toString();
props['%ehi'] = entities[entities.length - 1].id.toString();
props['%ecount'] = entities.length.toString();
props['%efullcount'] = fullEntityCount.toString();
2022-02-03 13:44:47 +00:00
props['%xofs'] = this.scope.state.xofs.toString();
props['%yofs'] = this.scope.state.yofs.toString();
2022-02-01 14:16:53 +00:00
}
// replace @labels
code = code.replace(label_re, (s: string, a: string) => `${label}__${a}`);
// replace {{...}} tags
code = code.replace(tag_re, (entire, group: string) => {
2022-02-01 16:38:54 +00:00
let toks = group.split(/\s+/);
if (toks.length == 0) throw new ECSError(`empty command`, action);
2022-02-01 14:16:53 +00:00
let cmd = group.charAt(0);
2022-02-03 16:44:29 +00:00
let rest = group.substring(1).trim();
2022-02-01 14:16:53 +00:00
switch (cmd) {
2022-02-01 16:38:54 +00:00
case '!': return this.__emit([rest]);
case '$': return this.__local([rest]);
case '^': return this.__use([rest]);
2022-02-03 16:44:29 +00:00
case '<': return this.__get([rest, '0']);
case '>': return this.__get([rest, '8']);
2022-02-01 14:16:53 +00:00
default:
2022-02-01 16:38:54 +00:00
let value = props[toks[0]];
2022-02-01 14:16:53 +00:00
if (value) return value;
2022-02-01 16:38:54 +00:00
let fn = (this as any)['__' + toks[0]];
if (fn) return fn.bind(this)(toks.slice(1));
throw new ECSError(`unrecognized command {{${toks[0]}}}`, action);
2022-02-01 14:16:53 +00:00
}
});
return code;
}
2022-02-03 16:44:29 +00:00
__get(args: string[]) {
return this.getset(args, false);
}
__set(args: string[]) {
return this.getset(args, true);
}
getset(args: string[], canwrite: boolean) {
2022-02-01 16:38:54 +00:00
let fieldName = args[0];
let bitofs = parseInt(args[1] || '0');
return this.generateCodeForField(fieldName, bitofs, canwrite);
2022-02-01 16:38:54 +00:00
}
2022-02-05 01:48:08 +00:00
__base(args: string[]) {
// TODO: refactor into generateCode..
let fieldName = args[0];
let bitofs = parseInt(args[1] || '0');
let component = this.em.singleComponentWithFieldName(this.qr.atypes, fieldName, this.action);
let field = component.fields.find(f => f.name == fieldName);
if (field == null) throw new ECSError(`no field named "${fieldName}" in component`, this.action);
return this.dialect.fieldsymbol(component, field, bitofs);
}
2022-02-03 22:04:48 +00:00
__index(args: string[]) {
2022-02-05 01:48:08 +00:00
// TODO: check select type and if we actually have an index...
let ident = args[0];
2022-02-03 22:04:48 +00:00
if (this.entities.length == 1) {
return this.dialect.absolute(ident);
} else {
return this.dialect.indexed_x(ident, 0); //TODO?
}
}
2022-02-01 16:38:54 +00:00
__use(args: string[]) {
return this.scope.includeResource(args[0]);
}
__emit(args: string[]) {
return this.scope.generateCodeForEvent(args[0]);
}
__local(args: string[]) {
let tempinc = parseInt(args[0]);
if (isNaN(tempinc)) throw new ECSError(`bad temporary offset`, this.action);
if (!this.sys.tempbytes) throw new ECSError(`this system has no locals`, this.action);
if (tempinc < 0 || tempinc >= this.sys.tempbytes) throw new ECSError(`this system only has ${this.sys.tempbytes} locals`, this.action);
2022-02-04 04:38:35 +00:00
return `${this.tmplabel}+${tempinc}`;
//return `TEMP+${this.scope.tempOffset}+${tempinc}`;
2022-02-01 16:38:54 +00:00
}
2022-02-01 14:16:53 +00:00
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;
if (joinfield) s = this.dialect.ASM_ITERATE_JOIN;
s = s.replace('{{%code}}', code);
return s;
}
2022-02-02 17:34:55 +00:00
wrapCodeInFilter(code: string) {
2022-02-03 14:51:28 +00:00
// TODO: :-p
const ents = this.entities;
const ents2 = this.oldState.x?.entities;
2022-02-03 14:51:28 +00:00
if (ents && ents2) {
let lo = ents[0].id;
2022-02-03 16:44:29 +00:00
let hi = ents[ents.length - 1].id;
2022-02-03 14:51:28 +00:00
let lo2 = ents2[0].id;
2022-02-03 16:44:29 +00:00
let hi2 = ents2[ents2.length - 1].id;
2022-02-03 14:51:28 +00:00
if (lo != lo2)
code = this.dialect.ASM_FILTER_RANGE_LO_X.replace('{{%code}}', code);
if (hi != hi2)
code = this.dialect.ASM_FILTER_RANGE_HI_X.replace('{{%code}}', code);
}
return code;
2022-02-02 17:34:55 +00:00
}
2022-02-03 22:04:48 +00:00
generateCodeForField(fieldName: string, bitofs: number, canWrite: boolean): string {
const action = this.action;
const qr = this.jr || this.qr; // TODO: why not both!
2022-02-01 14:16:53 +00:00
var component: ComponentType;
2022-02-03 22:44:43 +00:00
var baseLookup = false;
let entities: Entity[];
2022-02-01 14:16:53 +00:00
// is qualified field?
2022-02-03 22:44:43 +00:00
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);
fieldName = fname;
entities = [ent];
} else if (fieldName.indexOf(':') > 0) {
2022-02-01 14:16:53 +00:00
let [cname, fname] = fieldName.split(':');
component = this.em.getComponentByName(cname);
2022-02-03 16:44:29 +00:00
if (component == null) throw new ECSError(`no component named "${cname}"`, action)
2022-02-03 22:44:43 +00:00
entities = this.entities;
fieldName = fname;
baseLookup = true;
2022-02-01 14:16:53 +00:00
} else {
2022-02-02 17:34:55 +00:00
component = this.em.singleComponentWithFieldName(qr.atypes, fieldName, action);
2022-02-03 22:44:43 +00:00
entities = this.entities;
2022-02-01 14:16:53 +00:00
}
// find archetypes
let field = component.fields.find(f => f.name == fieldName);
2022-02-03 16:44:29 +00:00
if (field == null) throw new ECSError(`no field named "${fieldName}" in component`, action);
2022-02-05 01:48:08 +00:00
let ident = this.dialect.fieldsymbol(component, field, bitofs);
2022-02-01 14:16:53 +00:00
// see if all entities have the same constant value
2022-02-02 17:34:55 +00:00
// TODO: should be done somewhere else?
2022-02-01 14:16:53 +00:00
let constValues = new Set<DataValue>();
2022-02-03 22:04:48 +00:00
let isConst = false;
2022-02-03 22:44:43 +00:00
for (let e of entities) {
2022-02-01 14:16:53 +00:00
let constVal = e.consts[mksymbol(component, fieldName)];
2022-02-03 22:04:48 +00:00
if (constVal !== undefined) isConst = true;
2022-02-01 14:16:53 +00:00
constValues.add(constVal); // constVal === undefined is allowed
}
2022-02-03 16:44:29 +00:00
// can't write to constant
2022-02-03 22:04:48 +00:00
if (isConst && canWrite)
2022-02-03 16:44:29 +00:00
throw new ECSError(`can't write to constant field ${fieldName}`, action);
2022-02-01 14:16:53 +00:00
// is it a constant?
if (constValues.size == 1) {
let value = constValues.values().next().value as DataValue;
// TODO: what about symbols?
// TODO: use dialect
if (typeof value === 'number') {
return `#${(value >> bitofs) & 0xff}`;
}
}
// 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);
if (!range) throw new ECSError(`couldn't find field for ${component.name}:${fieldName}, maybe no entities?`); // TODO
// TODO: dialect
2022-02-04 20:51:53 +00:00
let eidofs = qr.entities.length && qr.entities[0].id - range.elo; // TODO: negative?
2022-02-03 22:44:43 +00:00
if (baseLookup) {
2022-02-01 14:16:53 +00:00
return this.dialect.absolute(ident);
2022-02-03 22:44:43 +00:00
} else if (entities.length == 1) {
2022-02-04 20:51:53 +00:00
return this.dialect.absolute(ident, eidofs);
2022-02-01 14:16:53 +00:00
} else {
2022-02-02 17:34:55 +00:00
let ir;
if (this.scope.state.x?.intersection(qr)) {
2022-02-02 17:34:55 +00:00
ir = this.scope.state.x;
eidofs -= this.scope.state.xofs;
}
else if (this.scope.state.y?.intersection(qr)) {
2022-02-02 17:34:55 +00:00
ir = this.scope.state.y;
eidofs -= this.scope.state.yofs;
2022-02-01 14:16:53 +00:00
}
2022-02-02 17:34:55 +00:00
if (!ir) throw new ECSError(`no intersection for index register`, action);
if (ir.entities.length == 0) throw new ECSError(`no common entities for index register`, action);
if (!ir.isContiguous()) throw new ECSError(`entities in query are not contiguous`, action);
if (ir == this.scope.state.x)
return this.dialect.indexed_x(ident, eidofs);
if (ir == this.scope.state.y)
return this.dialect.indexed_y(ident, eidofs);
throw new ECSError(`cannot find "${component.name}:${field.name}" in state`, action);
2022-02-01 14:16:53 +00:00
}
}
getJoinField(action: Action, atypes: ArchetypeMatch[], jtypes: ArchetypeMatch[]): ComponentFieldPair {
let refs = Array.from(this.scope.iterateArchetypeFields(atypes, (c, f) => f.dtype == 'ref'));
// TODO: better error message
2022-02-03 13:44:47 +00:00
if (refs.length == 0) throw new ECSError(`cannot find join fields`, action);
if (refs.length > 1) throw new ECSError(`cannot join multiple fields`, action);
2022-02-01 14:16:53 +00:00
// 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);
}
}
}
*/
}
}
2022-01-30 16:48:56 +00:00
export class EntityScope implements SourceLocated {
$loc: SourceLocation;
2022-01-27 18:43:27 +00:00
childScopes: EntityScope[] = [];
systems: System[] = [];
2022-01-27 18:43:27 +00:00
entities: Entity[] = [];
2022-02-04 01:38:35 +00:00
fieldtypes: { [name: string]: 'init' | 'const' } = {};
2022-02-01 14:07:33 +00:00
bss = new DataSegment();
rodata = new DataSegment();
code = new CodeSegment();
2022-01-26 16:54:57 +00:00
componentsInScope = new Set();
2022-01-27 01:12:49 +00:00
tempOffset = 0;
2022-01-29 03:13:33 +00:00
tempSize = 0;
2022-01-27 01:12:49 +00:00
maxTempBytes = 0;
2022-01-31 18:11:50 +00:00
resources = new Set<string>();
state = new ActionCPUState();
2022-02-03 02:06:44 +00:00
isDemo = false;
filePath = '';
2022-01-26 16:54:57 +00:00
constructor(
public readonly em: EntityManager,
2022-01-28 13:20:02 +00:00
public readonly dialect: Dialect_CA65,
2022-01-26 16:54:57 +00:00
public readonly name: string,
public readonly parent: EntityScope | undefined
) {
parent?.childScopes.push(this);
}
2022-01-27 18:43:27 +00:00
newEntity(etype: EntityArchetype): Entity {
2022-01-26 16:54:57 +00:00
// TODO: add parent ID? lock parent scope?
// TODO: name identical check?
2022-01-26 16:54:57 +00:00
let id = this.entities.length;
etype = this.em.addArchetype(etype);
2022-01-27 18:43:27 +00:00
let entity: Entity = { id, etype, consts: {}, inits: {} };
2022-01-26 16:54:57 +00:00
for (let c of etype.components) {
this.componentsInScope.add(c.name);
}
this.entities.push(entity);
return entity;
}
2022-02-03 22:04:48 +00:00
addUsingSystem(system: System) {
this.systems.push(system);
}
2022-02-03 22:44:43 +00:00
getEntityByName(name: string) {
return this.entities.find(e => e.name == name);
}
*iterateEntityFields(entities: Entity[]) {
for (let i = 0; i < entities.length; i++) {
let e = entities[i];
2022-01-26 16:54:57 +00:00
for (let c of e.etype.components) {
for (let f of c.fields) {
2022-01-27 18:43:27 +00:00
yield { i, e, c, f, v: e.consts[mksymbol(c, f.name)] };
2022-01-26 16:54:57 +00:00
}
}
}
}
2022-02-01 14:16:53 +00:00
*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) {
2022-02-01 14:16:53 +00:00
if (!filter || filter(c, f))
yield { i, c, f };
}
}
}
}
entitiesMatching(atypes: ArchetypeMatch[]) {
2022-02-01 14:16:53 +00:00
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;
}
hasComponent(ctype: ComponentType) {
return this.componentsInScope.has(ctype.name);
}
2022-01-26 16:54:57 +00:00
buildSegments() {
2022-01-31 19:28:55 +00:00
// build FieldArray for each component/field pair
// they will be different for bss/rodata segments
let iter = this.iterateEntityFields(this.entities);
2022-01-27 18:43:27 +00:00
for (var o = iter.next(); o.value; o = iter.next()) {
let { i, e, c, f, v } = o.value;
2022-01-31 19:28:55 +00:00
// constants and array pointers go into rodata
let segment = v === undefined && f.dtype != 'array' ? this.bss : this.rodata;
2022-01-27 01:12:49 +00:00
let cfname = mksymbol(c, f.name);
2022-01-31 19:28:55 +00:00
// determine range of indices for entities
2022-01-26 16:54:57 +00:00
let array = segment.fieldranges[cfname];
if (!array) {
2022-01-27 18:43:27 +00:00
array = segment.fieldranges[cfname] = { component: c, field: f, elo: i, ehi: i };
2022-01-26 16:54:57 +00:00
} else {
array.ehi = i;
}
//console.log(i,array,cfname);
}
}
// TODO: cull unused entity fields
2022-02-01 14:07:33 +00:00
allocateSegment(segment: DataSegment, readonly: boolean) {
2022-02-01 14:16:53 +00:00
let fields: FieldArray[] = Object.values(segment.fieldranges);
// TODO: fields.sort((a, b) => (a.ehi - a.elo + 1) * getPackedFieldSize(a.field));
2022-02-01 14:16:53 +00:00
let f: FieldArray | undefined;
2022-01-26 16:54:57 +00:00
while (f = fields.pop()) {
2022-01-31 19:28:55 +00:00
let rangelen = (f.ehi - f.elo + 1);
let alloc = !readonly;
2022-01-26 16:54:57 +00:00
// TODO: doesn't work for packed arrays too well
let bits = getPackedFieldSize(f.field);
// variable size? make it a pointer
2022-01-27 01:12:49 +00:00
if (bits == 0) bits = 16; // TODO?
2022-01-30 00:21:38 +00:00
let bytesperelem = Math.ceil(bits / 8);
2022-01-26 16:54:57 +00:00
// TODO: packing bits
// TODO: split arrays
2022-01-31 19:28:55 +00:00
let access = [];
2022-01-27 18:43:27 +00:00
for (let i = 0; i < bits; i += 8) {
2022-01-29 15:15:44 +00:00
let symbol = this.dialect.fieldsymbol(f.component, f.field, i);
2022-02-01 03:16:40 +00:00
access.push({ symbol, bit: i, width: 8 }); // TODO
2022-01-31 19:28:55 +00:00
if (alloc) {
2022-02-01 03:16:40 +00:00
segment.allocateBytes(symbol, rangelen); // TODO
2022-01-26 16:54:57 +00:00
}
}
2022-01-31 19:28:55 +00:00
f.access = access;
2022-01-26 16:54:57 +00:00
}
}
2022-02-01 14:07:33 +00:00
allocateROData(segment: DataSegment) {
let iter = this.iterateEntityFields(this.entities);
2022-01-27 18:43:27 +00:00
for (var o = iter.next(); o.value; o = iter.next()) {
let { i, e, c, f, v } = o.value;
2022-01-27 01:12:49 +00:00
let cfname = mksymbol(c, f.name);
2022-02-01 03:16:40 +00:00
let range = segment.fieldranges[cfname];
let entcount = range ? range.ehi - range.elo + 1 : 0;
// is this a constant value?
2022-01-31 19:28:55 +00:00
if (v === undefined) {
// this is not a constant
2022-02-01 03:16:40 +00:00
// is it a bounded array? (TODO)
2022-01-31 19:28:55 +00:00
if (f.dtype == 'array' && f.index) {
let datasym = this.dialect.datasymbol(c, f, e.id);
2022-02-05 01:48:08 +00:00
let databytes = getFieldLength(f.index);
let offset = this.bss.allocateBytes(datasym, databytes);
2022-01-31 19:28:55 +00:00
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);
2022-02-05 01:48:08 +00:00
if (f.baseoffset) datasym = `(${datasym}+${f.baseoffset})`;
2022-02-01 03:16:40 +00:00
segment.initdata[loofs + e.id - range.elo] = { symbol: datasym, bitofs: 0 };
segment.initdata[hiofs + e.id - range.elo] = { symbol: datasym, bitofs: 8 };
2022-01-31 19:28:55 +00:00
}
} else {
// this is a constant
2022-01-26 16:54:57 +00:00
// is it a byte array?
2022-02-05 01:48:08 +00:00
if (v instanceof Uint8Array && f.dtype == 'array') {
2022-01-29 15:15:44 +00:00
let datasym = this.dialect.datasymbol(c, f, e.id);
2022-01-31 19:28:55 +00:00
segment.allocateInitData(datasym, v);
2022-01-29 15:15:44 +00:00
let ptrlosym = this.dialect.fieldsymbol(c, f, 0);
let ptrhisym = this.dialect.fieldsymbol(c, f, 8);
2022-01-27 01:12:49 +00:00
let loofs = segment.allocateBytes(ptrlosym, entcount);
let hiofs = segment.allocateBytes(ptrhisym, entcount);
2022-02-05 01:48:08 +00:00
if (f.baseoffset) datasym = `(${datasym}+${f.baseoffset})`;
2022-02-01 03:16:40 +00:00
segment.initdata[loofs + e.id - range.elo] = { symbol: datasym, bitofs: 0 };
segment.initdata[hiofs + e.id - range.elo] = { symbol: datasym, bitofs: 8 };
2022-01-30 00:21:38 +00:00
// TODO: } else if (v instanceof Uint16Array) {
2022-01-27 20:39:37 +00:00
} else if (typeof v === 'number') {
// more than 1 entity, add an array
2022-02-02 17:34:55 +00:00
if (entcount > 1) {
2022-02-01 03:16:40 +00:00
if (!range.access) throw new ECSError(`no access for field ${cfname}`)
for (let a of range.access) {
2022-02-02 17:34:55 +00:00
segment.allocateBytes(a.symbol, entcount);
2022-02-01 03:16:40 +00:00
let ofs = segment.getByteOffset(range, a, e.id);
2022-02-02 17:34:55 +00:00
segment.initdata[ofs] = (v >> a.bit) & 0xff;
2022-02-01 03:16:40 +00:00
}
2022-01-27 20:39:37 +00:00
}
2022-02-01 03:16:40 +00:00
// TODO: what if mix of var, const, and init values?
2022-01-27 20:39:37 +00:00
} else {
2022-01-29 19:07:21 +00:00
throw new ECSError(`unhandled constant ${e.id}:${cfname}`);
2022-01-26 16:54:57 +00:00
}
}
}
//console.log(segment.initdata)
}
2022-02-01 14:07:33 +00:00
allocateInitData(segment: DataSegment) {
2022-01-31 18:11:50 +00:00
if (segment.size == 0) return ''; // TODO: warning for no init data?
2022-01-27 03:12:11 +00:00
let initbytes = new Uint8Array(segment.size);
let iter = this.iterateEntityFields(this.entities);
2022-01-27 18:43:27 +00:00
for (var o = iter.next(); o.value; o = iter.next()) {
let { i, e, c, f, v } = o.value;
2022-01-27 03:12:11 +00:00
let scfname = mkscopesymbol(this, c, f.name);
let initvalue = e.inits[scfname];
if (initvalue !== undefined) {
2022-02-01 03:16:40 +00:00
let range = segment.getFieldRange(c, f.name);
if (!range) throw new ECSError(`no range`, e);
if (!range.access) throw new ECSError(`no range access`, e);
if (typeof initvalue === 'number') {
for (let a of range.access) {
let offset = segment.getByteOffset(range, a, e.id);
2022-02-01 14:16:53 +00:00
initbytes[offset] = (initvalue >> a.bit) & ((1 << a.width) - 1);
2022-02-01 03:16:40 +00:00
}
2022-01-31 19:28:55 +00:00
} else if (initvalue instanceof Uint8Array) {
// TODO???
let datasym = this.dialect.datasymbol(c, f, e.id);
let ofs = this.bss.symbols[datasym];
initbytes.set(initvalue, ofs);
2022-01-27 03:12:11 +00:00
} else {
2022-01-29 03:13:33 +00:00
// TODO: init arrays?
2022-02-01 03:16:40 +00:00
throw new ECSError(`cannot initialize ${scfname} = ${initvalue}`); // TODO??
2022-01-27 03:12:11 +00:00
}
}
}
// build the final init buffer
// TODO: compress 0s?
let bufsym = this.name + '__INITDATA';
let bufofs = this.rodata.allocateInitData(bufsym, initbytes);
2022-01-28 13:20:02 +00:00
let code = this.dialect.INIT_FROM_ARRAY;
2022-01-27 03:12:11 +00:00
//TODO: function to repalce from dict?
2022-01-30 16:48:56 +00:00
code = code.replace('{{%nbytes}}', initbytes.length.toString())
code = code.replace('{{%src}}', bufsym);
code = code.replace('{{%dest}}', segment.getOriginSymbol());
2022-01-27 03:12:11 +00:00
return code;
}
// TODO: check type/range of value
2022-01-26 16:54:57 +00:00
setConstValue(e: Entity, component: ComponentType, fieldName: string, value: DataValue) {
2022-02-02 17:34:55 +00:00
let c = this.em.singleComponentWithFieldName([{ etype: e.etype, cmatch: [component] }], fieldName, e);
2022-01-27 01:12:49 +00:00
e.consts[mksymbol(component, fieldName)] = value;
2022-02-04 01:38:35 +00:00
this.fieldtypes[mksymbol(component, fieldName)] = 'const';
2022-01-26 16:54:57 +00:00
}
2022-01-27 03:12:11 +00:00
setInitValue(e: Entity, component: ComponentType, fieldName: string, value: DataValue) {
2022-02-02 17:34:55 +00:00
let c = this.em.singleComponentWithFieldName([{ etype: e.etype, cmatch: [component] }], fieldName, e);
2022-01-27 03:12:11 +00:00
e.inits[mkscopesymbol(this, component, fieldName)] = value;
2022-02-04 01:38:35 +00:00
this.fieldtypes[mksymbol(component, fieldName)] = 'init';
}
isConstOrInit(component: ComponentType, fieldName: string) : 'const' | 'init' {
return this.fieldtypes[mksymbol(component, fieldName)];
2022-01-27 03:12:11 +00:00
}
2022-01-26 16:54:57 +00:00
generateCodeForEvent(event: string): string {
// find systems that respond to event
// and have entities in this scope
2022-01-31 19:28:55 +00:00
let systems = this.em.event2systems[event];
2022-01-31 15:17:40 +00:00
if (!systems || systems.length == 0) {
2022-01-31 18:11:50 +00:00
// TODO: error or warning?
console.log(`warning: no system responds to "${event}"`); return '';
//throw new ECSError(`warning: no system responds to "${event}"`);
2022-01-26 16:54:57 +00:00
}
2022-01-31 18:11:50 +00:00
let s = this.dialect.code();
2022-01-26 16:54:57 +00:00
//s += `\n; event ${event}\n`;
2022-01-27 18:43:27 +00:00
let emitcode: { [event: string]: string } = {};
systems = systems.filter(s => this.systems.includes(s));
2022-01-26 16:54:57 +00:00
for (let sys of systems) {
2022-01-27 03:12:11 +00:00
// TODO: does this work if multiple actions?
2022-01-29 03:13:33 +00:00
// TODO: should 'emits' be on action?
2022-02-04 04:38:35 +00:00
// TODO: share storage
//if (sys.tempbytes) this.allocateTempBytes(sys.tempbytes);
let tmplabel = `${sys.name}_tmp`;
if (sys.tempbytes) this.bss.allocateBytes(tmplabel, sys.tempbytes);
//this.allocateTempBytes(1);
let numActions = 0;
2022-01-26 16:54:57 +00:00
for (let action of sys.actions) {
if (action.event == event) {
2022-01-29 03:13:33 +00:00
if (action.emits) {
for (let emit of action.emits) {
if (emitcode[emit]) {
console.log(`already emitted for ${sys.name}:${event}`);
}
//console.log('>', emit);
// TODO: cycles
emitcode[emit] = this.generateCodeForEvent(emit);
//console.log('<', emit, emitcode[emit].length);
}
}
2022-01-31 19:28:55 +00:00
// TODO: use Tokenizer so error msgs are better
2022-02-01 14:16:53 +00:00
let codeeval = new ActionEval(this, sys, action);
2022-02-04 04:38:35 +00:00
codeeval.tmplabel = tmplabel;
2022-02-02 17:34:55 +00:00
codeeval.begin();
2022-02-01 14:16:53 +00:00
s += this.dialect.comment(`<action ${sys.name}:${event}>`); // TODO
s += codeeval.codeToString();
s += this.dialect.comment(`</action ${sys.name}:${event}>`);
2022-01-27 01:12:49 +00:00
// TODO: check that this happens once?
2022-02-02 17:34:55 +00:00
codeeval.end();
2022-02-04 04:38:35 +00:00
numActions++;
2022-01-26 16:54:57 +00:00
}
2022-01-27 01:12:49 +00:00
}
2022-02-04 04:38:35 +00:00
// TODO: if (sys.tempbytes && numActions) this.allocateTempBytes(-sys.tempbytes);
2022-01-26 16:54:57 +00:00
}
return s;
}
2022-01-27 01:12:49 +00:00
allocateTempBytes(n: number) {
2022-01-29 03:13:33 +00:00
if (n > 0) this.tempOffset = this.tempSize;
this.tempSize += n;
this.maxTempBytes = Math.max(this.tempSize, this.maxTempBytes);
if (n < 0) this.tempOffset = this.tempSize;
2022-01-27 01:12:49 +00:00
}
2022-01-31 18:11:50 +00:00
includeResource(symbol: string): string {
this.resources.add(symbol);
2022-01-27 01:12:49 +00:00
return symbol;
}
analyzeEntities() {
this.buildSegments();
this.allocateSegment(this.bss, false);
this.allocateSegment(this.rodata, true);
this.allocateROData(this.rodata);
}
generateCode() {
this.tempOffset = this.maxTempBytes = 0;
// TODO: main scope?
if (this.name.toLowerCase() == 'main') {
this.code.addCodeFragment(this.dialect.TEMPLATE_INIT_MAIN);
}
2022-01-27 03:12:11 +00:00
let initcode = this.allocateInitData(this.bss);
this.code.addCodeFragment(initcode);
let start = this.generateCodeForEvent('start');
this.code.addCodeFragment(start);
2022-01-31 18:11:50 +00:00
for (let sub of Array.from(this.resources.values())) {
2022-01-27 01:12:49 +00:00
let code = this.generateCodeForEvent(sub);
this.code.addCodeFragment(code);
}
}
dump(file: SourceFileExport) {
this.analyzeEntities();
this.generateCode();
2022-02-04 17:45:14 +00:00
let dialect = this.dialect;
file.line(dialect.startScope(this.name));
file.line(dialect.segment(`${this.name}_DATA`, 'bss'));
2022-01-27 01:12:49 +00:00
if (this.maxTempBytes) this.bss.allocateBytes('TEMP', this.maxTempBytes);
2022-02-04 17:45:14 +00:00
this.bss.dump(file, dialect);
file.line(dialect.segment(`${this.name}_RODATA`, 'rodata'));
this.rodata.dump(file, dialect);
2022-02-01 14:07:33 +00:00
//file.segment(`${this.name}_CODE`, 'code');
2022-02-04 17:45:14 +00:00
file.line(dialect.label('__Start'));
2022-01-27 01:12:49 +00:00
this.code.dump(file);
for (let subscope of this.childScopes) {
// TODO: overlay child BSS segments
subscope.dump(file);
}
2022-02-04 17:45:14 +00:00
file.line(dialect.endScope(this.name));
2022-01-27 01:12:49 +00:00
}
2022-01-26 16:54:57 +00:00
}
export class EntityManager {
archetypes: { [key: string]: EntityArchetype } = {};
2022-01-27 18:43:27 +00:00
components: { [name: string]: ComponentType } = {};
systems: { [name: string]: System } = {};
topScopes: { [name: string]: EntityScope } = {};
event2systems: { [event: string]: System[] } = {};
name2cfpairs: { [cfname: string]: ComponentFieldPair[] } = {};
2022-02-03 13:44:47 +00:00
mainPath: string = '';
2022-02-03 16:44:29 +00:00
imported: { [path: string]: boolean } = {};
2022-02-03 13:44:47 +00:00
seq = 1;
2022-01-26 16:54:57 +00:00
constructor(public readonly dialect: Dialect_CA65) {
}
2022-01-26 16:54:57 +00:00
newScope(name: string, parent?: EntityScope) {
2022-02-04 00:25:36 +00:00
let existing = this.topScopes[name];
if (existing && !existing.isDemo)
throw new ECSError(`scope ${name} already defined`, existing);
2022-01-28 13:20:02 +00:00
let scope = new EntityScope(this, this.dialect, name, parent);
if (!parent) this.topScopes[name] = scope;
2022-01-26 16:54:57 +00:00
return scope;
}
defineComponent(ctype: ComponentType) {
2022-02-04 00:25:36 +00:00
let existing = this.components[ctype.name];
if (existing) throw new ECSError(`component ${ctype.name} already defined`, existing);
2022-01-31 19:28:55 +00:00
for (let field of ctype.fields) {
let list = this.name2cfpairs[field.name];
if (!list) list = this.name2cfpairs[field.name] = [];
2022-02-01 14:16:53 +00:00
list.push({ c: ctype, f: field });
2022-01-31 19:28:55 +00:00
}
2022-01-26 16:54:57 +00:00
return this.components[ctype.name] = ctype;
}
defineSystem(system: System) {
2022-02-04 00:25:36 +00:00
let existing = this.systems[system.name];
if (existing) throw new ECSError(`system ${system.name} already defined`, existing);
2022-01-31 15:17:40 +00:00
for (let a of system.actions) {
let event = a.event;
2022-01-31 19:28:55 +00:00
let list = this.event2systems[event];
if (list == null) list = this.event2systems[event] = [];
2022-01-31 15:17:40 +00:00
if (!list.includes(system)) list.push(system);
}
return this.systems[system.name] = system;
2022-01-26 16:54:57 +00:00
}
2022-02-01 14:16:53 +00:00
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;
}
2022-01-26 16:54:57 +00:00
componentsMatching(q: Query, etype: EntityArchetype) {
let list = [];
for (let c of etype.components) {
2022-02-02 17:34:55 +00:00
if (q.exclude?.includes(c)) {
2022-01-27 20:07:13 +00:00
return [];
}
2022-01-26 16:54:57 +00:00
// TODO: 0 includes == all entities?
2022-02-02 17:34:55 +00:00
if (q.include.length == 0 || q.include.includes(c)) {
2022-01-27 20:07:13 +00:00
list.push(c);
2022-01-26 16:54:57 +00:00
}
}
2022-01-27 20:07:13 +00:00
return list.length == q.include.length ? list : [];
2022-01-26 16:54:57 +00:00
}
archetypesMatching(q: Query) {
2022-01-27 18:43:27 +00:00
let result: ArchetypeMatch[] = [];
for (let etype of Object.values(this.archetypes)) {
2022-01-26 16:54:57 +00:00
let cmatch = this.componentsMatching(q, etype);
if (cmatch.length > 0) {
2022-01-27 18:43:27 +00:00
result.push({ etype, cmatch });
2022-01-26 16:54:57 +00:00
}
}
2022-01-26 16:54:57 +00:00
return result;
}
2022-01-29 03:13:33 +00:00
componentsWithFieldName(atypes: ArchetypeMatch[], fieldName: string) {
2022-01-27 01:12:49 +00:00
// TODO???
2022-01-29 03:13:33 +00:00
let comps = new Set<ComponentType>();
2022-01-27 01:12:49 +00:00
for (let at of atypes) {
for (let c of at.cmatch) {
for (let f of c.fields) {
if (f.name == fieldName)
2022-01-29 03:13:33 +00:00
comps.add(c);
2022-01-27 01:12:49 +00:00
}
}
}
2022-01-29 03:13:33 +00:00
return Array.from(comps);
2022-01-26 16:54:57 +00:00
}
getComponentByName(name: string): ComponentType {
return this.components[name];
}
getSystemByName(name: string): System {
return this.systems[name];
}
2022-02-02 17:34:55 +00:00
singleComponentWithFieldName(atypes: ArchetypeMatch[], fieldName: string, where: SourceLocated) {
2022-01-29 03:13:33 +00:00
let components = this.componentsWithFieldName(atypes, fieldName);
2022-01-31 19:28:55 +00:00
// TODO: use name2cfpairs?
2022-01-29 03:13:33 +00:00
if (components.length == 0) {
2022-02-02 17:34:55 +00:00
throw new ECSError(`cannot find component with field "${fieldName}"`, where);
2022-01-29 03:13:33 +00:00
}
if (components.length > 1) {
2022-02-02 17:34:55 +00:00
throw new ECSError(`ambiguous field name "${fieldName}"`, where);
2022-01-29 03:13:33 +00:00
}
return components[0];
}
toJSON() {
return JSON.stringify({
2022-01-28 00:02:37 +00:00
components: this.components,
systems: this.systems
2022-01-28 00:02:37 +00:00
})
}
exportToFile(file: SourceFileExport) {
file.text(this.dialect.HEADER); // TODO
for (let scope of Object.values(this.topScopes)) {
2022-02-03 13:44:47 +00:00
if (!scope.isDemo || scope.filePath == this.mainPath) {
2022-02-03 02:06:44 +00:00
scope.dump(file);
}
}
file.text(this.dialect.FOOTER); // TODO
}
2022-01-26 16:54:57 +00:00
}