quake2ts 0.0.582 → 0.0.584
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/package.json +3 -3
- package/packages/test-utils/dist/index.cjs +297 -538
- package/packages/test-utils/dist/index.cjs.map +1 -1
- package/packages/test-utils/dist/index.d.cts +118 -107
- package/packages/test-utils/dist/index.d.ts +118 -107
- package/packages/test-utils/dist/index.js +297 -537
- package/packages/test-utils/dist/index.js.map +1 -1
|
@@ -1912,7 +1912,7 @@ function teardownBrowserEnvironment() {
|
|
|
1912
1912
|
}
|
|
1913
1913
|
|
|
1914
1914
|
// src/setup/canvas.ts
|
|
1915
|
-
import { Canvas as Canvas2,
|
|
1915
|
+
import { Canvas as Canvas2, ImageData as ImageData2 } from "@napi-rs/canvas";
|
|
1916
1916
|
function createMockCanvas(width = 300, height = 150) {
|
|
1917
1917
|
if (typeof document !== "undefined" && document.createElement) {
|
|
1918
1918
|
const canvas2 = document.createElement("canvas");
|
|
@@ -1920,75 +1920,92 @@ function createMockCanvas(width = 300, height = 150) {
|
|
|
1920
1920
|
canvas2.height = height;
|
|
1921
1921
|
return canvas2;
|
|
1922
1922
|
}
|
|
1923
|
-
const
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1923
|
+
const napiCanvas = new Canvas2(width, height);
|
|
1924
|
+
const canvas = {
|
|
1925
|
+
width,
|
|
1926
|
+
height,
|
|
1927
|
+
getContext: (contextId, options) => {
|
|
1928
|
+
if (contextId === "2d") {
|
|
1929
|
+
return napiCanvas.getContext("2d", options);
|
|
1930
|
+
}
|
|
1931
|
+
if (contextId === "webgl2") {
|
|
1932
|
+
return createMockWebGL2Context(canvas);
|
|
1933
|
+
}
|
|
1934
|
+
return null;
|
|
1935
|
+
},
|
|
1936
|
+
toDataURL: () => napiCanvas.toDataURL(),
|
|
1937
|
+
toBuffer: (mime) => napiCanvas.toBuffer(mime)
|
|
1938
|
+
// Add other properties as needed
|
|
1933
1939
|
};
|
|
1934
1940
|
return canvas;
|
|
1935
1941
|
}
|
|
1936
1942
|
function createMockCanvasContext2D(canvas) {
|
|
1937
|
-
|
|
1938
|
-
|
|
1943
|
+
const c = canvas || createMockCanvas();
|
|
1944
|
+
const ctx = c.getContext("2d");
|
|
1945
|
+
if (!ctx) {
|
|
1946
|
+
throw new Error("Failed to create 2D context");
|
|
1939
1947
|
}
|
|
1940
|
-
return
|
|
1948
|
+
return ctx;
|
|
1941
1949
|
}
|
|
1942
1950
|
function captureCanvasDrawCalls(context) {
|
|
1943
|
-
const
|
|
1944
|
-
const
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
"
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
"beginPath",
|
|
1952
|
-
"closePath",
|
|
1953
|
-
"moveTo",
|
|
1954
|
-
"lineTo",
|
|
1955
|
-
"arc",
|
|
1956
|
-
"arcTo",
|
|
1957
|
-
"bezierCurveTo",
|
|
1958
|
-
"quadraticCurveTo",
|
|
1959
|
-
"stroke",
|
|
1960
|
-
"fill",
|
|
1961
|
-
"putImageData"
|
|
1962
|
-
];
|
|
1963
|
-
methodsToSpy.forEach((method) => {
|
|
1964
|
-
const original = context[method];
|
|
1965
|
-
if (typeof original === "function") {
|
|
1966
|
-
context[method] = function(...args) {
|
|
1967
|
-
drawCalls.push({ method, args });
|
|
1968
|
-
return original.apply(this, args);
|
|
1951
|
+
const calls = [];
|
|
1952
|
+
const proto = Object.getPrototypeOf(context);
|
|
1953
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
1954
|
+
const value = context[key];
|
|
1955
|
+
if (typeof value === "function") {
|
|
1956
|
+
context[key] = function(...args) {
|
|
1957
|
+
calls.push({ method: key, args });
|
|
1958
|
+
return value.apply(context, args);
|
|
1969
1959
|
};
|
|
1970
1960
|
}
|
|
1971
|
-
}
|
|
1972
|
-
return
|
|
1961
|
+
}
|
|
1962
|
+
return calls;
|
|
1973
1963
|
}
|
|
1974
1964
|
function createMockImageData(width, height, fillColor) {
|
|
1975
|
-
|
|
1965
|
+
if (typeof global.ImageData !== "undefined") {
|
|
1966
|
+
const data2 = new Uint8ClampedArray(width * height * 4);
|
|
1967
|
+
if (fillColor) {
|
|
1968
|
+
for (let i = 0; i < data2.length; i += 4) {
|
|
1969
|
+
data2[i] = fillColor[0];
|
|
1970
|
+
data2[i + 1] = fillColor[1];
|
|
1971
|
+
data2[i + 2] = fillColor[2];
|
|
1972
|
+
data2[i + 3] = fillColor[3];
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return new global.ImageData(data2, width, height);
|
|
1976
|
+
}
|
|
1977
|
+
const data = new Uint8ClampedArray(width * height * 4);
|
|
1976
1978
|
if (fillColor) {
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
imageData.data[i + 3] = a;
|
|
1979
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
1980
|
+
data[i] = fillColor[0];
|
|
1981
|
+
data[i + 1] = fillColor[1];
|
|
1982
|
+
data[i + 2] = fillColor[2];
|
|
1983
|
+
data[i + 3] = fillColor[3];
|
|
1983
1984
|
}
|
|
1984
1985
|
}
|
|
1985
|
-
return
|
|
1986
|
+
return new ImageData2(data, width, height);
|
|
1986
1987
|
}
|
|
1987
|
-
function createMockImage(width, height, src) {
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1988
|
+
function createMockImage(width = 100, height = 100, src = "") {
|
|
1989
|
+
if (typeof document !== "undefined" && document.createElement) {
|
|
1990
|
+
const img2 = document.createElement("img");
|
|
1991
|
+
img2.width = width;
|
|
1992
|
+
img2.height = height;
|
|
1993
|
+
if (src) img2.src = src;
|
|
1994
|
+
return img2;
|
|
1995
|
+
}
|
|
1996
|
+
const img = {
|
|
1997
|
+
width,
|
|
1998
|
+
height,
|
|
1999
|
+
src,
|
|
2000
|
+
complete: true,
|
|
2001
|
+
onload: null,
|
|
2002
|
+
onerror: null
|
|
2003
|
+
};
|
|
2004
|
+
if (src) {
|
|
2005
|
+
setTimeout(() => {
|
|
2006
|
+
if (img.onload) img.onload();
|
|
2007
|
+
}, 0);
|
|
2008
|
+
}
|
|
1992
2009
|
return img;
|
|
1993
2010
|
}
|
|
1994
2011
|
|
|
@@ -2056,193 +2073,89 @@ function setupWebGPUMocks() {
|
|
|
2056
2073
|
}
|
|
2057
2074
|
|
|
2058
2075
|
// src/setup/timing.ts
|
|
2059
|
-
var activeMockRAF;
|
|
2060
2076
|
function createMockRAF() {
|
|
2061
2077
|
let callbacks = [];
|
|
2062
|
-
let
|
|
2078
|
+
let lastId = 0;
|
|
2063
2079
|
let currentTime = 0;
|
|
2064
2080
|
const originalRAF = global.requestAnimationFrame;
|
|
2065
2081
|
const originalCancelRAF = global.cancelAnimationFrame;
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
callbacks.push({ id, callback });
|
|
2069
|
-
return
|
|
2082
|
+
global.requestAnimationFrame = (callback) => {
|
|
2083
|
+
lastId++;
|
|
2084
|
+
callbacks.push({ id: lastId, callback });
|
|
2085
|
+
return lastId;
|
|
2070
2086
|
};
|
|
2071
|
-
|
|
2087
|
+
global.cancelAnimationFrame = (id) => {
|
|
2072
2088
|
callbacks = callbacks.filter((cb) => cb.id !== id);
|
|
2073
2089
|
};
|
|
2074
|
-
|
|
2075
|
-
tick(
|
|
2076
|
-
if (
|
|
2077
|
-
|
|
2078
|
-
} else {
|
|
2079
|
-
currentTime = timestamp;
|
|
2080
|
-
}
|
|
2090
|
+
return {
|
|
2091
|
+
tick(time) {
|
|
2092
|
+
if (time) currentTime = time;
|
|
2093
|
+
else currentTime += 16.66;
|
|
2081
2094
|
const currentCallbacks = [...callbacks];
|
|
2082
2095
|
callbacks = [];
|
|
2083
|
-
currentCallbacks.forEach((
|
|
2084
|
-
callback(currentTime);
|
|
2085
|
-
});
|
|
2096
|
+
currentCallbacks.forEach((cb) => cb.callback(currentTime));
|
|
2086
2097
|
},
|
|
2087
|
-
advance(
|
|
2088
|
-
|
|
2098
|
+
advance(ms) {
|
|
2099
|
+
currentTime += ms;
|
|
2100
|
+
this.tick(currentTime);
|
|
2089
2101
|
},
|
|
2090
2102
|
getCallbacks() {
|
|
2091
|
-
return callbacks
|
|
2092
|
-
},
|
|
2093
|
-
reset() {
|
|
2094
|
-
callbacks = [];
|
|
2095
|
-
nextId = 1;
|
|
2096
|
-
currentTime = 0;
|
|
2097
|
-
},
|
|
2098
|
-
enable() {
|
|
2099
|
-
activeMockRAF = this;
|
|
2100
|
-
global.requestAnimationFrame = raf;
|
|
2101
|
-
global.cancelAnimationFrame = cancel;
|
|
2102
|
-
},
|
|
2103
|
-
disable() {
|
|
2104
|
-
if (activeMockRAF === this) {
|
|
2105
|
-
activeMockRAF = void 0;
|
|
2106
|
-
}
|
|
2107
|
-
if (originalRAF) {
|
|
2108
|
-
global.requestAnimationFrame = originalRAF;
|
|
2109
|
-
} else {
|
|
2110
|
-
delete global.requestAnimationFrame;
|
|
2111
|
-
}
|
|
2112
|
-
if (originalCancelRAF) {
|
|
2113
|
-
global.cancelAnimationFrame = originalCancelRAF;
|
|
2114
|
-
} else {
|
|
2115
|
-
delete global.cancelAnimationFrame;
|
|
2116
|
-
}
|
|
2103
|
+
return callbacks;
|
|
2117
2104
|
}
|
|
2118
2105
|
};
|
|
2119
|
-
return mock;
|
|
2120
2106
|
}
|
|
2121
2107
|
function createMockPerformance(startTime = 0) {
|
|
2122
|
-
let
|
|
2123
|
-
const
|
|
2124
|
-
now: () =>
|
|
2108
|
+
let now = startTime;
|
|
2109
|
+
const mockPerformance = {
|
|
2110
|
+
now: () => now,
|
|
2125
2111
|
timeOrigin: startTime,
|
|
2126
2112
|
timing: {
|
|
2127
2113
|
navigationStart: startTime
|
|
2128
2114
|
},
|
|
2129
|
-
|
|
2130
|
-
},
|
|
2131
|
-
clearMeasures: () => {
|
|
2115
|
+
mark: (_name) => {
|
|
2132
2116
|
},
|
|
2133
|
-
|
|
2117
|
+
measure: (_name, _start, _end) => {
|
|
2134
2118
|
},
|
|
2135
2119
|
getEntries: () => [],
|
|
2136
|
-
getEntriesByName: () => [],
|
|
2137
|
-
getEntriesByType: () => [],
|
|
2138
|
-
|
|
2139
|
-
},
|
|
2140
|
-
measure: () => {
|
|
2120
|
+
getEntriesByName: (_name) => [],
|
|
2121
|
+
getEntriesByType: (_type) => [],
|
|
2122
|
+
clearMarks: (_name) => {
|
|
2141
2123
|
},
|
|
2142
|
-
|
|
2124
|
+
clearMeasures: (_name) => {
|
|
2143
2125
|
},
|
|
2144
|
-
|
|
2145
|
-
addEventListener: () => {
|
|
2126
|
+
clearResourceTimings: () => {
|
|
2146
2127
|
},
|
|
2147
|
-
|
|
2128
|
+
setResourceTimingBufferSize: (_maxSize) => {
|
|
2148
2129
|
},
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
mockPerf.advance = (deltaMs) => {
|
|
2152
|
-
currentTime += deltaMs;
|
|
2130
|
+
onresourcetimingbufferfull: null,
|
|
2131
|
+
toJSON: () => ({})
|
|
2153
2132
|
};
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
}
|
|
2157
|
-
return
|
|
2133
|
+
if (typeof global.performance === "undefined") {
|
|
2134
|
+
global.performance = mockPerformance;
|
|
2135
|
+
}
|
|
2136
|
+
return mockPerformance;
|
|
2158
2137
|
}
|
|
2159
2138
|
function createControlledTimer() {
|
|
2160
|
-
|
|
2161
|
-
let timers = [];
|
|
2162
|
-
let nextId = 1;
|
|
2163
|
-
const originalSetTimeout = global.setTimeout;
|
|
2164
|
-
const originalClearTimeout = global.clearTimeout;
|
|
2165
|
-
const originalSetInterval = global.setInterval;
|
|
2166
|
-
const originalClearInterval = global.clearInterval;
|
|
2167
|
-
const mockSetTimeout = (callback, delay = 0, ...args) => {
|
|
2168
|
-
const id = nextId++;
|
|
2169
|
-
timers.push({ id, callback, dueTime: currentTime + delay, args });
|
|
2170
|
-
return id;
|
|
2171
|
-
};
|
|
2172
|
-
const mockClearTimeout = (id) => {
|
|
2173
|
-
timers = timers.filter((t) => t.id !== id);
|
|
2174
|
-
};
|
|
2175
|
-
const mockSetInterval = (callback, delay = 0, ...args) => {
|
|
2176
|
-
const id = nextId++;
|
|
2177
|
-
timers.push({ id, callback, dueTime: currentTime + delay, interval: delay, args });
|
|
2178
|
-
return id;
|
|
2179
|
-
};
|
|
2180
|
-
const mockClearInterval = (id) => {
|
|
2181
|
-
timers = timers.filter((t) => t.id !== id);
|
|
2182
|
-
};
|
|
2183
|
-
global.setTimeout = mockSetTimeout;
|
|
2184
|
-
global.clearTimeout = mockClearTimeout;
|
|
2185
|
-
global.setInterval = mockSetInterval;
|
|
2186
|
-
global.clearInterval = mockClearInterval;
|
|
2139
|
+
console.warn("createControlledTimer: Recommend using vi.useFakeTimers() instead.");
|
|
2187
2140
|
return {
|
|
2188
|
-
|
|
2189
|
-
this.advanceBy(0);
|
|
2141
|
+
advanceBy: (ms) => {
|
|
2190
2142
|
},
|
|
2191
|
-
|
|
2192
|
-
const targetTime = currentTime + ms;
|
|
2193
|
-
while (true) {
|
|
2194
|
-
let earliest = null;
|
|
2195
|
-
for (const t of timers) {
|
|
2196
|
-
if (!earliest || t.dueTime < earliest.dueTime) {
|
|
2197
|
-
earliest = t;
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
if (!earliest || earliest.dueTime > targetTime) {
|
|
2201
|
-
break;
|
|
2202
|
-
}
|
|
2203
|
-
currentTime = earliest.dueTime;
|
|
2204
|
-
const { callback, args, interval, id } = earliest;
|
|
2205
|
-
if (interval !== void 0) {
|
|
2206
|
-
earliest.dueTime += interval;
|
|
2207
|
-
if (interval === 0) earliest.dueTime += 1;
|
|
2208
|
-
} else {
|
|
2209
|
-
timers = timers.filter((t) => t.id !== id);
|
|
2210
|
-
}
|
|
2211
|
-
callback(...args);
|
|
2212
|
-
}
|
|
2213
|
-
currentTime = targetTime;
|
|
2143
|
+
runAll: () => {
|
|
2214
2144
|
},
|
|
2215
|
-
clear() {
|
|
2216
|
-
timers = [];
|
|
2217
|
-
},
|
|
2218
|
-
restore() {
|
|
2219
|
-
global.setTimeout = originalSetTimeout;
|
|
2220
|
-
global.clearTimeout = originalClearTimeout;
|
|
2221
|
-
global.setInterval = originalSetInterval;
|
|
2222
|
-
global.clearInterval = originalClearInterval;
|
|
2145
|
+
clear: () => {
|
|
2223
2146
|
}
|
|
2224
2147
|
};
|
|
2225
2148
|
}
|
|
2226
|
-
function simulateFrames(count,
|
|
2227
|
-
if (!activeMockRAF) {
|
|
2228
|
-
throw new Error("simulateFrames requires an active MockRAF. Ensure createMockRAF().enable() is called.");
|
|
2229
|
-
}
|
|
2149
|
+
function simulateFrames(count, frameTime = 16, callback) {
|
|
2230
2150
|
for (let i = 0; i < count; i++) {
|
|
2231
2151
|
if (callback) callback(i);
|
|
2232
|
-
activeMockRAF.advance(frameTimeMs);
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
function simulateFramesWithMock(mock, count, frameTimeMs = 16.6, callback) {
|
|
2236
|
-
for (let i = 0; i < count; i++) {
|
|
2237
|
-
if (callback) callback(i);
|
|
2238
|
-
mock.advance(frameTimeMs);
|
|
2239
2152
|
}
|
|
2240
2153
|
}
|
|
2241
2154
|
|
|
2242
2155
|
// src/setup/node.ts
|
|
2243
2156
|
function setupNodeEnvironment(options = {}) {
|
|
2244
|
-
|
|
2245
|
-
|
|
2157
|
+
}
|
|
2158
|
+
function teardownNodeEnvironment() {
|
|
2246
2159
|
}
|
|
2247
2160
|
|
|
2248
2161
|
// src/engine/rendering.ts
|
|
@@ -2304,186 +2217,141 @@ function createMockRenderingContext() {
|
|
|
2304
2217
|
}
|
|
2305
2218
|
|
|
2306
2219
|
// src/setup/storage.ts
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
const storage = new Map(Object.entries(initialData));
|
|
2220
|
+
function createStorageMock(initialData = {}) {
|
|
2221
|
+
const store = new Map(Object.entries(initialData));
|
|
2310
2222
|
return {
|
|
2311
|
-
getItem: (key) =>
|
|
2312
|
-
setItem: (key, value) =>
|
|
2313
|
-
removeItem: (key) =>
|
|
2314
|
-
clear: () =>
|
|
2315
|
-
key: (index) => Array.from(
|
|
2223
|
+
getItem: (key) => store.get(key) || null,
|
|
2224
|
+
setItem: (key, value) => store.set(key, value.toString()),
|
|
2225
|
+
removeItem: (key) => store.delete(key),
|
|
2226
|
+
clear: () => store.clear(),
|
|
2227
|
+
key: (index) => Array.from(store.keys())[index] || null,
|
|
2316
2228
|
get length() {
|
|
2317
|
-
return
|
|
2229
|
+
return store.size;
|
|
2318
2230
|
}
|
|
2319
2231
|
};
|
|
2320
2232
|
}
|
|
2233
|
+
function createMockLocalStorage(initialData = {}) {
|
|
2234
|
+
return createStorageMock(initialData);
|
|
2235
|
+
}
|
|
2321
2236
|
function createMockSessionStorage(initialData = {}) {
|
|
2322
|
-
return
|
|
2237
|
+
return createStorageMock(initialData);
|
|
2323
2238
|
}
|
|
2324
|
-
function createMockIndexedDB() {
|
|
2325
|
-
|
|
2326
|
-
throw new Error("IndexedDB mock not found. Ensure fake-indexeddb is loaded.");
|
|
2327
|
-
}
|
|
2328
|
-
return indexedDB;
|
|
2239
|
+
function createMockIndexedDB(databases) {
|
|
2240
|
+
return global.indexedDB;
|
|
2329
2241
|
}
|
|
2330
2242
|
function createStorageTestScenario(storageType = "local") {
|
|
2331
|
-
if (storageType === "indexed") {
|
|
2332
|
-
const dbName = `test-db-${Math.random().toString(36).substring(7)}`;
|
|
2333
|
-
const storeName = "test-store";
|
|
2334
|
-
const storage2 = createMockIndexedDB();
|
|
2335
|
-
return {
|
|
2336
|
-
storage: storage2,
|
|
2337
|
-
populate: async (data) => {
|
|
2338
|
-
return new Promise((resolve, reject) => {
|
|
2339
|
-
const req = storage2.open(dbName, 1);
|
|
2340
|
-
req.onupgradeneeded = (e) => {
|
|
2341
|
-
const db = e.target.result;
|
|
2342
|
-
db.createObjectStore(storeName);
|
|
2343
|
-
};
|
|
2344
|
-
req.onsuccess = (e) => {
|
|
2345
|
-
const db = e.target.result;
|
|
2346
|
-
const tx = db.transaction(storeName, "readwrite");
|
|
2347
|
-
const store = tx.objectStore(storeName);
|
|
2348
|
-
Object.entries(data).forEach(([k, v]) => store.put(v, k));
|
|
2349
|
-
tx.oncomplete = () => {
|
|
2350
|
-
db.close();
|
|
2351
|
-
resolve();
|
|
2352
|
-
};
|
|
2353
|
-
tx.onerror = () => reject(tx.error);
|
|
2354
|
-
};
|
|
2355
|
-
req.onerror = () => reject(req.error);
|
|
2356
|
-
});
|
|
2357
|
-
},
|
|
2358
|
-
verify: async (key, value) => {
|
|
2359
|
-
return new Promise((resolve, reject) => {
|
|
2360
|
-
const req = storage2.open(dbName, 1);
|
|
2361
|
-
req.onsuccess = (e) => {
|
|
2362
|
-
const db = e.target.result;
|
|
2363
|
-
if (!db.objectStoreNames.contains(storeName)) {
|
|
2364
|
-
db.close();
|
|
2365
|
-
resolve(false);
|
|
2366
|
-
return;
|
|
2367
|
-
}
|
|
2368
|
-
const tx = db.transaction(storeName, "readonly");
|
|
2369
|
-
const store = tx.objectStore(storeName);
|
|
2370
|
-
const getReq = store.get(key);
|
|
2371
|
-
getReq.onsuccess = () => {
|
|
2372
|
-
const result = getReq.result === value;
|
|
2373
|
-
db.close();
|
|
2374
|
-
resolve(result);
|
|
2375
|
-
};
|
|
2376
|
-
getReq.onerror = () => {
|
|
2377
|
-
db.close();
|
|
2378
|
-
resolve(false);
|
|
2379
|
-
};
|
|
2380
|
-
};
|
|
2381
|
-
req.onerror = () => reject(req.error);
|
|
2382
|
-
});
|
|
2383
|
-
}
|
|
2384
|
-
};
|
|
2385
|
-
}
|
|
2386
|
-
const storage = storageType === "local" ? createMockLocalStorage() : createMockSessionStorage();
|
|
2387
2243
|
return {
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
},
|
|
2392
|
-
verify(key, value) {
|
|
2393
|
-
return storage.getItem(key) === value;
|
|
2394
|
-
}
|
|
2244
|
+
localStorage: createMockLocalStorage(),
|
|
2245
|
+
sessionStorage: createMockSessionStorage(),
|
|
2246
|
+
indexedDB: createMockIndexedDB()
|
|
2395
2247
|
};
|
|
2396
2248
|
}
|
|
2397
2249
|
|
|
2398
2250
|
// src/setup/audio.ts
|
|
2399
|
-
function createMockAudioContext() {
|
|
2400
|
-
const context = {
|
|
2401
|
-
createGain: () => ({
|
|
2402
|
-
connect: () => {
|
|
2403
|
-
},
|
|
2404
|
-
gain: { value: 1, setValueAtTime: () => {
|
|
2405
|
-
} }
|
|
2406
|
-
}),
|
|
2407
|
-
createOscillator: () => ({
|
|
2408
|
-
connect: () => {
|
|
2409
|
-
},
|
|
2410
|
-
start: () => {
|
|
2411
|
-
},
|
|
2412
|
-
stop: () => {
|
|
2413
|
-
},
|
|
2414
|
-
frequency: { value: 440 }
|
|
2415
|
-
}),
|
|
2416
|
-
createBufferSource: () => ({
|
|
2417
|
-
connect: () => {
|
|
2418
|
-
},
|
|
2419
|
-
start: () => {
|
|
2420
|
-
},
|
|
2421
|
-
stop: () => {
|
|
2422
|
-
},
|
|
2423
|
-
buffer: null,
|
|
2424
|
-
playbackRate: { value: 1 },
|
|
2425
|
-
loop: false
|
|
2426
|
-
}),
|
|
2427
|
-
destination: {},
|
|
2428
|
-
currentTime: 0,
|
|
2429
|
-
state: "running",
|
|
2430
|
-
resume: async () => {
|
|
2431
|
-
},
|
|
2432
|
-
suspend: async () => {
|
|
2433
|
-
},
|
|
2434
|
-
close: async () => {
|
|
2435
|
-
},
|
|
2436
|
-
decodeAudioData: async (buffer) => ({
|
|
2437
|
-
duration: 1,
|
|
2438
|
-
length: 44100,
|
|
2439
|
-
sampleRate: 44100,
|
|
2440
|
-
numberOfChannels: 2,
|
|
2441
|
-
getChannelData: () => new Float32Array(44100)
|
|
2442
|
-
}),
|
|
2443
|
-
createBuffer: (channels, length2, sampleRate) => ({
|
|
2444
|
-
duration: length2 / sampleRate,
|
|
2445
|
-
length: length2,
|
|
2446
|
-
sampleRate,
|
|
2447
|
-
numberOfChannels: channels,
|
|
2448
|
-
getChannelData: () => new Float32Array(length2)
|
|
2449
|
-
}),
|
|
2450
|
-
// Helper to track events if needed
|
|
2451
|
-
_events: []
|
|
2452
|
-
};
|
|
2453
|
-
return new Proxy(context, {
|
|
2454
|
-
get(target, prop, receiver) {
|
|
2455
|
-
if (prop === "_events") return target._events;
|
|
2456
|
-
const value = Reflect.get(target, prop, receiver);
|
|
2457
|
-
if (typeof value === "function") {
|
|
2458
|
-
return (...args) => {
|
|
2459
|
-
target._events.push({ type: String(prop), args });
|
|
2460
|
-
return Reflect.apply(value, target, args);
|
|
2461
|
-
};
|
|
2462
|
-
}
|
|
2463
|
-
return value;
|
|
2464
|
-
}
|
|
2465
|
-
});
|
|
2466
|
-
}
|
|
2467
2251
|
function setupMockAudioContext() {
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2252
|
+
class MockAudioContext {
|
|
2253
|
+
constructor() {
|
|
2254
|
+
this.state = "suspended";
|
|
2255
|
+
this.destination = {};
|
|
2256
|
+
this.currentTime = 0;
|
|
2257
|
+
this.listener = {
|
|
2258
|
+
positionX: { value: 0 },
|
|
2259
|
+
positionY: { value: 0 },
|
|
2260
|
+
positionZ: { value: 0 },
|
|
2261
|
+
forwardX: { value: 0 },
|
|
2262
|
+
forwardY: { value: 0 },
|
|
2263
|
+
forwardZ: { value: 0 },
|
|
2264
|
+
upX: { value: 0 },
|
|
2265
|
+
upY: { value: 0 },
|
|
2266
|
+
upZ: { value: 0 },
|
|
2267
|
+
setOrientation: () => {
|
|
2268
|
+
},
|
|
2269
|
+
setPosition: () => {
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
createGain() {
|
|
2274
|
+
return {
|
|
2275
|
+
gain: { value: 1, linearRampToValueAtTime: () => {
|
|
2276
|
+
} },
|
|
2277
|
+
connect: () => {
|
|
2278
|
+
},
|
|
2279
|
+
disconnect: () => {
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
createBufferSource() {
|
|
2284
|
+
return {
|
|
2285
|
+
buffer: null,
|
|
2286
|
+
loop: false,
|
|
2287
|
+
playbackRate: { value: 1 },
|
|
2288
|
+
connect: () => {
|
|
2289
|
+
},
|
|
2290
|
+
start: () => {
|
|
2291
|
+
},
|
|
2292
|
+
stop: () => {
|
|
2293
|
+
},
|
|
2294
|
+
disconnect: () => {
|
|
2295
|
+
},
|
|
2296
|
+
onended: null
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
createPanner() {
|
|
2300
|
+
return {
|
|
2301
|
+
panningModel: "equalpower",
|
|
2302
|
+
distanceModel: "inverse",
|
|
2303
|
+
positionX: { value: 0 },
|
|
2304
|
+
positionY: { value: 0 },
|
|
2305
|
+
positionZ: { value: 0 },
|
|
2306
|
+
orientationX: { value: 0 },
|
|
2307
|
+
orientationY: { value: 0 },
|
|
2308
|
+
orientationZ: { value: 0 },
|
|
2309
|
+
coneInnerAngle: 360,
|
|
2310
|
+
coneOuterAngle: 360,
|
|
2311
|
+
coneOuterGain: 0,
|
|
2312
|
+
connect: () => {
|
|
2313
|
+
},
|
|
2314
|
+
disconnect: () => {
|
|
2315
|
+
},
|
|
2316
|
+
setPosition: () => {
|
|
2317
|
+
},
|
|
2318
|
+
setOrientation: () => {
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
createBuffer(numOfChannels, length2, sampleRate) {
|
|
2323
|
+
return {
|
|
2324
|
+
duration: length2 / sampleRate,
|
|
2325
|
+
length: length2,
|
|
2326
|
+
sampleRate,
|
|
2327
|
+
numberOfChannels: numOfChannels,
|
|
2328
|
+
getChannelData: () => new Float32Array(length2)
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
decodeAudioData(data, success) {
|
|
2332
|
+
const buffer = this.createBuffer(2, 100, 44100);
|
|
2333
|
+
if (success) success(buffer);
|
|
2334
|
+
return Promise.resolve(buffer);
|
|
2335
|
+
}
|
|
2336
|
+
resume() {
|
|
2337
|
+
return Promise.resolve();
|
|
2338
|
+
}
|
|
2339
|
+
suspend() {
|
|
2340
|
+
return Promise.resolve();
|
|
2341
|
+
}
|
|
2342
|
+
close() {
|
|
2343
|
+
return Promise.resolve();
|
|
2344
|
+
}
|
|
2476
2345
|
}
|
|
2346
|
+
global.AudioContext = MockAudioContext;
|
|
2347
|
+
global.webkitAudioContext = MockAudioContext;
|
|
2477
2348
|
}
|
|
2478
2349
|
function teardownMockAudioContext() {
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
delete global.window.AudioContext;
|
|
2482
|
-
delete global.window.webkitAudioContext;
|
|
2483
|
-
}
|
|
2350
|
+
delete global.AudioContext;
|
|
2351
|
+
delete global.webkitAudioContext;
|
|
2484
2352
|
}
|
|
2485
2353
|
function captureAudioEvents(context) {
|
|
2486
|
-
return
|
|
2354
|
+
return [];
|
|
2487
2355
|
}
|
|
2488
2356
|
|
|
2489
2357
|
// ../../node_modules/.pnpm/gl-matrix@3.4.4/node_modules/gl-matrix/esm/common.js
|
|
@@ -2973,217 +2841,110 @@ function simulateCameraMovement(camera, input, deltaTime) {
|
|
|
2973
2841
|
}
|
|
2974
2842
|
|
|
2975
2843
|
// src/e2e/playwright.ts
|
|
2976
|
-
import { chromium } from "playwright";
|
|
2977
|
-
import { createServer } from "http";
|
|
2978
|
-
import handler from "serve-handler";
|
|
2979
2844
|
async function createPlaywrightTestClient(options = {}) {
|
|
2980
|
-
let
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
return handler(request, response, {
|
|
2986
|
-
public: rootPath,
|
|
2987
|
-
cleanUrls: false,
|
|
2988
|
-
headers: [
|
|
2989
|
-
{
|
|
2990
|
-
source: "**/*",
|
|
2991
|
-
headers: [
|
|
2992
|
-
{ key: "Cache-Control", value: "no-cache" },
|
|
2993
|
-
{ key: "Access-Control-Allow-Origin", value: "*" },
|
|
2994
|
-
{ key: "Cross-Origin-Opener-Policy", value: "same-origin" },
|
|
2995
|
-
{ key: "Cross-Origin-Embedder-Policy", value: "require-corp" }
|
|
2996
|
-
]
|
|
2997
|
-
}
|
|
2998
|
-
]
|
|
2999
|
-
});
|
|
3000
|
-
});
|
|
3001
|
-
await new Promise((resolve) => {
|
|
3002
|
-
if (!staticServer) return;
|
|
3003
|
-
staticServer.listen(0, () => {
|
|
3004
|
-
const addr = staticServer?.address();
|
|
3005
|
-
const port = typeof addr === "object" ? addr?.port : 0;
|
|
3006
|
-
clientUrl = `http://localhost:${port}`;
|
|
3007
|
-
console.log(`Test client serving from ${rootPath} at ${clientUrl}`);
|
|
3008
|
-
resolve();
|
|
3009
|
-
});
|
|
3010
|
-
});
|
|
2845
|
+
let playwright;
|
|
2846
|
+
try {
|
|
2847
|
+
playwright = await import("playwright");
|
|
2848
|
+
} catch (e) {
|
|
2849
|
+
throw new Error("Playwright is not installed. Please install it to use this utility.");
|
|
3011
2850
|
}
|
|
3012
|
-
const browser = await chromium.launch({
|
|
3013
|
-
headless: options.headless ?? true
|
|
3014
|
-
args: [
|
|
3015
|
-
"--use-gl=egl",
|
|
3016
|
-
"--ignore-gpu-blocklist",
|
|
3017
|
-
...options.launchOptions?.args || []
|
|
3018
|
-
],
|
|
3019
|
-
...options.launchOptions
|
|
2851
|
+
const browser = await playwright.chromium.launch({
|
|
2852
|
+
headless: options.headless ?? true
|
|
3020
2853
|
});
|
|
3021
|
-
const width = options.width || 1280;
|
|
3022
|
-
const height = options.height || 720;
|
|
3023
2854
|
const context = await browser.newContext({
|
|
3024
|
-
viewport: { width, height }
|
|
3025
|
-
deviceScaleFactor: 1,
|
|
3026
|
-
...options.contextOptions
|
|
2855
|
+
viewport: options.viewport || { width: 1280, height: 720 }
|
|
3027
2856
|
});
|
|
3028
2857
|
const page = await context.newPage();
|
|
3029
|
-
const close = async () => {
|
|
3030
|
-
await browser.close();
|
|
3031
|
-
if (staticServer) {
|
|
3032
|
-
staticServer.close();
|
|
3033
|
-
}
|
|
3034
|
-
};
|
|
3035
|
-
const navigate = async (url) => {
|
|
3036
|
-
const targetUrl = url || clientUrl;
|
|
3037
|
-
if (!targetUrl) throw new Error("No URL to navigate to");
|
|
3038
|
-
let finalUrl = targetUrl;
|
|
3039
|
-
if (options.serverUrl && !targetUrl.includes("connect=")) {
|
|
3040
|
-
const separator = targetUrl.includes("?") ? "&" : "?";
|
|
3041
|
-
finalUrl = `${targetUrl}${separator}connect=${encodeURIComponent(options.serverUrl)}`;
|
|
3042
|
-
}
|
|
3043
|
-
console.log(`Navigating to: ${finalUrl}`);
|
|
3044
|
-
await page.goto(finalUrl, { waitUntil: "domcontentloaded" });
|
|
3045
|
-
};
|
|
3046
2858
|
return {
|
|
3047
|
-
browser,
|
|
3048
|
-
context,
|
|
3049
2859
|
page,
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
waitForGame: async (timeout = 1e4) => {
|
|
3054
|
-
await waitForGameReady(page, timeout);
|
|
2860
|
+
browser,
|
|
2861
|
+
async navigate(url) {
|
|
2862
|
+
await page.goto(url);
|
|
3055
2863
|
},
|
|
3056
|
-
|
|
2864
|
+
async waitForGame() {
|
|
2865
|
+
await waitForGameReady(page);
|
|
2866
|
+
},
|
|
2867
|
+
async injectInput(type, data) {
|
|
3057
2868
|
await page.evaluate(({ type: type2, data: data2 }) => {
|
|
3058
|
-
|
|
2869
|
+
console.log("Injecting input", type2, data2);
|
|
3059
2870
|
}, { type, data });
|
|
2871
|
+
},
|
|
2872
|
+
async screenshot(name) {
|
|
2873
|
+
return await page.screenshot({ path: `${name}.png` });
|
|
2874
|
+
},
|
|
2875
|
+
async close() {
|
|
2876
|
+
await browser.close();
|
|
3060
2877
|
}
|
|
3061
2878
|
};
|
|
3062
2879
|
}
|
|
3063
2880
|
async function waitForGameReady(page, timeout = 1e4) {
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
}, null, { timeout });
|
|
3068
|
-
} catch (e) {
|
|
3069
|
-
await page.waitForSelector("canvas", { timeout });
|
|
3070
|
-
}
|
|
2881
|
+
await page.waitForFunction(() => {
|
|
2882
|
+
return window.game && window.game.isRunning;
|
|
2883
|
+
}, { timeout });
|
|
3071
2884
|
}
|
|
3072
2885
|
async function captureGameState(page) {
|
|
3073
2886
|
return await page.evaluate(() => {
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
2887
|
+
const game = window.game;
|
|
2888
|
+
return {
|
|
2889
|
+
time: game ? game.time : 0,
|
|
2890
|
+
entities: game && game.entities ? game.entities.length : 0
|
|
2891
|
+
};
|
|
3078
2892
|
});
|
|
3079
2893
|
}
|
|
3080
2894
|
|
|
3081
2895
|
// src/e2e/network.ts
|
|
3082
|
-
var CONDITIONS = {
|
|
3083
|
-
"good": {
|
|
3084
|
-
offline: false,
|
|
3085
|
-
downloadThroughput: 10 * 1024 * 1024,
|
|
3086
|
-
// 10 Mbps
|
|
3087
|
-
uploadThroughput: 5 * 1024 * 1024,
|
|
3088
|
-
// 5 Mbps
|
|
3089
|
-
latency: 20
|
|
3090
|
-
},
|
|
3091
|
-
"slow": {
|
|
3092
|
-
offline: false,
|
|
3093
|
-
downloadThroughput: 500 * 1024,
|
|
3094
|
-
// 500 Kbps
|
|
3095
|
-
uploadThroughput: 500 * 1024,
|
|
3096
|
-
latency: 400
|
|
3097
|
-
},
|
|
3098
|
-
"unstable": {
|
|
3099
|
-
offline: false,
|
|
3100
|
-
downloadThroughput: 1 * 1024 * 1024,
|
|
3101
|
-
uploadThroughput: 1 * 1024 * 1024,
|
|
3102
|
-
latency: 100
|
|
3103
|
-
},
|
|
3104
|
-
"offline": {
|
|
3105
|
-
offline: true,
|
|
3106
|
-
downloadThroughput: 0,
|
|
3107
|
-
uploadThroughput: 0,
|
|
3108
|
-
latency: 0
|
|
3109
|
-
}
|
|
3110
|
-
};
|
|
3111
2896
|
function simulateNetworkCondition(condition) {
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
}
|
|
3115
|
-
|
|
2897
|
+
switch (condition) {
|
|
2898
|
+
case "good":
|
|
2899
|
+
return { latency: 20, jitter: 5, packetLoss: 0, bandwidth: 10 * 1024 * 1024 };
|
|
2900
|
+
case "slow":
|
|
2901
|
+
return { latency: 150, jitter: 20, packetLoss: 0.01, bandwidth: 1 * 1024 * 1024 };
|
|
2902
|
+
case "unstable":
|
|
2903
|
+
return { latency: 100, jitter: 100, packetLoss: 0.05, bandwidth: 512 * 1024 };
|
|
2904
|
+
case "offline":
|
|
2905
|
+
return { latency: 0, jitter: 0, packetLoss: 1, bandwidth: 0 };
|
|
2906
|
+
case "custom":
|
|
2907
|
+
default:
|
|
2908
|
+
return { latency: 0, jitter: 0, packetLoss: 0, bandwidth: Infinity };
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
function createCustomNetworkCondition(latency, jitter, packetLoss) {
|
|
3116
2912
|
return {
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
latency: latency + Math.random() * jitter,
|
|
3123
|
-
downloadThroughput: baseConfig?.downloadThroughput || -1,
|
|
3124
|
-
uploadThroughput: baseConfig?.uploadThroughput || -1
|
|
3125
|
-
});
|
|
3126
|
-
},
|
|
3127
|
-
async clear(page) {
|
|
3128
|
-
const client = await page.context().newCDPSession(page);
|
|
3129
|
-
await client.send("Network.emulateNetworkConditions", {
|
|
3130
|
-
offline: false,
|
|
3131
|
-
latency: 0,
|
|
3132
|
-
downloadThroughput: -1,
|
|
3133
|
-
uploadThroughput: -1
|
|
3134
|
-
});
|
|
3135
|
-
}
|
|
2913
|
+
latency,
|
|
2914
|
+
jitter,
|
|
2915
|
+
packetLoss,
|
|
2916
|
+
bandwidth: Infinity
|
|
2917
|
+
// Default to unlimited unless specified
|
|
3136
2918
|
};
|
|
3137
2919
|
}
|
|
3138
|
-
|
|
3139
|
-
const simulator = createCustomNetworkCondition(0, 0, 0, {
|
|
3140
|
-
offline: false,
|
|
3141
|
-
latency: 0,
|
|
3142
|
-
downloadThroughput: bytesPerSecond,
|
|
3143
|
-
uploadThroughput: bytesPerSecond
|
|
3144
|
-
});
|
|
3145
|
-
await simulator.apply(page);
|
|
2920
|
+
function throttleBandwidth(bytesPerSecond) {
|
|
3146
2921
|
}
|
|
3147
2922
|
|
|
3148
2923
|
// src/e2e/visual.ts
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
}
|
|
3162
|
-
async function compareScreenshots(baseline, current, threshold = 0.1) {
|
|
3163
|
-
if (baseline.equals(current)) {
|
|
3164
|
-
return { pixelDiff: 0, matched: true };
|
|
2924
|
+
async function captureGameScreenshot(page, name) {
|
|
2925
|
+
return await page.screenshot({ path: `${name}.png` });
|
|
2926
|
+
}
|
|
2927
|
+
function compareScreenshots(baseline, current, threshold = 0.01) {
|
|
2928
|
+
if (baseline.length !== current.length) {
|
|
2929
|
+
return { diffPercentage: 1 };
|
|
2930
|
+
}
|
|
2931
|
+
let diffPixels = 0;
|
|
2932
|
+
const totalPixels = baseline.length;
|
|
2933
|
+
for (let i = 0; i < baseline.length; i++) {
|
|
2934
|
+
if (baseline[i] !== current[i]) {
|
|
2935
|
+
diffPixels++;
|
|
2936
|
+
}
|
|
3165
2937
|
}
|
|
2938
|
+
const diffPercentage = diffPixels / totalPixels;
|
|
3166
2939
|
return {
|
|
3167
|
-
|
|
3168
|
-
//
|
|
3169
|
-
matched: false
|
|
2940
|
+
diffPercentage
|
|
2941
|
+
// Generating a diff image buffer would require a library like pixelmatch
|
|
3170
2942
|
};
|
|
3171
2943
|
}
|
|
3172
|
-
function createVisualTestScenario(
|
|
2944
|
+
function createVisualTestScenario(sceneName) {
|
|
3173
2945
|
return {
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
},
|
|
3177
|
-
async compare(snapshotName, baselineDir) {
|
|
3178
|
-
const name = `${sceneName}-${snapshotName}`;
|
|
3179
|
-
const current = await captureGameScreenshot(page, name, { dir: "__screenshots__/current" });
|
|
3180
|
-
try {
|
|
3181
|
-
const baselinePath = path.join(baselineDir, `${name}.png`);
|
|
3182
|
-
const baseline = await fs.readFile(baselinePath);
|
|
3183
|
-
return await compareScreenshots(baseline, current);
|
|
3184
|
-
} catch (e) {
|
|
3185
|
-
return { pixelDiff: -1, matched: false };
|
|
3186
|
-
}
|
|
2946
|
+
sceneName,
|
|
2947
|
+
setup: async () => {
|
|
3187
2948
|
}
|
|
3188
2949
|
};
|
|
3189
2950
|
}
|
|
@@ -3214,7 +2975,6 @@ export {
|
|
|
3214
2975
|
createMockAI,
|
|
3215
2976
|
createMockAmmoItem,
|
|
3216
2977
|
createMockArmorItem,
|
|
3217
|
-
createMockAudioContext,
|
|
3218
2978
|
createMockCamera,
|
|
3219
2979
|
createMockCanvas,
|
|
3220
2980
|
createMockCanvasContext2D,
|
|
@@ -3300,7 +3060,6 @@ export {
|
|
|
3300
3060
|
simulateBandwidthLimit,
|
|
3301
3061
|
simulateCameraMovement,
|
|
3302
3062
|
simulateFrames,
|
|
3303
|
-
simulateFramesWithMock,
|
|
3304
3063
|
simulateHandshake,
|
|
3305
3064
|
simulateNetworkCondition,
|
|
3306
3065
|
simulatePlayerInput,
|
|
@@ -3313,6 +3072,7 @@ export {
|
|
|
3313
3072
|
stairTrace,
|
|
3314
3073
|
teardownBrowserEnvironment,
|
|
3315
3074
|
teardownMockAudioContext,
|
|
3075
|
+
teardownNodeEnvironment,
|
|
3316
3076
|
throttleBandwidth,
|
|
3317
3077
|
verifySnapshotConsistency,
|
|
3318
3078
|
waitForGameReady
|