@trioforge/crucible 0.2.0 → 0.3.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/maths/spline/package.json +4 -0
- package/out/internal/boot.luau +5 -0
- package/out/internal/network/claims.luau +1 -9
- package/out/internal/network/client-session.luau +3 -1
- package/out/internal/network/server-session.luau +3 -1
- package/out/maths/spline/curve.d.ts +35 -0
- package/out/maths/spline/curve.luau +1122 -0
- package/out/maths/spline/index.d.ts +2 -0
- package/out/maths/spline/init.luau +8 -0
- package/out/server/index.d.ts +1 -0
- package/out/server/init.luau +9 -1
- package/out/testing/index.d.ts +38 -0
- package/out/testing/init.luau +189 -0
- package/out/util/random/index.d.ts +24 -0
- package/out/util/random/init.luau +301 -0
- package/package.json +16 -1
- package/testing/package.json +4 -0
- package/transformer/index.js +50 -12
- package/util/random/package.json +4 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local exports = {}
|
|
4
|
+
local _curve = TS.import(script, script, "curve")
|
|
5
|
+
exports.catmullRom = _curve.catmullRom
|
|
6
|
+
exports.cubicBezier = _curve.cubicBezier
|
|
7
|
+
exports.uniformBSpline = _curve.uniformBSpline
|
|
8
|
+
return exports
|
package/out/server/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BootOptions } from "../index";
|
|
2
2
|
import type { PayloadArgs, ToClient } from "../network";
|
|
3
3
|
import "../internal/data/record-bindings";
|
|
4
|
+
export declare const BOOTED_ATTRIBUTE = "CrucibleBooted";
|
|
4
5
|
export { config, playerConfig, configReady, refreshConfigs } from "../configs";
|
|
5
6
|
export type { Config, PlayerConfig } from "../configs";
|
|
6
7
|
export { publish } from "./publish";
|
package/out/server/init.luau
CHANGED
|
@@ -13,6 +13,7 @@ if not game:GetService("RunService"):IsServer() then
|
|
|
13
13
|
error("@trioforge/crucible/server loaded on the client")
|
|
14
14
|
end
|
|
15
15
|
local session
|
|
16
|
+
local BOOTED_ATTRIBUTE = "CrucibleBooted"
|
|
16
17
|
local _configs = TS.import(script, script.Parent, "configs")
|
|
17
18
|
exports.config = _configs.config
|
|
18
19
|
exports.playerConfig = _configs.playerConfig
|
|
@@ -37,7 +38,13 @@ local function boot(options)
|
|
|
37
38
|
bootSide({
|
|
38
39
|
name = "server",
|
|
39
40
|
renders = false,
|
|
40
|
-
booted = function()
|
|
41
|
+
booted = function()
|
|
42
|
+
local service = game:GetService("ServerScriptService")
|
|
43
|
+
service:SetAttribute(BOOTED_ATTRIBUTE, true)
|
|
44
|
+
return function()
|
|
45
|
+
return service:SetAttribute(BOOTED_ATTRIBUTE, nil)
|
|
46
|
+
end
|
|
47
|
+
end,
|
|
41
48
|
network = network,
|
|
42
49
|
}, options)
|
|
43
50
|
end
|
|
@@ -58,5 +65,6 @@ do
|
|
|
58
65
|
_container.broadcast = broadcast
|
|
59
66
|
end
|
|
60
67
|
exports.boot = boot
|
|
68
|
+
exports.BOOTED_ATTRIBUTE = BOOTED_ATTRIBUTE
|
|
61
69
|
exports.Network = Network
|
|
62
70
|
return exports
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CrucibleBehaviour } from "../crucible-behaviour";
|
|
2
|
+
import { type MessageKind } from "../internal/network/catalog";
|
|
3
|
+
import type { Branch } from "../internal/network/schema";
|
|
4
|
+
import type { Subsystem } from "../subsystem";
|
|
5
|
+
export { n } from "../internal/network/schema";
|
|
6
|
+
/**
|
|
7
|
+
* Builds one Subsystem with the constructor arguments given, without `boot`. Its `receive()` fields register, and
|
|
8
|
+
* the spec calls its hooks as methods.
|
|
9
|
+
*/
|
|
10
|
+
export declare function subsystem<T extends Subsystem, A extends unknown[]>(klass: new (...args: A) => T, ...args: A): T;
|
|
11
|
+
/**
|
|
12
|
+
* Builds one behaviour bound to `instance`, which may be a plain table. Each `attribute()` field reads the instance
|
|
13
|
+
* through `GetAttribute` and is refused as binding refuses it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function behaviour<T extends CrucibleBehaviour<string>, A extends unknown[]>(klass: new (...args: A) => T, instance: Instance, ...args: A): T;
|
|
16
|
+
/** The callback a built Subsystem's `receive()` field registered for `message`. */
|
|
17
|
+
export declare function receiver(message: unknown): ((...args: never[]) => unknown) | undefined;
|
|
18
|
+
/** The name, kind and schema nodes of a declared message. */
|
|
19
|
+
export declare function message(token: unknown): {
|
|
20
|
+
name: string;
|
|
21
|
+
kind: MessageKind;
|
|
22
|
+
input: Branch;
|
|
23
|
+
output?: Branch;
|
|
24
|
+
} | undefined;
|
|
25
|
+
/** Sends `value` through the message's compiled codec and back, as the wire does, and returns what arrives. */
|
|
26
|
+
export declare function transfer<T>(token: unknown, value: T, which?: "input" | "output"): T;
|
|
27
|
+
/** Seals the message catalog and lists the names in wire order. It seals once per module graph. */
|
|
28
|
+
export declare function sealMessages(): readonly string[];
|
|
29
|
+
/** What the transformer registered for a class. */
|
|
30
|
+
export declare function registration(klass: object): {
|
|
31
|
+
id: string;
|
|
32
|
+
kind: "subsystem";
|
|
33
|
+
} | {
|
|
34
|
+
id: string;
|
|
35
|
+
kind: "behaviour";
|
|
36
|
+
tag: string;
|
|
37
|
+
instanceClasses: readonly string[];
|
|
38
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local exports = {}
|
|
4
|
+
local _construction_claim = TS.import(script, script.Parent, "internal", "construction-claim")
|
|
5
|
+
local clearClaim = _construction_claim.clearClaim
|
|
6
|
+
local setClaim = _construction_claim.setClaim
|
|
7
|
+
local track = TS.import(script, script.Parent, "internal", "lifecycle").track
|
|
8
|
+
local _claims = TS.import(script, script.Parent, "internal", "network", "claims")
|
|
9
|
+
local closeClaims = _claims.closeClaims
|
|
10
|
+
local openClaims = _claims.openClaims
|
|
11
|
+
local receiverOf = _claims.receiverOf
|
|
12
|
+
local _catalog = TS.import(script, script.Parent, "internal", "network", "catalog")
|
|
13
|
+
local definitionOf = _catalog.definitionOf
|
|
14
|
+
local seal = _catalog.seal
|
|
15
|
+
local _codec = TS.import(script, script.Parent, "internal", "network", "codec")
|
|
16
|
+
local Reader = _codec.Reader
|
|
17
|
+
local Writer = _codec.Writer
|
|
18
|
+
local recordOf = TS.import(script, script.Parent, "internal", "registry").recordOf
|
|
19
|
+
exports.n = TS.import(script, script.Parent, "internal", "network", "schema").n
|
|
20
|
+
local function recordFor(klass)
|
|
21
|
+
return recordOf(klass) or error(`{tostring(klass)} is not a registered Crucible class; compile it with the Crucible transformer`, 0)
|
|
22
|
+
end
|
|
23
|
+
local function definitionFor(token)
|
|
24
|
+
return definitionOf(token) or error("not a message declared with toServer, toClient or askServer", 0)
|
|
25
|
+
end
|
|
26
|
+
local function construct(klass, state, args)
|
|
27
|
+
local id = (state.record).id
|
|
28
|
+
local thread = coroutine.create(function()
|
|
29
|
+
setClaim(state)
|
|
30
|
+
return klass.new(unpack(args))
|
|
31
|
+
end)
|
|
32
|
+
local ok, result = coroutine.resume(thread)
|
|
33
|
+
local yielded = ok and coroutine.status(thread) ~= "dead"
|
|
34
|
+
if yielded then
|
|
35
|
+
coroutine.close(thread)
|
|
36
|
+
end
|
|
37
|
+
clearClaim(thread)
|
|
38
|
+
if yielded then
|
|
39
|
+
error(`{id}'s constructor yielded; boot faults a constructor that yields`, 0)
|
|
40
|
+
end
|
|
41
|
+
if not ok then
|
|
42
|
+
error(result, 0)
|
|
43
|
+
end
|
|
44
|
+
return result
|
|
45
|
+
end
|
|
46
|
+
--[[
|
|
47
|
+
*
|
|
48
|
+
* Builds one Subsystem with the constructor arguments given, without `boot`. Its `receive()` fields register, and
|
|
49
|
+
* the spec calls its hooks as methods.
|
|
50
|
+
|
|
51
|
+
]]
|
|
52
|
+
local function subsystem(klass, ...)
|
|
53
|
+
local args = { ... }
|
|
54
|
+
local record = recordFor(klass)
|
|
55
|
+
if record.kind ~= 0 then
|
|
56
|
+
error(`{record.id} is a behaviour; build it with behaviour()`, 0)
|
|
57
|
+
end
|
|
58
|
+
-- A spec's RunService double only needs IsClient when the class has a receive() field.
|
|
59
|
+
openClaims({
|
|
60
|
+
side = function()
|
|
61
|
+
return if game:GetService("RunService"):IsClient() then "client" else "server"
|
|
62
|
+
end,
|
|
63
|
+
retired = function() end,
|
|
64
|
+
})
|
|
65
|
+
local ok, result = pcall(construct, klass, track(record), args)
|
|
66
|
+
closeClaims(false)
|
|
67
|
+
if not ok then
|
|
68
|
+
error(result, 0)
|
|
69
|
+
end
|
|
70
|
+
return result
|
|
71
|
+
end
|
|
72
|
+
--[[
|
|
73
|
+
*
|
|
74
|
+
* Builds one behaviour bound to `instance`, which may be a plain table. Each `attribute()` field reads the instance
|
|
75
|
+
* through `GetAttribute` and is refused as binding refuses it.
|
|
76
|
+
|
|
77
|
+
]]
|
|
78
|
+
local function behaviour(klass, instance, ...)
|
|
79
|
+
local args = { ... }
|
|
80
|
+
local record = recordFor(klass)
|
|
81
|
+
if record.kind ~= 1 then
|
|
82
|
+
error(`{record.id} is a Subsystem; build it with subsystem()`, 0)
|
|
83
|
+
end
|
|
84
|
+
local attributes
|
|
85
|
+
if record.attributes ~= nil then
|
|
86
|
+
attributes = {}
|
|
87
|
+
for field, descriptor in record.attributes do
|
|
88
|
+
local value = instance:GetAttribute(descriptor.name)
|
|
89
|
+
if not (if value == nil then not descriptor.required else descriptor.check(value)) then
|
|
90
|
+
error(`{record.id}: attribute {descriptor.name} is invalid ({typeof(value)})`, 0)
|
|
91
|
+
end
|
|
92
|
+
attributes[field] = value
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
return construct(klass, track(record, instance, attributes), args)
|
|
96
|
+
end
|
|
97
|
+
--* The callback a built Subsystem's `receive()` field registered for `message`.
|
|
98
|
+
local function receiver(message)
|
|
99
|
+
local _result = receiverOf(definitionFor(message))
|
|
100
|
+
if _result ~= nil then
|
|
101
|
+
_result = _result.callback
|
|
102
|
+
end
|
|
103
|
+
return _result
|
|
104
|
+
end
|
|
105
|
+
--* The name, kind and schema nodes of a declared message.
|
|
106
|
+
local function message(token)
|
|
107
|
+
local definition = definitionOf(token)
|
|
108
|
+
if definition == nil then
|
|
109
|
+
return nil
|
|
110
|
+
end
|
|
111
|
+
local _object = {
|
|
112
|
+
name = definition.name,
|
|
113
|
+
kind = definition.kind,
|
|
114
|
+
input = definition.input.node,
|
|
115
|
+
}
|
|
116
|
+
local _left = "output"
|
|
117
|
+
local _result = definition.output
|
|
118
|
+
if _result ~= nil then
|
|
119
|
+
_result = _result.node
|
|
120
|
+
end
|
|
121
|
+
_object[_left] = _result
|
|
122
|
+
return _object
|
|
123
|
+
end
|
|
124
|
+
local function failure(err)
|
|
125
|
+
local _err = err
|
|
126
|
+
local _condition = type(_err) == "table"
|
|
127
|
+
if _condition then
|
|
128
|
+
_condition = err.codecFailure == true
|
|
129
|
+
end
|
|
130
|
+
if _condition then
|
|
131
|
+
local _binding = err
|
|
132
|
+
local path = _binding.path
|
|
133
|
+
local reason = _binding.reason
|
|
134
|
+
return `{path}: {reason}`
|
|
135
|
+
end
|
|
136
|
+
return tostring(err)
|
|
137
|
+
end
|
|
138
|
+
--* Sends `value` through the message's compiled codec and back, as the wire does, and returns what arrives.
|
|
139
|
+
local function transfer(token, value, which)
|
|
140
|
+
if which == nil then
|
|
141
|
+
which = "input"
|
|
142
|
+
end
|
|
143
|
+
local definition = definitionFor(token)
|
|
144
|
+
local codec = definition[which] or error(`{definition.name} has no {which} codec`, 0)
|
|
145
|
+
local writer = Writer.new()
|
|
146
|
+
local encoded, encodeError = pcall(codec.encode, writer, value, "payload")
|
|
147
|
+
if not encoded then
|
|
148
|
+
error(`{definition.name} refused to encode {failure(encodeError)}`, 0)
|
|
149
|
+
end
|
|
150
|
+
local packet, sidecar, slots = writer:finish(buffer.create(0))
|
|
151
|
+
local reader = Reader.new(packet, 0, sidecar, slots)
|
|
152
|
+
local decoded, result = pcall(codec.decode, reader, "payload")
|
|
153
|
+
if not decoded then
|
|
154
|
+
error(`{definition.name} failed to decode {failure(result)}`, 0)
|
|
155
|
+
end
|
|
156
|
+
if not reader:finished() then
|
|
157
|
+
error(`{definition.name} left trailing bytes or slots`, 0)
|
|
158
|
+
end
|
|
159
|
+
return result
|
|
160
|
+
end
|
|
161
|
+
--* Seals the message catalog and lists the names in wire order. It seals once per module graph.
|
|
162
|
+
local function sealMessages()
|
|
163
|
+
local catalog = seal()
|
|
164
|
+
return if catalog == "" then {} else string.split(catalog, "\n")
|
|
165
|
+
end
|
|
166
|
+
--* What the transformer registered for a class.
|
|
167
|
+
local function registration(klass)
|
|
168
|
+
local record = recordFor(klass)
|
|
169
|
+
if record.kind == 0 then
|
|
170
|
+
return {
|
|
171
|
+
id = record.id,
|
|
172
|
+
kind = "subsystem",
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
return {
|
|
176
|
+
id = record.id,
|
|
177
|
+
kind = "behaviour",
|
|
178
|
+
tag = record.tag,
|
|
179
|
+
instanceClasses = record.instanceClasses,
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
exports.subsystem = subsystem
|
|
183
|
+
exports.behaviour = behaviour
|
|
184
|
+
exports.receiver = receiver
|
|
185
|
+
exports.message = message
|
|
186
|
+
exports.transfer = transfer
|
|
187
|
+
exports.sealMessages = sealMessages
|
|
188
|
+
exports.registration = registration
|
|
189
|
+
return exports
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type SeedPart = string | number;
|
|
2
|
+
export type RngState = readonly [number, number, number, number];
|
|
3
|
+
export declare class Rng {
|
|
4
|
+
private a;
|
|
5
|
+
private b;
|
|
6
|
+
private c;
|
|
7
|
+
private d;
|
|
8
|
+
constructor(...seed: SeedPart[]);
|
|
9
|
+
static fromState(state: RngState): Rng;
|
|
10
|
+
state(): RngState;
|
|
11
|
+
clone(): Rng;
|
|
12
|
+
fork(): Rng;
|
|
13
|
+
uint32(): number;
|
|
14
|
+
float(): number;
|
|
15
|
+
float(min: number, max: number): number;
|
|
16
|
+
int(min: number, max: number): number;
|
|
17
|
+
chance(probability: number): boolean;
|
|
18
|
+
normal(mean?: number, deviation?: number): number;
|
|
19
|
+
pick<T extends defined>(items: readonly T[]): T;
|
|
20
|
+
weightedIndex(weights: readonly number[]): number;
|
|
21
|
+
weighted<T extends defined>(items: readonly T[], weight: (item: T, index: number) => number): T;
|
|
22
|
+
shuffle<T extends defined>(items: readonly T[]): T[];
|
|
23
|
+
sample<T extends defined>(items: readonly T[], count: number): T[];
|
|
24
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local WORD = 2 ^ 32
|
|
3
|
+
local DOUBLE = 2 ^ 53
|
|
4
|
+
-- Luau has no 32-bit multiply, and a full product of two words overflows a double's 53 bits, so it splits one side.
|
|
5
|
+
local function imul(a, b)
|
|
6
|
+
local low = a * (b % 65536)
|
|
7
|
+
local high = ((a * math.floor(b / 65536)) % WORD) * 65536
|
|
8
|
+
return (low + high) % WORD
|
|
9
|
+
end
|
|
10
|
+
local function fmix(h)
|
|
11
|
+
h = imul(bit32.bxor(h, bit32.rshift(h, 16)), 0x85ebca6b)
|
|
12
|
+
h = imul(bit32.bxor(h, bit32.rshift(h, 13)), 0xc2b2ae35)
|
|
13
|
+
return bit32.bxor(h, bit32.rshift(h, 16))
|
|
14
|
+
end
|
|
15
|
+
local STRING = 0x5354524e
|
|
16
|
+
local NUMBER = 0x4e554d42
|
|
17
|
+
local function words(parts)
|
|
18
|
+
local result = { #parts }
|
|
19
|
+
for _, part in parts do
|
|
20
|
+
if type(part) == "string" then
|
|
21
|
+
local _arg1 = #part
|
|
22
|
+
-- ▼ Array.push ▼
|
|
23
|
+
table.insert(result, STRING)
|
|
24
|
+
table.insert(result, _arg1)
|
|
25
|
+
-- ▲ Array.push ▲
|
|
26
|
+
for index = 1, #part, 4 do
|
|
27
|
+
local a, b, c, d = string.byte(part, index, index + 3)
|
|
28
|
+
local _condition = a
|
|
29
|
+
if _condition == nil then
|
|
30
|
+
_condition = 0
|
|
31
|
+
end
|
|
32
|
+
local _condition_1 = b
|
|
33
|
+
if _condition_1 == nil then
|
|
34
|
+
_condition_1 = 0
|
|
35
|
+
end
|
|
36
|
+
local _exp = _condition + _condition_1 * 0x100
|
|
37
|
+
local _condition_2 = c
|
|
38
|
+
if _condition_2 == nil then
|
|
39
|
+
_condition_2 = 0
|
|
40
|
+
end
|
|
41
|
+
local _exp_1 = _exp + _condition_2 * 0x10000
|
|
42
|
+
local _condition_3 = d
|
|
43
|
+
if _condition_3 == nil then
|
|
44
|
+
_condition_3 = 0
|
|
45
|
+
end
|
|
46
|
+
table.insert(result, _exp_1 + _condition_3 * 0x1000000)
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
if part ~= part or part % 1 ~= 0 or math.abs(part) > DOUBLE then
|
|
50
|
+
error(`a seed number must be an integer within ±2^53, got {part}`, 3)
|
|
51
|
+
end
|
|
52
|
+
local _arg1 = part % WORD
|
|
53
|
+
local _arg2 = math.floor(part / WORD) % WORD
|
|
54
|
+
-- ▼ Array.push ▼
|
|
55
|
+
table.insert(result, NUMBER)
|
|
56
|
+
table.insert(result, _arg1)
|
|
57
|
+
table.insert(result, _arg2)
|
|
58
|
+
-- ▲ Array.push ▲
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
return result
|
|
62
|
+
end
|
|
63
|
+
local LANES = { 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a }
|
|
64
|
+
local function hashState(input)
|
|
65
|
+
-- ▼ ReadonlyArray.map ▼
|
|
66
|
+
local _newValue = table.create(#LANES)
|
|
67
|
+
local _callback = function(lane)
|
|
68
|
+
local h = lane
|
|
69
|
+
for _, word in input do
|
|
70
|
+
h = fmix((imul(bit32.bxor(h, word), 0x9e3779b1) + lane) % WORD)
|
|
71
|
+
end
|
|
72
|
+
return fmix(h)
|
|
73
|
+
end
|
|
74
|
+
for _k, _v in LANES do
|
|
75
|
+
_newValue[_k] = _callback(_v, _k - 1, LANES)
|
|
76
|
+
end
|
|
77
|
+
-- ▲ ReadonlyArray.map ▲
|
|
78
|
+
local state = _newValue
|
|
79
|
+
-- ▼ ReadonlyArray.every ▼
|
|
80
|
+
local _result = true
|
|
81
|
+
local _callback_1 = function(word)
|
|
82
|
+
return word == 0
|
|
83
|
+
end
|
|
84
|
+
for _k, _v in state do
|
|
85
|
+
if not _callback_1(_v, _k - 1, state) then
|
|
86
|
+
_result = false
|
|
87
|
+
break
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
-- ▲ ReadonlyArray.every ▲
|
|
91
|
+
if _result then
|
|
92
|
+
state[1] = 1
|
|
93
|
+
end
|
|
94
|
+
return { state[1], state[2], state[3], state[4] }
|
|
95
|
+
end
|
|
96
|
+
local function entropy()
|
|
97
|
+
return { math.random(0, 0xffff), math.random(0, 0xffff), math.random(0, 0xffff), math.floor(os.clock() * 1e6) % WORD }
|
|
98
|
+
end
|
|
99
|
+
local Rng
|
|
100
|
+
do
|
|
101
|
+
Rng = setmetatable({}, {
|
|
102
|
+
__tostring = function()
|
|
103
|
+
return "Rng"
|
|
104
|
+
end,
|
|
105
|
+
})
|
|
106
|
+
Rng.__index = Rng
|
|
107
|
+
function Rng.new(...)
|
|
108
|
+
local self = setmetatable({}, Rng)
|
|
109
|
+
return self:constructor(...) or self
|
|
110
|
+
end
|
|
111
|
+
function Rng:constructor(...)
|
|
112
|
+
local seed = { ... }
|
|
113
|
+
local _binding = hashState(if #seed > 0 then words(seed) else entropy())
|
|
114
|
+
self.a = _binding[1]
|
|
115
|
+
self.b = _binding[2]
|
|
116
|
+
self.c = _binding[3]
|
|
117
|
+
self.d = _binding[4]
|
|
118
|
+
end
|
|
119
|
+
function Rng:fromState(state)
|
|
120
|
+
local rng = Rng.new(0)
|
|
121
|
+
for _, word in state do
|
|
122
|
+
if word % 1 ~= 0 or word < 0 or word >= WORD then
|
|
123
|
+
error(`a state word must be a uint32, got {word}`, 2)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
-- ▼ ReadonlyArray.every ▼
|
|
127
|
+
local _result = true
|
|
128
|
+
local _callback = function(word)
|
|
129
|
+
return word == 0
|
|
130
|
+
end
|
|
131
|
+
for _k, _v in state do
|
|
132
|
+
if not _callback(_v, _k - 1, state) then
|
|
133
|
+
_result = false
|
|
134
|
+
break
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
-- ▲ ReadonlyArray.every ▲
|
|
138
|
+
if _result then
|
|
139
|
+
error("a state can't be all zeroes", 2)
|
|
140
|
+
end
|
|
141
|
+
local _binding = state
|
|
142
|
+
rng.a = _binding[1]
|
|
143
|
+
rng.b = _binding[2]
|
|
144
|
+
rng.c = _binding[3]
|
|
145
|
+
rng.d = _binding[4]
|
|
146
|
+
return rng
|
|
147
|
+
end
|
|
148
|
+
function Rng:state()
|
|
149
|
+
return { self.a, self.b, self.c, self.d }
|
|
150
|
+
end
|
|
151
|
+
function Rng:clone()
|
|
152
|
+
return Rng:fromState(self:state())
|
|
153
|
+
end
|
|
154
|
+
function Rng:fork()
|
|
155
|
+
return Rng.new(self:uint32(), self:uint32(), self:uint32(), self:uint32())
|
|
156
|
+
end
|
|
157
|
+
function Rng:uint32()
|
|
158
|
+
local result = (bit32.lrotate((self.b * 5) % WORD, 7) * 9) % WORD
|
|
159
|
+
local t = bit32.lshift(self.b, 9)
|
|
160
|
+
self.c = bit32.bxor(self.c, self.a)
|
|
161
|
+
self.d = bit32.bxor(self.d, self.b)
|
|
162
|
+
self.b = bit32.bxor(self.b, self.c)
|
|
163
|
+
self.a = bit32.bxor(self.a, self.d)
|
|
164
|
+
self.c = bit32.bxor(self.c, t)
|
|
165
|
+
self.d = bit32.lrotate(self.d, 11)
|
|
166
|
+
return result
|
|
167
|
+
end
|
|
168
|
+
function Rng:float(min, max)
|
|
169
|
+
local unit = (bit32.rshift(self:uint32(), 5) * 2 ^ 26 + bit32.rshift(self:uint32(), 6)) / DOUBLE
|
|
170
|
+
if min == nil or max == nil then
|
|
171
|
+
return unit
|
|
172
|
+
end
|
|
173
|
+
if not (min <= max) then
|
|
174
|
+
error(`float needs min <= max, got {min} and {max}`, 2)
|
|
175
|
+
end
|
|
176
|
+
return min + unit * (max - min)
|
|
177
|
+
end
|
|
178
|
+
function Rng:int(min, max)
|
|
179
|
+
if min % 1 ~= 0 or max % 1 ~= 0 then
|
|
180
|
+
error(`int needs integer bounds, got {min} and {max}`, 2)
|
|
181
|
+
end
|
|
182
|
+
local span = max - min + 1
|
|
183
|
+
if span < 1 then
|
|
184
|
+
error(`int needs min <= max, got {min} and {max}`, 2)
|
|
185
|
+
end
|
|
186
|
+
if span > WORD then
|
|
187
|
+
error(`int spans at most 2^32 values, got {span}`, 2)
|
|
188
|
+
end
|
|
189
|
+
local limit = WORD - (WORD % span)
|
|
190
|
+
local value = self:uint32()
|
|
191
|
+
while value >= limit do
|
|
192
|
+
value = self:uint32()
|
|
193
|
+
end
|
|
194
|
+
return min + (value % span)
|
|
195
|
+
end
|
|
196
|
+
function Rng:chance(probability)
|
|
197
|
+
return self:float() < probability
|
|
198
|
+
end
|
|
199
|
+
function Rng:normal(mean, deviation)
|
|
200
|
+
if mean == nil then
|
|
201
|
+
mean = 0
|
|
202
|
+
end
|
|
203
|
+
if deviation == nil then
|
|
204
|
+
deviation = 1
|
|
205
|
+
end
|
|
206
|
+
local radius = math.sqrt(-2 * math.log(1 - self:float()))
|
|
207
|
+
return mean + deviation * radius * math.cos(2 * math.pi * self:float())
|
|
208
|
+
end
|
|
209
|
+
function Rng:pick(items)
|
|
210
|
+
if #items == 0 then
|
|
211
|
+
error("can't pick from an empty array", 2)
|
|
212
|
+
end
|
|
213
|
+
return items[self:int(0, #items - 1) + 1]
|
|
214
|
+
end
|
|
215
|
+
function Rng:weightedIndex(weights)
|
|
216
|
+
local total = 0
|
|
217
|
+
for _, weight in weights do
|
|
218
|
+
if not (weight >= 0 and weight < math.huge) then
|
|
219
|
+
error(`a weight must be finite and non-negative, got {weight}`, 2)
|
|
220
|
+
end
|
|
221
|
+
total += weight
|
|
222
|
+
end
|
|
223
|
+
if total <= 0 then
|
|
224
|
+
error("weights must add up to more than zero", 2)
|
|
225
|
+
end
|
|
226
|
+
local roll = self:float() * total
|
|
227
|
+
local last = 0
|
|
228
|
+
for index = 0, #weights - 1 do
|
|
229
|
+
if weights[index + 1] <= 0 then
|
|
230
|
+
continue
|
|
231
|
+
end
|
|
232
|
+
last = index
|
|
233
|
+
roll -= weights[index + 1]
|
|
234
|
+
if roll < 0 then
|
|
235
|
+
return index
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
-- Rounding can leave a sliver of the roll past the last bucket.
|
|
239
|
+
return last
|
|
240
|
+
end
|
|
241
|
+
function Rng:weighted(items, weight)
|
|
242
|
+
local _exp = items
|
|
243
|
+
local _self = self
|
|
244
|
+
local _items = items
|
|
245
|
+
local _weight = weight
|
|
246
|
+
-- ▼ ReadonlyArray.map ▼
|
|
247
|
+
local _newValue = table.create(#_items)
|
|
248
|
+
for _k, _v in _items do
|
|
249
|
+
_newValue[_k] = _weight(_v, _k - 1, _items)
|
|
250
|
+
end
|
|
251
|
+
-- ▲ ReadonlyArray.map ▲
|
|
252
|
+
return _exp[_self:weightedIndex(_newValue) + 1]
|
|
253
|
+
end
|
|
254
|
+
function Rng:shuffle(items)
|
|
255
|
+
local _array = {}
|
|
256
|
+
local _length = #_array
|
|
257
|
+
table.move(items, 1, #items, _length + 1, _array)
|
|
258
|
+
local result = _array
|
|
259
|
+
for index = #result - 1, 1, -1 do
|
|
260
|
+
local other = self:int(0, index)
|
|
261
|
+
local _index = index + 1
|
|
262
|
+
local _index_1 = other + 1
|
|
263
|
+
result[_index], result[_index_1] = result[other + 1], result[index + 1]
|
|
264
|
+
end
|
|
265
|
+
return result
|
|
266
|
+
end
|
|
267
|
+
function Rng:sample(items, count)
|
|
268
|
+
if count % 1 ~= 0 or count < 0 or count > #items then
|
|
269
|
+
error(`sample needs a count from 0 to {#items}, got {count}`, 2)
|
|
270
|
+
end
|
|
271
|
+
local _array = {}
|
|
272
|
+
local _length = #_array
|
|
273
|
+
table.move(items, 1, #items, _length + 1, _array)
|
|
274
|
+
local pool = _array
|
|
275
|
+
local result = {}
|
|
276
|
+
do
|
|
277
|
+
local index = 0
|
|
278
|
+
local _shouldIncrement = false
|
|
279
|
+
while true do
|
|
280
|
+
if _shouldIncrement then
|
|
281
|
+
index += 1
|
|
282
|
+
else
|
|
283
|
+
_shouldIncrement = true
|
|
284
|
+
end
|
|
285
|
+
if not (index < count) then
|
|
286
|
+
break
|
|
287
|
+
end
|
|
288
|
+
local other = self:int(index, #pool - 1)
|
|
289
|
+
local _index = index + 1
|
|
290
|
+
local _index_1 = other + 1
|
|
291
|
+
pool[_index], pool[_index_1] = pool[other + 1], pool[index + 1]
|
|
292
|
+
local _arg0 = pool[index + 1]
|
|
293
|
+
table.insert(result, _arg0)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
return result
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
return {
|
|
300
|
+
Rng = Rng,
|
|
301
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trioforge/crucible",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "out/init.luau",
|
|
6
6
|
"types": "out/index.d.ts",
|
|
@@ -25,6 +25,18 @@
|
|
|
25
25
|
"types": "./out/vide/index.d.ts",
|
|
26
26
|
"default": "./out/vide/init.luau"
|
|
27
27
|
},
|
|
28
|
+
"./util/random": {
|
|
29
|
+
"types": "./out/util/random/index.d.ts",
|
|
30
|
+
"default": "./out/util/random/init.luau"
|
|
31
|
+
},
|
|
32
|
+
"./maths/spline": {
|
|
33
|
+
"types": "./out/maths/spline/index.d.ts",
|
|
34
|
+
"default": "./out/maths/spline/init.luau"
|
|
35
|
+
},
|
|
36
|
+
"./testing": {
|
|
37
|
+
"types": "./out/testing/index.d.ts",
|
|
38
|
+
"default": "./out/testing/init.luau"
|
|
39
|
+
},
|
|
28
40
|
"./transformer": "./transformer/index.js"
|
|
29
41
|
},
|
|
30
42
|
"peerDependencies": {
|
|
@@ -43,6 +55,9 @@
|
|
|
43
55
|
"client",
|
|
44
56
|
"emit",
|
|
45
57
|
"vide",
|
|
58
|
+
"util",
|
|
59
|
+
"maths",
|
|
60
|
+
"testing",
|
|
46
61
|
"LICENSE"
|
|
47
62
|
],
|
|
48
63
|
"repository": {
|