2022-01-26 16:54:57 +00:00
|
|
|
|
2022-01-29 19:07:21 +00:00
|
|
|
import { SourceLocated, SourceLocation } from "../workertypes";
|
2022-02-09 04:21:23 +00:00
|
|
|
import { Bin, Packer } from "./binpack";
|
2022-01-29 19:07:21 +00:00
|
|
|
|
|
|
|
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 };
|
2022-02-15 17:25:00 +00:00
|
|
|
// TODO: need scope scoping?
|
2022-01-27 18:43:27 +00:00
|
|
|
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-22 16:35:41 +00:00
|
|
|
include: ComponentType[];
|
2022-02-02 17:34:55 +00:00
|
|
|
exclude?: ComponentType[];
|
2022-02-20 00:20:03 +00:00
|
|
|
entities?: Entity[];
|
2022-02-03 20:46:10 +00:00
|
|
|
limit?: number;
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
|
|
|
|
2022-02-08 22:30:11 +00:00
|
|
|
export class SystemStats {
|
|
|
|
tempstartseq: number | undefined;
|
|
|
|
tempendseq: number | undefined;
|
|
|
|
}
|
|
|
|
|
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-10 21:51:03 +00:00
|
|
|
export interface SystemInstanceParameters {
|
2022-02-17 15:48:50 +00:00
|
|
|
query?: Query;
|
2022-02-10 21:51:03 +00:00
|
|
|
refEntity?: Entity;
|
|
|
|
refField?: ComponentFieldPair;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface SystemInstance extends SourceLocated {
|
|
|
|
system: System;
|
|
|
|
params: SystemInstanceParameters;
|
|
|
|
id: number;
|
|
|
|
}
|
|
|
|
|
2022-02-19 21:39:44 +00:00
|
|
|
export const SELECT_TYPE = ['once', 'foreach', 'join', 'with', 'if', 'select', 'unroll'] as const;
|
2022-02-07 20:58:03 +00:00
|
|
|
|
|
|
|
export type SelectType = typeof SELECT_TYPE[number];
|
2022-02-03 13:44:47 +00:00
|
|
|
|
2022-02-11 04:01:05 +00:00
|
|
|
export interface ActionContext {
|
2022-02-10 15:21:24 +00:00
|
|
|
system: System
|
|
|
|
scope: EntityScope | null
|
|
|
|
}
|
|
|
|
|
|
|
|
export class ActionNode implements SourceLocated {
|
|
|
|
constructor(
|
2022-02-11 04:01:05 +00:00
|
|
|
public readonly owner: ActionContext,
|
2022-02-10 15:21:24 +00:00
|
|
|
public readonly $loc: SourceLocation
|
|
|
|
) { }
|
2022-02-11 04:01:05 +00:00
|
|
|
|
|
|
|
children?: ActionNode[];
|
2022-02-10 15:21:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export class CodeLiteralNode extends ActionNode {
|
|
|
|
constructor(
|
2022-02-11 04:01:05 +00:00
|
|
|
owner: ActionContext,
|
2022-02-10 15:21:24 +00:00
|
|
|
$loc: SourceLocation,
|
|
|
|
public readonly text: string
|
|
|
|
) {
|
|
|
|
super(owner, $loc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class CodePlaceholderNode extends ActionNode {
|
|
|
|
constructor(
|
2022-02-11 04:01:05 +00:00
|
|
|
owner: ActionContext,
|
2022-02-10 15:21:24 +00:00
|
|
|
$loc: SourceLocation,
|
|
|
|
public readonly args: string[]
|
|
|
|
) {
|
|
|
|
super(owner, $loc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-11 04:01:05 +00:00
|
|
|
class QueryNode extends ActionNode {
|
|
|
|
}
|
|
|
|
|
|
|
|
class WrapperNode extends ActionNode {
|
|
|
|
}
|
|
|
|
|
|
|
|
class LoopNode extends ActionNode {
|
|
|
|
}
|
|
|
|
|
2022-02-10 15:21:24 +00:00
|
|
|
|
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-02-17 18:52:13 +00:00
|
|
|
critical?: boolean;
|
2022-02-19 10:06:25 +00:00
|
|
|
fitbytes?: number;
|
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-19 21:39:44 +00:00
|
|
|
select: 'foreach' | 'join' | 'with' | 'if' | 'select' | 'unroll'
|
2022-02-03 13:44:47 +00:00
|
|
|
query: Query
|
2022-02-08 12:18:28 +00:00
|
|
|
direction?: 'asc' | 'desc'
|
2022-02-03 13:44:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface ActionWithJoin extends ActionWithQuery {
|
|
|
|
select: 'join'
|
|
|
|
join?: Query
|
|
|
|
}
|
|
|
|
|
|
|
|
export type Action = ActionWithQuery | ActionWithJoin | ActionOnce;
|
2022-01-28 17:22:59 +00:00
|
|
|
|
2022-02-09 17:45:40 +00:00
|
|
|
export type DataValue = number | boolean | Uint8Array | Uint16Array | Uint32Array;
|
2022-01-26 16:54:57 +00:00
|
|
|
|
2022-02-15 17:25:00 +00:00
|
|
|
export type DataField = { name: string; $loc: SourceLocation } & DataType;
|
2022-01-26 16:54:57 +00:00
|
|
|
|
|
|
|
export type DataType = IntType | ArrayType | RefType;
|
|
|
|
|
|
|
|
export interface IntType {
|
|
|
|
dtype: 'int'
|
|
|
|
lo: number
|
|
|
|
hi: number
|
2022-02-20 16:15:16 +00:00
|
|
|
defvalue?: number
|
2022-02-22 18:33:57 +00:00
|
|
|
enums?: { [name: string] : number }
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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-02-10 21:51:03 +00:00
|
|
|
export interface ComponentFieldPair {
|
2022-01-29 03:13:33 +00:00
|
|
|
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-02-08 12:18:28 +00:00
|
|
|
ASM_ITERATE_EACH_ASC = `
|
2022-01-28 13:20:02 +00:00
|
|
|
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-02-09 17:45:40 +00:00
|
|
|
jne @__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
|
|
|
|
2022-02-08 12:18:28 +00:00
|
|
|
ASM_ITERATE_EACH_DESC = `
|
|
|
|
ldx #{{%ecount}}-1
|
|
|
|
@__each:
|
|
|
|
{{%code}}
|
|
|
|
dex
|
2022-02-09 17:45:40 +00:00
|
|
|
jpl @__each
|
2022-02-08 12:18:28 +00:00
|
|
|
@__exit:
|
|
|
|
`;
|
|
|
|
|
|
|
|
ASM_ITERATE_JOIN_ASC = `
|
2022-01-30 15:01:55 +00:00
|
|
|
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-02-09 17:45:40 +00:00
|
|
|
jne @__each
|
2022-01-30 16:48:56 +00:00
|
|
|
@__exit:
|
2022-02-08 12:18:28 +00:00
|
|
|
`;
|
|
|
|
|
|
|
|
ASM_ITERATE_JOIN_DESC = `
|
|
|
|
ldy #{{%ecount}}-1
|
|
|
|
@__each:
|
|
|
|
ldx {{%joinfield}},y
|
|
|
|
{{%code}}
|
|
|
|
dey
|
2022-02-09 17:45:40 +00:00
|
|
|
jpl @__each
|
2022-02-08 12:18:28 +00:00
|
|
|
@__exit:
|
2022-01-30 15:01:55 +00:00
|
|
|
`;
|
|
|
|
|
2022-02-06 02:43:45 +00:00
|
|
|
ASM_FILTER_RANGE_LO_X = `
|
2022-02-02 17:34:55 +00:00
|
|
|
cpx #{{%xofs}}
|
2022-02-09 17:45:40 +00:00
|
|
|
jcc @__skipxlo
|
2022-02-03 14:51:28 +00:00
|
|
|
{{%code}}
|
|
|
|
@__skipxlo:
|
|
|
|
`
|
|
|
|
|
2022-02-06 02:43:45 +00:00
|
|
|
ASM_FILTER_RANGE_HI_X = `
|
2022-02-02 17:34:55 +00:00
|
|
|
cpx #{{%xofs}}+{{%ecount}}
|
2022-02-09 17:45:40 +00:00
|
|
|
jcs @__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-10 21:51:03 +00:00
|
|
|
ASM_LOOKUP_REF_X = `
|
|
|
|
ldx {{%reffield}}
|
|
|
|
{{%code}}
|
|
|
|
`
|
|
|
|
|
2022-02-06 02:43:45 +00:00
|
|
|
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
|
|
|
`
|
2022-01-28 17:22:59 +00:00
|
|
|
|
|
|
|
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-01-28 17:22:59 +00:00
|
|
|
}
|
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-28 17:22:59 +00:00
|
|
|
}
|
2022-01-29 15:15:44 +00:00
|
|
|
fieldsymbol(component: ComponentType, field: DataField, bitofs: number) {
|
|
|
|
return `${component.name}_${field.name}_b${bitofs}`;
|
|
|
|
}
|
2022-02-09 17:45:40 +00:00
|
|
|
datasymbol(component: ComponentType, field: DataField, eid: number, bitofs: number) {
|
|
|
|
return `${component.name}_${field.name}_e${eid}_b${bitofs}`;
|
2022-01-29 15:15:44 +00:00
|
|
|
}
|
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) {
|
2022-02-20 13:37:51 +00:00
|
|
|
return `.endscope\n${this.scopeSymbol(name)} = ${name}::__Start`
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
2022-02-20 13:37:51 +00:00
|
|
|
scopeSymbol(name: string) {
|
|
|
|
return `${name}__Start`;
|
|
|
|
}
|
2022-02-19 10:06:25 +00:00
|
|
|
align(value: number) {
|
|
|
|
return `.align ${value}`;
|
|
|
|
}
|
|
|
|
alignSegmentStart() {
|
|
|
|
return this.label('__ALIGNORIGIN');
|
|
|
|
}
|
2022-02-19 17:51:48 +00:00
|
|
|
warningIfPageCrossed(startlabel: string) {
|
|
|
|
return `
|
|
|
|
.assert >(${startlabel}) = >(*), error, "${startlabel} crosses a page boundary!"`
|
|
|
|
}
|
2022-02-19 10:06:25 +00:00
|
|
|
warningIfMoreThan(bytes: number, startlabel: string) {
|
|
|
|
return `
|
2022-02-19 22:19:16 +00:00
|
|
|
.assert (* - ${startlabel}) <= ${bytes}, error, .sprintf("${startlabel} does not fit in ${bytes} bytes, it took %d!", (* - ${startlabel}))`
|
2022-02-19 10:06:25 +00:00
|
|
|
}
|
|
|
|
alignIfLessThan(bytes: number) {
|
|
|
|
return `
|
|
|
|
.if <(* - __ALIGNORIGIN) > 256-${bytes}
|
|
|
|
.align $100
|
|
|
|
.endif`
|
|
|
|
}
|
2022-02-17 18:52:13 +00:00
|
|
|
segment(segtype: 'rodata' | 'bss' | 'code') {
|
2022-01-27 01:12:49 +00:00
|
|
|
if (segtype == 'bss') {
|
2022-02-17 18:52:13 +00:00
|
|
|
return `.zeropage`;
|
|
|
|
} else if (segtype == 'rodata') {
|
2022-02-22 16:35:41 +00:00
|
|
|
return '.rodata';
|
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}`
|
2022-02-22 16:35:41 +00:00
|
|
|
else return `.byte ((${b.symbol} >> ${b.bitofs})&255)`
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
|
|
|
}
|
2022-02-10 21:51:03 +00:00
|
|
|
tempLabel(inst: SystemInstance) {
|
2022-02-22 16:35:41 +00:00
|
|
|
return `${inst.system.name}__${inst.id}__tmp`;
|
2022-02-09 13:39:41 +00:00
|
|
|
}
|
|
|
|
equate(symbol: string, value: string): string {
|
|
|
|
return `${symbol} = ${value}`;
|
|
|
|
}
|
2022-02-17 18:52:13 +00:00
|
|
|
call(symbol: string) {
|
|
|
|
return ` jsr ${symbol}`;
|
|
|
|
}
|
2022-02-20 13:37:51 +00:00
|
|
|
jump(symbol: string) {
|
|
|
|
return ` jmp ${symbol}`;
|
|
|
|
}
|
2022-02-17 18:52:13 +00:00
|
|
|
return() {
|
|
|
|
return ' rts';
|
|
|
|
}
|
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 } = {};
|
2022-02-09 13:39:41 +00:00
|
|
|
equates: { [sym: string]: string } = {};
|
2022-01-27 18:43:27 +00:00
|
|
|
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;
|
2022-02-17 15:58:13 +00:00
|
|
|
this.declareSymbol(name, ofs);
|
2022-01-30 00:21:38 +00:00
|
|
|
this.size += bytes;
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
return ofs;
|
|
|
|
}
|
2022-02-17 15:58:13 +00:00
|
|
|
declareSymbol(name: string, ofs: number) {
|
|
|
|
this.symbols[name] = ofs;
|
|
|
|
if (!this.ofs2sym.has(ofs))
|
|
|
|
this.ofs2sym.set(ofs, []);
|
|
|
|
this.ofs2sym.get(ofs)?.push(name);
|
|
|
|
}
|
2022-02-22 18:33:57 +00:00
|
|
|
// TODO: ordering should not matter, but it does
|
2022-02-22 16:35:41 +00:00
|
|
|
findExistingInitData(bytes: Uint8Array) {
|
|
|
|
for (let i=0; i<this.size - bytes.length; i++) {
|
|
|
|
for (var j=0; j<bytes.length; j++) {
|
|
|
|
if (this.initdata[i+j] !== bytes[j]) break;
|
|
|
|
}
|
|
|
|
if (j == bytes.length) return i;
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
allocateInitData(name: string, bytes: Uint8Array) {
|
2022-02-22 16:35:41 +00:00
|
|
|
let ofs = this.findExistingInitData(bytes);
|
|
|
|
if (ofs >= 0) {
|
|
|
|
this.declareSymbol(name, ofs);
|
|
|
|
} else {
|
|
|
|
ofs = this.allocateBytes(name, bytes.length);
|
|
|
|
for (let i = 0; i < bytes.length; i++) {
|
|
|
|
this.initdata[ofs + i] = bytes[i];
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
|
|
|
}
|
2022-02-04 17:45:14 +00:00
|
|
|
dump(file: SourceFileExport, dialect: Dialect_CA65) {
|
2022-02-09 13:39:41 +00:00
|
|
|
// TODO: fewer lines
|
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-02-09 13:39:41 +00:00
|
|
|
for (let [symbol,value] of Object.entries(this.equates)) {
|
|
|
|
file.line(dialect.equate(symbol, value));
|
|
|
|
}
|
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) {
|
2022-02-15 17:25:00 +00:00
|
|
|
if (entityID < range.elo) throw new ECSError(`entity ID ${entityID} too low for ${access.symbol}`);
|
|
|
|
if (entityID > range.ehi) throw new ECSError(`entity ID ${entityID} too high for ${access.symbol}`);
|
2022-02-01 03:16:40 +00:00
|
|
|
let ofs = this.symbols[access.symbol];
|
|
|
|
if (ofs !== undefined) {
|
|
|
|
return ofs + entityID - range.elo;
|
|
|
|
}
|
|
|
|
throw new ECSError(`cannot find field access for ${access.symbol}`);
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
2022-02-06 02:43:45 +00:00
|
|
|
class UninitDataSegment extends DataSegment {
|
|
|
|
}
|
|
|
|
|
|
|
|
class ConstDataSegment extends DataSegment {
|
|
|
|
}
|
|
|
|
|
2022-02-22 18:33:57 +00:00
|
|
|
// TODO: none of this makes sense
|
2022-01-26 16:54:57 +00:00
|
|
|
function getFieldBits(f: IntType) {
|
2022-02-22 18:33:57 +00:00
|
|
|
//let n = Math.abs(f.lo) + f.hi + 1;
|
2022-01-26 16:54:57 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-02-03 20:46:10 +00:00
|
|
|
class EntitySet {
|
2022-02-06 17:27:08 +00:00
|
|
|
atypes: EntityArchetype[];
|
2022-02-02 17:34:55 +00:00
|
|
|
entities: Entity[];
|
|
|
|
scope;
|
|
|
|
|
2022-02-06 17:27:08 +00:00
|
|
|
constructor(scope: EntityScope, query?: Query, a?: EntityArchetype[], e?: Entity[]) {
|
2022-02-02 17:34:55 +00:00
|
|
|
this.scope = scope;
|
|
|
|
if (query) {
|
2022-02-20 00:20:03 +00:00
|
|
|
if (query.entities) {
|
|
|
|
this.entities = query.entities.slice(0);
|
|
|
|
this.atypes = [];
|
|
|
|
for (let e of this.entities)
|
|
|
|
if (!this.atypes.includes(e.etype))
|
|
|
|
this.atypes.push(e.etype);
|
|
|
|
} else {
|
|
|
|
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-03 20:46:10 +00:00
|
|
|
}
|
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);
|
|
|
|
}
|
2022-02-03 20:46:10 +00:00
|
|
|
intersection(qr: EntitySet) {
|
2022-02-02 17:34:55 +00:00
|
|
|
let ents = this.entities.filter(e => qr.entities.includes(e));
|
2022-02-06 17:27:08 +00:00
|
|
|
let atypes = this.atypes.filter(a1 => qr.atypes.find(a2 => a2 == a1));
|
2022-02-03 20:46:10 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-15 17:25:00 +00:00
|
|
|
class IndexRegister {
|
|
|
|
lo: number | null;
|
|
|
|
hi: number | null;
|
|
|
|
elo: number;
|
|
|
|
ehi: number;
|
|
|
|
eset: EntitySet | undefined;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
public readonly scope: EntityScope,
|
|
|
|
eset?: EntitySet
|
|
|
|
) {
|
|
|
|
this.elo = 0;
|
|
|
|
this.ehi = scope.entities.length - 1;
|
|
|
|
this.lo = null;
|
|
|
|
this.hi = null;
|
|
|
|
if (eset) { this.narrowInPlace(eset); }
|
|
|
|
}
|
|
|
|
entityCount() {
|
|
|
|
return this.ehi - this.elo + 1;
|
|
|
|
}
|
2022-02-16 17:54:44 +00:00
|
|
|
clone() {
|
|
|
|
return Object.assign(new IndexRegister(this.scope), this);
|
|
|
|
}
|
|
|
|
narrow(eset: EntitySet, action?: SourceLocated) {
|
|
|
|
let i = this.clone();
|
|
|
|
return i.narrowInPlace(eset, action) ? i : null;
|
|
|
|
}
|
|
|
|
narrowInPlace(eset: EntitySet, action?: SourceLocated): boolean {
|
|
|
|
if (this.scope != eset.scope) throw new ECSError(`scope mismatch`, action);
|
|
|
|
if (!eset.isContiguous()) throw new ECSError(`entities are not contiguous`, action);
|
|
|
|
if (this.eset) {
|
|
|
|
this.eset = this.eset.intersection(eset);
|
|
|
|
} else {
|
|
|
|
this.eset = eset;
|
|
|
|
}
|
|
|
|
if (this.eset.entities.length == 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
let newelo = this.eset.entities[0].id;
|
|
|
|
let newehi = this.eset.entities[this.eset.entities.length - 1].id;
|
|
|
|
if (this.lo === null || this.hi === null) {
|
2022-02-15 17:25:00 +00:00
|
|
|
this.lo = 0;
|
2022-02-16 17:54:44 +00:00
|
|
|
this.hi = newehi - newelo;
|
2022-02-16 20:50:48 +00:00
|
|
|
this.elo = newelo;
|
|
|
|
this.ehi = newehi;
|
2022-02-15 17:25:00 +00:00
|
|
|
} else {
|
2022-02-16 20:50:48 +00:00
|
|
|
//if (action) console.log((action as any).event, this.elo, '-', this.ehi, '->', newelo, '..', newehi);
|
|
|
|
this.lo += newelo - this.elo;
|
|
|
|
this.hi += newehi - this.ehi;
|
2022-02-15 17:25:00 +00:00
|
|
|
}
|
2022-02-16 17:54:44 +00:00
|
|
|
return true;
|
2022-02-15 17:25:00 +00:00
|
|
|
}
|
2022-02-16 20:50:48 +00:00
|
|
|
// TODO: removegi
|
|
|
|
offset() {
|
|
|
|
return this.lo || 0;
|
|
|
|
}
|
2022-02-15 17:25:00 +00:00
|
|
|
}
|
|
|
|
|
2022-02-03 20:46:10 +00:00
|
|
|
// todo: generalize
|
|
|
|
class ActionCPUState {
|
2022-02-15 17:25:00 +00:00
|
|
|
xreg: IndexRegister | null = null;
|
|
|
|
yreg: IndexRegister | null = null;
|
2022-02-02 17:34:55 +00:00
|
|
|
}
|
|
|
|
|
2022-02-01 14:16:53 +00:00
|
|
|
class ActionEval {
|
2022-02-08 12:18:28 +00:00
|
|
|
em : EntityManager;
|
|
|
|
dialect : Dialect_CA65;
|
2022-02-03 20:46:10 +00:00
|
|
|
qr: EntitySet;
|
|
|
|
jr: EntitySet | undefined;
|
|
|
|
oldState : ActionCPUState;
|
|
|
|
entities : Entity[];
|
2022-02-04 04:38:35 +00:00
|
|
|
tmplabel = '';
|
2022-02-17 18:52:13 +00:00
|
|
|
label : string;
|
2022-02-23 14:43:19 +00:00
|
|
|
seq : number;
|
2022-02-19 10:06:25 +00:00
|
|
|
//used = new Set<string>(); // TODO
|
2022-02-02 17:34:55 +00:00
|
|
|
|
2022-02-01 14:16:53 +00:00
|
|
|
constructor(
|
|
|
|
readonly scope: EntityScope,
|
2022-02-10 21:51:03 +00:00
|
|
|
readonly instance: SystemInstance,
|
2022-02-15 17:25:00 +00:00
|
|
|
readonly action: Action,
|
|
|
|
readonly eventargs: string[])
|
2022-02-03 16:44:29 +00:00
|
|
|
{
|
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-10 21:51:03 +00:00
|
|
|
this.tmplabel = this.dialect.tempLabel(this.instance);
|
2022-02-03 13:44:47 +00:00
|
|
|
let q = (action as ActionWithQuery).query;
|
2022-02-11 04:01:05 +00:00
|
|
|
if (q)
|
|
|
|
this.qr = new EntitySet(scope, q);
|
|
|
|
else
|
|
|
|
this.qr = new EntitySet(scope, undefined, [], []);
|
2022-02-10 21:51:03 +00:00
|
|
|
// TODO? error if none?
|
|
|
|
if (instance.params.refEntity && instance.params.refField) {
|
|
|
|
let rf = instance.params.refField;
|
|
|
|
if (rf.f.dtype == 'ref') {
|
|
|
|
let rq = rf.f.query;
|
|
|
|
this.qr = this.qr.intersection(new EntitySet(scope, rq));
|
|
|
|
//console.log('with', instance.params, rq, this.qr);
|
|
|
|
}
|
2022-02-17 15:48:50 +00:00
|
|
|
} else if (instance.params.query) {
|
|
|
|
this.qr = this.qr.intersection(new EntitySet(scope, instance.params.query));
|
2022-02-10 21:51:03 +00:00
|
|
|
}
|
2022-02-03 20:46:10 +00:00
|
|
|
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-23 14:43:19 +00:00
|
|
|
this.seq = this.em.seq++;
|
|
|
|
this.label = `${this.instance.system.name}__${action.event}__${this.seq}`;
|
2022-02-02 17:34:55 +00:00
|
|
|
}
|
|
|
|
begin() {
|
2022-02-19 08:37:19 +00:00
|
|
|
let state = this.scope.state = Object.assign(new ActionCPUState(), this.scope.state);
|
2022-02-02 17:34:55 +00:00
|
|
|
// TODO: generalize to other cpus/langs
|
|
|
|
switch (this.action.select) {
|
2022-02-19 08:37:19 +00:00
|
|
|
case 'once':
|
|
|
|
state.xreg = state.yreg = null;
|
|
|
|
break;
|
2022-02-02 17:34:55 +00:00
|
|
|
case 'foreach':
|
2022-02-19 21:39:44 +00:00
|
|
|
case 'unroll':
|
2022-02-16 17:54:44 +00:00
|
|
|
if (state.xreg && state.yreg) throw new ECSError('no more index registers', this.action);
|
|
|
|
if (state.xreg) state.yreg = new IndexRegister(this.scope, this.qr);
|
|
|
|
else state.xreg = new IndexRegister(this.scope, this.qr);
|
2022-02-02 17:34:55 +00:00
|
|
|
break;
|
|
|
|
case 'join':
|
2022-02-20 13:37:51 +00:00
|
|
|
// TODO: Joins don't work in superman (arrays offset?)
|
|
|
|
// ignore the join query, use the ref
|
2022-02-16 17:54:44 +00:00
|
|
|
if (state.xreg || state.yreg) throw new ECSError('no free index registers for join', this.action);
|
2022-02-03 20:46:10 +00:00
|
|
|
this.jr = new EntitySet(this.scope, (this.action as ActionWithJoin).join);
|
2022-02-16 17:54:44 +00:00
|
|
|
state.xreg = new IndexRegister(this.scope, this.jr);
|
|
|
|
state.yreg = new IndexRegister(this.scope, this.qr);
|
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':
|
2022-02-03 20:46:10 +00:00
|
|
|
// TODO: what if not in X because 1 element?
|
2022-02-16 17:54:44 +00:00
|
|
|
if (state.xreg && state.xreg.eset) {
|
|
|
|
state.xreg = state.xreg.narrow(this.qr, this.action);
|
|
|
|
if (state.xreg == null || state.xreg.eset?.entities == null) {
|
|
|
|
if (this.action.select == 'if') {
|
|
|
|
this.entities = [];
|
|
|
|
break; // "if" failed
|
|
|
|
} else {
|
|
|
|
throw new ECSError(`no entities in statement`, this.action);
|
|
|
|
}
|
2022-02-03 15:24:00 +00:00
|
|
|
} else {
|
2022-02-16 17:54:44 +00:00
|
|
|
this.entities = state.xreg.eset.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-10 21:51:03 +00:00
|
|
|
if (this.instance.params.refEntity && this.instance.params.refField) {
|
2022-02-16 17:54:44 +00:00
|
|
|
if (state.xreg)
|
|
|
|
state.xreg.eset = this.qr;
|
|
|
|
else
|
|
|
|
state.xreg = new IndexRegister(this.scope, this.qr);
|
2022-02-10 21:51:03 +00:00
|
|
|
// ???
|
|
|
|
} else if (this.qr.entities.length != 1)
|
|
|
|
throw new ECSError(`${this.instance.system.name} query outside of loop must match exactly one entity`, this.action); //TODO
|
2022-02-03 13:44:47 +00:00
|
|
|
}
|
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 {
|
2022-02-19 21:39:44 +00:00
|
|
|
const allowEmpty = ['if','foreach','unroll','join'];
|
2022-02-04 01:38:35 +00:00
|
|
|
if (this.entities.length == 0 && allowEmpty.includes(this.action.select))
|
2022-02-03 20:46:10 +00:00
|
|
|
return '';
|
|
|
|
|
2022-02-19 08:37:19 +00:00
|
|
|
let { code, props } = this.getCodeAndProps(this.action);
|
2022-02-17 18:52:13 +00:00
|
|
|
// replace @labels
|
|
|
|
code = this.replaceLabels(code, this.label);
|
|
|
|
// replace {{...}} tags
|
|
|
|
// TODO: use nodes instead
|
2022-02-19 08:37:19 +00:00
|
|
|
code = this.replaceTags(code, this.action, props);
|
2022-02-17 18:52:13 +00:00
|
|
|
return code;
|
|
|
|
}
|
|
|
|
private getCodeAndProps(action: Action) {
|
2022-02-01 14:16:53 +00:00
|
|
|
let code = action.text;
|
|
|
|
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) {
|
2022-02-17 18:52:13 +00:00
|
|
|
//let jentities = this.jr.entities;
|
|
|
|
// TODO? if (jentities.length == 0) return '';
|
2022-02-04 01:38:35 +00:00
|
|
|
// 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
|
2022-02-03 20:46:10 +00:00
|
|
|
let fullEntityCount = this.qr.entities.length; //entities.length.toString();
|
|
|
|
let entities = this.entities;
|
2022-02-09 17:45:40 +00:00
|
|
|
// TODO: let loopreduce = !loopents || entities.length < loopents.length;
|
|
|
|
//console.log(action.event, entities.length, loopents.length);
|
2022-02-03 13:44:47 +00:00
|
|
|
// filter entities from loop?
|
2022-02-09 17:45:40 +00:00
|
|
|
// TODO: when to ignore if entities.length == 1 and not in for loop?
|
|
|
|
if (action.select == 'with') {
|
2022-02-10 21:51:03 +00:00
|
|
|
// TODO? when to load x?
|
|
|
|
if (this.instance.params.refEntity && this.instance.params.refField) {
|
|
|
|
let re = this.instance.params.refEntity;
|
|
|
|
let rf = this.instance.params.refField;
|
|
|
|
code = this.wrapCodeInRefLookup(code);
|
|
|
|
// TODO: only fetches 1st entity in list, need offset
|
2022-02-15 17:25:00 +00:00
|
|
|
let range = this.scope.getFieldRange(rf.c, rf.f.name);
|
2022-02-10 21:51:03 +00:00
|
|
|
let eidofs = re.id - range.elo;
|
|
|
|
props['%reffield'] = `${this.dialect.fieldsymbol(rf.c, rf.f, 0)}+${eidofs}`;
|
|
|
|
} else {
|
|
|
|
code = this.wrapCodeInFilter(code);
|
|
|
|
}
|
2022-02-03 13:44:47 +00:00
|
|
|
}
|
2022-02-09 17:45:40 +00:00
|
|
|
if (action.select == 'if') {
|
2022-02-03 15:24:00 +00:00
|
|
|
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-19 21:39:44 +00:00
|
|
|
if (action.select == 'unroll' && entities.length > 1) {
|
|
|
|
throw new ECSError('unroll is not yet implemented');
|
|
|
|
}
|
2022-02-03 13:44:47 +00:00
|
|
|
// define properties
|
2022-02-20 00:20:03 +00:00
|
|
|
if (entities.length) {
|
|
|
|
props['%elo'] = entities[0].id.toString();
|
|
|
|
props['%ehi'] = entities[entities.length - 1].id.toString();
|
|
|
|
}
|
2022-02-03 13:44:47 +00:00
|
|
|
props['%ecount'] = entities.length.toString();
|
2022-02-03 20:46:10 +00:00
|
|
|
props['%efullcount'] = fullEntityCount.toString();
|
2022-02-16 17:54:44 +00:00
|
|
|
// TODO
|
2022-02-16 20:50:48 +00:00
|
|
|
props['%xofs'] = (this.scope.state.xreg?.offset() || 0).toString();
|
|
|
|
props['%yofs'] = (this.scope.state.yreg?.offset() || 0).toString();
|
2022-02-01 14:16:53 +00:00
|
|
|
}
|
2022-02-17 18:52:13 +00:00
|
|
|
return { code, props };
|
|
|
|
}
|
|
|
|
private replaceTags(code: string, action: Action, props: { [name: string]: string; }) {
|
|
|
|
const tag_re = /\{\{(.+?)\}\}/g;
|
2022-02-01 14:16:53 +00:00
|
|
|
code = code.replace(tag_re, (entire, group: string) => {
|
2022-02-01 16:38:54 +00:00
|
|
|
let toks = group.split(/\s+/);
|
2022-02-17 18:52:13 +00:00
|
|
|
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-15 17:25:00 +00:00
|
|
|
let arg0 = toks[0].substring(1).trim();
|
|
|
|
let args = [arg0].concat(toks.slice(1));
|
2022-02-01 14:16:53 +00:00
|
|
|
switch (cmd) {
|
2022-02-15 17:25:00 +00:00
|
|
|
case '!': return this.__emit(args);
|
|
|
|
case '$': return this.__local(args);
|
|
|
|
case '^': return this.__use(args);
|
|
|
|
case '#': return this.__arg(args);
|
2022-02-19 23:52:24 +00:00
|
|
|
case '&': return this.__eid(args);
|
2022-02-15 17:25:00 +00:00
|
|
|
case '<': return this.__get([arg0, '0']);
|
|
|
|
case '>': return this.__get([arg0, '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-17 18:52:13 +00:00
|
|
|
if (value)
|
|
|
|
return value;
|
2022-02-01 16:38:54 +00:00
|
|
|
let fn = (this as any)['__' + toks[0]];
|
2022-02-17 18:52:13 +00:00
|
|
|
if (fn)
|
|
|
|
return fn.bind(this)(toks.slice(1));
|
2022-02-01 16:38:54 +00:00
|
|
|
throw new ECSError(`unrecognized command {{${toks[0]}}}`, action);
|
2022-02-01 14:16:53 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
return code;
|
|
|
|
}
|
2022-02-17 18:52:13 +00:00
|
|
|
private replaceLabels(code: string, label: string) {
|
|
|
|
const label_re = /@(\w+)\b/g;
|
|
|
|
code = code.replace(label_re, (s: string, a: string) => `${label}__${a}`);
|
|
|
|
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');
|
2022-02-03 20:46:10 +00:00
|
|
|
return this.generateCodeForField(fieldName, bitofs, canwrite);
|
2022-02-01 16:38:54 +00:00
|
|
|
}
|
2022-02-09 17:45:40 +00:00
|
|
|
parseFieldArgs(args: string[]) {
|
2022-02-05 01:48:08 +00:00
|
|
|
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);
|
2022-02-09 17:45:40 +00:00
|
|
|
return { component, field, bitofs };
|
|
|
|
}
|
|
|
|
__base(args: string[]) {
|
|
|
|
let { component, field, bitofs } = this.parseFieldArgs(args);
|
2022-02-05 01:48:08 +00:00
|
|
|
return this.dialect.fieldsymbol(component, field, bitofs);
|
|
|
|
}
|
2022-02-09 17:45:40 +00:00
|
|
|
__data(args: string[]) {
|
|
|
|
let { component, field, bitofs } = this.parseFieldArgs(args);
|
2022-02-17 15:48:50 +00:00
|
|
|
if (this.qr.entities.length != 1) throw new ECSError(`data operates on exactly one entity`, this.action); // TODO?
|
2022-02-09 17:45:40 +00:00
|
|
|
let eid = this.qr.entities[0].id; // TODO?
|
|
|
|
return this.dialect.datasymbol(component, field, eid, bitofs);
|
|
|
|
}
|
2022-02-17 15:48:50 +00:00
|
|
|
__const(args: string[]) {
|
|
|
|
let { component, field, bitofs } = this.parseFieldArgs(args);
|
|
|
|
if (this.qr.entities.length != 1) throw new ECSError(`const operates on exactly one entity`, this.action); // TODO?
|
|
|
|
let constVal = this.qr.entities[0].consts[mksymbol(component, field.name)];
|
|
|
|
if (constVal === undefined) throw new ECSError(`field is not constant`, this.action); // TODO?
|
|
|
|
if (typeof constVal !== 'number') throw new ECSError(`field is not numeric`, this.action); // TODO?
|
|
|
|
return constVal << 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-16 20:50:48 +00:00
|
|
|
let index = parseInt(args[1] || '0');
|
2022-02-03 22:04:48 +00:00
|
|
|
if (this.entities.length == 1) {
|
|
|
|
return this.dialect.absolute(ident);
|
|
|
|
} else {
|
2022-02-16 20:50:48 +00:00
|
|
|
return this.dialect.indexed_x(ident, index); //TODO?
|
2022-02-03 22:04:48 +00:00
|
|
|
}
|
|
|
|
}
|
2022-02-19 23:52:24 +00:00
|
|
|
__eid(args: string[]) {
|
|
|
|
let e = this.scope.getEntityByName(args[0] || '?');
|
|
|
|
if (!e) throw new ECSError(`can't find entity named "${args[0]}"`, this.action);
|
|
|
|
return e.id.toString();
|
|
|
|
}
|
2022-02-01 16:38:54 +00:00
|
|
|
__use(args: string[]) {
|
|
|
|
return this.scope.includeResource(args[0]);
|
|
|
|
}
|
|
|
|
__emit(args: string[]) {
|
2022-02-05 05:56:43 +00:00
|
|
|
let event = args[0];
|
2022-02-15 17:25:00 +00:00
|
|
|
let eventargs = args.slice(1);
|
|
|
|
return this.scope.generateCodeForEvent(event, eventargs);
|
2022-02-01 16:38:54 +00:00
|
|
|
}
|
|
|
|
__local(args: string[]) {
|
|
|
|
let tempinc = parseInt(args[0]);
|
2022-02-10 21:51:03 +00:00
|
|
|
let tempbytes = this.instance.system.tempbytes;
|
2022-02-01 16:38:54 +00:00
|
|
|
if (isNaN(tempinc)) throw new ECSError(`bad temporary offset`, this.action);
|
2022-02-10 21:51:03 +00:00
|
|
|
if (!tempbytes) throw new ECSError(`this system has no locals`, this.action);
|
|
|
|
if (tempinc < 0 || tempinc >= tempbytes) throw new ECSError(`this system only has ${tempbytes} locals`, this.action);
|
|
|
|
this.scope.updateTempLiveness(this.instance);
|
2022-02-04 04:38:35 +00:00
|
|
|
return `${this.tmplabel}+${tempinc}`;
|
2022-02-01 16:38:54 +00:00
|
|
|
}
|
2022-02-15 17:25:00 +00:00
|
|
|
__arg(args: string[]) {
|
2022-02-19 08:37:19 +00:00
|
|
|
let argindex = parseInt(args[0] || '0');
|
|
|
|
let argvalue = this.eventargs[argindex] || '';
|
2022-02-19 10:06:25 +00:00
|
|
|
//this.used.add(`arg_${argindex}_${argvalue}`);
|
2022-02-19 08:37:19 +00:00
|
|
|
return argvalue;
|
2022-02-15 17:25:00 +00:00
|
|
|
}
|
2022-02-20 13:37:51 +00:00
|
|
|
__start(args: string[]) {
|
|
|
|
let startSymbol = this.dialect.scopeSymbol(args[0]);
|
|
|
|
return this.dialect.jump(startSymbol);
|
|
|
|
}
|
2022-02-08 12:18:28 +00:00
|
|
|
wrapCodeInLoop(code: string, action: ActionWithQuery, ents: Entity[], joinfield?: ComponentFieldPair): string {
|
2022-02-01 14:16:53 +00:00
|
|
|
// TODO: check ents
|
|
|
|
// TODO: check segment bounds
|
|
|
|
// TODO: what if 0 or 1 entitites?
|
2022-02-08 12:18:28 +00:00
|
|
|
// TODO: check > 127 or > 255
|
|
|
|
let dir = action.direction;
|
|
|
|
let s = dir == 'desc' ? this.dialect.ASM_ITERATE_EACH_DESC : this.dialect.ASM_ITERATE_EACH_ASC;
|
|
|
|
if (joinfield) s = dir == 'desc' ? this.dialect.ASM_ITERATE_JOIN_DESC : this.dialect.ASM_ITERATE_JOIN_ASC;
|
2022-02-01 14:16:53 +00:00
|
|
|
s = s.replace('{{%code}}', code);
|
|
|
|
return s;
|
|
|
|
}
|
2022-02-02 17:34:55 +00:00
|
|
|
wrapCodeInFilter(code: string) {
|
2022-02-16 20:50:48 +00:00
|
|
|
// TODO: :-p filters too often?
|
2022-02-03 20:46:10 +00:00
|
|
|
const ents = this.entities;
|
2022-02-16 17:54:44 +00:00
|
|
|
const ents2 = this.oldState.xreg?.eset?.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-10 21:51:03 +00:00
|
|
|
wrapCodeInRefLookup(code: string) {
|
|
|
|
code = this.dialect.ASM_LOOKUP_REF_X.replace('{{%code}}', code);
|
|
|
|
return code;
|
|
|
|
}
|
2022-02-03 22:04:48 +00:00
|
|
|
generateCodeForField(fieldName: string, bitofs: number, canWrite: boolean): string {
|
2022-02-03 20:46:10 +00:00
|
|
|
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;
|
2022-02-15 17:25:00 +00:00
|
|
|
var entityLookup = false;
|
2022-02-03 22:44:43 +00:00
|
|
|
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);
|
2022-02-15 17:25:00 +00:00
|
|
|
component = this.em.singleComponentWithFieldName([ent.etype], fname, action);
|
2022-02-03 22:44:43 +00:00
|
|
|
fieldName = fname;
|
|
|
|
entities = [ent];
|
2022-02-15 17:25:00 +00:00
|
|
|
entityLookup = true;
|
2022-02-03 22:44:43 +00:00
|
|
|
} 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
|
2022-02-15 17:25:00 +00:00
|
|
|
let range = this.scope.getFieldRange(component, field.name);
|
2022-02-01 14:16:53 +00:00
|
|
|
if (!range) throw new ECSError(`couldn't find field for ${component.name}:${fieldName}, maybe no entities?`); // TODO
|
|
|
|
// TODO: dialect
|
2022-02-15 17:25:00 +00:00
|
|
|
// TODO: doesnt work for entity.field
|
2022-02-09 17:45:40 +00:00
|
|
|
// TODO: array field baseoffset?
|
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-20 13:37:51 +00:00
|
|
|
// TODO: qr or this.entites?
|
|
|
|
let eidofs = entities[0].id - range.elo; // TODO: negative?
|
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;
|
2022-02-16 17:54:44 +00:00
|
|
|
let int;
|
2022-02-16 20:50:48 +00:00
|
|
|
let eidofs;
|
2022-02-16 17:54:44 +00:00
|
|
|
let xreg = this.scope.state.xreg;
|
|
|
|
let yreg = this.scope.state.yreg;
|
|
|
|
if (xreg && (int = xreg.eset?.intersection(qr))) {
|
2022-02-16 20:50:48 +00:00
|
|
|
//console.log(eidofs,'x',qr.entities[0].id,xreg.elo,int.entities[0].id,xreg.offset(),range.elo);
|
2022-02-16 17:54:44 +00:00
|
|
|
ir = xreg.eset;
|
2022-02-16 20:50:48 +00:00
|
|
|
//eidofs -= xreg.offset();
|
|
|
|
//eidofs -= int.entities[0].id - xreg.elo;
|
|
|
|
eidofs = xreg.elo - range.elo;
|
|
|
|
} else if (yreg && (int = yreg.eset?.intersection(qr))) {
|
2022-02-16 17:54:44 +00:00
|
|
|
ir = yreg.eset;
|
2022-02-16 20:50:48 +00:00
|
|
|
//eidofs -= yreg.offset();
|
|
|
|
eidofs = yreg.elo - range.elo;
|
|
|
|
} else {
|
|
|
|
ir = null;
|
|
|
|
eidofs = 0;
|
|
|
|
}
|
|
|
|
if (!ir) {
|
|
|
|
throw new ECSError(`no intersection for index register`, action);
|
2022-02-01 14:16:53 +00:00
|
|
|
}
|
2022-02-02 17:34:55 +00:00
|
|
|
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);
|
2022-02-16 17:54:44 +00:00
|
|
|
if (ir == this.scope.state.xreg?.eset)
|
2022-02-02 17:34:55 +00:00
|
|
|
return this.dialect.indexed_x(ident, eidofs);
|
2022-02-16 17:54:44 +00:00
|
|
|
if (ir == this.scope.state.yreg?.eset)
|
2022-02-02 17:34:55 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
2022-02-06 17:27:08 +00:00
|
|
|
getJoinField(action: Action, atypes: EntityArchetype[], jtypes: EntityArchetype[]): ComponentFieldPair {
|
2022-02-01 14:16:53 +00:00
|
|
|
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-02-19 10:06:25 +00:00
|
|
|
isSubroutineSized(code: string) {
|
|
|
|
// TODO?
|
|
|
|
if (code.length > 20000) return false;
|
2022-02-23 14:43:19 +00:00
|
|
|
if (code.split('\n ').length >= 4) return true; // TODO: :^/
|
2022-02-19 10:06:25 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class EventCodeStats {
|
|
|
|
constructor(
|
|
|
|
public readonly inst: SystemInstance,
|
|
|
|
public readonly action: Action,
|
2022-02-23 14:43:19 +00:00
|
|
|
public readonly eventcode: string
|
2022-02-19 10:06:25 +00:00
|
|
|
) { }
|
2022-02-23 14:43:19 +00:00
|
|
|
labels : string[] = [];
|
|
|
|
count : number = 0;
|
2022-02-01 14:16:53 +00:00
|
|
|
}
|
|
|
|
|
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[] = [];
|
2022-02-10 21:51:03 +00:00
|
|
|
instances: SystemInstance[] = [];
|
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-10 21:51:03 +00:00
|
|
|
sysstats = new Map<SystemInstance, SystemStats>();
|
2022-02-06 02:43:45 +00:00
|
|
|
bss = new UninitDataSegment();
|
|
|
|
rodata = new ConstDataSegment();
|
2022-02-01 14:07:33 +00:00
|
|
|
code = new CodeSegment();
|
2022-01-26 16:54:57 +00:00
|
|
|
componentsInScope = new Set();
|
2022-01-31 18:11:50 +00:00
|
|
|
resources = new Set<string>();
|
2022-02-03 20:46:10 +00:00
|
|
|
state = new ActionCPUState();
|
2022-02-03 02:06:44 +00:00
|
|
|
isDemo = false;
|
|
|
|
filePath = '';
|
2022-01-26 16:54:57 +00:00
|
|
|
|
2022-02-19 10:06:25 +00:00
|
|
|
eventSeq : number;
|
2022-02-23 14:43:19 +00:00
|
|
|
eventCodeStats : { [code:string] : EventCodeStats };
|
2022-02-19 10:06:25 +00:00
|
|
|
inCritical = 0;
|
|
|
|
|
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-02-20 16:15:16 +00:00
|
|
|
newEntity(etype: EntityArchetype, name: string): 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-02-20 16:15:16 +00:00
|
|
|
if (name && this.getEntityByName(name))
|
|
|
|
throw new ECSError(`already an entity named "${name}"`);
|
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);
|
|
|
|
}
|
2022-02-20 16:15:16 +00:00
|
|
|
entity.name = name;
|
2022-01-26 16:54:57 +00:00
|
|
|
this.entities.push(entity);
|
|
|
|
return entity;
|
|
|
|
}
|
2022-02-10 21:51:03 +00:00
|
|
|
newSystemInstance(inst: SystemInstance) {
|
|
|
|
if (!inst) throw new Error();
|
|
|
|
inst.id = this.instances.length+1;
|
|
|
|
this.instances.push(inst);
|
|
|
|
return inst;
|
|
|
|
}
|
|
|
|
newSystemInstanceWithDefaults(system: System) {
|
|
|
|
return this.newSystemInstance({ system, params: {}, id:0 });
|
2022-02-03 22:04:48 +00:00
|
|
|
}
|
2022-02-19 08:37:19 +00:00
|
|
|
getSystemInstanceNamed(name: string) {
|
|
|
|
return this.instances.find(sys => sys.system.name == name);
|
|
|
|
}
|
2022-02-03 22:44:43 +00:00
|
|
|
getEntityByName(name: string) {
|
|
|
|
return this.entities.find(e => e.name == name);
|
|
|
|
}
|
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-02-06 17:27:08 +00:00
|
|
|
*iterateArchetypeFields(arch: EntityArchetype[], filter?: (c: ComponentType, f: DataField) => boolean) {
|
2022-01-30 15:01:55 +00:00
|
|
|
for (let i = 0; i < arch.length; i++) {
|
|
|
|
let a = arch[i];
|
2022-02-06 17:27:08 +00:00
|
|
|
for (let c of a.components) {
|
2022-01-30 15:01:55 +00:00
|
|
|
for (let f of c.fields) {
|
2022-02-01 14:16:53 +00:00
|
|
|
if (!filter || filter(c, f))
|
2022-01-30 15:01:55 +00:00
|
|
|
yield { i, c, f };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-06 17:27:08 +00:00
|
|
|
entitiesMatching(atypes: EntityArchetype[]) {
|
2022-02-01 14:16:53 +00:00
|
|
|
let result: Entity[] = [];
|
2022-01-30 15:01:55 +00:00
|
|
|
for (let e of this.entities) {
|
|
|
|
for (let a of atypes) {
|
|
|
|
// TODO: what about subclasses?
|
|
|
|
// TODO: very scary identity ocmpare
|
2022-02-06 17:27:08 +00:00
|
|
|
if (e.etype === a) {
|
2022-01-30 15:01:55 +00:00
|
|
|
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
|
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-31 19:28:55 +00:00
|
|
|
// constants and array pointers go into rodata
|
2022-01-27 01:12:49 +00:00
|
|
|
let cfname = mksymbol(c, f.name);
|
2022-02-15 17:25:00 +00:00
|
|
|
let ftype = this.fieldtypes[cfname];
|
|
|
|
let segment = ftype == 'const' ? this.rodata : this.bss;
|
|
|
|
if (v === undefined && ftype == 'const')
|
|
|
|
throw new ECSError(`no value for const field ${cfname}`, e);
|
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;
|
2022-02-22 16:35:41 +00:00
|
|
|
if (array.ehi - array.elo + 1 >= 256)
|
|
|
|
throw new ECSError(`too many entities have field ${cfname}, limit is 256`);
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
2022-02-20 16:15:16 +00:00
|
|
|
// set default values for entity/field
|
|
|
|
if (ftype == 'init') {
|
|
|
|
if (f.dtype == 'int' && f.defvalue !== undefined) {
|
|
|
|
let ecfname = mkscopesymbol(this, c, f.name);
|
|
|
|
if (e.inits[ecfname] == null)
|
|
|
|
this.setInitValue(e, c, f, f.defvalue);
|
|
|
|
}
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
|
|
|
}
|
2022-02-03 20:46:10 +00:00
|
|
|
// TODO: cull unused entity fields
|
2022-02-10 18:55:36 +00:00
|
|
|
allocateSegment(segment: DataSegment, alloc: boolean, type: 'init' | 'const' | undefined) {
|
2022-02-01 14:16:53 +00:00
|
|
|
let fields: FieldArray[] = 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-02-10 18:55:36 +00:00
|
|
|
for (let f of fields) {
|
|
|
|
if (this.fieldtypes[mksymbol(f.component, f.field.name)] == type) {
|
2022-02-10 21:33:27 +00:00
|
|
|
//console.log(f.component.name, f.field.name, type);
|
2022-02-10 18:55:36 +00:00
|
|
|
let rangelen = (f.ehi - f.elo + 1);
|
|
|
|
// TODO: doesn't work for packed arrays too well
|
|
|
|
let bits = getPackedFieldSize(f.field);
|
|
|
|
// variable size? make it a pointer
|
|
|
|
if (bits == 0) bits = 16; // TODO?
|
|
|
|
let bytesperelem = Math.ceil(bits / 8);
|
|
|
|
// TODO: packing bits
|
|
|
|
// TODO: split arrays
|
|
|
|
let access = [];
|
|
|
|
for (let i = 0; i < bits; i += 8) {
|
|
|
|
let symbol = this.dialect.fieldsymbol(f.component, f.field, i);
|
|
|
|
access.push({ symbol, bit: i, width: 8 }); // TODO
|
|
|
|
if (alloc) {
|
|
|
|
segment.allocateBytes(symbol, rangelen); // TODO
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
2022-02-10 18:55:36 +00:00
|
|
|
f.access = access;
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-01 14:07:33 +00:00
|
|
|
allocateROData(segment: DataSegment) {
|
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-02-10 21:33:27 +00:00
|
|
|
// TODO: what if mix of var, const, and init values?
|
|
|
|
if (this.fieldtypes[cfname] == 'const') {
|
|
|
|
let range = segment.fieldranges[cfname];
|
|
|
|
let entcount = range ? range.ehi - range.elo + 1 : 0;
|
|
|
|
if (v == null && f.dtype == 'int') v = 0;
|
|
|
|
if (v == null && f.dtype == 'ref') v = 0;
|
2022-02-15 17:25:00 +00:00
|
|
|
if (v == null && f.dtype == 'array')
|
|
|
|
throw new ECSError(`no default value for array ${cfname}`, e);
|
2022-02-10 21:33:27 +00:00
|
|
|
//console.log(c.name, f.name, '#'+e.id, '=', v);
|
2022-01-31 19:28:55 +00:00
|
|
|
// this is a constant
|
2022-01-26 16:54:57 +00:00
|
|
|
// is it a byte array?
|
2022-02-09 17:45:40 +00:00
|
|
|
//TODO? if (ArrayBuffer.isView(v) && f.dtype == '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 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-15 17:25:00 +00:00
|
|
|
let datasym = this.dialect.datasymbol(c, f, e.id, 0);
|
|
|
|
segment.allocateInitData(datasym, v);
|
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-27 20:39:37 +00:00
|
|
|
} else if (typeof v === 'number') {
|
|
|
|
// more than 1 entity, add an array
|
2022-02-10 21:33:27 +00:00
|
|
|
// TODO: infer need for array by usage
|
|
|
|
/*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-15 00:09:14 +00:00
|
|
|
// TODO: this happens if you forget a const field on an object?
|
2022-02-15 17:25:00 +00:00
|
|
|
if (e.id < range.elo) throw new ECSError('entity out of range ' + c.name + ' ' + f.name, e);
|
|
|
|
if (segment.initdata[ofs] !== undefined) throw new ECSError('initdata already set ' + ofs), e;
|
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-10 21:33:27 +00:00
|
|
|
} else if (v == null && f.dtype == 'array' && f.index) {
|
|
|
|
// TODO
|
|
|
|
let datasym = this.dialect.datasymbol(c, f, e.id, 0);
|
|
|
|
let databytes = getFieldLength(f.index);
|
|
|
|
let offset = this.bss.allocateBytes(datasym, databytes);
|
|
|
|
// TODO? this.allocatePointerArray(c, f, datasym, entcount);
|
|
|
|
let ptrlosym = this.dialect.fieldsymbol(c, f, 0);
|
|
|
|
let ptrhisym = this.dialect.fieldsymbol(c, f, 8);
|
|
|
|
// TODO: what if we don't need a pointer array?
|
|
|
|
let loofs = segment.allocateBytes(ptrlosym, entcount);
|
|
|
|
let hiofs = segment.allocateBytes(ptrhisym, entcount);
|
|
|
|
if (f.baseoffset) datasym = `(${datasym}+${f.baseoffset})`;
|
|
|
|
segment.initdata[loofs + e.id - range.elo] = { symbol: datasym, bitofs: 0 };
|
|
|
|
segment.initdata[hiofs + e.id - range.elo] = { symbol: datasym, bitofs: 8 };
|
2022-01-27 20:39:37 +00:00
|
|
|
} else {
|
2022-02-09 17:45:40 +00:00
|
|
|
// TODO: bad error message - should say "wrong type, should be array"
|
|
|
|
throw new ECSError(`unhandled constant ${e.id}:${cfname} -- ${typeof v}`);
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
//console.log(segment.initdata)
|
|
|
|
}
|
2022-02-01 14:07:33 +00:00
|
|
|
allocateInitData(segment: DataSegment) {
|
2022-02-22 16:35:41 +00:00
|
|
|
if (segment.size == 0) return '';
|
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) {
|
2022-02-01 03:16:40 +00:00
|
|
|
let range = segment.getFieldRange(c, f.name);
|
2022-02-10 18:55:36 +00:00
|
|
|
if (!range) throw new ECSError(`no init range for ${scfname}`, e);
|
|
|
|
if (!range.access) throw new ECSError(`no init range access for ${scfname}`, e);
|
2022-02-01 03:16:40 +00:00
|
|
|
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) {
|
2022-02-09 17:45:40 +00:00
|
|
|
// TODO: 16/32...
|
|
|
|
let datasym = this.dialect.datasymbol(c, f, e.id, 0);
|
2022-01-31 19:28:55 +00:00
|
|
|
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;
|
|
|
|
}
|
2022-02-15 17:25:00 +00:00
|
|
|
getFieldRange(c: ComponentType, fn: string) {
|
|
|
|
return this.bss.getFieldRange(c, fn) || this.rodata.getFieldRange(c, fn);
|
|
|
|
}
|
2022-02-20 13:37:51 +00:00
|
|
|
setConstValue(e: Entity, component: ComponentType, field: DataField, value: DataValue) {
|
|
|
|
this.setConstInitValue(e, component, field, value, 'const');
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
2022-02-20 13:37:51 +00:00
|
|
|
setInitValue(e: Entity, component: ComponentType, field: DataField, value: DataValue) {
|
|
|
|
this.setConstInitValue(e, component, field, value, 'init');
|
2022-02-15 17:25:00 +00:00
|
|
|
}
|
2022-02-20 13:37:51 +00:00
|
|
|
setConstInitValue(e: Entity, component: ComponentType, field: DataField, value: DataValue,
|
|
|
|
type: 'const'|'init')
|
|
|
|
{
|
2022-02-22 16:35:41 +00:00
|
|
|
this.checkFieldValue(field, value);
|
2022-02-20 13:37:51 +00:00
|
|
|
let fieldName = field.name;
|
2022-02-15 17:25:00 +00:00
|
|
|
let cfname = mksymbol(component, fieldName);
|
|
|
|
let ecfname = mkscopesymbol(this, component, fieldName);
|
|
|
|
if (e.consts[cfname] !== undefined) throw new ECSError(`"${fieldName}" is already defined as a constant`, e);
|
2022-02-20 16:15:16 +00:00
|
|
|
if (e.inits[ecfname] !== undefined) throw new ECSError(`"${fieldName}" is already defined as a variable`, e);
|
2022-02-15 17:25:00 +00:00
|
|
|
if (type == 'const') e.consts[cfname] = value;
|
|
|
|
if (type == 'init') e.inits[ecfname] = value;
|
|
|
|
this.fieldtypes[cfname] = type;
|
2022-02-04 01:38:35 +00:00
|
|
|
}
|
|
|
|
isConstOrInit(component: ComponentType, fieldName: string) : 'const' | 'init' {
|
|
|
|
return this.fieldtypes[mksymbol(component, fieldName)];
|
2022-01-27 03:12:11 +00:00
|
|
|
}
|
2022-02-22 18:33:57 +00:00
|
|
|
getConstValue(entity: Entity, fieldName: string) {
|
|
|
|
let component = this.em.singleComponentWithFieldName([entity.etype], fieldName, entity);
|
|
|
|
let cfname = mksymbol(component, fieldName);
|
|
|
|
return entity.consts[cfname];
|
|
|
|
}
|
2022-02-22 16:35:41 +00:00
|
|
|
checkFieldValue(field: DataField, value: DataValue) {
|
2022-02-20 13:37:51 +00:00
|
|
|
if (field.dtype == 'array') {
|
|
|
|
if (!(value instanceof Uint8Array))
|
|
|
|
throw new ECSError(`This "${field.name}" value should be an array.`);
|
|
|
|
} else if (typeof value !== 'number') {
|
|
|
|
throw new ECSError(`This "${field.name}" ${field.dtype} value should be an number.`);
|
|
|
|
} else {
|
|
|
|
if (field.dtype == 'int') {
|
|
|
|
if (value < field.lo || value > field.hi)
|
|
|
|
throw new ECSError(`This "${field.name}" value is out of range, should be between ${field.lo} and ${field.hi}.`);
|
|
|
|
} else if (field.dtype == 'ref') {
|
|
|
|
// TODO: allow override if number
|
|
|
|
let eset = new EntitySet(this, field.query);
|
|
|
|
if (value < 0 || value >= eset.entities.length)
|
|
|
|
throw new ECSError(`This "${field.name}" value is out of range for this ref type.`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-02-19 08:37:19 +00:00
|
|
|
generateCodeForEvent(event: string, args?: string[], codelabel?: string): string {
|
2022-01-26 16:54:57 +00:00
|
|
|
// 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?
|
|
|
|
//throw new ECSError(`warning: no system responds to "${event}"`);
|
2022-02-08 22:30:11 +00:00
|
|
|
console.log(`warning: no system responds to "${event}"`);
|
|
|
|
return '';
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
2022-02-08 22:30:11 +00:00
|
|
|
this.eventSeq++;
|
|
|
|
// generate code
|
2022-02-19 10:06:25 +00:00
|
|
|
let code = '';
|
2022-02-19 22:53:19 +00:00
|
|
|
// is there a label? generate it first
|
2022-02-19 08:37:19 +00:00
|
|
|
if (codelabel) { code += this.dialect.label(codelabel) + '\n'; }
|
2022-02-19 22:53:19 +00:00
|
|
|
// if "start" event, initialize data segment
|
|
|
|
if (event == 'start') {
|
|
|
|
code += this.allocateInitData(this.bss);
|
|
|
|
}
|
|
|
|
// iterate all instances and generate matching events
|
2022-02-15 17:25:00 +00:00
|
|
|
let eventCount = 0;
|
2022-02-10 21:51:03 +00:00
|
|
|
let instances = this.instances.filter(inst => systems.includes(inst.system));
|
|
|
|
for (let inst of instances) {
|
|
|
|
let sys = inst.system;
|
2022-01-26 16:54:57 +00:00
|
|
|
for (let action of sys.actions) {
|
|
|
|
if (action.event == event) {
|
2022-02-15 17:25:00 +00:00
|
|
|
eventCount++;
|
2022-01-31 19:28:55 +00:00
|
|
|
// TODO: use Tokenizer so error msgs are better
|
2022-02-05 05:56:43 +00:00
|
|
|
// TODO: keep event tree
|
2022-02-15 17:25:00 +00:00
|
|
|
let codeeval = new ActionEval(this, inst, action, args || []);
|
2022-02-02 17:34:55 +00:00
|
|
|
codeeval.begin();
|
2022-02-19 10:06:25 +00:00
|
|
|
if (action.critical) this.inCritical++;
|
|
|
|
let eventcode = codeeval.codeToString();
|
|
|
|
if (action.critical) this.inCritical--;
|
|
|
|
if (!this.inCritical && codeeval.isSubroutineSized(eventcode)) {
|
2022-02-23 14:43:19 +00:00
|
|
|
let normcode = this.normalizeCode(eventcode, action);
|
2022-02-19 10:06:25 +00:00
|
|
|
// TODO: label rewriting messes this up
|
2022-02-23 14:43:19 +00:00
|
|
|
let estats = this.eventCodeStats[normcode];
|
2022-02-19 10:06:25 +00:00
|
|
|
if (!estats) {
|
2022-02-23 14:43:19 +00:00
|
|
|
estats = this.eventCodeStats[normcode] = new EventCodeStats(
|
|
|
|
inst, action, eventcode);
|
2022-02-19 10:06:25 +00:00
|
|
|
}
|
2022-02-23 14:43:19 +00:00
|
|
|
estats.labels.push(codeeval.label);
|
2022-02-19 10:06:25 +00:00
|
|
|
estats.count++;
|
|
|
|
if (action.critical) estats.count++; // always make critical event subroutines
|
|
|
|
}
|
2022-02-17 18:52:13 +00:00
|
|
|
let s = '';
|
2022-02-23 14:43:19 +00:00
|
|
|
s += this.dialect.comment(`start action ${codeeval.label}`);
|
2022-02-19 10:06:25 +00:00
|
|
|
s += eventcode;
|
2022-02-23 14:43:19 +00:00
|
|
|
s += this.dialect.comment(`end action ${codeeval.label}`);
|
2022-02-17 18:52:13 +00:00
|
|
|
code += s;
|
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-01-26 16:54:57 +00:00
|
|
|
}
|
2022-01-27 01:12:49 +00:00
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
2022-02-15 17:25:00 +00:00
|
|
|
if (eventCount == 0) {
|
|
|
|
console.log(`warning: event ${event} not handled`);
|
|
|
|
}
|
2022-02-17 18:52:13 +00:00
|
|
|
return code;
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
2022-02-23 14:43:19 +00:00
|
|
|
normalizeCode(code: string, action: Action) {
|
|
|
|
// TODO: use dialect to help with this
|
|
|
|
code = code.replace(/(\w+__\w+__)(\d+)(\w+)/g, (z,a,b,c) => a+c);
|
|
|
|
return code;
|
|
|
|
}
|
2022-02-10 21:51:03 +00:00
|
|
|
getSystemStats(inst: SystemInstance) : SystemStats {
|
|
|
|
let stats = this.sysstats.get(inst);
|
2022-02-08 22:30:11 +00:00
|
|
|
if (!stats) {
|
|
|
|
stats = new SystemStats();
|
2022-02-10 21:51:03 +00:00
|
|
|
this.sysstats.set(inst, stats);
|
2022-02-08 22:30:11 +00:00
|
|
|
}
|
|
|
|
return stats;
|
|
|
|
}
|
2022-02-10 21:51:03 +00:00
|
|
|
updateTempLiveness(inst: SystemInstance) {
|
|
|
|
let stats = this.getSystemStats(inst);
|
2022-02-08 22:30:11 +00:00
|
|
|
let n = this.eventSeq;
|
|
|
|
if (stats.tempstartseq && stats.tempendseq) {
|
|
|
|
stats.tempstartseq = Math.min(stats.tempstartseq, n);
|
|
|
|
stats.tempendseq = Math.max(stats.tempendseq, n);
|
|
|
|
} else {
|
|
|
|
stats.tempstartseq = stats.tempendseq = n;
|
|
|
|
}
|
|
|
|
}
|
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-02-09 13:39:41 +00:00
|
|
|
private allocateTempVars() {
|
2022-02-09 04:21:23 +00:00
|
|
|
let pack = new Packer();
|
2022-02-09 13:39:41 +00:00
|
|
|
let maxTempBytes = 128 - this.bss.size; // TODO: multiple data segs
|
|
|
|
let bssbin = new Bin({ left:0, top:0, bottom: this.eventSeq+1, right: maxTempBytes });
|
|
|
|
pack.bins.push(bssbin);
|
2022-02-10 21:51:03 +00:00
|
|
|
for (let instance of this.instances) {
|
|
|
|
let stats = this.getSystemStats(instance);
|
|
|
|
if (instance.system.tempbytes && stats.tempstartseq && stats.tempendseq) {
|
2022-02-09 04:21:23 +00:00
|
|
|
let v = {
|
2022-02-10 21:51:03 +00:00
|
|
|
inst: instance,
|
2022-02-09 04:21:23 +00:00
|
|
|
top: stats.tempstartseq,
|
|
|
|
bottom: stats.tempendseq+1,
|
2022-02-10 21:51:03 +00:00
|
|
|
width: instance.system.tempbytes,
|
2022-02-09 04:21:23 +00:00
|
|
|
height: stats.tempendseq - stats.tempstartseq + 1,
|
2022-02-19 18:38:16 +00:00
|
|
|
label: instance.system.name
|
2022-02-09 04:21:23 +00:00
|
|
|
};
|
|
|
|
pack.boxes.push(v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!pack.pack()) console.log('cannot pack temporary local vars'); // TODO
|
2022-02-15 17:25:00 +00:00
|
|
|
//console.log('tempvars', pack);
|
2022-02-09 13:39:41 +00:00
|
|
|
if (bssbin.extents.right > 0) {
|
2022-02-17 15:58:13 +00:00
|
|
|
let tempofs = this.bss.allocateBytes('TEMP', bssbin.extents.right);
|
2022-02-09 13:39:41 +00:00
|
|
|
for (let b of pack.boxes) {
|
2022-02-10 21:51:03 +00:00
|
|
|
let inst : SystemInstance = (b as any).inst;
|
2022-02-16 20:50:48 +00:00
|
|
|
//console.log(inst.system.name, b.box?.left);
|
2022-02-17 15:58:13 +00:00
|
|
|
if (b.box) this.bss.declareSymbol(this.dialect.tempLabel(inst), tempofs + b.box.left);
|
|
|
|
//this.bss.equates[this.dialect.tempLabel(inst)] = `TEMP+${b.box?.left}`;
|
2022-02-09 13:39:41 +00:00
|
|
|
}
|
2022-02-09 04:21:23 +00:00
|
|
|
}
|
2022-02-19 18:38:16 +00:00
|
|
|
console.log(pack.toSVGUrl());
|
2022-02-09 04:21:23 +00:00
|
|
|
}
|
2022-02-09 13:39:41 +00:00
|
|
|
private analyzeEntities() {
|
2022-01-27 01:12:49 +00:00
|
|
|
this.buildSegments();
|
2022-02-10 18:55:36 +00:00
|
|
|
this.allocateSegment(this.bss, true, 'init'); // initialized vars
|
2022-02-10 21:33:27 +00:00
|
|
|
this.allocateSegment(this.bss, true, undefined); // uninitialized vars
|
2022-02-10 18:55:36 +00:00
|
|
|
this.allocateSegment(this.rodata, false, 'const'); // constants
|
2022-01-27 01:12:49 +00:00
|
|
|
this.allocateROData(this.rodata);
|
|
|
|
}
|
2022-02-09 13:39:41 +00:00
|
|
|
private generateCode() {
|
2022-02-19 10:06:25 +00:00
|
|
|
this.eventSeq = 0;
|
2022-02-23 14:43:19 +00:00
|
|
|
this.eventCodeStats = {};
|
2022-02-06 17:47:51 +00:00
|
|
|
let isMainScope = this.parent == null;
|
|
|
|
let start;
|
2022-02-08 22:30:11 +00:00
|
|
|
let initsys = this.em.getSystemByName('Init');
|
|
|
|
if (isMainScope && initsys) {
|
2022-02-10 21:51:03 +00:00
|
|
|
this.newSystemInstanceWithDefaults(initsys); //TODO: what if none?
|
2022-02-06 17:47:51 +00:00
|
|
|
start = this.generateCodeForEvent('main_init');
|
|
|
|
} else {
|
|
|
|
start = this.generateCodeForEvent('start');
|
2022-02-02 22:32:04 +00:00
|
|
|
}
|
2022-02-19 10:06:25 +00:00
|
|
|
start = this.replaceSubroutines(start);
|
2022-01-27 03:12:11 +00:00
|
|
|
this.code.addCodeFragment(start);
|
2022-01-31 18:11:50 +00:00
|
|
|
for (let sub of Array.from(this.resources.values())) {
|
2022-02-19 08:37:19 +00:00
|
|
|
if (!this.getSystemInstanceNamed(sub)) {
|
|
|
|
let sys = this.em.getSystemByName(sub);
|
|
|
|
if (!sys) throw new ECSError(`cannot find resource named "${sub}"`);
|
|
|
|
this.newSystemInstanceWithDefaults(sys);
|
|
|
|
}
|
|
|
|
let code = this.generateCodeForEvent(sub, [], sub);
|
2022-02-19 10:06:25 +00:00
|
|
|
this.code.addCodeFragment(code); // TODO: should be rodata?
|
2022-01-27 01:12:49 +00:00
|
|
|
}
|
2022-02-08 22:30:11 +00:00
|
|
|
//this.showStats();
|
|
|
|
}
|
2022-02-19 10:06:25 +00:00
|
|
|
replaceSubroutines(code: string) {
|
|
|
|
// TODO: bin-packing for critical code
|
2022-02-19 18:38:16 +00:00
|
|
|
// TODO: doesn't work with nested subroutines?
|
2022-02-19 22:53:19 +00:00
|
|
|
// TODO: doesn't work between scopes
|
2022-02-19 10:06:25 +00:00
|
|
|
let allsubs : string[] = [];
|
2022-02-23 14:43:19 +00:00
|
|
|
for (let stats of Object.values(this.eventCodeStats)) {
|
2022-02-19 10:06:25 +00:00
|
|
|
if (stats.count > 1) {
|
|
|
|
if (allsubs.length == 0) {
|
|
|
|
allsubs = [
|
|
|
|
this.dialect.segment('rodata'),
|
|
|
|
this.dialect.alignSegmentStart()
|
|
|
|
]
|
|
|
|
} else if (stats.action.fitbytes) {
|
|
|
|
allsubs.push(this.dialect.alignIfLessThan(stats.action.fitbytes));
|
|
|
|
}
|
2022-02-23 14:43:19 +00:00
|
|
|
let subcall = this.dialect.call(stats.labels[0]);
|
|
|
|
for (let label of stats.labels) {
|
|
|
|
// TODO: use dialect
|
|
|
|
let startdelim = `;;; start action ${label}`
|
|
|
|
let enddelim = `;;; end action ${label}`
|
|
|
|
let istart = code.indexOf(startdelim);
|
|
|
|
let iend = code.indexOf(enddelim, istart);
|
|
|
|
if (istart >= 0 && iend > istart) {
|
|
|
|
code = code.substring(0, istart) + subcall + code.substring(iend + enddelim.length);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let substart = stats.labels[0];
|
2022-02-19 10:06:25 +00:00
|
|
|
let sublines = [
|
|
|
|
this.dialect.segment('rodata'),
|
|
|
|
this.dialect.label(substart),
|
2022-02-23 14:43:19 +00:00
|
|
|
stats.eventcode,
|
2022-02-19 10:06:25 +00:00
|
|
|
this.dialect.return(),
|
|
|
|
];
|
2022-02-19 17:51:48 +00:00
|
|
|
if (stats.action.critical) {
|
|
|
|
sublines.push(this.dialect.warningIfPageCrossed(substart));
|
|
|
|
}
|
2022-02-19 10:06:25 +00:00
|
|
|
if (stats.action.fitbytes) {
|
|
|
|
sublines.push(this.dialect.warningIfMoreThan(stats.action.fitbytes, substart));
|
|
|
|
}
|
|
|
|
allsubs = allsubs.concat(sublines);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
code += allsubs.join('\n');
|
|
|
|
return code;
|
|
|
|
}
|
2022-02-08 22:30:11 +00:00
|
|
|
showStats() {
|
2022-02-10 21:51:03 +00:00
|
|
|
for (let inst of this.instances) {
|
|
|
|
// TODO?
|
|
|
|
console.log(inst.system.name, this.getSystemStats(inst));
|
2022-02-08 22:30:11 +00:00
|
|
|
}
|
2022-01-27 01:12:49 +00:00
|
|
|
}
|
2022-02-06 17:47:51 +00:00
|
|
|
private dumpCodeTo(file: SourceFileExport) {
|
2022-02-04 17:45:14 +00:00
|
|
|
let dialect = this.dialect;
|
|
|
|
file.line(dialect.startScope(this.name));
|
2022-02-17 18:52:13 +00:00
|
|
|
file.line(dialect.segment('bss'));
|
2022-02-04 17:45:14 +00:00
|
|
|
this.bss.dump(file, dialect);
|
2022-02-17 18:52:13 +00:00
|
|
|
file.line(dialect.segment('code')); // TODO: rodata for aligned?
|
2022-02-04 17:45:14 +00:00
|
|
|
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);
|
2022-02-02 23:03:22 +00:00
|
|
|
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-02-09 13:39:41 +00:00
|
|
|
dump(file: SourceFileExport) {
|
|
|
|
this.analyzeEntities();
|
|
|
|
this.generateCode();
|
|
|
|
this.allocateTempVars();
|
|
|
|
this.dumpCodeTo(file);
|
|
|
|
}
|
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 } = {};
|
2022-02-02 23:03:22 +00:00
|
|
|
topScopes: { [name: string]: EntityScope } = {};
|
2022-02-02 22:32:04 +00:00
|
|
|
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
|
|
|
|
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-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);
|
2022-02-02 23:03:22 +00:00
|
|
|
if (!parent) this.topScopes[name] = scope;
|
2022-01-26 16:54:57 +00:00
|
|
|
return scope;
|
|
|
|
}
|
2022-02-10 15:21:24 +00:00
|
|
|
deferComponent(name: string) {
|
|
|
|
this.components[name] = { name, fields: [] };
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
defineComponent(ctype: ComponentType) {
|
2022-02-04 00:25:36 +00:00
|
|
|
let existing = this.components[ctype.name];
|
2022-02-22 16:59:40 +00:00
|
|
|
// we can defer component definitions, just declare a component with 0 fields?
|
2022-02-10 15:21:24 +00:00
|
|
|
if (existing && existing.fields.length > 0)
|
|
|
|
throw new ECSError(`component ${ctype.name} already defined`, existing);
|
2022-02-22 16:59:40 +00:00
|
|
|
if (existing) {
|
|
|
|
existing.fields = ctype.fields;
|
|
|
|
ctype = 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-02-22 16:59:40 +00:00
|
|
|
this.components[ctype.name] = ctype;
|
|
|
|
return ctype;
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
|
|
|
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);
|
|
|
|
}
|
2022-01-28 17:22:59 +00:00
|
|
|
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 {
|
2022-01-30 15:01:55 +00:00
|
|
|
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-02-06 17:27:08 +00:00
|
|
|
let result = new Set<EntityArchetype>();
|
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-02-06 17:27:08 +00:00
|
|
|
result.add(etype);
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
2022-01-30 15:01:55 +00:00
|
|
|
}
|
2022-02-06 17:27:08 +00:00
|
|
|
return Array.from(result.values());
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|
2022-02-06 17:27:08 +00:00
|
|
|
componentsWithFieldName(atypes: EntityArchetype[], 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) {
|
2022-02-06 17:27:08 +00:00
|
|
|
for (let c of at.components) {
|
2022-01-27 01:12:49 +00:00
|
|
|
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-02-02 22:32:04 +00:00
|
|
|
getSystemByName(name: string): System {
|
|
|
|
return this.systems[name];
|
|
|
|
}
|
2022-02-06 17:27:08 +00:00
|
|
|
singleComponentWithFieldName(atypes: EntityArchetype[], fieldName: string, where: SourceLocated) {
|
2022-02-22 16:59:40 +00:00
|
|
|
let cfpairs = this.name2cfpairs[fieldName];
|
2022-02-22 18:33:57 +00:00
|
|
|
if (!cfpairs) throw new ECSError(`cannot find field named "${fieldName}"`, where);
|
2022-02-22 16:59:40 +00:00
|
|
|
let filtered = cfpairs.filter(cf => atypes.find(a => a.components.includes(cf.c)));
|
|
|
|
if (filtered.length == 0) {
|
2022-02-22 18:33:57 +00:00
|
|
|
throw new ECSError(`cannot find component with field "${fieldName}" in this context`, where);
|
2022-01-29 03:13:33 +00:00
|
|
|
}
|
2022-02-22 16:59:40 +00:00
|
|
|
if (filtered.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
|
|
|
}
|
2022-02-22 16:59:40 +00:00
|
|
|
return filtered[0].c;
|
2022-01-29 03:13:33 +00:00
|
|
|
}
|
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-02-02 22:32:04 +00:00
|
|
|
exportToFile(file: SourceFileExport) {
|
2022-02-02 23:03:22 +00:00
|
|
|
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);
|
|
|
|
}
|
2022-02-02 22:32:04 +00:00
|
|
|
}
|
|
|
|
}
|
2022-01-26 16:54:57 +00:00
|
|
|
}
|