@quenty/streamingutils 10.20.0 → 10.22.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/CHANGELOG.md +11 -0
- package/deploy.nevermore.json +10 -0
- package/package.json +10 -3
- package/src/Client/StreamingCinematicsServiceClient.lua +122 -0
- package/src/Server/StreamingCinematicsService.lua +91 -0
- package/src/Shared/ReplicationFocusTracker.lua +87 -0
- package/src/Shared/ReplicationFocusTracker.spec.lua +84 -0
- package/src/jest.config.lua +3 -0
- package/test/default.project.json +17 -0
- package/test/scripts/Server/ServerMain.server.lua +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [10.22.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/streamingutils@10.21.0...@quenty/streamingutils@10.22.0) (2026-07-22)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @quenty/streamingutils
|
|
9
|
+
|
|
10
|
+
# [10.21.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/streamingutils@10.20.0...@quenty/streamingutils@10.21.0) (2026-07-20)
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- Integrate teleport data service directly into save slots for cross-game continuation ([ffa7496](https://github.com/Quenty/NevermoreEngine/commit/ffa7496047b80a82623440221c019fdfe09e31ea))
|
|
15
|
+
- **streamingutils:** add StreamingCinematicsService for camera streaming focus ([4853cd6](https://github.com/Quenty/NevermoreEngine/commit/4853cd6d64c9ca32ea454ca7ebe69697c0baba1e))
|
|
16
|
+
|
|
6
17
|
# [10.20.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/streamingutils@10.19.0...@quenty/streamingutils@10.20.0) (2026-07-18)
|
|
7
18
|
|
|
8
19
|
**Note:** Version bump only for package @quenty/streamingutils
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/streamingutils",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.22.0",
|
|
4
4
|
"description": "Provides utilities for working with Roblox's streaming system",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -29,11 +29,18 @@
|
|
|
29
29
|
"Quenty"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@quenty/instanceutils": "13.32.0",
|
|
32
33
|
"@quenty/loader": "10.11.0",
|
|
33
|
-
"@quenty/
|
|
34
|
+
"@quenty/maid": "3.10.0",
|
|
35
|
+
"@quenty/nevermore-test-runner": "1.4.0",
|
|
36
|
+
"@quenty/promise": "10.20.0",
|
|
37
|
+
"@quenty/remoting": "12.35.0",
|
|
38
|
+
"@quenty/rx": "13.30.0",
|
|
39
|
+
"@quenty/servicebag": "11.19.0",
|
|
40
|
+
"@quentystudios/jest-lua": "3.10.0-quenty.2"
|
|
34
41
|
},
|
|
35
42
|
"publishConfig": {
|
|
36
43
|
"access": "public"
|
|
37
44
|
},
|
|
38
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "93f15b200c0f465a0b5304fcf05f5cfe7fdcbd1d"
|
|
39
46
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Client half of the streaming-cinematics system. While a cinematic camera is active, feeds the
|
|
4
|
+
camera position up to the [StreamingCinematicsService] (throttled) so the server can stream world
|
|
5
|
+
content in around it -- covering the cases where the player has no character, or the character is
|
|
6
|
+
far from the camera.
|
|
7
|
+
|
|
8
|
+
@client
|
|
9
|
+
@class StreamingCinematicsServiceClient
|
|
10
|
+
]=]
|
|
11
|
+
|
|
12
|
+
local require = require(script.Parent.loader).load(script)
|
|
13
|
+
|
|
14
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
15
|
+
local Workspace = game:GetService("Workspace")
|
|
16
|
+
|
|
17
|
+
local Maid = require("Maid")
|
|
18
|
+
local Observable = require("Observable")
|
|
19
|
+
local Remoting = require("Remoting")
|
|
20
|
+
local Rx = require("Rx")
|
|
21
|
+
local RxInstanceUtils = require("RxInstanceUtils")
|
|
22
|
+
local ServiceBag = require("ServiceBag")
|
|
23
|
+
|
|
24
|
+
-- A handful of updates per second is plenty for streaming; the pan is slow and the streaming radius
|
|
25
|
+
-- dwarfs the per-update movement. leading+trailing so the first and final resting positions both send.
|
|
26
|
+
local SEND_RATE_SECONDS = 0.25
|
|
27
|
+
|
|
28
|
+
local StreamingCinematicsServiceClient = {}
|
|
29
|
+
StreamingCinematicsServiceClient.ServiceName = "StreamingCinematicsServiceClient"
|
|
30
|
+
|
|
31
|
+
export type StreamingCinematicsServiceClient = typeof(setmetatable(
|
|
32
|
+
{} :: {
|
|
33
|
+
_serviceBag: ServiceBag.ServiceBag,
|
|
34
|
+
_maid: Maid.Maid,
|
|
35
|
+
_remoting: any,
|
|
36
|
+
},
|
|
37
|
+
{} :: typeof({ __index = StreamingCinematicsServiceClient })
|
|
38
|
+
))
|
|
39
|
+
|
|
40
|
+
function StreamingCinematicsServiceClient.Init(
|
|
41
|
+
self: StreamingCinematicsServiceClient,
|
|
42
|
+
serviceBag: ServiceBag.ServiceBag
|
|
43
|
+
): ()
|
|
44
|
+
assert(not (self :: any)._remoting, "Already initialized")
|
|
45
|
+
self._serviceBag = assert(serviceBag, "No serviceBag")
|
|
46
|
+
self._maid = Maid.new()
|
|
47
|
+
|
|
48
|
+
self._remoting = self._maid:Add(Remoting.Client.new(ReplicatedStorage, "StreamingCinematics"))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
--[=[
|
|
52
|
+
Sends a single focus position to the server (or nil to clear). Low-level; prefer
|
|
53
|
+
[StreamingCinematicsServiceClient:PushCameraFocus].
|
|
54
|
+
@param position Vector3?
|
|
55
|
+
]=]
|
|
56
|
+
function StreamingCinematicsServiceClient.SetFocus(self: StreamingCinematicsServiceClient, position: Vector3?): ()
|
|
57
|
+
self._remoting.SetFocus:FireServer(position)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
--[=[
|
|
61
|
+
Observes the current camera's position, following [Workspace.CurrentCamera] as it changes.
|
|
62
|
+
@return Observable<Vector3?>
|
|
63
|
+
]=]
|
|
64
|
+
function StreamingCinematicsServiceClient.ObserveCurrentCameraPosition(
|
|
65
|
+
_self: StreamingCinematicsServiceClient
|
|
66
|
+
): Observable.Observable<Vector3?>
|
|
67
|
+
local observeCamera: any = RxInstanceUtils.observeProperty(Workspace, "CurrentCamera")
|
|
68
|
+
|
|
69
|
+
local switchToCFrame: any = Rx.switchMap(function(camera): any
|
|
70
|
+
if not camera then
|
|
71
|
+
return Rx.of(nil)
|
|
72
|
+
end
|
|
73
|
+
return RxInstanceUtils.observeProperty(camera, "CFrame")
|
|
74
|
+
end)
|
|
75
|
+
local toPosition: any = Rx.map(function(cframe): Vector3?
|
|
76
|
+
return if cframe then cframe.Position else nil
|
|
77
|
+
end)
|
|
78
|
+
|
|
79
|
+
return observeCamera:Pipe({ switchToCFrame, toPosition })
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
--[=[
|
|
83
|
+
Streams world content around a cinematic camera until the returned cleanup runs. Defaults to
|
|
84
|
+
following the current camera; pass an observable of `Vector3?` to drive the focus explicitly.
|
|
85
|
+
Sends are throttled to [SEND_RATE_SECONDS].
|
|
86
|
+
|
|
87
|
+
@param observePosition Observable<Vector3?>? -- Optional override
|
|
88
|
+
@return function -- Cleanup; clears the focus when called
|
|
89
|
+
]=]
|
|
90
|
+
function StreamingCinematicsServiceClient.PushCameraFocus(
|
|
91
|
+
self: StreamingCinematicsServiceClient,
|
|
92
|
+
observePosition: Observable.Observable<Vector3?>?
|
|
93
|
+
): () -> ()
|
|
94
|
+
local source: any = observePosition or self:ObserveCurrentCameraPosition()
|
|
95
|
+
|
|
96
|
+
local maid = Maid.new()
|
|
97
|
+
|
|
98
|
+
maid:GiveTask(source
|
|
99
|
+
:Pipe({
|
|
100
|
+
Rx.throttleTime(SEND_RATE_SECONDS, {
|
|
101
|
+
leading = true,
|
|
102
|
+
trailing = true,
|
|
103
|
+
}),
|
|
104
|
+
})
|
|
105
|
+
:Subscribe(function(position: Vector3?)
|
|
106
|
+
self:SetFocus(position)
|
|
107
|
+
end))
|
|
108
|
+
|
|
109
|
+
maid:GiveTask(function()
|
|
110
|
+
self:SetFocus(nil)
|
|
111
|
+
end)
|
|
112
|
+
|
|
113
|
+
return function()
|
|
114
|
+
maid:DoCleaning()
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
function StreamingCinematicsServiceClient.Destroy(self: StreamingCinematicsServiceClient): ()
|
|
119
|
+
self._maid:DoCleaning()
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
return StreamingCinematicsServiceClient
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Server half of the streaming-cinematics system. Receives a focus position from a client
|
|
4
|
+
(who has no character, or whose character is far from a cinematic camera) and points that
|
|
5
|
+
player's `ReplicationFocus` at it via a [ReplicationFocusTracker], so world content streams
|
|
6
|
+
in around the cinematic camera. Sending nil clears it.
|
|
7
|
+
|
|
8
|
+
@server
|
|
9
|
+
@class StreamingCinematicsService
|
|
10
|
+
]=]
|
|
11
|
+
|
|
12
|
+
local require = require(script.Parent.loader).load(script)
|
|
13
|
+
|
|
14
|
+
local Players = game:GetService("Players")
|
|
15
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
16
|
+
|
|
17
|
+
local Maid = require("Maid")
|
|
18
|
+
local Remoting = require("Remoting")
|
|
19
|
+
local ReplicationFocusTracker = require("ReplicationFocusTracker")
|
|
20
|
+
|
|
21
|
+
local StreamingCinematicsService = {}
|
|
22
|
+
StreamingCinematicsService.ServiceName = "StreamingCinematicsService"
|
|
23
|
+
|
|
24
|
+
export type StreamingCinematicsService = typeof(setmetatable(
|
|
25
|
+
{} :: {
|
|
26
|
+
_maid: Maid.Maid,
|
|
27
|
+
_trackers: { [Player]: ReplicationFocusTracker.ReplicationFocusTracker },
|
|
28
|
+
_remoting: any,
|
|
29
|
+
},
|
|
30
|
+
{} :: typeof({ __index = StreamingCinematicsService })
|
|
31
|
+
))
|
|
32
|
+
|
|
33
|
+
function StreamingCinematicsService.Init(self: StreamingCinematicsService): ()
|
|
34
|
+
assert(not (self :: any)._remoting, "Already initialized")
|
|
35
|
+
self._maid = Maid.new()
|
|
36
|
+
|
|
37
|
+
self._trackers = {}
|
|
38
|
+
|
|
39
|
+
self._remoting = self._maid:Add(Remoting.Server.new(ReplicatedStorage, "StreamingCinematics"))
|
|
40
|
+
self._remoting:DeclareEvent("SetFocus")
|
|
41
|
+
|
|
42
|
+
self._maid:GiveTask(self._remoting.SetFocus:Connect(function(player: Player, position: Vector3?)
|
|
43
|
+
self:_setFocus(player, position)
|
|
44
|
+
end))
|
|
45
|
+
|
|
46
|
+
self._maid:GiveTask(Players.PlayerRemoving:Connect(function(player: Player)
|
|
47
|
+
self:_clearFocus(player)
|
|
48
|
+
end))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
function StreamingCinematicsService._setFocus(self: StreamingCinematicsService, player: Player, position: Vector3?): ()
|
|
52
|
+
if position == nil then
|
|
53
|
+
self:_clearFocus(player)
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if typeof(position) ~= "Vector3" then
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
-- Never create a tracker for a player who is already leaving/gone; PlayerRemoving owns their
|
|
62
|
+
-- cleanup, so a late remote must not resurrect per-player state.
|
|
63
|
+
if not player:IsDescendantOf(game) then
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
local tracker = self._trackers[player]
|
|
68
|
+
if not tracker then
|
|
69
|
+
tracker = ReplicationFocusTracker.new(player)
|
|
70
|
+
self._trackers[player] = tracker
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
tracker:SetPosition(position)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
function StreamingCinematicsService._clearFocus(self: StreamingCinematicsService, player: Player): ()
|
|
77
|
+
local tracker = self._trackers[player]
|
|
78
|
+
if tracker then
|
|
79
|
+
tracker:Destroy()
|
|
80
|
+
self._trackers[player] = nil
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
function StreamingCinematicsService.Destroy(self: StreamingCinematicsService): ()
|
|
85
|
+
for player in self._trackers do
|
|
86
|
+
self:_clearFocus(player)
|
|
87
|
+
end
|
|
88
|
+
self._maid:DoCleaning()
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
return StreamingCinematicsService
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Keeps a single hidden part positioned at a point and assigned as a [Player]'s
|
|
4
|
+
`ReplicationFocus`, so Roblox streams world content around that point. Reuses one part
|
|
5
|
+
across position updates and clears the focus (and destroys the part) on cleanup.
|
|
6
|
+
|
|
7
|
+
The subject is duck-typed at runtime -- anything with a settable `ReplicationFocus` -- so tests
|
|
8
|
+
pass a plain table; the production caller always passes a [Player].
|
|
9
|
+
|
|
10
|
+
@class ReplicationFocusTracker
|
|
11
|
+
]=]
|
|
12
|
+
|
|
13
|
+
local require = require(script.Parent.loader).load(script)
|
|
14
|
+
|
|
15
|
+
local Workspace = game:GetService("Workspace")
|
|
16
|
+
|
|
17
|
+
local Maid = require("Maid")
|
|
18
|
+
|
|
19
|
+
local FOCUS_PART_NAME = "StreamingCinematicFocus"
|
|
20
|
+
|
|
21
|
+
local ReplicationFocusTracker = {}
|
|
22
|
+
ReplicationFocusTracker.ClassName = "ReplicationFocusTracker"
|
|
23
|
+
ReplicationFocusTracker.__index = ReplicationFocusTracker
|
|
24
|
+
|
|
25
|
+
export type ReplicationFocusTracker = typeof(setmetatable(
|
|
26
|
+
{} :: {
|
|
27
|
+
_subject: Player,
|
|
28
|
+
_maid: Maid.Maid,
|
|
29
|
+
_part: BasePart?,
|
|
30
|
+
},
|
|
31
|
+
{} :: typeof({ __index = ReplicationFocusTracker })
|
|
32
|
+
))
|
|
33
|
+
|
|
34
|
+
function ReplicationFocusTracker.new(subject: Player): ReplicationFocusTracker
|
|
35
|
+
local self: ReplicationFocusTracker = setmetatable({} :: any, ReplicationFocusTracker)
|
|
36
|
+
|
|
37
|
+
self._subject = assert(subject, "No subject")
|
|
38
|
+
self._maid = Maid.new()
|
|
39
|
+
|
|
40
|
+
return self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
--[=[
|
|
44
|
+
Moves the focus to `position`, creating and assigning the part on first call.
|
|
45
|
+
@param position Vector3
|
|
46
|
+
]=]
|
|
47
|
+
function ReplicationFocusTracker.SetPosition(self: ReplicationFocusTracker, position: Vector3): ()
|
|
48
|
+
assert(typeof(position) == "Vector3", "Bad position")
|
|
49
|
+
|
|
50
|
+
local existing = self._part
|
|
51
|
+
if existing then
|
|
52
|
+
existing.Position = position
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
local part = Instance.new("Part")
|
|
57
|
+
part.Name = FOCUS_PART_NAME
|
|
58
|
+
part.Anchored = true
|
|
59
|
+
part.CanCollide = false
|
|
60
|
+
part.CanQuery = false
|
|
61
|
+
part.CanTouch = false
|
|
62
|
+
part.Transparency = 1
|
|
63
|
+
part.Size = Vector3.one
|
|
64
|
+
part.Archivable = false
|
|
65
|
+
part.Position = position
|
|
66
|
+
part.Parent = Workspace.Terrain
|
|
67
|
+
|
|
68
|
+
self._part = part
|
|
69
|
+
self._maid:GiveTask(part)
|
|
70
|
+
self._subject.ReplicationFocus = part
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
--[=[
|
|
74
|
+
Whether a focus part currently exists (i.e. [ReplicationFocusTracker:SetPosition] has run).
|
|
75
|
+
@return boolean
|
|
76
|
+
]=]
|
|
77
|
+
function ReplicationFocusTracker.IsActive(self: ReplicationFocusTracker): boolean
|
|
78
|
+
return self._part ~= nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
function ReplicationFocusTracker.Destroy(self: ReplicationFocusTracker): ()
|
|
82
|
+
self._subject.ReplicationFocus = nil
|
|
83
|
+
self._maid:DoCleaning()
|
|
84
|
+
self._part = nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
return ReplicationFocusTracker
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
@class ReplicationFocusTracker.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Jest = require("Jest")
|
|
9
|
+
local ReplicationFocusTracker = require("ReplicationFocusTracker")
|
|
10
|
+
|
|
11
|
+
local describe = Jest.Globals.describe
|
|
12
|
+
local expect = Jest.Globals.expect
|
|
13
|
+
local it = Jest.Globals.it
|
|
14
|
+
|
|
15
|
+
type FakeSubject = { ReplicationFocus: BasePart? }
|
|
16
|
+
|
|
17
|
+
describe("ReplicationFocusTracker", function()
|
|
18
|
+
it("creates a hidden part and assigns it as the subject's ReplicationFocus", function()
|
|
19
|
+
local subject: FakeSubject = {}
|
|
20
|
+
local tracker = ReplicationFocusTracker.new(subject :: any)
|
|
21
|
+
|
|
22
|
+
tracker:SetPosition(Vector3.new(1, 2, 3))
|
|
23
|
+
|
|
24
|
+
local part = subject.ReplicationFocus
|
|
25
|
+
assert(part, "expected a focus part")
|
|
26
|
+
expect(part:IsA("BasePart")).toEqual(true)
|
|
27
|
+
expect(part.Position).toEqual(Vector3.new(1, 2, 3))
|
|
28
|
+
expect(part.Anchored).toEqual(true)
|
|
29
|
+
expect(part.CanCollide).toEqual(false)
|
|
30
|
+
|
|
31
|
+
tracker:Destroy()
|
|
32
|
+
end)
|
|
33
|
+
|
|
34
|
+
it("reuses the same part across position updates", function()
|
|
35
|
+
local subject: FakeSubject = {}
|
|
36
|
+
local tracker = ReplicationFocusTracker.new(subject :: any)
|
|
37
|
+
|
|
38
|
+
tracker:SetPosition(Vector3.new(1, 0, 0))
|
|
39
|
+
local first = subject.ReplicationFocus
|
|
40
|
+
assert(first, "expected a focus part")
|
|
41
|
+
|
|
42
|
+
tracker:SetPosition(Vector3.new(5, 0, 0))
|
|
43
|
+
|
|
44
|
+
expect(subject.ReplicationFocus).toBe(first)
|
|
45
|
+
expect(first.Position).toEqual(Vector3.new(5, 0, 0))
|
|
46
|
+
|
|
47
|
+
tracker:Destroy()
|
|
48
|
+
end)
|
|
49
|
+
|
|
50
|
+
it("reports active state", function()
|
|
51
|
+
local subject: FakeSubject = {}
|
|
52
|
+
local tracker = ReplicationFocusTracker.new(subject :: any)
|
|
53
|
+
|
|
54
|
+
expect(tracker:IsActive()).toEqual(false)
|
|
55
|
+
tracker:SetPosition(Vector3.new(0, 0, 0))
|
|
56
|
+
expect(tracker:IsActive()).toEqual(true)
|
|
57
|
+
|
|
58
|
+
tracker:Destroy()
|
|
59
|
+
end)
|
|
60
|
+
|
|
61
|
+
it("clears the ReplicationFocus and destroys the part on Destroy", function()
|
|
62
|
+
local subject: FakeSubject = {}
|
|
63
|
+
local tracker = ReplicationFocusTracker.new(subject :: any)
|
|
64
|
+
|
|
65
|
+
tracker:SetPosition(Vector3.new(1, 2, 3))
|
|
66
|
+
local part = subject.ReplicationFocus
|
|
67
|
+
assert(part, "expected a focus part")
|
|
68
|
+
|
|
69
|
+
tracker:Destroy()
|
|
70
|
+
|
|
71
|
+
expect(subject.ReplicationFocus).toEqual(nil)
|
|
72
|
+
-- Destroyed parts are reparented to nil.
|
|
73
|
+
expect(part.Parent).toEqual(nil)
|
|
74
|
+
end)
|
|
75
|
+
|
|
76
|
+
it("does nothing to a subject that was never positioned", function()
|
|
77
|
+
local subject: FakeSubject = {}
|
|
78
|
+
local tracker = ReplicationFocusTracker.new(subject :: any)
|
|
79
|
+
|
|
80
|
+
tracker:Destroy()
|
|
81
|
+
|
|
82
|
+
expect(subject.ReplicationFocus).toEqual(nil)
|
|
83
|
+
end)
|
|
84
|
+
end)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "StreamingUtilsTest",
|
|
3
|
+
"tree": {
|
|
4
|
+
"$className": "DataModel",
|
|
5
|
+
"ServerScriptService": {
|
|
6
|
+
"$properties": {
|
|
7
|
+
"LoadStringEnabled": true
|
|
8
|
+
},
|
|
9
|
+
"streamingutils": {
|
|
10
|
+
"$path": ".."
|
|
11
|
+
},
|
|
12
|
+
"Script": {
|
|
13
|
+
"$path": "scripts/Server"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
@class ServerMain
|
|
4
|
+
]]
|
|
5
|
+
local ServerScriptService = game:GetService("ServerScriptService")
|
|
6
|
+
|
|
7
|
+
local root = ServerScriptService.streamingutils
|
|
8
|
+
local loader = root:FindFirstChild("LoaderUtils", true).Parent
|
|
9
|
+
local require = require(loader).bootstrapGame(root)
|
|
10
|
+
|
|
11
|
+
local NevermoreTestRunnerUtils = require("NevermoreTestRunnerUtils")
|
|
12
|
+
|
|
13
|
+
if NevermoreTestRunnerUtils.runTestsIfNeededAsync(root) then
|
|
14
|
+
return
|
|
15
|
+
end
|