optipack 1.0.0 → 1.0.1
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.
- package/BENCHMARKING.md +127 -0
- package/README.md +84 -179
- package/package.json +36 -34
- package/src/bench.js +231 -0
- package/src/cli.js +114 -49
- package/{test/harness/setup.js → src/harness/driver.js} +28 -10
- package/src/harness/loader-hooks.mjs +63 -0
- package/src/harness/mock/@minecraft/common.mjs +17 -0
- package/src/harness/mock/@minecraft/debug-utilities.mjs +10 -0
- package/src/harness/mock/@minecraft/diagnostics.mjs +14 -0
- package/src/harness/mock/@minecraft/server-admin.mjs +14 -0
- package/src/harness/mock/@minecraft/server-editor.mjs +39 -0
- package/src/harness/mock/@minecraft/server-extras.mjs +538 -0
- package/src/harness/mock/@minecraft/server-gametest.mjs +22 -0
- package/src/harness/mock/@minecraft/server-graphics.mjs +21 -0
- package/src/harness/mock/@minecraft/server-net.mjs +15 -0
- package/src/harness/mock/@minecraft/server-ui.mjs +125 -0
- package/{test → src/harness}/mock/@minecraft/server.mjs +160 -40
- package/src/harness/mock/permissive.mjs +71 -0
- package/src/pragma.js +1 -1
- package/test/differential.test.mjs +2 -2
- package/test/example.test.mjs +3 -3
- package/test/fetch-corpus.mjs +92 -0
- package/test/infra.test.mjs +10 -0
- package/test/server-ui-forms/expected.js +15 -0
- package/test/server-ui-forms/expects.json +1 -0
- package/test/server-ui-forms/input.js +25 -0
- package/test/server-ui-forms/scenario.json +1 -0
- package/test/transforms.test.mjs +4 -4
- package/test/update-mock-stubs.mjs +91 -0
- package/test/loader-hooks.mjs +0 -38
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissive stub of @minecraft/server-graphics@1.0.0 for the differential harness.
|
|
3
|
+
* Generated from the module's real index.d.ts (see test/update-mock-stubs.mjs).
|
|
4
|
+
* Every runtime export is a permissive value: callable, constructable, and
|
|
5
|
+
* chainable; calls and constructions record harness effects so differential
|
|
6
|
+
* equivalence holds. NOT a behavioral model of the engine module.
|
|
7
|
+
*/
|
|
8
|
+
import { permissive } from '../permissive.mjs';
|
|
9
|
+
|
|
10
|
+
export const BiomeAtmospherics = permissive('server-graphics.BiomeAtmospherics');
|
|
11
|
+
export const BiomeColorGrading = permissive('server-graphics.BiomeColorGrading');
|
|
12
|
+
export const BiomeLighting = permissive('server-graphics.BiomeLighting');
|
|
13
|
+
export const BiomeWater = permissive('server-graphics.BiomeWater');
|
|
14
|
+
export const getBiomeAtmospherics = permissive('server-graphics.getBiomeAtmospherics');
|
|
15
|
+
export const getBiomeColorGrading = permissive('server-graphics.getBiomeColorGrading');
|
|
16
|
+
export const getBiomeLighting = permissive('server-graphics.getBiomeLighting');
|
|
17
|
+
export const getBiomeWater = permissive('server-graphics.getBiomeWater');
|
|
18
|
+
export const getPlayerAtmospherics = permissive('server-graphics.getPlayerAtmospherics');
|
|
19
|
+
export const getPlayerColorGrading = permissive('server-graphics.getPlayerColorGrading');
|
|
20
|
+
export const getPlayerLighting = permissive('server-graphics.getPlayerLighting');
|
|
21
|
+
export const getPlayerWater = permissive('server-graphics.getPlayerWater');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissive stub of @minecraft/server-net@1.0.0-beta.11940b24 for the differential harness.
|
|
3
|
+
* Generated from the module's real index.d.ts (see test/update-mock-stubs.mjs).
|
|
4
|
+
* Every runtime export is a permissive value: callable, constructable, and
|
|
5
|
+
* chainable; calls and constructions record harness effects so differential
|
|
6
|
+
* equivalence holds. NOT a behavioral model of the engine module.
|
|
7
|
+
*/
|
|
8
|
+
import { permissive } from '../permissive.mjs';
|
|
9
|
+
|
|
10
|
+
export const HttpClient = permissive('server-net.HttpClient');
|
|
11
|
+
export const HttpHeader = permissive('server-net.HttpHeader');
|
|
12
|
+
export const HttpRequest = permissive('server-net.HttpRequest');
|
|
13
|
+
export const HttpRequestMethod = permissive('server-net.HttpRequestMethod');
|
|
14
|
+
export const HttpResponse = permissive('server-net.HttpResponse');
|
|
15
|
+
export const http = permissive('server-net.http');
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Functional mock of @minecraft/server-ui for the differential harness.
|
|
3
|
+
*
|
|
4
|
+
* Models the widely used form surface accurately: ActionFormData,
|
|
5
|
+
* MessageFormData, ModalFormData (chainable builders), uiManager.showPlayer,
|
|
6
|
+
* and the response types. `.show()`/`showPlayer()` record a harness effect
|
|
7
|
+
* carrying the form's kind and title, then resolve with a scripted response
|
|
8
|
+
* from the recorder (key: `uiResponse:<entityId>`, default: a non-canceled
|
|
9
|
+
* selection). Names present in the real 2.2.0 module but not modeled in depth
|
|
10
|
+
* (CustomForm, MessageBox, observables, error classes) are bound to permissive
|
|
11
|
+
* stubs so any fixture using them still links and runs.
|
|
12
|
+
*/
|
|
13
|
+
import { permissive } from '../permissive.mjs';
|
|
14
|
+
|
|
15
|
+
const rec = () => globalThis.__opkRecorder;
|
|
16
|
+
|
|
17
|
+
function scriptedResponse(player) {
|
|
18
|
+
const r = rec();
|
|
19
|
+
const key = 'uiResponse:' + (player && player.id !== undefined ? player.id : 'unknown');
|
|
20
|
+
const base = r ? r.scriptedRead(key, undefined) : undefined;
|
|
21
|
+
if (base) return base;
|
|
22
|
+
return { selection: 0, formValues: [0], canceled: false, cancelationReason: undefined };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class FormBase {
|
|
26
|
+
constructor(kind) {
|
|
27
|
+
this.__kind = kind;
|
|
28
|
+
this.__data = { title: undefined, body: undefined, buttons: [], fields: [] };
|
|
29
|
+
}
|
|
30
|
+
__chain(mutator) {
|
|
31
|
+
mutator();
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
title(text) { return this.__chain(() => { this.__data.title = String(text); }); }
|
|
35
|
+
body(text) { return this.__chain(() => { this.__data.body = String(text); }); }
|
|
36
|
+
__show(player) {
|
|
37
|
+
const r = rec();
|
|
38
|
+
if (r) r.effect('ui.show', { kind: this.__kind, title: this.__data.title, player: player && player.id !== undefined ? player.id : null });
|
|
39
|
+
const response = scriptedResponse(player);
|
|
40
|
+
return Promise.resolve(response);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ActionFormData extends FormBase {
|
|
45
|
+
constructor() { super('ActionFormData'); }
|
|
46
|
+
button(text, iconPath) { return this.__chain(() => { this.__data.buttons.push({ text: String(text), iconPath }); }); }
|
|
47
|
+
divider() { return this.__chain(() => { this.__data.buttons.push({ divider: true }); }); }
|
|
48
|
+
header(text) { return this.__chain(() => { this.__data.header = String(text); }); }
|
|
49
|
+
label(text) { return this.__chain(() => { this.__data.fields.push({ type: 'label', text: String(text) }); }); }
|
|
50
|
+
show(player) { return this.__show(player); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class MessageFormData extends FormBase {
|
|
54
|
+
constructor() { super('MessageFormData'); }
|
|
55
|
+
button1(text) { return this.__chain(() => { this.__data.buttons[0] = { text: String(text) }; }); }
|
|
56
|
+
button2(text) { return this.__chain(() => { this.__data.buttons[1] = { text: String(text) }; }); }
|
|
57
|
+
show(player) { return this.__show(player); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class ModalFormData extends FormBase {
|
|
61
|
+
constructor() { super('ModalFormData'); }
|
|
62
|
+
textField(label, placeholder, options) { return this.__chain(() => { this.__data.fields.push({ type: 'textField', label: String(label), placeholder, options }); }); }
|
|
63
|
+
slider(label, min, max, step, deflt) { return this.__chain(() => { this.__data.fields.push({ type: 'slider', label: String(label), min, max, step, default: deflt }); }); }
|
|
64
|
+
toggle(label, deflt) { return this.__chain(() => { this.__data.fields.push({ type: 'toggle', label: String(label), default: deflt }); }); }
|
|
65
|
+
dropdown(label, options, deflt) { return this.__chain(() => { this.__data.fields.push({ type: 'dropdown', label: String(label), options, default: deflt }); }); }
|
|
66
|
+
divider() { return this.__chain(() => { this.__data.fields.push({ type: 'divider' }); }); }
|
|
67
|
+
header(text) { return this.__chain(() => { this.__data.header = String(text); }); }
|
|
68
|
+
label(text) { return this.__chain(() => { this.__data.fields.push({ type: 'label', text: String(text) }); }); }
|
|
69
|
+
submitButton(text) { return this.__chain(() => { this.__data.submitButtonText = String(text); }); }
|
|
70
|
+
show(player) { return this.__show(player); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class UIManagerImpl {
|
|
74
|
+
showPlayer(player, form) {
|
|
75
|
+
const r = rec();
|
|
76
|
+
if (r) r.effect('ui.showPlayer', { kind: form && form.__kind, title: form && form.__data && form.__data.title, player: player && player.id !== undefined ? player.id : null });
|
|
77
|
+
const response = scriptedResponse(player);
|
|
78
|
+
return Promise.resolve(response);
|
|
79
|
+
}
|
|
80
|
+
close(player) {
|
|
81
|
+
const r = rec();
|
|
82
|
+
if (r) r.effect('ui.close', { player: player && player.id !== undefined ? player.id : null });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const uiManager = new UIManagerImpl();
|
|
87
|
+
|
|
88
|
+
// Response constructors: usable with `new`, mirror the scripted/plain shape.
|
|
89
|
+
export class FormResponse {
|
|
90
|
+
constructor(init = {}) { Object.assign(this, { canceled: false, cancelationReason: undefined }, init); }
|
|
91
|
+
}
|
|
92
|
+
export class ActionFormResponse extends FormResponse {
|
|
93
|
+
constructor(init = {}) { super({ selection: 0, ...init }); }
|
|
94
|
+
}
|
|
95
|
+
export class MessageFormResponse extends FormResponse {
|
|
96
|
+
constructor(init = {}) { super({ selection: 0, ...init }); }
|
|
97
|
+
}
|
|
98
|
+
export class ModalFormResponse extends FormResponse {
|
|
99
|
+
constructor(init = {}) { super({ formValues: [], ...init }); }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const FormCancelationReason = Object.freeze({
|
|
103
|
+
UserBusy: 'UserBusy',
|
|
104
|
+
UserClosed: 'UserClosed',
|
|
105
|
+
ServerCrashed: 'ServerCrashed',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Present in the real module but not modeled in depth: permissive stubs so
|
|
109
|
+
// named imports link and recorded effects stay identical on both sides.
|
|
110
|
+
export const CustomForm = permissive('server-ui.CustomForm');
|
|
111
|
+
export const MessageBox = permissive('server-ui.MessageBox');
|
|
112
|
+
export const ObservableBoolean = permissive('server-ui.ObservableBoolean');
|
|
113
|
+
export const ObservableNumber = permissive('server-ui.ObservableNumber');
|
|
114
|
+
export const ObservableString = permissive('server-ui.ObservableString');
|
|
115
|
+
export const ObservableUIRawMessage = permissive('server-ui.ObservableUIRawMessage');
|
|
116
|
+
export const DataDrivenScreenClosedReason = permissive('server-ui.DataDrivenScreenClosedReason');
|
|
117
|
+
export const FormRejectError = permissive('server-ui.FormRejectError');
|
|
118
|
+
export const FormRejectReason = permissive('server-ui.FormRejectReason');
|
|
119
|
+
export const FormVisibilityError = permissive('server-ui.FormVisibilityError');
|
|
120
|
+
export const FormVisibilityErrorReason = permissive('server-ui.FormVisibilityErrorReason');
|
|
121
|
+
export const InvalidFormError = permissive('server-ui.InvalidFormError');
|
|
122
|
+
export const InvalidFormModificationError = permissive('server-ui.InvalidFormModificationError');
|
|
123
|
+
export const InvalidObservableError = permissive('server-ui.InvalidObservableError');
|
|
124
|
+
export const PlayerLeftError = permissive('server-ui.PlayerLeftError');
|
|
125
|
+
export const TextFilteringError = permissive('server-ui.TextFilteringError');
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mock @minecraft/server for differential
|
|
2
|
+
* Mock @minecraft/server for the differential harness and `optipack bench`.
|
|
3
3
|
*
|
|
4
4
|
* Effect-recorder contract: only MUTATING calls and registrations are recorded
|
|
5
5
|
* as effects. Pure reads return scripted sequences and are intentionally not
|
|
6
6
|
* recorded — transforms may legitimately eliminate reads, that is the point.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Boundary counters: the optipack-relevant metric — host-boundary crossings
|
|
9
|
+
* (location reads, getComponent, getDynamicProperty, ...) — is counted on the
|
|
10
|
+
* recorder (`r.counters`). Location reads return a FRESH object per read, like
|
|
11
|
+
* the engine (each read allocates a new Vector3), so allocation pressure is
|
|
12
|
+
* visible in the counter too.
|
|
13
|
+
*
|
|
14
|
+
* Tolerance: arbitrary real-world scripts use wider API surface than any mock
|
|
15
|
+
* can enumerate. world/Dimension/Entity instances are wrapped so unknown
|
|
16
|
+
* members resolve to permissive values that record `stub.*` effects (identical
|
|
17
|
+
* on both sides of a comparison), and event collections accept ANY event name.
|
|
18
|
+
* Known hot-path members keep real implementations with counters.
|
|
11
19
|
*/
|
|
12
20
|
|
|
13
21
|
const rec = () => globalThis.__opkRecorder;
|
|
@@ -23,6 +31,11 @@ function effect(call, args) {
|
|
|
23
31
|
if (r) r.effect(call, args === undefined ? null : args);
|
|
24
32
|
}
|
|
25
33
|
|
|
34
|
+
function count(kind) {
|
|
35
|
+
const r = rec();
|
|
36
|
+
if (r) r.count(kind);
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
function serialize(v) {
|
|
27
40
|
if (v === null) return null;
|
|
28
41
|
const t = typeof v;
|
|
@@ -38,18 +51,87 @@ function serialize(v) {
|
|
|
38
51
|
return String(v);
|
|
39
52
|
}
|
|
40
53
|
|
|
54
|
+
/** Permissive fallback for unknown members on wrapped host objects. */
|
|
55
|
+
function unknownMember(label) {
|
|
56
|
+
const asPrimitive = (hint) => (hint === 'number' ? 0 : label);
|
|
57
|
+
const fn = function (...args) {
|
|
58
|
+
effect('stub.call', { target: label, args: serialize(args) });
|
|
59
|
+
return unknownMember(label + '()');
|
|
60
|
+
};
|
|
61
|
+
fn.toString = () => label;
|
|
62
|
+
fn.valueOf = () => label;
|
|
63
|
+
return new Proxy(fn, {
|
|
64
|
+
get(target, prop, receiver) {
|
|
65
|
+
if (prop === Symbol.toPrimitive) return asPrimitive;
|
|
66
|
+
if (typeof prop === 'symbol') return undefined;
|
|
67
|
+
if (prop === 'then' || prop === 'catch' || prop === 'finally' || prop === 'toJSON') return undefined;
|
|
68
|
+
if (prop === 'toString' || prop === 'valueOf') return Reflect.get(target, prop, receiver);
|
|
69
|
+
return unknownMember(label + '.' + String(prop));
|
|
70
|
+
},
|
|
71
|
+
construct(target, args) {
|
|
72
|
+
effect('stub.new', { target: label, args: serialize(args) });
|
|
73
|
+
return unknownMember('new ' + label);
|
|
74
|
+
},
|
|
75
|
+
apply(target, thisArg, args) {
|
|
76
|
+
effect('stub.call', { target: label, args: serialize(args) });
|
|
77
|
+
return unknownMember(label + '()');
|
|
78
|
+
},
|
|
79
|
+
has() { return true; },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class Block {
|
|
84
|
+
constructor(location, dimensionId) {
|
|
85
|
+
this.location = location;
|
|
86
|
+
this.__dim = dimensionId;
|
|
87
|
+
this.typeId = scriptedRead(`blockType:${this.__dim}`, 'minecraft:stone');
|
|
88
|
+
}
|
|
89
|
+
get permutation() {
|
|
90
|
+
count('boundary.block.permutation');
|
|
91
|
+
return scriptedRead(`blockPerm:${this.__dim}`, { type: this.typeId });
|
|
92
|
+
}
|
|
93
|
+
setPermutation(permutation) {
|
|
94
|
+
effect('block.setPermutation', { dimension: this.__dim, location: serialize(this.location), permutation: serialize(permutation) });
|
|
95
|
+
}
|
|
96
|
+
setType(blockType) {
|
|
97
|
+
effect('block.setType', { dimension: this.__dim, location: serialize(this.location), type: serialize(blockType) });
|
|
98
|
+
}
|
|
99
|
+
getComponent(name) {
|
|
100
|
+
count('boundary.getComponent');
|
|
101
|
+
return scriptedRead(`component:block:${name}`, undefined);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function wrapHost(instance, label) {
|
|
106
|
+
return new Proxy(instance, {
|
|
107
|
+
get(target, prop, receiver) {
|
|
108
|
+
if (typeof prop === 'symbol' || prop === 'then' || prop === 'catch' || prop === 'finally') {
|
|
109
|
+
return typeof prop === 'symbol' ? undefined : Reflect.get(target, prop, receiver);
|
|
110
|
+
}
|
|
111
|
+
if (prop in target) return Reflect.get(target, prop, receiver);
|
|
112
|
+
return unknownMember(label + '.' + String(prop));
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
41
117
|
class Dimension {
|
|
42
118
|
constructor(id) {
|
|
43
119
|
this.id = id;
|
|
44
120
|
}
|
|
45
121
|
getEntities(options) {
|
|
122
|
+
count('boundary.getEntities');
|
|
46
123
|
effect('dimension.getEntities', { dimension: this.id, options: serialize(options) });
|
|
47
124
|
return scriptedRead(`entities:${this.id}`, []);
|
|
48
125
|
}
|
|
49
126
|
getPlayers() {
|
|
127
|
+
count('boundary.getPlayers');
|
|
50
128
|
effect('dimension.getPlayers', { dimension: this.id });
|
|
51
129
|
return scriptedRead(`players:${this.id}`, []);
|
|
52
130
|
}
|
|
131
|
+
get players() {
|
|
132
|
+
count('boundary.players');
|
|
133
|
+
return scriptedRead(`players:${this.id}`, []);
|
|
134
|
+
}
|
|
53
135
|
spawnEntity(type, location) {
|
|
54
136
|
effect('dimension.spawnEntity', { dimension: this.id, type, location: serialize(location) });
|
|
55
137
|
return scriptedRead(`spawn:${this.id}:${type}`, null);
|
|
@@ -60,6 +142,19 @@ class Dimension {
|
|
|
60
142
|
runCommand(commandString) {
|
|
61
143
|
effect('dimension.runCommand', { dimension: this.id, command: commandString });
|
|
62
144
|
}
|
|
145
|
+
getBlock(location) {
|
|
146
|
+
count('boundary.getBlock');
|
|
147
|
+
return new Block({ x: location.x, y: location.y, z: location.z }, this.id);
|
|
148
|
+
}
|
|
149
|
+
getEntitiesAtBlockLocation(location) {
|
|
150
|
+
count('boundary.getEntitiesAtBlockLocation');
|
|
151
|
+
effect('dimension.getEntitiesAtBlockLocation', { dimension: this.id, location: serialize(location) });
|
|
152
|
+
return scriptedRead(`entitiesAt:${this.id}`, []);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function wrapDimension(dim) {
|
|
157
|
+
return wrapHost(dim, 'dimension.' + dim.id);
|
|
63
158
|
}
|
|
64
159
|
|
|
65
160
|
class Entity {
|
|
@@ -67,6 +162,7 @@ class Entity {
|
|
|
67
162
|
this._id = id;
|
|
68
163
|
this.typeId = typeId;
|
|
69
164
|
this._dimension = dimension;
|
|
165
|
+
return wrapHost(this, 'entity.' + id);
|
|
70
166
|
}
|
|
71
167
|
get id() {
|
|
72
168
|
return this._id;
|
|
@@ -75,19 +171,30 @@ class Entity {
|
|
|
75
171
|
return scriptedRead(`name:${this._id}`, 'player' + this._id);
|
|
76
172
|
}
|
|
77
173
|
get location() {
|
|
78
|
-
|
|
174
|
+
count('boundary.location');
|
|
175
|
+
const v = scriptedRead(`location:${this._id}`, { x: 0, y: 0, z: 0 });
|
|
176
|
+
// The engine allocates a fresh Vector3 on every read; mirror that.
|
|
177
|
+
return { x: v.x, y: v.y, z: v.z };
|
|
79
178
|
}
|
|
80
179
|
get dimension() {
|
|
180
|
+
count('boundary.dimension');
|
|
81
181
|
return this._dimension;
|
|
82
182
|
}
|
|
83
183
|
getComponent(componentId) {
|
|
84
|
-
|
|
184
|
+
count('boundary.getComponent');
|
|
185
|
+
const v = scriptedRead(`component:${this._id}:${componentId}`, undefined);
|
|
186
|
+
// Real-world scripts assume queried components exist; default to a
|
|
187
|
+
// chainable permissive component instead of undefined (identical on both
|
|
188
|
+
// sides of a comparison).
|
|
189
|
+
return v === undefined ? unknownMember('entity.' + this._id + '.component.' + componentId) : v;
|
|
85
190
|
}
|
|
86
191
|
getComponents() {
|
|
192
|
+
count('boundary.getComponents');
|
|
87
193
|
effect('entity.getComponents', { entity: this._id });
|
|
88
194
|
return [];
|
|
89
195
|
}
|
|
90
196
|
getDynamicProperty(identifier) {
|
|
197
|
+
count('boundary.getDynamicProperty');
|
|
91
198
|
return scriptedRead(`dp:${this._id}:${identifier}`, undefined);
|
|
92
199
|
}
|
|
93
200
|
setDynamicProperty(identifier, value) {
|
|
@@ -106,6 +213,12 @@ class Entity {
|
|
|
106
213
|
effect('entity.applyDamage', { entity: this._id, amount, options: serialize(options) });
|
|
107
214
|
return scriptedRead(`damage:${this._id}`, true);
|
|
108
215
|
}
|
|
216
|
+
applyKnockback(direction, horizontalStrength, verticalStrength) {
|
|
217
|
+
effect('entity.applyKnockback', { entity: this._id, direction: serialize(direction), horizontalStrength, verticalStrength });
|
|
218
|
+
}
|
|
219
|
+
applyImpulse(impulse) {
|
|
220
|
+
effect('entity.applyImpulse', { entity: this._id, impulse: serialize(impulse) });
|
|
221
|
+
}
|
|
109
222
|
kill() {
|
|
110
223
|
effect('entity.kill', { entity: this._id });
|
|
111
224
|
}
|
|
@@ -127,9 +240,11 @@ class Entity {
|
|
|
127
240
|
effect('entity.triggerEvent', { entity: this._id, eventName });
|
|
128
241
|
}
|
|
129
242
|
getRotation() {
|
|
243
|
+
count('boundary.getRotation');
|
|
130
244
|
return scriptedRead(`rotation:${this._id}`, { x: 0, y: 0 });
|
|
131
245
|
}
|
|
132
246
|
getVelocity() {
|
|
247
|
+
count('boundary.getVelocity');
|
|
133
248
|
return scriptedRead(`velocity:${this._id}`, { x: 0, y: 0, z: 0 });
|
|
134
249
|
}
|
|
135
250
|
}
|
|
@@ -149,60 +264,63 @@ function makeEvent(fullName) {
|
|
|
149
264
|
};
|
|
150
265
|
}
|
|
151
266
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
playerInteractWithBlock: makeEvent('world.beforeEvents.playerInteractWithBlock'),
|
|
166
|
-
playerInteractWithEntity: makeEvent('world.beforeEvents.playerInteractWithEntity'),
|
|
167
|
-
itemUse: makeEvent('world.beforeEvents.itemUse'),
|
|
168
|
-
chatSend: makeEvent('world.beforeEvents.chatSend'),
|
|
169
|
-
};
|
|
267
|
+
// Event collections accept ANY event name (real-world scripts use the full
|
|
268
|
+
// API surface; unknown names record the same effect shape).
|
|
269
|
+
function eventCollection(prefix) {
|
|
270
|
+
return new Proxy({}, {
|
|
271
|
+
get(target, prop) {
|
|
272
|
+
if (typeof prop === 'symbol' || prop === 'then') return undefined;
|
|
273
|
+
if (prop in target) return target[prop];
|
|
274
|
+
const ev = makeEvent(`${prefix}.${String(prop)}`);
|
|
275
|
+
target[prop] = ev;
|
|
276
|
+
return ev;
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
170
280
|
|
|
171
281
|
function currentPlayers() {
|
|
172
282
|
const r = rec();
|
|
173
283
|
if (!r) return [];
|
|
174
|
-
|
|
284
|
+
if (!r._players) {
|
|
285
|
+
const specs = (r.state && r.state.players) || [];
|
|
286
|
+
r._players = specs.map((s) => new Entity(s.id, s.typeId || 'minecraft:player', wrapDimension(new Dimension(s.dimension || 'minecraft:overworld'))));
|
|
287
|
+
}
|
|
175
288
|
return r._players;
|
|
176
289
|
}
|
|
177
290
|
|
|
178
291
|
function ensurePlayers(r) {
|
|
179
292
|
if (r._players) return r._players;
|
|
180
293
|
const specs = (r.state && r.state.players) || [];
|
|
181
|
-
r._players = specs.map((s) => new Entity(s.id, s.typeId || 'minecraft:player', new Dimension(s.dimension || 'minecraft:overworld')));
|
|
294
|
+
r._players = specs.map((s) => new Entity(s.id, s.typeId || 'minecraft:player', wrapDimension(new Dimension(s.dimension || 'minecraft:overworld'))));
|
|
182
295
|
return r._players;
|
|
183
296
|
}
|
|
184
297
|
|
|
185
|
-
// The differential driver hydrates event payloads via
|
|
298
|
+
// The differential driver hydrates event payloads via these hooks.
|
|
186
299
|
if (typeof globalThis !== 'undefined') {
|
|
187
300
|
globalThis.__opkEnsurePlayers = ensurePlayers;
|
|
301
|
+
globalThis.__opkMakeDimension = (id) => wrapDimension(new Dimension(id));
|
|
302
|
+
globalThis.__opkMakeBlock = (location, dimensionId) => new Block(location, dimensionId || 'minecraft:overworld');
|
|
188
303
|
}
|
|
189
304
|
|
|
190
|
-
|
|
305
|
+
const worldImpl = {
|
|
191
306
|
get afterEvents() {
|
|
192
|
-
return
|
|
307
|
+
return eventCollection('world.afterEvents');
|
|
193
308
|
},
|
|
194
309
|
get beforeEvents() {
|
|
195
|
-
return
|
|
310
|
+
return eventCollection('world.beforeEvents');
|
|
196
311
|
},
|
|
197
312
|
getAllPlayers() {
|
|
313
|
+
count('boundary.getAllPlayers');
|
|
198
314
|
effect('world.getAllPlayers', null);
|
|
199
315
|
return currentPlayers();
|
|
200
316
|
},
|
|
201
317
|
getPlayers() {
|
|
318
|
+
count('boundary.getPlayers');
|
|
202
319
|
effect('world.getPlayers', null);
|
|
203
320
|
return currentPlayers();
|
|
204
321
|
},
|
|
205
322
|
getDynamicProperty(identifier) {
|
|
323
|
+
count('boundary.getDynamicProperty');
|
|
206
324
|
return scriptedRead(`world.dp:${identifier}`, undefined);
|
|
207
325
|
},
|
|
208
326
|
setDynamicProperty(identifier, value) {
|
|
@@ -217,21 +335,17 @@ export const world = {
|
|
|
217
335
|
},
|
|
218
336
|
getDimension(dimensionId) {
|
|
219
337
|
effect('world.getDimension', { id: dimensionId });
|
|
220
|
-
return new Dimension(dimensionId);
|
|
338
|
+
return wrapDimension(new Dimension(dimensionId));
|
|
221
339
|
},
|
|
222
340
|
playSound(soundId, options) {
|
|
223
341
|
effect('world.playSound', { soundId, options: serialize(options) });
|
|
224
342
|
},
|
|
225
343
|
};
|
|
226
344
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const systemAfterEvents = {
|
|
232
|
-
tick: makeEvent('system.afterEvents.tick'),
|
|
233
|
-
watchdogTerminate: makeEvent('system.afterEvents.watchdogTerminate'),
|
|
234
|
-
};
|
|
345
|
+
export const world = wrapHost(worldImpl, 'world');
|
|
346
|
+
|
|
347
|
+
const systemBeforeEvents = eventCollection('system.beforeEvents');
|
|
348
|
+
const systemAfterEvents = eventCollection('system.afterEvents');
|
|
235
349
|
|
|
236
350
|
export const system = {
|
|
237
351
|
get beforeEvents() {
|
|
@@ -268,7 +382,13 @@ export const system = {
|
|
|
268
382
|
},
|
|
269
383
|
};
|
|
270
384
|
|
|
271
|
-
export { Entity, Dimension };
|
|
385
|
+
export { Entity, Dimension, Block };
|
|
272
386
|
export const TicksPerSecond = 20;
|
|
273
387
|
export const GameMode = { survival: 'survival', creative: 'creative', adventure: 'adventure' };
|
|
388
|
+
|
|
389
|
+
// Full API surface: every other runtime export of the real module is a
|
|
390
|
+
// permissive value (generated from the real index.d.ts) so real-world
|
|
391
|
+
// scripts' named imports link under the harness.
|
|
392
|
+
export * from './server-extras.mjs';
|
|
393
|
+
|
|
274
394
|
export default { world, system, Entity, Dimension, TicksPerSecond, GameMode };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissive stub factory for @minecraft/* runtime modules that optipack's
|
|
3
|
+
* differential harness does not model in depth.
|
|
4
|
+
*
|
|
5
|
+
* Every exported name from a module's real index.d.ts is bound to a permissive
|
|
6
|
+
* value: callable, constructable, and property-accessible indefinitely. Calls
|
|
7
|
+
* and constructions are recorded as harness effects (identically on the
|
|
8
|
+
* original and optimized sides), so differential equivalence still holds while
|
|
9
|
+
* scripts exercise these modules. Property GETS are silent — only calls and
|
|
10
|
+
* `new` produce effects — so chained accessors do not spam the trace.
|
|
11
|
+
*
|
|
12
|
+
* Not a behavioral model: it exists so fixtures importing these modules link
|
|
13
|
+
* and run. Behavior differences between stub and engine are identical on both
|
|
14
|
+
* sides of a differential comparison, which is the only claim it supports.
|
|
15
|
+
*/
|
|
16
|
+
const rec = () => globalThis.__opkRecorder;
|
|
17
|
+
|
|
18
|
+
function record(call, args) {
|
|
19
|
+
const r = rec();
|
|
20
|
+
if (r) r.effect(call, args);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function shallow(v, depth = 0) {
|
|
24
|
+
if (v === null) return null;
|
|
25
|
+
const t = typeof v;
|
|
26
|
+
if (t === 'string' || t === 'number' || t === 'boolean' || t === 'undefined') return v;
|
|
27
|
+
if (depth > 2) return String(t);
|
|
28
|
+
if (Array.isArray(v)) return v.map((x) => shallow(x, depth + 1));
|
|
29
|
+
if (t === 'object') {
|
|
30
|
+
const out = {};
|
|
31
|
+
for (const k of Object.keys(v)) out[k] = shallow(v[k], depth + 1);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
return String(t);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const NON_VALUE_SYMBOLS = new Set([
|
|
38
|
+
'then', 'catch', 'finally', 'toJSON', 'inspect', 'constructor',
|
|
39
|
+
Symbol.toPrimitive, Symbol.toStringTag, Symbol.iterator,
|
|
40
|
+
'nodejs.util.inspect.custom', 'asymmetricMatcher',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
export function permissive(label) {
|
|
44
|
+
const asPrimitive = (hint) => (hint === 'number' ? 0 : label);
|
|
45
|
+
const fn = function (...args) {
|
|
46
|
+
record('stub.call', { target: label, args: shallow(args) });
|
|
47
|
+
return permissive(label + '()');
|
|
48
|
+
};
|
|
49
|
+
fn.toString = () => label;
|
|
50
|
+
fn.valueOf = () => label;
|
|
51
|
+
return new Proxy(fn, {
|
|
52
|
+
get(target, prop, receiver) {
|
|
53
|
+
if (prop === Symbol.toPrimitive) return asPrimitive;
|
|
54
|
+
if (typeof prop === 'symbol') return undefined;
|
|
55
|
+
if (prop === 'then' || prop === 'catch' || prop === 'finally' || prop === 'toJSON') return undefined;
|
|
56
|
+
if (prop === 'toString' || prop === 'valueOf') return Reflect.get(target, prop, receiver);
|
|
57
|
+
return permissive(label + '.' + String(prop));
|
|
58
|
+
},
|
|
59
|
+
construct(target, args) {
|
|
60
|
+
record('stub.new', { target: label, args: shallow(args) });
|
|
61
|
+
return permissive('new ' + label);
|
|
62
|
+
},
|
|
63
|
+
apply(target, thisArg, args) {
|
|
64
|
+
record('stub.call', { target: label, args: shallow(args) });
|
|
65
|
+
return permissive(label + '()');
|
|
66
|
+
},
|
|
67
|
+
has() {
|
|
68
|
+
return true;
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
package/src/pragma.js
CHANGED
|
@@ -75,7 +75,7 @@ export class PragmaInfo {
|
|
|
75
75
|
* start line only; callers walk ancestors.
|
|
76
76
|
*/
|
|
77
77
|
disabledAt(node) {
|
|
78
|
-
if (!node.loc) return null;
|
|
78
|
+
if (!node || !node.loc) return null;
|
|
79
79
|
const start = node.loc.start.line;
|
|
80
80
|
const direct = this.info.disableByLine.get(start);
|
|
81
81
|
if (direct) return direct;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
2
|
* Bespoke differential scenarios.
|
|
3
3
|
*
|
|
4
4
|
* Every scenario loads the original and optimized entry with the mock
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import test from 'node:test';
|
|
10
10
|
import assert from 'node:assert/strict';
|
|
11
|
-
import { differentialCheck, runVariant, pathToUrl, buildVariantPack } from '
|
|
11
|
+
import { differentialCheck, runVariant, pathToUrl, buildVariantPack } from '../src/harness/driver.js';
|
|
12
12
|
|
|
13
13
|
const PLAYER_READS = {
|
|
14
14
|
'location:p1': [{ x: 1, y: 64, z: 2 }, { x: 2, y: 65, z: 3 }],
|
package/test/example.test.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
2
|
* Example pack end-to-end: run the CLI over example/, verify parse validity,
|
|
3
3
|
* manifest fidelity, effects-equivalence across the whole script graph, report
|
|
4
4
|
* snapshot, and determinism.
|
|
@@ -57,7 +57,7 @@ test('example: effects-equivalence across the whole script graph', async () => {
|
|
|
57
57
|
const res = spawnSync(process.execPath, [CLI, EXAMPLE, out], { encoding: 'utf8' });
|
|
58
58
|
assert.equal(res.status, 0, res.stderr);
|
|
59
59
|
|
|
60
|
-
const { runVariant, assertSameEffects, pathToUrl } = await import('
|
|
60
|
+
const { runVariant, assertSameEffects, pathToUrl } = await import('../src/harness/driver.js');
|
|
61
61
|
const scenario = {
|
|
62
62
|
reads: {
|
|
63
63
|
'location:p1': [{ x: 10, y: 64, z: 20 }, { x: 12, y: 65, z: 22 }],
|
|
@@ -100,7 +100,7 @@ test('example: report snapshot is stable', () => {
|
|
|
100
100
|
assert.deepEqual(got, expected, 'report matches snapshot');
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
test('example: deterministic output
|
|
103
|
+
test('example: deterministic output — two runs are byte-identical', () => {
|
|
104
104
|
const out1 = mkOut('optipack-det1-');
|
|
105
105
|
const out2 = mkOut('optipack-det2-');
|
|
106
106
|
spawnSync(process.execPath, [CLI, EXAMPLE, out1], { encoding: 'utf8' });
|