@datamoc/mw_games 0.8.1 → 0.9.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/MWG - Best Practices.md +97 -0
  3. package/MWG - Deep Code and Architecture Review.md +238 -0
  4. package/README.md +7 -4
  5. package/dist/core/Sanitize.d.ts +8 -0
  6. package/dist/core/Sanitize.d.ts.map +1 -1
  7. package/dist/core/Sanitize.js +20 -0
  8. package/dist/core/Sanitize.js.map +1 -1
  9. package/dist/core/Save.d.ts +8 -1
  10. package/dist/core/Save.d.ts.map +1 -1
  11. package/dist/core/Save.js +26 -7
  12. package/dist/core/Save.js.map +1 -1
  13. package/dist/mw_games.global.js +107 -91
  14. package/dist/mwl/compiler.d.ts +7 -7
  15. package/dist/mwl/compiler.js +7 -7
  16. package/dist/mwl/content.d.ts +1 -1
  17. package/dist/mwl/content.js +1 -1
  18. package/dist/mwl/grammar.d.ts +18 -8
  19. package/dist/mwl/grammar.d.ts.map +1 -1
  20. package/dist/mwl/grammar.js +227 -92
  21. package/dist/mwl/grammar.js.map +1 -1
  22. package/dist/mwl/persistence.d.ts +4 -1
  23. package/dist/mwl/persistence.d.ts.map +1 -1
  24. package/dist/mwl/persistence.js +6 -1
  25. package/dist/mwl/persistence.js.map +1 -1
  26. package/dist/mwl/readers.d.ts +2 -2
  27. package/dist/mwl/readers.js +2 -2
  28. package/dist/mwl/report.d.ts +2 -2
  29. package/dist/mwl/report.js +2 -2
  30. package/dist/mwl/runtime.d.ts +30 -1
  31. package/dist/mwl/runtime.d.ts.map +1 -1
  32. package/dist/mwl/runtime.js +204 -209
  33. package/dist/mwl/runtime.js.map +1 -1
  34. package/dist/mwl/schema.d.ts +1 -1
  35. package/dist/mwl/schema.js +1 -1
  36. package/dist/two-d/Game.d.ts +10 -4
  37. package/dist/two-d/Game.d.ts.map +1 -1
  38. package/dist/two-d/Game.js.map +1 -1
  39. package/dist/two-d/render/Types2D.d.ts +6 -4
  40. package/dist/two-d/render/Types2D.d.ts.map +1 -1
  41. package/dist/two-d/render/Types2D.js +6 -4
  42. package/dist/two-d/render/Types2D.js.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/dist/version.d.ts.map +1 -1
  45. package/dist/version.js +1 -1
  46. package/dist/version.js.map +1 -1
  47. package/package.json +5 -2
@@ -84,7 +84,7 @@ const variableReference = /\$([A-Za-z_][A-Za-z0-9_.]*)/g;
84
84
  * ```ts
85
85
  * import { compile, MwlRuntime } from '@datamoc/mw_games/mwl';
86
86
  *
87
- * const runtime = new MwlRuntime(compile('[game]\nschema=0.1\n[/game]'), {
87
+ * const runtime = new MwlRuntime(compile("[{ tag: 'game', schema: 0.1 }]"), {
88
88
  * resolveMap: () => 'Gg,Gg\nGg,Gg',
89
89
  * });
90
90
  * console.log(runtime.world.turn); // 1
@@ -560,220 +560,215 @@ export class MwlRuntime {
560
560
  unitAt(x, y) {
561
561
  return Object.values(this.world.units).find((unit) => unit.alive && unit.x === x && unit.y === y);
562
562
  }
563
+ /**
564
+ * One entry per MWL command tag, keyed exactly as `executeNode` used to switch on. Each
565
+ * handler is a small, independently-named method below rather than a case body, so a
566
+ * command's own logic is a search away instead of one branch inside a 200-line function -
567
+ * the decomposition this project's own review flagged `mwl/runtime.ts`'s size for for,
568
+ * done for the one function that was actually a single undifferentiated dispatch. State
569
+ * (`this.world` and the rest) still lives on the instance; only the dispatch itself moved.
570
+ */
571
+ static { this.commandHandlers = {
572
+ message: (self, node) => self.showMessage(node.attributes),
573
+ spawn: (self, node) => self.cmdSpawn(node),
574
+ move: (self, node) => self.cmdMove(node),
575
+ kill: (self, node) => self.cmdKill(node),
576
+ fire_event: (self, node) => self.cmdFireEvent(node),
577
+ store_unit: (self, node) => self.cmdStoreUnit(node),
578
+ unstore_unit: (self, node) => self.cmdUnstoreUnit(node),
579
+ recall: (self, node) => self.cmdRecall(node),
580
+ modify_unit: (self, node) => self.cmdModifyUnit(node),
581
+ heal_unit: (self, node) => self.cmdHealUnit(node),
582
+ set_terrain: (self, node) => self.setTerrain(integer(node, 'x', 0), integer(node, 'y', 0), required(node, 'terrain')),
583
+ capture_village: (self, node) => self.cmdCaptureVillage(node),
584
+ clear_shroud: (self, node) => self.cmdClearShroud(node),
585
+ role: (self, node) => self.cmdRole(node),
586
+ attack: (self, node) => self.attack(node.attributes.defender ?? node.attributes.target ?? required(node, 'target'), integer(node, 'amount', 0)),
587
+ modify_gold: (self, node) => self.addGold(node.attributes.side ?? required(node, 'target'), integer(node, 'delta', integer(node, 'amount', 0))),
588
+ gold: (self, node) => self.addGold(required(node, 'side'), integer(node, 'delta', integer(node, 'amount', 0))),
589
+ set_variable: (self, node) => self.cmdSetVariable(node),
590
+ while: (self, node) => self.cmdWhile(node),
591
+ foreach: (self, node) => self.cmdForeach(node),
592
+ switch: (self, node) => self.cmdSwitch(node),
593
+ end_turn: (self) => self.endTurn(),
594
+ win: (self, node) => self.cmdWin(node),
595
+ lose: (self, node) => self.cmdLose(node),
596
+ endlevel: (self, node) => self.cmdEndlevel(node),
597
+ if: (self, node) => self.cmdIf(node),
598
+ //reachable at top level for a free-standing [else]; a paired one is run by its [if]
599
+ else: (self, node) => self.runBlock(self.commandChildren(node)),
600
+ hook: (self, node) => self.runHook(node),
601
+ }; }
563
602
  executeNode(node) {
564
603
  const name = node.tag === 'command' ? node.attributes.name : node.tag;
565
604
  if (!name)
566
605
  throw new Error('MWL command is missing name');
567
- switch (name) {
568
- case 'message': {
569
- this.showMessage(node.attributes);
570
- break;
571
- }
572
- case 'spawn':
573
- this.spawnUnit(node.attributes.type ?? '', node.attributes.side, integer(node, 'x', 0), integer(node, 'y', 0), node.attributes.id ?? node.attributes.target, node.attributes.hp === undefined ? undefined : integer(node, 'hp', 1));
574
- break;
575
- case 'move':
576
- this.applyMove(node.attributes.unit ?? required(node, 'target'), integer(node, 'x', 0), integer(node, 'y', 0));
577
- break;
578
- case 'kill': {
579
- //a named unit that is not there is a content error; a filter that matches nobody is not,
580
- //which is what WML's own `[kill]` does. An empty filter matches every unit, there as
581
- //here, which is worth knowing before writing one.
582
- const named = node.attributes.unit ?? node.attributes.target;
583
- if (named === undefined)
584
- killMatching(this.world, node.attributes);
585
- else
586
- this.killUnit(named);
587
- break;
588
- }
589
- case 'fire_event': {
590
- const id = node.attributes.id ?? node.attributes.name;
591
- if (!id)
592
- throw new Error('[fire_event] requires id');
593
- this.fireEvent(id);
594
- break;
595
- }
596
- case 'store_unit': {
597
- const variable = node.attributes.variable ?? node.attributes.name ?? required(node, 'variable');
598
- this.setVariableAt(variable, this.matchingUnits(node, ['variable', 'name']).map(([id, unit]) => ({ id, ...unitSnapshot(unit) })));
599
- break;
600
- }
601
- case 'unstore_unit': {
602
- const variable = required(node, 'variable');
603
- for (const entry of this.storedUnits(variable))
604
- this.restoreUnit(entry, {});
605
- break;
606
- }
607
- case 'recall': {
608
- const stored = this.storedUnits(required(node, 'variable'));
609
- const wanted = node.attributes.id ?? node.attributes.unit;
610
- const entry = wanted === undefined ? stored[0] : stored.find((candidate) => candidate.id === wanted);
611
- if (!entry)
612
- throw new Error(`[recall] found no stored unit${wanted === undefined ? '' : ` named ${wanted}`}`);
613
- const placement = {};
614
- if (node.attributes.x !== undefined)
615
- placement.x = integer(node, 'x', 0);
616
- if (node.attributes.y !== undefined)
617
- placement.y = integer(node, 'y', 0);
618
- if (node.attributes.side !== undefined)
619
- placement.side = node.attributes.side;
620
- this.restoreUnit(entry, placement);
621
- break;
622
- }
623
- case 'modify_unit': {
624
- //WML separates which units (`[filter]`) from what changes (`[set]`); a bare
625
- //`[modify_unit] hp=5` with no `[set]` changes every unit it matches
626
- const changes = node.children.find((child) => child.tag === 'set')?.attributes ?? node.attributes;
627
- for (const [, unit] of this.matchingUnits(node))
628
- applyUnitChanges(unit, changes);
629
- break;
630
- }
631
- case 'heal_unit': {
632
- const amount = optionalInteger(node, 'amount');
633
- const absolute = optionalInteger(node, 'hp');
634
- if (amount === undefined && absolute === undefined)
635
- throw new Error('[heal_unit] requires amount or hp');
636
- for (const [, unit] of this.matchingUnits(node))
637
- unit.hp = absolute ?? unit.hp + (amount ?? 0);
638
- break;
639
- }
640
- case 'set_terrain':
641
- this.setTerrain(integer(node, 'x', 0), integer(node, 'y', 0), required(node, 'terrain'));
642
- break;
643
- case 'capture_village': {
644
- const x = integer(node, 'x', 0);
645
- const y = integer(node, 'y', 0);
646
- const entry = { x, y, side: required(node, 'side') };
647
- if (node.attributes.name !== undefined)
648
- entry.name = node.attributes.name;
649
- (this.world.villages ??= {})[`${x},${y}`] = entry;
650
- break;
651
- }
652
- case 'clear_shroud': {
653
- const side = required(node, 'side');
654
- const x = integer(node, 'x', 0);
655
- const y = integer(node, 'y', 0);
656
- const radius = integer(node, 'radius', 1);
657
- const cleared = new Set(this.world.clearedShroud?.[side] ?? []);
658
- for (let dy = -radius; dy <= radius; dy++)
659
- for (let dx = -radius; dx <= radius; dx++)
660
- cleared.add(`${x + dx},${y + dy}`);
661
- (this.world.clearedShroud ??= {})[side] = [...cleared];
662
- break;
663
- }
664
- case 'role': {
665
- const name = node.attributes.role ?? node.attributes.name;
666
- if (!name)
667
- throw new Error('[role] requires role');
668
- //`role`/`name` here name the role being assigned, not a unit to match, so the filter
669
- //must not read either back as one - the same exclusion the scenario-level role uses
670
- const matched = this.matchingUnits(node, ['role', 'name']);
671
- for (const [, unit] of matched)
672
- unit.role = name;
673
- (this.world.roles ??= {})[name] = matched.map(([id]) => id);
674
- break;
675
- }
676
- case 'attack':
677
- this.attack(node.attributes.defender ?? node.attributes.target ?? required(node, 'target'), integer(node, 'amount', 0));
678
- break;
679
- case 'modify_gold':
680
- this.addGold(node.attributes.side ?? required(node, 'target'), integer(node, 'delta', integer(node, 'amount', 0)));
681
- break;
682
- case 'gold':
683
- this.addGold(required(node, 'side'), integer(node, 'delta', integer(node, 'amount', 0)));
684
- break;
685
- case 'set_variable': {
686
- // `name`/`target` is a path written out, `path` is one content builds from
687
- // variables. Only the second goes through expansion: a `$` in `name` is not
688
- // a reference today, and reading it as one would change what content means.
689
- const computed = node.attributes.path;
690
- this.setVariable(computed === undefined
691
- ? node.tag === 'command'
692
- ? required(node, 'target')
693
- : required(node, 'name')
694
- : this.expandPath(computed), node.attributes.value ?? '', node.attributes.mode);
695
- break;
696
- }
697
- case 'while': {
698
- const limit = integer(node, 'max_iterations', 1000);
699
- if (limit < 1 || limit > 100_000)
700
- throw new Error('MWL while max_iterations must be between 1 and 100000');
701
- for (let iteration = 0; iteration < limit && this.nodeConditionMatches(node); iteration++)
702
- this.runBlock(this.commandChildren(node));
703
- break;
704
- }
705
- case 'foreach': {
706
- const source = this.variableAt(required(node, 'variable'));
707
- const entries = Array.isArray(source)
708
- ? source.map((value, index) => [String(index), value])
709
- : typeof source === 'string'
710
- ? source
711
- .split(',')
712
- .map((value, index) => [String(index), value.trim()])
713
- .filter(([, value]) => value !== '')
714
- : source && typeof source === 'object'
715
- ? Object.entries(source)
716
- : [];
717
- const item = node.attributes.item ?? 'item';
718
- const indexName = node.attributes.index ?? 'index';
719
- for (let index = 0; index < entries.length; index++) {
720
- this.setVariableAt(item, entries[index][1]);
721
- this.setVariableAt(indexName, index);
722
- this.runBlock(this.commandChildren(node));
723
- }
724
- break;
725
- }
726
- case 'switch': {
727
- const value = this.variableAt(required(node, 'variable'));
728
- const selected = node.children.find((child) => child.tag === 'case' &&
729
- child.attributes.equals !== undefined &&
730
- sameValue(value, child.attributes.equals));
731
- const fallback = node.children.find((child) => child.tag === 'default');
732
- this.runBlock((selected ?? fallback)?.children ?? []);
733
- break;
734
- }
735
- case 'end_turn':
736
- this.endTurn();
737
- break;
738
- case 'win':
739
- this.markSideResult(node.attributes.side, 'won');
740
- this.world.status = 'won';
741
- break;
742
- case 'lose':
743
- this.markSideResult(node.attributes.side, 'lost');
744
- this.world.status = 'lost';
745
- break;
746
- case 'endlevel': {
747
- const result = node.attributes.result === 'defeat' ? 'defeat' : 'victory';
748
- this.world.status = result === 'defeat' ? 'lost' : 'won';
749
- this.world.carryover = endLevelCarryover(this.world, endLevelSide(this.world, node.attributes.side), {
750
- result,
751
- bonus: integerAttribute(node, 'bonus'),
752
- carryoverPercentage: numberAttribute(node, 'carryover_percentage'),
753
- carryoverAdd: booleanAttribute(node, 'carryover_add'),
754
- nextScenario: node.attributes.next_scenario ?? null,
755
- });
756
- break;
757
- }
758
- case 'if': {
759
- //an immediately following [else] is this branch's other half, not a separate
760
- //command: the condition picks one body, and runBlock skips the [else] itself
761
- const fallback = node.children.find((child) => child.tag === 'else');
762
- const taken = this.nodeConditionMatches(node) ? node : fallback;
763
- this.runBlock(taken ? this.commandChildren(taken) : []);
764
- break;
765
- }
766
- case 'else':
767
- //reachable at top level for a free-standing [else]; a paired one is run by its [if]
768
- this.runBlock(this.commandChildren(node));
769
- break;
770
- case 'hook':
771
- this.runHook(node);
772
- break;
773
- default:
774
- throw new Error(`unknown MWL command: ${name}`);
606
+ const handler = MwlRuntime.commandHandlers[name];
607
+ if (!handler)
608
+ throw new Error(`unknown MWL command: ${name}`);
609
+ handler(this, node);
610
+ }
611
+ cmdSpawn(node) {
612
+ this.spawnUnit(node.attributes.type ?? '', node.attributes.side, integer(node, 'x', 0), integer(node, 'y', 0), node.attributes.id ?? node.attributes.target, node.attributes.hp === undefined ? undefined : integer(node, 'hp', 1));
613
+ }
614
+ cmdMove(node) {
615
+ this.applyMove(node.attributes.unit ?? required(node, 'target'), integer(node, 'x', 0), integer(node, 'y', 0));
616
+ }
617
+ cmdKill(node) {
618
+ //a named unit that is not there is a content error; a filter that matches nobody is not,
619
+ //which is what WML's own `[kill]` does. An empty filter matches every unit, there as
620
+ //here, which is worth knowing before writing one.
621
+ const named = node.attributes.unit ?? node.attributes.target;
622
+ if (named === undefined)
623
+ killMatching(this.world, node.attributes);
624
+ else
625
+ this.killUnit(named);
626
+ }
627
+ cmdFireEvent(node) {
628
+ const id = node.attributes.id ?? node.attributes.name;
629
+ if (!id)
630
+ throw new Error('[fire_event] requires id');
631
+ this.fireEvent(id);
632
+ }
633
+ cmdStoreUnit(node) {
634
+ const variable = node.attributes.variable ?? node.attributes.name ?? required(node, 'variable');
635
+ this.setVariableAt(variable, this.matchingUnits(node, ['variable', 'name']).map(([id, unit]) => ({ id, ...unitSnapshot(unit) })));
636
+ }
637
+ cmdUnstoreUnit(node) {
638
+ const variable = required(node, 'variable');
639
+ for (const entry of this.storedUnits(variable))
640
+ this.restoreUnit(entry, {});
641
+ }
642
+ cmdRecall(node) {
643
+ const stored = this.storedUnits(required(node, 'variable'));
644
+ const wanted = node.attributes.id ?? node.attributes.unit;
645
+ const entry = wanted === undefined ? stored[0] : stored.find((candidate) => candidate.id === wanted);
646
+ if (!entry)
647
+ throw new Error(`[recall] found no stored unit${wanted === undefined ? '' : ` named ${wanted}`}`);
648
+ const placement = {};
649
+ if (node.attributes.x !== undefined)
650
+ placement.x = integer(node, 'x', 0);
651
+ if (node.attributes.y !== undefined)
652
+ placement.y = integer(node, 'y', 0);
653
+ if (node.attributes.side !== undefined)
654
+ placement.side = node.attributes.side;
655
+ this.restoreUnit(entry, placement);
656
+ }
657
+ cmdModifyUnit(node) {
658
+ //WML separates which units (`[filter]`) from what changes (`[set]`); a bare
659
+ //`[modify_unit] hp=5` with no `[set]` changes every unit it matches
660
+ const changes = node.children.find((child) => child.tag === 'set')?.attributes ?? node.attributes;
661
+ for (const [, unit] of this.matchingUnits(node))
662
+ applyUnitChanges(unit, changes);
663
+ }
664
+ cmdHealUnit(node) {
665
+ const amount = optionalInteger(node, 'amount');
666
+ const absolute = optionalInteger(node, 'hp');
667
+ if (amount === undefined && absolute === undefined)
668
+ throw new Error('[heal_unit] requires amount or hp');
669
+ for (const [, unit] of this.matchingUnits(node))
670
+ unit.hp = absolute ?? unit.hp + (amount ?? 0);
671
+ }
672
+ cmdCaptureVillage(node) {
673
+ const x = integer(node, 'x', 0);
674
+ const y = integer(node, 'y', 0);
675
+ const entry = { x, y, side: required(node, 'side') };
676
+ if (node.attributes.name !== undefined)
677
+ entry.name = node.attributes.name;
678
+ (this.world.villages ??= {})[`${x},${y}`] = entry;
679
+ }
680
+ cmdClearShroud(node) {
681
+ const side = required(node, 'side');
682
+ const x = integer(node, 'x', 0);
683
+ const y = integer(node, 'y', 0);
684
+ const radius = integer(node, 'radius', 1);
685
+ const cleared = new Set(this.world.clearedShroud?.[side] ?? []);
686
+ for (let dy = -radius; dy <= radius; dy++)
687
+ for (let dx = -radius; dx <= radius; dx++)
688
+ cleared.add(`${x + dx},${y + dy}`);
689
+ (this.world.clearedShroud ??= {})[side] = [...cleared];
690
+ }
691
+ cmdRole(node) {
692
+ const name = node.attributes.role ?? node.attributes.name;
693
+ if (!name)
694
+ throw new Error('[role] requires role');
695
+ //`role`/`name` here name the role being assigned, not a unit to match, so the filter
696
+ //must not read either back as one - the same exclusion the scenario-level role uses
697
+ const matched = this.matchingUnits(node, ['role', 'name']);
698
+ for (const [, unit] of matched)
699
+ unit.role = name;
700
+ (this.world.roles ??= {})[name] = matched.map(([id]) => id);
701
+ }
702
+ cmdSetVariable(node) {
703
+ // `name`/`target` is a path written out, `path` is one content builds from
704
+ // variables. Only the second goes through expansion: a `$` in `name` is not
705
+ // a reference today, and reading it as one would change what content means.
706
+ const computed = node.attributes.path;
707
+ this.setVariable(computed === undefined
708
+ ? node.tag === 'command'
709
+ ? required(node, 'target')
710
+ : required(node, 'name')
711
+ : this.expandPath(computed), node.attributes.value ?? '', node.attributes.mode);
712
+ }
713
+ cmdWhile(node) {
714
+ const limit = integer(node, 'max_iterations', 1000);
715
+ if (limit < 1 || limit > 100_000)
716
+ throw new Error('MWL while max_iterations must be between 1 and 100000');
717
+ for (let iteration = 0; iteration < limit && this.nodeConditionMatches(node); iteration++)
718
+ this.runBlock(this.commandChildren(node));
719
+ }
720
+ cmdForeach(node) {
721
+ const source = this.variableAt(required(node, 'variable'));
722
+ const entries = Array.isArray(source)
723
+ ? source.map((value, index) => [String(index), value])
724
+ : typeof source === 'string'
725
+ ? source
726
+ .split(',')
727
+ .map((value, index) => [String(index), value.trim()])
728
+ .filter(([, value]) => value !== '')
729
+ : source && typeof source === 'object'
730
+ ? Object.entries(source)
731
+ : [];
732
+ const item = node.attributes.item ?? 'item';
733
+ const indexName = node.attributes.index ?? 'index';
734
+ for (let index = 0; index < entries.length; index++) {
735
+ this.setVariableAt(item, entries[index][1]);
736
+ this.setVariableAt(indexName, index);
737
+ this.runBlock(this.commandChildren(node));
775
738
  }
776
739
  }
740
+ cmdSwitch(node) {
741
+ const value = this.variableAt(required(node, 'variable'));
742
+ const selected = node.children.find((child) => child.tag === 'case' && child.attributes.equals !== undefined && sameValue(value, child.attributes.equals));
743
+ const fallback = node.children.find((child) => child.tag === 'default');
744
+ this.runBlock((selected ?? fallback)?.children ?? []);
745
+ }
746
+ cmdWin(node) {
747
+ this.markSideResult(node.attributes.side, 'won');
748
+ this.world.status = 'won';
749
+ }
750
+ cmdLose(node) {
751
+ this.markSideResult(node.attributes.side, 'lost');
752
+ this.world.status = 'lost';
753
+ }
754
+ cmdEndlevel(node) {
755
+ const result = node.attributes.result === 'defeat' ? 'defeat' : 'victory';
756
+ this.world.status = result === 'defeat' ? 'lost' : 'won';
757
+ this.world.carryover = endLevelCarryover(this.world, endLevelSide(this.world, node.attributes.side), {
758
+ result,
759
+ bonus: integerAttribute(node, 'bonus'),
760
+ carryoverPercentage: numberAttribute(node, 'carryover_percentage'),
761
+ carryoverAdd: booleanAttribute(node, 'carryover_add'),
762
+ nextScenario: node.attributes.next_scenario ?? null,
763
+ });
764
+ }
765
+ cmdIf(node) {
766
+ //an immediately following [else] is this branch's other half, not a separate
767
+ //command: the condition picks one body, and runBlock skips the [else] itself
768
+ const fallback = node.children.find((child) => child.tag === 'else');
769
+ const taken = this.nodeConditionMatches(node) ? node : fallback;
770
+ this.runBlock(taken ? this.commandChildren(taken) : []);
771
+ }
777
772
  /**
778
773
  * A node's children that are commands to run, dropping the condition children the schema
779
774
  * allows a branch to carry. A `[condition]` is read by the branch itself, never executed: