2022-01-26 16:54:57 +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)
|
2022-01-28 00:02:37 +00:00
|
|
|
// optional components? on or off
|
|
|
|
// union components? either X or Y or Z...
|
2022-01-26 16:54:57 +00:00
|
|
|
//
|
|
|
|
// 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?
|
2022-01-27 20:39:37 +00:00
|
|
|
//
|
|
|
|
// 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
|
2022-01-29 15:15:44 +00:00
|
|
|
//
|
|
|
|
// 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
|
2022-01-30 15:01:55 +00:00
|
|
|
// join thru a reference? load both x and y
|
2022-01-26 16:54:57 +00:00
|
|
|
|
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;
|
2022-01-28 17:22:59 +00:00
|
|
|
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-01-28 17:22:59 +00:00
|
|
|
include: string[]; // TODO: make ComponentType
|
2022-01-26 16:54:57 +00:00
|
|
|
listen?: string[];
|
|
|
|
exclude?: string[];
|
|
|
|
updates?: string[];
|
|
|
|
}
|
|
|
|
|
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-01-30 16:48:56 +00:00
|
|
|
export interface Action extends SourceLocated {
|
2022-01-26 16:54:57 +00:00
|
|
|
text: string;
|
|
|
|
event: string;
|
2022-01-28 17:22:59 +00:00
|
|
|
select: SelectType
|
2022-01-30 15:01:55 +00:00
|
|
|
query: Query;
|
|
|
|
join?: Query;
|
|
|
|
limit?: number;
|
2022-01-29 03:13:33 +00:00
|
|
|
emits?: string[];
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
|
|
|
|
2022-01-30 15:01:55 +00:00
|
|
|
export type SelectType = 'once' | 'foreach' | 'source' | 'join';
|
2022-01-28 17:22:59 +00:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-01-28 17:22:59 +00:00
|
|
|
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-01-30 15:01:55 +00:00
|
|
|
|
|
|
|
readonly ASM_ITERATE_JOIN = `
|
|
|
|
ldy #0
|
|
|
|
@__each:
|
2022-01-30 16:48:56 +00:00
|
|
|
ldx {{%joinfield}},y
|
|
|
|
{{%code}}
|
2022-01-30 15:01:55 +00:00
|
|
|
iny
|
2022-01-30 16:48:56 +00:00
|
|
|
cpy #{{%ecount}}
|
2022-01-30 15:01:55 +00:00
|
|
|
bne @__each
|
2022-01-30 16:48:56 +00:00
|
|
|
@__exit:
|
2022-01-30 15:01:55 +00:00
|
|
|
`;
|
|
|
|
|
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
|
2022-01-28 17:22:59 +00:00
|
|
|
bne :-
|
2022-01-28 13:20:02 +00:00
|
|
|
`
|
|
|
|
readonly HEADER = `
|
|
|
|
.include "vcs-ca65.h"
|
|
|
|
`
|
|
|
|
readonly FOOTER = `
|
|
|
|
.segment "VECTORS"
|
|
|
|
VecNMI: .word Start
|
|
|
|
VecReset: .word Start
|
|
|
|
VecBRK: .word Start
|
|
|
|
`
|
|
|
|
readonly TEMPLATE_INIT = `
|
|
|
|
Start:
|
|
|
|
CLEAN_START
|
|
|
|
`
|
2022-01-28 17:22:59 +00:00
|
|
|
|
|
|
|
comment(s: string) {
|
|
|
|
return `\n;;; ${s}\n`;
|
|
|
|
}
|
|
|
|
absolute(ident: string) {
|
|
|
|
return ident;
|
|
|
|
}
|
|
|
|
indexed_x(ident: string) {
|
|
|
|
return ident + ',x';
|
|
|
|
}
|
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-01-28 13:20:02 +00:00
|
|
|
}
|
|
|
|
|
2022-01-29 15:15:44 +00:00
|
|
|
// TODO: merge with Dialect?
|
2022-01-28 17:22:59 +00:00
|
|
|
export class SourceFileExport {
|
2022-01-27 18:43:27 +00:00
|
|
|
lines: string[] = [];
|
2022-01-26 16:54:57 +00:00
|
|
|
|
|
|
|
comment(text: string) {
|
|
|
|
this.lines.push(';' + text);
|
|
|
|
}
|
|
|
|
segment(seg: string, segtype: 'rodata' | 'bss') {
|
2022-01-27 01:12:49 +00:00
|
|
|
if (segtype == 'bss') {
|
2022-01-27 20:39:37 +00:00
|
|
|
this.lines.push(`.segment "ZEROPAGE"`); // TODO
|
2022-01-27 01:12:49 +00:00
|
|
|
} else {
|
2022-01-27 15:54:12 +00:00
|
|
|
this.lines.push(`.segment "CODE"`); // TODO
|
2022-01-27 01:12:49 +00:00
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
|
|
|
label(sym: string) {
|
|
|
|
this.lines.push(`${sym}:`);
|
|
|
|
}
|
|
|
|
byte(b: number | ConstByte | undefined) {
|
|
|
|
if (b === undefined) {
|
2022-01-27 15:54:12 +00:00
|
|
|
this.lines.push(` .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-01-26 16:54:57 +00:00
|
|
|
this.lines.push(` .byte ${b}`)
|
|
|
|
} else {
|
2022-01-27 15:54:12 +00:00
|
|
|
if (b.bitofs == 0) this.lines.push(` .byte <${b.symbol}`)
|
|
|
|
else if (b.bitofs == 8) this.lines.push(` .byte >${b.symbol}`)
|
|
|
|
else this.lines.push(` .byte (${b.symbol} >> ${b.bitofs})`) // TODO?
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
|
|
|
}
|
2022-01-27 01:12:49 +00:00
|
|
|
text(s: string) {
|
|
|
|
for (let l of s.split('\n'))
|
|
|
|
this.lines.push(l);
|
|
|
|
}
|
2022-01-29 18:24:38 +00:00
|
|
|
debug_file(path: string) {
|
|
|
|
this.lines.push(` .dbg file, "${path}", 0, 0`);
|
|
|
|
}
|
|
|
|
debug_line(path: string, line: number) {
|
|
|
|
this.lines.push(` .dbg line, "${path}", ${line}`);
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
toString() {
|
|
|
|
return this.lines.join('\n');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Segment {
|
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)[] = [];
|
2022-01-27 18:43:27 +00:00
|
|
|
codefrags: string[] = [];
|
2022-01-26 16:54:57 +00:00
|
|
|
|
2022-01-27 01:12:49 +00:00
|
|
|
addCodeFragment(code: string) {
|
|
|
|
this.codefrags.push(code);
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
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];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
dump(file: SourceFileExport) {
|
2022-01-27 01:12:49 +00:00
|
|
|
for (let code of this.codefrags) {
|
|
|
|
file.text(code);
|
|
|
|
}
|
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) {
|
|
|
|
for (let sym of syms) file.label(sym);
|
|
|
|
}
|
|
|
|
file.byte(this.initdata[i]);
|
|
|
|
}
|
|
|
|
}
|
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-01-27 03:12:11 +00:00
|
|
|
getSegmentByteOffset(component: ComponentType, fieldName: string, bitofs: number, entityID: number) {
|
|
|
|
let range = this.getFieldRange(component, fieldName);
|
|
|
|
if (range && range.access) {
|
|
|
|
let a = range.access[0]; // TODO: bitofs
|
|
|
|
let ofs = this.symbols[a.symbol];
|
|
|
|
if (ofs !== undefined) {
|
|
|
|
return ofs + entityID - range.elo;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
|
|
|
function getPackedFieldSize(f: DataType, constValue?: DataValue): number {
|
|
|
|
if (f.dtype == 'int') {
|
|
|
|
return getFieldBits(f);
|
|
|
|
} if (f.dtype == 'array' && f.index) {
|
|
|
|
return getPackedFieldSize(f.index) * getPackedFieldSize(f.elem);
|
|
|
|
} 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;
|
|
|
|
}
|
|
|
|
|
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[] = [];
|
|
|
|
entities: Entity[] = [];
|
2022-01-26 16:54:57 +00:00
|
|
|
bss = new Segment();
|
|
|
|
rodata = new Segment();
|
|
|
|
code = new Segment();
|
|
|
|
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>();
|
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?
|
2022-01-30 15:01:55 +00:00
|
|
|
// TODO: name identical check?
|
2022-01-26 16:54:57 +00:00
|
|
|
let id = this.entities.length;
|
2022-01-30 15:01:55 +00:00
|
|
|
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-01-30 15:01:55 +00:00
|
|
|
*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-01-30 15:01:55 +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) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
hasComponent(ctype: ComponentType) {
|
|
|
|
return this.componentsInScope.has(ctype.name);
|
|
|
|
}
|
2022-01-30 16:48:56 +00:00
|
|
|
getJoinField(action: Action, atypes: ArchetypeMatch[], jtypes: ArchetypeMatch[]) : ComponentFieldPair {
|
2022-01-30 15:01:55 +00:00
|
|
|
let refs = Array.from(this.iterateArchetypeFields(atypes, (c,f) => f.dtype == 'ref'));
|
2022-01-30 16:48:56 +00:00
|
|
|
// TODO: better error message
|
2022-01-31 15:17:40 +00:00
|
|
|
if (refs.length == 0) throw new ECSError(`cannot find join fields`, action.query);
|
|
|
|
if (refs.length > 1) throw new ECSError(`cannot join multiple fields`, action.query);
|
2022-01-30 15:01:55 +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-26 16:54:57 +00:00
|
|
|
buildSegments() {
|
2022-01-30 15:01:55 +00:00
|
|
|
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-26 16:54:57 +00:00
|
|
|
let segment = v === undefined ? this.bss : this.rodata;
|
2022-01-27 01:12:49 +00:00
|
|
|
let cfname = mksymbol(c, f.name);
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
allocateSegment(segment: Segment, readonly: boolean) {
|
|
|
|
let fields = Object.values(segment.fieldranges);
|
2022-01-28 17:22:59 +00:00
|
|
|
// TODO: fields.sort((a, b) => (a.ehi - a.elo + 1) * getPackedFieldSize(a.field));
|
2022-01-26 16:54:57 +00:00
|
|
|
let f;
|
|
|
|
while (f = fields.pop()) {
|
2022-01-27 01:12:49 +00:00
|
|
|
let name = mksymbol(f.component, f.field.name);
|
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-26 16:54:57 +00:00
|
|
|
let rangelen = (f.ehi - f.elo + 1);
|
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
|
|
|
|
f.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-01-27 18:43:27 +00:00
|
|
|
f.access.push({ symbol, bit: 0, width: 8 }); // TODO
|
2022-01-26 16:54:57 +00:00
|
|
|
if (!readonly) {
|
|
|
|
segment.allocateBytes(symbol, rangelen * bytesperelem); // TODO
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
allocateROData(segment: Segment) {
|
2022-01-30 15:01:55 +00:00
|
|
|
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-01-26 16:54:57 +00:00
|
|
|
let fieldrange = segment.fieldranges[cfname];
|
|
|
|
if (v !== undefined) {
|
2022-01-27 20:39:37 +00:00
|
|
|
let entcount = fieldrange.ehi - fieldrange.elo + 1;
|
2022-01-26 16:54:57 +00:00
|
|
|
// is it a byte array?
|
|
|
|
if (v instanceof Uint8Array) {
|
2022-01-29 15:15:44 +00:00
|
|
|
let datasym = this.dialect.datasymbol(c, f, e.id);
|
|
|
|
let ptrlosym = this.dialect.fieldsymbol(c, f, 0);
|
|
|
|
let ptrhisym = this.dialect.fieldsymbol(c, f, 8);
|
2022-01-27 01:12:49 +00:00
|
|
|
segment.allocateInitData(datasym, v);
|
|
|
|
let loofs = segment.allocateBytes(ptrlosym, entcount);
|
|
|
|
let hiofs = segment.allocateBytes(ptrhisym, entcount);
|
2022-01-27 18:43:27 +00:00
|
|
|
segment.initdata[loofs + e.id - fieldrange.elo] = { symbol: datasym, bitofs: 0 };
|
|
|
|
segment.initdata[hiofs + e.id - fieldrange.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
|
|
|
|
// TODO: what if > 8 bits?
|
|
|
|
// TODO: what if mix of var, const, and init values?
|
|
|
|
if (fieldrange.ehi > fieldrange.elo) {
|
2022-01-29 15:15:44 +00:00
|
|
|
let datasym = this.dialect.fieldsymbol(c, f, 0);
|
2022-01-27 20:39:37 +00:00
|
|
|
let base = segment.allocateBytes(datasym, entcount);
|
|
|
|
segment.initdata[base + e.id - fieldrange.elo] = v;
|
2022-01-30 00:21:38 +00:00
|
|
|
//console.error(cfname, datasym, base, e.id, fieldrange.elo, entcount, v);
|
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-01-27 03:12:11 +00:00
|
|
|
allocateInitData(segment: Segment) {
|
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);
|
2022-01-30 15:01:55 +00:00
|
|
|
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) {
|
|
|
|
let offset = segment.getSegmentByteOffset(c, f.name, 0, e.id);
|
|
|
|
if (offset !== undefined && typeof initvalue === 'number') {
|
|
|
|
initbytes[offset] = initvalue; // TODO: > 8 bits?
|
|
|
|
} else {
|
2022-01-29 03:13:33 +00:00
|
|
|
// TODO: init arrays?
|
2022-01-29 19:07:21 +00:00
|
|
|
throw new ECSError(`cannot initialize ${scfname}: ${offset} ${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;
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
setConstValue(e: Entity, component: ComponentType, fieldName: string, value: DataValue) {
|
2022-01-29 03:13:33 +00:00
|
|
|
let c = this.em.singleComponentWithFieldName([{etype: e.etype, cmatch:[component]}], fieldName, "setConstValue");
|
2022-01-27 01:12:49 +00:00
|
|
|
e.consts[mksymbol(component, fieldName)] = value;
|
2022-01-29 15:15:44 +00:00
|
|
|
if (this.em.symbols[mksymbol(component, fieldName)] == 'init')
|
2022-01-30 16:48:56 +00:00
|
|
|
throw new ECSError(`Can't mix const and init values for a component field`, e);
|
2022-01-29 15:15:44 +00:00
|
|
|
this.em.symbols[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-01-29 15:15:44 +00:00
|
|
|
let c = this.em.singleComponentWithFieldName([{etype: e.etype, cmatch:[component]}], fieldName, "setInitValue");
|
2022-01-27 03:12:11 +00:00
|
|
|
e.inits[mkscopesymbol(this, component, fieldName)] = value;
|
2022-01-29 15:15:44 +00:00
|
|
|
if (this.em.symbols[mksymbol(component, fieldName)] == 'const')
|
2022-01-30 16:48:56 +00:00
|
|
|
throw new ECSError(`Can't mix const and init values for a component field`, e);
|
2022-01-29 15:15:44 +00:00
|
|
|
this.em.symbols[mksymbol(component, fieldName)] = 'init';
|
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 15:17:40 +00:00
|
|
|
let systems = this.em.event2system[event];
|
|
|
|
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 } = {};
|
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-01-27 01:12:49 +00:00
|
|
|
if (sys.tempbytes) this.allocateTempBytes(sys.tempbytes);
|
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-27 01:12:49 +00:00
|
|
|
let code = this.replaceCode(action.text, sys, action);
|
2022-01-28 17:22:59 +00:00
|
|
|
s += this.dialect.comment(`<action ${sys.name}:${event}>`);
|
2022-01-26 16:54:57 +00:00
|
|
|
s += code;
|
2022-01-28 17:22:59 +00:00
|
|
|
s += this.dialect.comment(`</action ${sys.name}:${event}>`);
|
2022-01-27 01:12:49 +00:00
|
|
|
// TODO: check that this happens once?
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
2022-01-27 01:12:49 +00:00
|
|
|
}
|
2022-01-29 03:13:33 +00:00
|
|
|
if (sys.tempbytes) 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-27 18:43:27 +00:00
|
|
|
replaceCode(code: string, sys: System, action: Action): string {
|
2022-01-30 15:01:55 +00:00
|
|
|
const tag_re = /\{\{(.+?)\}\}/g;
|
2022-01-30 16:48:56 +00:00
|
|
|
const label_re = /@(\w+)\b/g;
|
|
|
|
|
2022-01-31 18:11:50 +00:00
|
|
|
let label = `${sys.name}__${action.event}`; // TODO: better label that won't conflict (seq?)
|
2022-01-27 18:43:27 +00:00
|
|
|
let atypes = this.em.archetypesMatching(action.query);
|
2022-01-27 01:12:49 +00:00
|
|
|
let entities = this.entitiesMatching(atypes);
|
2022-01-27 18:43:27 +00:00
|
|
|
// TODO: detect cycles
|
|
|
|
// TODO: "source"?
|
2022-01-28 17:22:59 +00:00
|
|
|
// TODO: what if only 1 item?
|
2022-01-30 16:48:56 +00:00
|
|
|
let props : {[name: string] : string} = {};
|
2022-01-30 00:21:38 +00:00
|
|
|
if (action.select == 'foreach') {
|
2022-01-30 15:01:55 +00:00
|
|
|
code = this.wrapCodeInLoop(code, action, entities);
|
|
|
|
}
|
|
|
|
if (action.select == 'join' && action.join) {
|
|
|
|
let jtypes = this.em.archetypesMatching(action.join);
|
|
|
|
let jentities = this.entitiesMatching(jtypes);
|
2022-01-31 15:17:40 +00:00
|
|
|
if (jentities.length == 0)
|
|
|
|
throw new ECSError(`join query for ${label} doesn't match any entities`, action.join); // TODO
|
2022-01-30 16:48:56 +00:00
|
|
|
let joinfield = this.getJoinField(action, atypes, jtypes);
|
2022-01-30 15:01:55 +00:00
|
|
|
// TODO: what if only 1 item?
|
2022-01-30 16:48:56 +00:00
|
|
|
// TODO: should be able to access fields via Y reg
|
2022-01-30 15:01:55 +00:00
|
|
|
code = this.wrapCodeInLoop(code, action, entities, joinfield);
|
|
|
|
atypes = jtypes;
|
|
|
|
entities = jentities;
|
2022-01-30 16:48:56 +00:00
|
|
|
props['%joinfield'] = this.dialect.fieldsymbol(joinfield.c, joinfield.f, 0);
|
|
|
|
}
|
|
|
|
props['%efullcount'] = entities.length.toString();
|
|
|
|
if (action.limit) {
|
|
|
|
entities = entities.slice(0, action.limit);
|
2022-01-27 01:12:49 +00:00
|
|
|
}
|
2022-01-31 15:17:40 +00:00
|
|
|
if (entities.length == 0)
|
|
|
|
throw new ECSError(`query for ${label} doesn't match any entities`, action.query); // TODO
|
2022-01-30 16:48:56 +00:00
|
|
|
// define properties
|
|
|
|
props['%elo'] = entities[0].id.toString();
|
|
|
|
props['%ehi'] = entities[entities.length - 1].id.toString();
|
|
|
|
props['%ecount'] = entities.length.toString();
|
2022-01-30 00:21:38 +00:00
|
|
|
// replace @labels
|
2022-01-30 16:48:56 +00:00
|
|
|
code = code.replace(label_re, (s: string, a: string) => `${label}__${a}`);
|
2022-01-30 00:21:38 +00:00
|
|
|
// replace {{...}} tags
|
2022-01-30 16:48:56 +00:00
|
|
|
code = code.replace(tag_re, (entire, group: string) => {
|
2022-01-27 01:12:49 +00:00
|
|
|
let cmd = group.charAt(0);
|
|
|
|
let rest = group.substring(1);
|
|
|
|
switch (cmd) {
|
|
|
|
case '!': // emit event
|
|
|
|
return this.generateCodeForEvent(rest);
|
|
|
|
case '.': // auto label
|
2022-01-27 15:54:12 +00:00
|
|
|
case '@': // auto label
|
|
|
|
return `${label}_${rest}`;
|
2022-01-30 16:48:56 +00:00
|
|
|
case '$': // temp byte (TODO: check to make sure not overflowing)
|
2022-01-27 01:12:49 +00:00
|
|
|
return `TEMP+${this.tempOffset}+${rest}`;
|
2022-01-27 20:07:13 +00:00
|
|
|
case '=':
|
|
|
|
// TODO?
|
2022-01-27 01:12:49 +00:00
|
|
|
case '<': // low byte
|
|
|
|
return this.generateCodeForField(sys, action, atypes, entities, rest, 0);
|
|
|
|
case '>': // high byte
|
|
|
|
return this.generateCodeForField(sys, action, atypes, entities, rest, 8);
|
2022-01-31 18:11:50 +00:00
|
|
|
case '^': // resource reference
|
|
|
|
return this.includeResource(rest);
|
2022-01-27 01:12:49 +00:00
|
|
|
default:
|
2022-01-30 16:48:56 +00:00
|
|
|
let value = props[group];
|
|
|
|
if (value) return value;
|
|
|
|
else throw new ECSError(`unrecognized command {{${group}}} in ${entire}`);
|
2022-01-27 01:12:49 +00:00
|
|
|
}
|
|
|
|
});
|
2022-01-30 16:48:56 +00:00
|
|
|
return code;
|
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;
|
|
|
|
}
|
2022-01-30 15:01:55 +00:00
|
|
|
wrapCodeInLoop(code: string, action: Action, ents: Entity[], joinfield?: ComponentFieldPair): string {
|
2022-01-27 01:12:49 +00:00
|
|
|
// TODO: check ents
|
|
|
|
// TODO: check segment bounds
|
2022-01-30 15:01:55 +00:00
|
|
|
// TODO: what if 0 or 1 entitites?
|
2022-01-28 13:20:02 +00:00
|
|
|
let s = this.dialect.ASM_ITERATE_EACH;
|
2022-01-30 15:01:55 +00:00
|
|
|
if (joinfield) s = this.dialect.ASM_ITERATE_JOIN;
|
2022-01-30 16:48:56 +00:00
|
|
|
s = s.replace('{{%code}}', code);
|
2022-01-27 01:12:49 +00:00
|
|
|
return s;
|
|
|
|
}
|
2022-01-27 18:43:27 +00:00
|
|
|
generateCodeForField(sys: System, action: Action,
|
2022-01-27 01:12:49 +00:00
|
|
|
atypes: ArchetypeMatch[], entities: Entity[],
|
|
|
|
fieldName: string, bitofs: number): string {
|
2022-01-29 15:15:44 +00:00
|
|
|
|
|
|
|
var component : ComponentType;
|
|
|
|
var qualified = false;
|
|
|
|
// is qualified field?
|
2022-01-30 15:01:55 +00:00
|
|
|
if (fieldName.indexOf(':') > 0) {
|
|
|
|
let [cname,fname] = fieldName.split(':');
|
2022-01-29 15:15:44 +00:00
|
|
|
component = this.em.getComponentByName(cname);
|
|
|
|
fieldName = fname;
|
|
|
|
qualified = true;
|
2022-01-29 19:07:21 +00:00
|
|
|
if (component == null) throw new ECSError(`no component named "${cname}"`)
|
2022-01-29 15:15:44 +00:00
|
|
|
} else {
|
|
|
|
component = this.em.singleComponentWithFieldName(atypes, fieldName, `${sys.name}:${action.event}`);
|
|
|
|
}
|
2022-01-27 01:12:49 +00:00
|
|
|
// find archetypes
|
2022-01-29 15:15:44 +00:00
|
|
|
let field = component.fields.find(f => f.name == fieldName);
|
2022-01-29 19:07:21 +00:00
|
|
|
if (field == null) throw new ECSError(`no field named "${fieldName}" in component`)
|
2022-01-27 01:12:49 +00:00
|
|
|
// see if all entities have the same constant value
|
2022-01-28 13:20:02 +00:00
|
|
|
let constValues = new Set<DataValue>();
|
2022-01-27 01:12:49 +00:00
|
|
|
for (let e of entities) {
|
2022-01-27 19:48:45 +00:00
|
|
|
let constVal = e.consts[mksymbol(component, fieldName)];
|
|
|
|
constValues.add(constVal); // constVal === undefined is allowed
|
|
|
|
}
|
|
|
|
// is it a constant?
|
|
|
|
if (constValues.size == 1) {
|
2022-01-28 13:20:02 +00:00
|
|
|
let value = constValues.values().next().value as DataValue;
|
|
|
|
// TODO: what about symbols?
|
|
|
|
// TODO: use dialect
|
|
|
|
if (typeof value === 'number') {
|
2022-01-27 19:48:45 +00:00
|
|
|
if (bitofs == 0) return `#<${value}`;
|
|
|
|
if (bitofs == 8) return `#>${value}`;
|
|
|
|
// TODO: bitofs?
|
|
|
|
}
|
2022-01-27 01:12:49 +00:00
|
|
|
}
|
|
|
|
// TODO: offset > 0?
|
2022-01-29 15:15:44 +00:00
|
|
|
// TODO: don't mix const and init data
|
|
|
|
let range = this.bss.getFieldRange(component, fieldName) || this.rodata.getFieldRange(component, fieldName);
|
2022-01-29 19:07:21 +00:00
|
|
|
if (!range) throw new ECSError(`couldn't find field for ${component.name}:${fieldName}, maybe no entities?`); // TODO
|
2022-01-29 15:15:44 +00:00
|
|
|
let eidofs = range.elo - entities[0].id;
|
2022-01-28 13:20:02 +00:00
|
|
|
// TODO: dialect
|
2022-01-29 15:15:44 +00:00
|
|
|
let ident = this.dialect.fieldsymbol(component, field, bitofs);
|
|
|
|
if (qualified) {
|
|
|
|
return this.dialect.absolute(ident);
|
|
|
|
} else if (action.select == 'once') {
|
2022-01-27 20:39:37 +00:00
|
|
|
if (entities.length != 1)
|
2022-01-29 19:07:21 +00:00
|
|
|
throw new ECSError(`can't choose multiple entities for ${fieldName} with select=once`);
|
2022-01-29 15:15:44 +00:00
|
|
|
return this.dialect.absolute(ident);
|
2022-01-27 20:39:37 +00:00
|
|
|
} else {
|
2022-01-29 15:15:44 +00:00
|
|
|
// TODO: right direction?
|
|
|
|
if (eidofs > 0) {
|
|
|
|
ident += '+' + eidofs;
|
|
|
|
} else if (eidofs < 0) {
|
|
|
|
ident += '' + eidofs;
|
|
|
|
}
|
|
|
|
return this.dialect.indexed_x(ident);
|
2022-01-27 20:39:37 +00:00
|
|
|
}
|
2022-01-27 01:12:49 +00:00
|
|
|
}
|
|
|
|
analyzeEntities() {
|
|
|
|
this.buildSegments();
|
|
|
|
this.allocateSegment(this.bss, false);
|
|
|
|
this.allocateSegment(this.rodata, true);
|
|
|
|
this.allocateROData(this.rodata);
|
|
|
|
}
|
|
|
|
generateCode() {
|
|
|
|
this.tempOffset = this.maxTempBytes = 0;
|
2022-01-28 13:20:02 +00:00
|
|
|
this.code.addCodeFragment(this.dialect.TEMPLATE_INIT);
|
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) {
|
2022-01-28 13:20:02 +00:00
|
|
|
file.text(this.dialect.HEADER); // TODO
|
2022-01-27 01:12:49 +00:00
|
|
|
file.segment(`${this.name}_DATA`, 'bss');
|
|
|
|
if (this.maxTempBytes) this.bss.allocateBytes('TEMP', this.maxTempBytes);
|
|
|
|
this.bss.dump(file);
|
|
|
|
file.segment(`${this.name}_CODE`, 'rodata');
|
|
|
|
this.rodata.dump(file);
|
|
|
|
this.code.dump(file);
|
2022-01-28 13:20:02 +00:00
|
|
|
file.text(this.dialect.FOOTER); // TODO
|
2022-01-27 01:12:49 +00:00
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export class EntityManager {
|
2022-01-30 15:01:55 +00:00
|
|
|
archetypes: { [key: string]: EntityArchetype } = {};
|
2022-01-27 18:43:27 +00:00
|
|
|
components: { [name: string]: ComponentType } = {};
|
|
|
|
systems: { [name: string]: System } = {};
|
|
|
|
scopes: { [name: string]: EntityScope } = {};
|
2022-01-29 15:15:44 +00:00
|
|
|
symbols: { [name: string] : 'init' | 'const' } = {};
|
2022-01-31 15:17:40 +00:00
|
|
|
event2system: { [name: string]: System[] } = {};
|
2022-01-26 16:54:57 +00:00
|
|
|
|
2022-01-28 17:22:59 +00:00
|
|
|
constructor(public readonly dialect: Dialect_CA65) {
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
newScope(name: string, parent?: EntityScope) {
|
2022-01-28 13:20:02 +00:00
|
|
|
let scope = new EntityScope(this, this.dialect, name, parent);
|
2022-01-29 19:07:21 +00:00
|
|
|
if (this.scopes[name]) throw new ECSError(`scope ${name} already defined`);
|
2022-01-26 16:54:57 +00:00
|
|
|
this.scopes[name] = scope;
|
|
|
|
return scope;
|
|
|
|
}
|
|
|
|
defineComponent(ctype: ComponentType) {
|
2022-01-29 19:07:21 +00:00
|
|
|
if (this.components[ctype.name]) throw new ECSError(`component ${ctype.name} already defined`);
|
2022-01-26 16:54:57 +00:00
|
|
|
return this.components[ctype.name] = ctype;
|
|
|
|
}
|
|
|
|
defineSystem(system: System) {
|
2022-01-29 19:07:21 +00:00
|
|
|
if (this.systems[system.name]) throw new ECSError(`system ${system.name} already defined`);
|
2022-01-31 15:17:40 +00:00
|
|
|
for (let a of system.actions) {
|
|
|
|
let event = a.event;
|
|
|
|
let list = this.event2system[event];
|
|
|
|
if (list == null) list = this.event2system[event] = [];
|
|
|
|
if (!list.includes(system)) list.push(system);
|
|
|
|
}
|
2022-01-28 17:22:59 +00:00
|
|
|
return this.systems[system.name] = system;
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
2022-01-30 15:01:55 +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) {
|
|
|
|
let cname = c.name;
|
2022-01-27 20:07:13 +00:00
|
|
|
if (q.exclude?.includes(cname)) {
|
|
|
|
return [];
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
// TODO: 0 includes == all entities?
|
|
|
|
if (q.include.length == 0 || q.include.includes(cname)) {
|
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[] = [];
|
2022-01-30 15:01:55 +00:00
|
|
|
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-30 15:01:55 +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
|
|
|
}
|
2022-01-28 17:22:59 +00:00
|
|
|
getComponentByName(name: string): ComponentType {
|
|
|
|
return this.components[name];
|
|
|
|
}
|
2022-01-29 03:13:33 +00:00
|
|
|
singleComponentWithFieldName(atypes: ArchetypeMatch[], fieldName: string, where: string) {
|
|
|
|
let components = this.componentsWithFieldName(atypes, fieldName);
|
|
|
|
if (components.length == 0) {
|
2022-01-29 19:07:21 +00:00
|
|
|
throw new ECSError(`cannot find component with field "${fieldName}" in ${where}`);
|
2022-01-29 03:13:33 +00:00
|
|
|
}
|
|
|
|
if (components.length > 1) {
|
2022-01-29 19:07:21 +00:00
|
|
|
throw new ECSError(`ambiguous field name "${fieldName}" in ${where}`);
|
2022-01-29 03:13:33 +00:00
|
|
|
}
|
|
|
|
return components[0];
|
|
|
|
}
|
2022-01-28 17:22:59 +00:00
|
|
|
toJSON() {
|
|
|
|
return JSON.stringify({
|
2022-01-28 00:02:37 +00:00
|
|
|
components: this.components,
|
2022-01-28 17:22:59 +00:00
|
|
|
systems: this.systems
|
2022-01-28 00:02:37 +00:00
|
|
|
})
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|