@series-inc/rundot-kinetix 0.0.0-bootstrap.0
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/README.md +77 -0
- package/authoring.d.ts +30 -0
- package/index.d.ts +221 -0
- package/native/component_runtime.cpp +341 -0
- package/native/component_runtime.hpp +112 -0
- package/native/deterministic_math.cpp +594 -0
- package/native/deterministic_math.hpp +21 -0
- package/native/kinetix-f64-native-profile.cpp +406 -0
- package/native/kinetix-f64-native-profile.hpp +5 -0
- package/native/kinetix-fixed1000-native-profile.cpp +777 -0
- package/native/kinetix-fixed1000-native-profile.hpp +58 -0
- package/native/kinetix-fixed1000-physics.cpp +887 -0
- package/native/kinetix-fixed1000-physics.hpp +28 -0
- package/native/kinetix-installed-f64-renderer.cpp +344 -0
- package/native/kinetix-installed-f64-renderer.hpp +35 -0
- package/native/kinetix-installed-f64-runtime.cpp +1085 -0
- package/native/kinetix-installed-f64-runtime.hpp +141 -0
- package/native/kinetix-installed-fixed1000-data.hpp +77 -0
- package/native/kinetix-native-main.cpp +37 -0
- package/native/kinetix-native-runtime.cpp +20 -0
- package/native/kinetix-native-runtime.hpp +25 -0
- package/native/kinetix-render-projection.cpp +20 -0
- package/native/kinetix-render-projection.hpp +14 -0
- package/package.json +65 -0
- package/runtime.d.ts +76 -0
- package/scripts/build-native.mjs +67 -0
- package/scripts/emit-product-digests.mjs +33 -0
- package/scripts/preflight.mjs +76 -0
- package/src/index.mjs +57 -0
- package/src/kinetix-authoring.mjs +69 -0
- package/src/kinetix-baker.mjs +587 -0
- package/src/kinetix-deterministic-math.mjs +1044 -0
- package/src/kinetix-fixed1000-runtime-adapter.mjs +33 -0
- package/src/kinetix-fixed1000-runtime.mjs +954 -0
- package/src/kinetix-installed-f64-reference.mjs +157 -0
- package/src/kinetix-installed-f64-render-frames.mjs +53 -0
- package/src/kinetix-installed-f64-renderer.mjs +240 -0
- package/src/kinetix-installed-f64-runtime-adapter.mjs +68 -0
- package/src/kinetix-installed-f64-runtime.mjs +607 -0
- package/src/kinetix-installed-mechanics.mjs +377 -0
- package/src/kinetix-installed-systems.mjs +181 -0
- package/src/kinetix-native-product.mjs +72 -0
- package/src/kinetix-product-contract.mjs +121 -0
- package/src/kinetix-project-compiler.mjs +1017 -0
- package/src/kinetix-render-projection.mjs +28 -0
- package/src/kinetix-runtime-adapter-utils.mjs +168 -0
- package/src/kinetix-runtime-contract.mjs +54 -0
- package/src/kinetix-session-config.mjs +170 -0
- package/src/kinetix-source-snapshot.mjs +24 -0
- package/src/kinetix-survival-runtime-adapter.mjs +305 -0
- package/src/kinetix-survival-runtime.generated.mjs +1 -0
- package/src/kinetix-world-kernel.mjs +580 -0
- package/src/kinetix-world-runtime.mjs +171 -0
- package/src/runtime.mjs +14 -0
- package/src/shared/f64-bits.mjs +14 -0
- package/src/shared/kinetix-envelope-v1.mjs +589 -0
- package/src/shared/render-records-v1.mjs +168 -0
- package/src/shared/sha256.mjs +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @series-inc/rundot-kinetix
|
|
2
|
+
|
|
3
|
+
Kinetix owns RUN.game's deterministic simulation supply chain and runtime. It
|
|
4
|
+
compiles data-only project declarations into exact products, derives runtime
|
|
5
|
+
identity from those products, and exposes the only simulation lifecycle used by
|
|
6
|
+
Syncplay.
|
|
7
|
+
|
|
8
|
+
## Ownership boundary
|
|
9
|
+
|
|
10
|
+
| Concern | Owner |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Committed source capture, upload authority, build lifecycle | Pack API + CLI |
|
|
13
|
+
| Compilation, exact product schema, runtime identity | Kinetix compiler/baker |
|
|
14
|
+
| Artifact immutability, signing, key trust, catalog eligibility | Pack service + KMS/GCS/Firestore |
|
|
15
|
+
| Deterministic state, RNG streams, step, checkpoint bytes, canonical checksum | Kinetix runtime |
|
|
16
|
+
| Inputs, prediction, confirmation, rollback policy, replay log, late join, secrets, transport | Syncplay |
|
|
17
|
+
| Rendering/interpolation/effects | Presentation adapters outside authoritative state |
|
|
18
|
+
|
|
19
|
+
Kinetix never imports Syncplay. Syncplay runtime code may import exactly the
|
|
20
|
+
browser-safe `/runtime` subpath; its build tooling may additionally import
|
|
21
|
+
`/authoring`. A dependency-direction test enforces this and rejects any restored
|
|
22
|
+
composition package.
|
|
23
|
+
|
|
24
|
+
## Authoring
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { defineKinetixProject } from '@series-inc/rundot-kinetix/authoring';
|
|
28
|
+
|
|
29
|
+
export const project = defineKinetixProject({
|
|
30
|
+
id: 'arena',
|
|
31
|
+
installedProfile: 'fixed-1000-3d',
|
|
32
|
+
tickRate: 30,
|
|
33
|
+
inputSchema: { id: 'arena-input/v1' },
|
|
34
|
+
sessionConfigSchema: {
|
|
35
|
+
id: 'arena-session/v1',
|
|
36
|
+
version: 1,
|
|
37
|
+
maxBytes: 65536,
|
|
38
|
+
fields: { playerCount: { type: 'integer', min: 1, max: 8 } },
|
|
39
|
+
},
|
|
40
|
+
mechanics: [],
|
|
41
|
+
components: [],
|
|
42
|
+
systems: [],
|
|
43
|
+
presentationBindings: [],
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Declarations are canonical data only. Functions, classes, and creator-authored
|
|
48
|
+
step callbacks are rejected. Match initialization is encoded separately as
|
|
49
|
+
bounded canonical session-config bytes; changing it changes the config digest,
|
|
50
|
+
not the signed mechanics product or runtime identity.
|
|
51
|
+
|
|
52
|
+
## Runtime ABI
|
|
53
|
+
|
|
54
|
+
The ESM-only `/runtime` entrypoint is browser-safe and exposes immutable runtime
|
|
55
|
+
identity, ordered atomic batch advance, opaque checkpoint capture/restore,
|
|
56
|
+
inspection, explicit serialization/hydration, checksums, and release. The
|
|
57
|
+
reference world backend uses deep-copy checkpoints today. Installed f64 and
|
|
58
|
+
fixed-1000 products have direct adapters to the same ABI.
|
|
59
|
+
|
|
60
|
+
The ABI is batched so a future WASM/native backend crosses the boundary once per
|
|
61
|
+
run rather than once per entity. Acceleration is a measured decision: simulation
|
|
62
|
+
must account for at least 30% of end-to-end frame cost, and a prototype must beat
|
|
63
|
+
optimized JS by at least 20% including checkpoint and boundary overhead while
|
|
64
|
+
remaining bit-identical in Chromium and WebKit.
|
|
65
|
+
|
|
66
|
+
## Build and test
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install
|
|
70
|
+
npm run build:native
|
|
71
|
+
npm test
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The suite covers the runtime ABI, browser-safe graph, world rollback/hydration,
|
|
75
|
+
installed adapters, data-only authoring, canonical session config, compiler-
|
|
76
|
+
derived identity, source guards, dependency direction, and JS/C++ per-tick hash
|
|
77
|
+
parity.
|
package/authoring.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type KinetixInstalledProfileName = 'deterministic-f64' | 'fixed-1000-3d';
|
|
2
|
+
|
|
3
|
+
export type KinetixSessionField =
|
|
4
|
+
| { type: 'string'; maxBytes: number; required?: boolean }
|
|
5
|
+
| { type: 'integer'; min: number; max: number; required?: boolean }
|
|
6
|
+
| { type: 'boolean'; required?: boolean }
|
|
7
|
+
| { type: 'enum'; values: readonly string[]; required?: boolean };
|
|
8
|
+
|
|
9
|
+
export interface KinetixSessionConfigSchema {
|
|
10
|
+
id: string;
|
|
11
|
+
version: number;
|
|
12
|
+
maxBytes: number;
|
|
13
|
+
fields: Readonly<Record<string, KinetixSessionField>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface KinetixProjectDeclaration {
|
|
17
|
+
id: string;
|
|
18
|
+
installedProfile: KinetixInstalledProfileName;
|
|
19
|
+
tickRate: 10 | 20 | 30 | 60;
|
|
20
|
+
inputSchema: Readonly<Record<string, unknown>>;
|
|
21
|
+
sessionConfigSchema: KinetixSessionConfigSchema;
|
|
22
|
+
mechanics: readonly Readonly<Record<string, unknown>>[];
|
|
23
|
+
components: readonly Readonly<Record<string, unknown>>[];
|
|
24
|
+
systems: readonly Readonly<Record<string, unknown>>[];
|
|
25
|
+
presentationBindings: readonly Readonly<Record<string, unknown>>[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function defineKinetixProject<const Project extends KinetixProjectDeclaration>(
|
|
29
|
+
project: Project,
|
|
30
|
+
): Readonly<Project>;
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Hand-authored public surface. The runtime is ESM JavaScript (.mjs) moved
|
|
2
|
+
// verbatim out of the console lab; these declarations give consumers types
|
|
3
|
+
// without touching a byte of runtime behavior.
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
assertKinetixRuntime,
|
|
7
|
+
assertKinetixRuntimeIdentity,
|
|
8
|
+
kinetixRuntimeAbiVersion,
|
|
9
|
+
} from './runtime';
|
|
10
|
+
export type {
|
|
11
|
+
KinetixAdvanceRequest,
|
|
12
|
+
KinetixAdvanceResult,
|
|
13
|
+
KinetixRuntime,
|
|
14
|
+
KinetixRuntimeIdentity,
|
|
15
|
+
} from './runtime';
|
|
16
|
+
export { createInstalledSurvivalRuntime } from './runtime';
|
|
17
|
+
export { defineKinetixProject } from './authoring';
|
|
18
|
+
export type { KinetixProjectDeclaration, KinetixSessionConfigSchema } from './authoring';
|
|
19
|
+
|
|
20
|
+
export type KinetixTargetProfile = 'deterministic-f64' | 'fixed-1000-3d';
|
|
21
|
+
|
|
22
|
+
export interface KinetixCompileInput {
|
|
23
|
+
projectRoot: string;
|
|
24
|
+
expectedCommit: string;
|
|
25
|
+
targetProfile: KinetixTargetProfile;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface KinetixDiagnostic {
|
|
29
|
+
code: string;
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface KinetixEnvelope {
|
|
34
|
+
profiles: { id: string }[];
|
|
35
|
+
entities: {
|
|
36
|
+
id: string;
|
|
37
|
+
components: { id: string; type: string; properties: Record<string, unknown> }[];
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface KinetixCompiled {
|
|
42
|
+
envelope: KinetixEnvelope | null;
|
|
43
|
+
diagnostics: KinetixDiagnostic[];
|
|
44
|
+
sourceSnapshot: unknown;
|
|
45
|
+
semanticCoverage: unknown;
|
|
46
|
+
unmatchedBehaviors: unknown[];
|
|
47
|
+
runtimeContract: KinetixRuntimeContract | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface KinetixRuntimeContract {
|
|
51
|
+
readonly tickRate: 10 | 20 | 30 | 60;
|
|
52
|
+
readonly targetProfile: KinetixTargetProfile;
|
|
53
|
+
readonly numericProfile: string;
|
|
54
|
+
readonly deterministicAbi: string;
|
|
55
|
+
readonly deterministicVersion: string;
|
|
56
|
+
readonly inputSchema: Readonly<Record<string, unknown>>;
|
|
57
|
+
readonly stateSchema: Readonly<Record<string, unknown>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface KinetixSystem {
|
|
61
|
+
id: string;
|
|
62
|
+
phase: string;
|
|
63
|
+
phaseIndex: number;
|
|
64
|
+
order: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface KinetixProducts {
|
|
68
|
+
sourceDigest: string;
|
|
69
|
+
manifest: {
|
|
70
|
+
runtime: KinetixRuntimeContract;
|
|
71
|
+
systems: KinetixSystem[];
|
|
72
|
+
payloads: { name: string; digest: string; byteLength: number }[];
|
|
73
|
+
};
|
|
74
|
+
simulation: unknown;
|
|
75
|
+
presentation: unknown;
|
|
76
|
+
binding: unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface KinetixCompilation {
|
|
80
|
+
compiled: KinetixCompiled;
|
|
81
|
+
products: KinetixProducts;
|
|
82
|
+
runtimeIdentity: import('./runtime').KinetixRuntimeIdentity;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface KinetixInstalledProfile {
|
|
86
|
+
readonly abi: string;
|
|
87
|
+
readonly runtimeContract: KinetixRuntimeContract;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface KinetixWorldHandle {
|
|
91
|
+
readonly id: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface KinetixWorld {
|
|
95
|
+
step(input: unknown): number;
|
|
96
|
+
snapshot(): unknown;
|
|
97
|
+
restore(snapshot: unknown): void;
|
|
98
|
+
canonicalStateBytes(): Uint8Array;
|
|
99
|
+
read(handle: KinetixWorldHandle): {
|
|
100
|
+
components: { type: string; properties: Record<string, unknown> }[];
|
|
101
|
+
};
|
|
102
|
+
directHandle(entityId: string): KinetixWorldHandle;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface KinetixRenderRecord {
|
|
106
|
+
kind: string;
|
|
107
|
+
source: string;
|
|
108
|
+
component: string;
|
|
109
|
+
transform: unknown;
|
|
110
|
+
bindingDigest: string;
|
|
111
|
+
worldTick: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface KinetixRenderProjection {
|
|
115
|
+
project(): KinetixRenderRecord[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface KinetixBakeResult {
|
|
119
|
+
valid: boolean;
|
|
120
|
+
products: KinetixProducts | null;
|
|
121
|
+
runtimeIdentity: import('./runtime').KinetixRuntimeIdentity | null;
|
|
122
|
+
diagnostics: KinetixDiagnostic[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function compileKinetixProject(input: KinetixCompileInput): KinetixCompiled;
|
|
126
|
+
export function compileTrustedKinetixSource(input: {
|
|
127
|
+
sourceRoot: string;
|
|
128
|
+
expectedCommit: string;
|
|
129
|
+
targetProfile: KinetixTargetProfile;
|
|
130
|
+
classifyInstalledBranches?: boolean;
|
|
131
|
+
}): KinetixCompiled;
|
|
132
|
+
export function kinetixCanonicalSourceDigest(sourceRoot: string): string;
|
|
133
|
+
|
|
134
|
+
export function bakeKinetixEnvelope(
|
|
135
|
+
envelope: KinetixEnvelope,
|
|
136
|
+
installedProfiles: Map<string, KinetixInstalledProfile>,
|
|
137
|
+
): KinetixBakeResult;
|
|
138
|
+
export function bakeKinetixProjectDefinition(project: import('./authoring').KinetixProjectDeclaration): {
|
|
139
|
+
products: KinetixProducts;
|
|
140
|
+
runtimeIdentity: import('./runtime').KinetixRuntimeIdentity;
|
|
141
|
+
contentDigest: string;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export function encodeKinetixSessionConfig(
|
|
145
|
+
schema: import('./authoring').KinetixSessionConfigSchema,
|
|
146
|
+
value: Readonly<Record<string, unknown>>,
|
|
147
|
+
): Uint8Array;
|
|
148
|
+
export function decodeKinetixSessionConfig(
|
|
149
|
+
schema: import('./authoring').KinetixSessionConfigSchema,
|
|
150
|
+
bytes: Uint8Array,
|
|
151
|
+
): Record<string, unknown>;
|
|
152
|
+
export function kinetixSessionConfigDigest(bytes: Uint8Array): string;
|
|
153
|
+
export function createInstalledF64Runtime(options: Record<string, unknown>): import('./runtime').KinetixRuntime<unknown, unknown>;
|
|
154
|
+
export function createFixed1000Runtime(options: Record<string, unknown>): import('./runtime').KinetixRuntime<unknown, unknown>;
|
|
155
|
+
|
|
156
|
+
export function stableKinetixProductBytes(products: KinetixProducts): Buffer;
|
|
157
|
+
export function stableKinetixSourceBytes(envelope: KinetixEnvelope): Buffer;
|
|
158
|
+
export function scanKinetixPayloadTree(value: unknown): void;
|
|
159
|
+
export function validateInstalledKinetixProfile(profileId: string, profile: KinetixInstalledProfile): void;
|
|
160
|
+
export function assertKinetixProductContract(products: KinetixProducts): KinetixProducts;
|
|
161
|
+
export function assertKinetixRuntimeContract(contract: KinetixRuntimeContract): KinetixRuntimeContract;
|
|
162
|
+
export function deriveKinetixRuntimeIdentity(
|
|
163
|
+
products: KinetixProducts,
|
|
164
|
+
runtimeContract?: KinetixRuntimeContract,
|
|
165
|
+
): import('./runtime').KinetixRuntimeIdentity;
|
|
166
|
+
export function assertKinetixRuntimeIdentityMatchesProducts(
|
|
167
|
+
products: KinetixProducts,
|
|
168
|
+
identity: import('./runtime').KinetixRuntimeIdentity,
|
|
169
|
+
): import('./runtime').KinetixRuntimeIdentity;
|
|
170
|
+
export const kinetixBakerVersion: string;
|
|
171
|
+
export const kinetixPhases: readonly string[];
|
|
172
|
+
|
|
173
|
+
export function createInstalledKinetixProfile(
|
|
174
|
+
targetProfile: KinetixTargetProfile,
|
|
175
|
+
options?: { fixedPhysics?: unknown },
|
|
176
|
+
): KinetixInstalledProfile;
|
|
177
|
+
export function kinetixRuntimeContractForProfile(
|
|
178
|
+
targetProfile: KinetixTargetProfile,
|
|
179
|
+
): KinetixRuntimeContract;
|
|
180
|
+
|
|
181
|
+
export function createKinetixWorld(options: {
|
|
182
|
+
worldId: string;
|
|
183
|
+
products: KinetixProducts;
|
|
184
|
+
installedProfiles: Map<string, KinetixInstalledProfile>;
|
|
185
|
+
initialization?: Record<string, unknown>;
|
|
186
|
+
}): KinetixWorld;
|
|
187
|
+
|
|
188
|
+
export function createKinetixWorldRuntime(options: {
|
|
189
|
+
world: KinetixWorld;
|
|
190
|
+
identity: import('./runtime').KinetixRuntimeIdentity;
|
|
191
|
+
sessionConfigBytes?: Uint8Array;
|
|
192
|
+
}): import('./runtime').KinetixRuntime<unknown, unknown>;
|
|
193
|
+
|
|
194
|
+
export function createKinetixRenderProjection(options: {
|
|
195
|
+
world: KinetixWorld;
|
|
196
|
+
products: KinetixProducts;
|
|
197
|
+
}): KinetixRenderProjection;
|
|
198
|
+
|
|
199
|
+
/** Compiles twice and byte-compares, so a nondeterministic product fails loudly. */
|
|
200
|
+
export function compileStableKinetixProducts(input: KinetixCompileInput): KinetixCompilation;
|
|
201
|
+
export function compileStableKinetixProductsFromTrustedSource(input: {
|
|
202
|
+
sourceRoot: string;
|
|
203
|
+
expectedCommit: string;
|
|
204
|
+
targetProfile: KinetixTargetProfile;
|
|
205
|
+
}): KinetixCompilation;
|
|
206
|
+
export function nativeFixed1000ProductBytes(products: KinetixProducts, pack: unknown): Buffer;
|
|
207
|
+
|
|
208
|
+
export function createFixedRuntime(pack: unknown, physics: unknown): unknown;
|
|
209
|
+
export function tickFixedRuntime(runtime: unknown, slots: unknown): void;
|
|
210
|
+
export function canonicalFixedStateBytes(runtime: unknown): Uint8Array;
|
|
211
|
+
export function canonicalFixedTickHash(runtime: unknown): string;
|
|
212
|
+
|
|
213
|
+
export function runF64Replay(
|
|
214
|
+
pack: unknown,
|
|
215
|
+
replay: unknown,
|
|
216
|
+
options?: { hashEveryTick?: boolean },
|
|
217
|
+
): { tickHashes: string[]; finalDigest: string; metrics: unknown };
|
|
218
|
+
export function canonicalF64TickHash(runtime: unknown): string;
|
|
219
|
+
|
|
220
|
+
/** Builds the portable C++ core and returns the binary path. */
|
|
221
|
+
export function buildNative(): string;
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#include "component_runtime.hpp"
|
|
2
|
+
|
|
3
|
+
#include <array>
|
|
4
|
+
#include <cstring>
|
|
5
|
+
|
|
6
|
+
namespace rundot {
|
|
7
|
+
|
|
8
|
+
namespace {
|
|
9
|
+
|
|
10
|
+
// Minimal SHA-256 (FIPS 180-4), streaming, no dependencies.
|
|
11
|
+
struct Sha256 {
|
|
12
|
+
std::uint32_t state[8];
|
|
13
|
+
std::uint64_t bitLength = 0;
|
|
14
|
+
std::uint8_t buffer[64];
|
|
15
|
+
std::size_t bufferLength = 0;
|
|
16
|
+
|
|
17
|
+
Sha256() { reset(); }
|
|
18
|
+
|
|
19
|
+
void reset() {
|
|
20
|
+
static constexpr std::uint32_t kInit[8] = {
|
|
21
|
+
0x6a09e667u, 0xbb67ae85u, 0x3c6ef372u, 0xa54ff53au,
|
|
22
|
+
0x510e527fu, 0x9b05688cu, 0x1f83d9abu, 0x5be0cd19u,
|
|
23
|
+
};
|
|
24
|
+
std::memcpy(state, kInit, sizeof(kInit));
|
|
25
|
+
bitLength = 0;
|
|
26
|
+
bufferLength = 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static std::uint32_t rotr(std::uint32_t value, std::uint32_t bits) {
|
|
30
|
+
return (value >> bits) | (value << (32 - bits));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
void transform(const std::uint8_t* chunk) {
|
|
34
|
+
static constexpr std::uint32_t kRound[64] = {
|
|
35
|
+
0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u,
|
|
36
|
+
0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u,
|
|
37
|
+
0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau,
|
|
38
|
+
0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u,
|
|
39
|
+
0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u,
|
|
40
|
+
0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u,
|
|
41
|
+
0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u,
|
|
42
|
+
0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u,
|
|
43
|
+
};
|
|
44
|
+
std::uint32_t schedule[64];
|
|
45
|
+
for (int i = 0; i < 16; ++i) {
|
|
46
|
+
schedule[i] = (static_cast<std::uint32_t>(chunk[i * 4]) << 24) |
|
|
47
|
+
(static_cast<std::uint32_t>(chunk[i * 4 + 1]) << 16) |
|
|
48
|
+
(static_cast<std::uint32_t>(chunk[i * 4 + 2]) << 8) |
|
|
49
|
+
static_cast<std::uint32_t>(chunk[i * 4 + 3]);
|
|
50
|
+
}
|
|
51
|
+
for (int i = 16; i < 64; ++i) {
|
|
52
|
+
const std::uint32_t s0 = rotr(schedule[i - 15], 7) ^ rotr(schedule[i - 15], 18) ^ (schedule[i - 15] >> 3);
|
|
53
|
+
const std::uint32_t s1 = rotr(schedule[i - 2], 17) ^ rotr(schedule[i - 2], 19) ^ (schedule[i - 2] >> 10);
|
|
54
|
+
schedule[i] = schedule[i - 16] + s0 + schedule[i - 7] + s1;
|
|
55
|
+
}
|
|
56
|
+
std::uint32_t a = state[0], b = state[1], c = state[2], d = state[3];
|
|
57
|
+
std::uint32_t e = state[4], f = state[5], g = state[6], h = state[7];
|
|
58
|
+
for (int i = 0; i < 64; ++i) {
|
|
59
|
+
const std::uint32_t s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
60
|
+
const std::uint32_t ch = (e & f) ^ (~e & g);
|
|
61
|
+
const std::uint32_t temp1 = h + s1 + ch + kRound[i] + schedule[i];
|
|
62
|
+
const std::uint32_t s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
63
|
+
const std::uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
|
|
64
|
+
const std::uint32_t temp2 = s0 + maj;
|
|
65
|
+
h = g;
|
|
66
|
+
g = f;
|
|
67
|
+
f = e;
|
|
68
|
+
e = d + temp1;
|
|
69
|
+
d = c;
|
|
70
|
+
c = b;
|
|
71
|
+
b = a;
|
|
72
|
+
a = temp1 + temp2;
|
|
73
|
+
}
|
|
74
|
+
state[0] += a; state[1] += b; state[2] += c; state[3] += d;
|
|
75
|
+
state[4] += e; state[5] += f; state[6] += g; state[7] += h;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
void update(const std::uint8_t* data, std::size_t size) {
|
|
79
|
+
bitLength += static_cast<std::uint64_t>(size) * 8u;
|
|
80
|
+
while (size > 0) {
|
|
81
|
+
const std::size_t take = std::min<std::size_t>(64 - bufferLength, size);
|
|
82
|
+
std::memcpy(buffer + bufferLength, data, take);
|
|
83
|
+
bufferLength += take;
|
|
84
|
+
data += take;
|
|
85
|
+
size -= take;
|
|
86
|
+
if (bufferLength == 64) {
|
|
87
|
+
transform(buffer);
|
|
88
|
+
bufferLength = 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
std::array<std::uint8_t, 32> digest() {
|
|
94
|
+
std::array<std::uint8_t, 32> out{};
|
|
95
|
+
const std::uint64_t finalBitLength = bitLength;
|
|
96
|
+
buffer[bufferLength++] = 0x80;
|
|
97
|
+
if (bufferLength > 56) {
|
|
98
|
+
while (bufferLength < 64) buffer[bufferLength++] = 0x00;
|
|
99
|
+
transform(buffer);
|
|
100
|
+
bufferLength = 0;
|
|
101
|
+
}
|
|
102
|
+
while (bufferLength < 56) buffer[bufferLength++] = 0x00;
|
|
103
|
+
for (int i = 0; i < 8; ++i) {
|
|
104
|
+
buffer[56 + i] = static_cast<std::uint8_t>((finalBitLength >> (56 - i * 8)) & 0xffu);
|
|
105
|
+
}
|
|
106
|
+
transform(buffer);
|
|
107
|
+
for (int i = 0; i < 8; ++i) {
|
|
108
|
+
out[i * 4] = static_cast<std::uint8_t>((state[i] >> 24) & 0xffu);
|
|
109
|
+
out[i * 4 + 1] = static_cast<std::uint8_t>((state[i] >> 16) & 0xffu);
|
|
110
|
+
out[i * 4 + 2] = static_cast<std::uint8_t>((state[i] >> 8) & 0xffu);
|
|
111
|
+
out[i * 4 + 3] = static_cast<std::uint8_t>(state[i] & 0xffu);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
std::string toHex(const std::array<std::uint8_t, 32>& digest) {
|
|
118
|
+
static constexpr char kHex[] = "0123456789abcdef";
|
|
119
|
+
std::string out;
|
|
120
|
+
out.resize(64);
|
|
121
|
+
for (std::size_t i = 0; i < digest.size(); ++i) {
|
|
122
|
+
out[i * 2] = kHex[(digest[i] >> 4) & 0x0f];
|
|
123
|
+
out[i * 2 + 1] = kHex[digest[i] & 0x0f];
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
void appendU32(std::vector<std::uint8_t>& buffer, std::uint32_t value) {
|
|
129
|
+
buffer.push_back(static_cast<std::uint8_t>(value & 0xffu));
|
|
130
|
+
buffer.push_back(static_cast<std::uint8_t>((value >> 8) & 0xffu));
|
|
131
|
+
buffer.push_back(static_cast<std::uint8_t>((value >> 16) & 0xffu));
|
|
132
|
+
buffer.push_back(static_cast<std::uint8_t>((value >> 24) & 0xffu));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
void appendU64(std::vector<std::uint8_t>& buffer, std::uint64_t value) {
|
|
136
|
+
for (int i = 0; i < 8; ++i) {
|
|
137
|
+
buffer.push_back(static_cast<std::uint8_t>((value >> (i * 8)) & 0xffu));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
void appendI64(std::vector<std::uint8_t>& buffer, std::int64_t value) {
|
|
142
|
+
appendU64(buffer, static_cast<std::uint64_t>(value));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
void appendString(std::vector<std::uint8_t>& buffer, const std::string& value) {
|
|
146
|
+
appendU32(buffer, static_cast<std::uint32_t>(value.size()));
|
|
147
|
+
buffer.insert(buffer.end(), value.begin(), value.end());
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
void appendTickState(std::vector<std::uint8_t>& buffer, const std::vector<EntityState>& entities, std::int64_t tick) {
|
|
151
|
+
appendU64(buffer, static_cast<std::uint64_t>(tick));
|
|
152
|
+
for (const EntityState& entity : entities) {
|
|
153
|
+
appendString(buffer, entity.id);
|
|
154
|
+
for (const ComponentRecord& record : entity.components) {
|
|
155
|
+
switch (record.type) {
|
|
156
|
+
case ComponentType::Transform: {
|
|
157
|
+
appendString(buffer, "Transform");
|
|
158
|
+
const TransformState& transform = entity.transform;
|
|
159
|
+
appendI64(buffer, transform.position.x);
|
|
160
|
+
appendI64(buffer, transform.position.y);
|
|
161
|
+
appendI64(buffer, transform.position.z);
|
|
162
|
+
appendI64(buffer, transform.rotation.x);
|
|
163
|
+
appendI64(buffer, transform.rotation.y);
|
|
164
|
+
appendI64(buffer, transform.rotation.z);
|
|
165
|
+
appendI64(buffer, transform.scale.x);
|
|
166
|
+
appendI64(buffer, transform.scale.y);
|
|
167
|
+
appendI64(buffer, transform.scale.z);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case ComponentType::FixedTickMover: {
|
|
171
|
+
appendString(buffer, "FixedTickMover");
|
|
172
|
+
const MoverState& mover = entity.movers[record.index];
|
|
173
|
+
appendI64(buffer, mover.velocityPerTick.x);
|
|
174
|
+
appendI64(buffer, mover.velocityPerTick.y);
|
|
175
|
+
appendI64(buffer, mover.velocityPerTick.z);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case ComponentType::InputActionState: {
|
|
179
|
+
appendString(buffer, "InputActionState");
|
|
180
|
+
const InputState& input = entity.inputs[record.index];
|
|
181
|
+
appendString(buffer, input.actionId);
|
|
182
|
+
appendString(buffer, input.outputField);
|
|
183
|
+
buffer.push_back(input.pressed ? 1 : 0);
|
|
184
|
+
appendI64(buffer, input.lastEdgeTick);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case ComponentType::ReplicatedState: {
|
|
188
|
+
appendString(buffer, "ReplicatedState");
|
|
189
|
+
appendString(buffer, entity.replicated.authority);
|
|
190
|
+
appendU32(buffer, static_cast<std::uint32_t>(entity.replicated.fields.size()));
|
|
191
|
+
for (const std::string& field : entity.replicated.fields) {
|
|
192
|
+
appendString(buffer, field);
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
bool checkedAdd(std::int64_t left, std::int64_t right, std::int64_t& out) {
|
|
202
|
+
return !__builtin_add_overflow(left, right, &out);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
} // namespace
|
|
206
|
+
|
|
207
|
+
std::string sha256Hex(const std::uint8_t* data, std::size_t size) {
|
|
208
|
+
Sha256 hasher;
|
|
209
|
+
hasher.update(data, size);
|
|
210
|
+
return toHex(hasher.digest());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
RunResult runReplay(std::vector<EntityState>& entities, const Replay& replay, bool hashEveryTick) {
|
|
214
|
+
RunResult result;
|
|
215
|
+
Sha256 streamHash;
|
|
216
|
+
std::vector<std::uint8_t> tickBuffer;
|
|
217
|
+
tickBuffer.reserve(4096 + entities.size() * 256);
|
|
218
|
+
if (hashEveryTick) {
|
|
219
|
+
result.tickHashes.reserve(static_cast<std::size_t>(replay.tickCount));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
std::size_t actionCursor = 0;
|
|
223
|
+
std::size_t correctionCursor = 0;
|
|
224
|
+
|
|
225
|
+
for (std::int64_t tick = 0; tick < replay.tickCount; ++tick) {
|
|
226
|
+
while (correctionCursor < replay.corrections.size() && replay.corrections[correctionCursor].tick == tick) {
|
|
227
|
+
const ReplayCorrection& correction = replay.corrections[correctionCursor];
|
|
228
|
+
++correctionCursor;
|
|
229
|
+
EntityState* target = nullptr;
|
|
230
|
+
for (EntityState& entity : entities) {
|
|
231
|
+
if (entity.id == correction.entityId) {
|
|
232
|
+
target = &entity;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (target == nullptr) {
|
|
237
|
+
result.faulted = true;
|
|
238
|
+
result.fault = {"COMPONENT_RUNTIME_CORRECTION_UNKNOWN_ENTITY", tick, correction.entityId};
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
if (!target->hasReplicated) {
|
|
242
|
+
result.faulted = true;
|
|
243
|
+
result.fault = {"COMPONENT_RUNTIME_CORRECTION_NOT_REPLICATED", tick, correction.entityId};
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
for (const CorrectionField& field : correction.fields) {
|
|
247
|
+
bool allowed = false;
|
|
248
|
+
for (const std::string& allowedField : target->replicated.fields) {
|
|
249
|
+
if (allowedField == field.field) {
|
|
250
|
+
allowed = true;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!allowed) {
|
|
255
|
+
result.faulted = true;
|
|
256
|
+
result.fault = {"COMPONENT_RUNTIME_CORRECTION_FIELD_NOT_ALLOWED", tick, correction.entityId};
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
if (field.field == "position" || field.field == "rotation" || field.field == "scale") {
|
|
260
|
+
if (!target->hasTransform) {
|
|
261
|
+
result.faulted = true;
|
|
262
|
+
result.fault = {"COMPONENT_RUNTIME_CORRECTION_NO_TRANSFORM", tick, correction.entityId};
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
if (field.field == "position") target->transform.position = field.vec;
|
|
266
|
+
else if (field.field == "rotation") target->transform.rotation = field.vec;
|
|
267
|
+
else target->transform.scale = field.vec;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
bool matched = false;
|
|
271
|
+
for (InputState& input : target->inputs) {
|
|
272
|
+
if (input.outputField == field.field) {
|
|
273
|
+
matched = true;
|
|
274
|
+
if (input.pressed != field.boolValue) {
|
|
275
|
+
input.pressed = field.boolValue;
|
|
276
|
+
input.lastEdgeTick = tick;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!matched) {
|
|
281
|
+
result.faulted = true;
|
|
282
|
+
result.fault = {"COMPONENT_RUNTIME_CORRECTION_FIELD_UNKNOWN", tick, correction.entityId};
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
while (actionCursor < replay.actions.size() && replay.actions[actionCursor].tick == tick) {
|
|
289
|
+
const ReplayAction& action = replay.actions[actionCursor];
|
|
290
|
+
++actionCursor;
|
|
291
|
+
for (EntityState& entity : entities) {
|
|
292
|
+
for (InputState& input : entity.inputs) {
|
|
293
|
+
if (input.actionId != action.actionId) continue;
|
|
294
|
+
if (input.pressed != action.pressed) {
|
|
295
|
+
input.pressed = action.pressed;
|
|
296
|
+
input.lastEdgeTick = tick;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (EntityState& entity : entities) {
|
|
303
|
+
for (const MoverState& mover : entity.movers) {
|
|
304
|
+
if (!entity.hasTransform) {
|
|
305
|
+
result.faulted = true;
|
|
306
|
+
result.fault = {"COMPONENT_RUNTIME_MOVER_NO_TRANSFORM", tick, entity.id};
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
std::int64_t x = 0;
|
|
310
|
+
std::int64_t y = 0;
|
|
311
|
+
std::int64_t z = 0;
|
|
312
|
+
if (!checkedAdd(entity.transform.position.x, mover.velocityPerTick.x, x) ||
|
|
313
|
+
!checkedAdd(entity.transform.position.y, mover.velocityPerTick.y, y) ||
|
|
314
|
+
!checkedAdd(entity.transform.position.z, mover.velocityPerTick.z, z)) {
|
|
315
|
+
result.faulted = true;
|
|
316
|
+
result.fault = {"COMPONENT_RUNTIME_OVERFLOW", tick, entity.id};
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
entity.transform.position = {x, y, z};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (hashEveryTick || tick == replay.tickCount - 1) {
|
|
324
|
+
tickBuffer.clear();
|
|
325
|
+
appendTickState(tickBuffer, entities, tick);
|
|
326
|
+
const std::string tickHash = sha256Hex(tickBuffer.data(), tickBuffer.size());
|
|
327
|
+
if (hashEveryTick) {
|
|
328
|
+
result.tickHashes.push_back(tickHash);
|
|
329
|
+
} else {
|
|
330
|
+
result.tickHashes.assign(1, tickHash);
|
|
331
|
+
}
|
|
332
|
+
streamHash.update(reinterpret_cast<const std::uint8_t*>(tickHash.data()), tickHash.size());
|
|
333
|
+
}
|
|
334
|
+
result.completedTicks += 1;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
result.finalDigest = toHex(streamHash.digest());
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
} // namespace rundot
|