@quenty/templateprovider 11.6.0 → 11.7.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 CHANGED
@@ -3,6 +3,18 @@
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
+ # [11.7.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/templateprovider@11.6.0...@quenty/templateprovider@11.7.0) (2024-10-06)
7
+
8
+
9
+ ### Features
10
+
11
+ * Modernize template provider to new specifications ([7b5df24](https://github.com/Quenty/NevermoreEngine/commit/7b5df24cd0759668d2a5a7d59f1798bca9ddf0f9))
12
+ * Remove TemplateContainerUtils.reparentFromWorkspaceIfNeeded() ([ecf8939](https://github.com/Quenty/NevermoreEngine/commit/ecf893949ba127d97b45c946cf320cf7b987e9eb))
13
+
14
+
15
+
16
+
17
+
6
18
  # [11.6.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/templateprovider@11.5.0...@quenty/templateprovider@11.6.0) (2024-10-04)
7
19
 
8
20
  **Note:** Version bump only for package @quenty/templateprovider
package/README.md CHANGED
@@ -20,28 +20,12 @@ Base of a template retrieval system
20
20
  npm install @quenty/templateprovider --save
21
21
  ```
22
22
 
23
- ## Usage
24
- Usage is designed to be simple.
23
+ ## Deferred replication template behavior
25
24
 
26
- ### `TemplateProvider.new(container, replicationParent)`
25
+ 1. We want to defer replication of templates until client requests the template
26
+ 2. Then we want to send the template to the client via PlayerGui methods
27
+ 3. We want to decide whether or not to bother doing this
27
28
 
28
- If `replicationParent` is given then contents loaded from the cloud will be replicated to the replicationParent when on the server.
29
-
30
- ### `TemplateProvider:Init()`
31
-
32
- Initializes the template provider, downloading components and other things needed
33
-
34
- ### `TemplateProvider:Clone(templateName)`
35
-
36
- ### `TemplateProvider:Get(templateName)`
37
-
38
- ### `TemplateProvider:AddContainer(container)`
39
-
40
- ### `TemplateProvider:RemoveContainer(container)`
41
-
42
- ### `TemplateProvider:IsAvailable(templateName)`
43
-
44
- ### `TemplateProvider:GetAll()`
45
-
46
- ### `TemplateProvider:GetContainers()`
29
+ This will prevent memory usage of unused templates on the client, which happens with the cars and a variety of other game-systems.
47
30
 
31
+ We can't store the stuff initially in cameras or in team-create Roblox won't replicate the stuff. But we can move on run-time and hope...
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/templateprovider",
3
- "version": "11.6.0",
3
+ "version": "11.7.0",
4
4
  "description": "Base of a template retrieval system",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -25,15 +25,22 @@
25
25
  "Quenty"
26
26
  ],
27
27
  "dependencies": {
28
- "@quenty/baseobject": "^10.6.0",
29
- "@quenty/insertserviceutils": "^10.6.0",
30
- "@quenty/loader": "^10.6.0",
28
+ "@quenty/baseobject": "^10.7.0",
29
+ "@quenty/brio": "^14.9.0",
30
+ "@quenty/collectionserviceutils": "^8.9.0",
31
+ "@quenty/ducktype": "^5.7.0",
32
+ "@quenty/insertserviceutils": "^10.7.0",
33
+ "@quenty/instanceutils": "^13.9.0",
34
+ "@quenty/loader": "^10.7.0",
31
35
  "@quenty/maid": "^3.4.0",
32
- "@quenty/promise": "^10.6.0",
36
+ "@quenty/observablecollection": "^12.9.0",
37
+ "@quenty/promise": "^10.7.0",
38
+ "@quenty/promisemaid": "^5.7.0",
39
+ "@quenty/rx": "^13.9.0",
33
40
  "@quenty/string": "^3.3.0"
34
41
  },
35
42
  "publishConfig": {
36
43
  "access": "public"
37
44
  },
38
- "gitHead": "035abfa088c854a73e1c65b350267eaa17669646"
45
+ "gitHead": "67c5dbf46f6f45213812f3f117419a5534936a0b"
39
46
  }
@@ -0,0 +1,13 @@
1
+ --[=[
2
+ @class TemplateReplicationModes
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local Table = require("Table")
8
+
9
+ return Table.readonly({
10
+ CLIENT = "client";
11
+ SERVER = "server";
12
+ SHARED = "shared";
13
+ })
@@ -0,0 +1,26 @@
1
+ --[=[
2
+ @class TemplateReplicationModesUtils
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local RunService = game:GetService("RunService")
8
+ local TemplateReplicationModes = require("TemplateReplicationModes")
9
+
10
+ local TemplateReplicationModesUtils = {}
11
+
12
+ function TemplateReplicationModesUtils.inferReplicationMode()
13
+ if not RunService:IsRunning() then
14
+ return TemplateReplicationModes.SHARED
15
+ end
16
+
17
+ if RunService:IsServer() then
18
+ return TemplateReplicationModes.SERVER
19
+ elseif RunService:IsClient() then
20
+ return TemplateReplicationModes.CLIENT
21
+ else
22
+ return TemplateReplicationModes.SHARED
23
+ end
24
+ end
25
+
26
+ return TemplateReplicationModesUtils
@@ -1,59 +1,21 @@
1
1
  --[=[
2
2
  Like a template provider, but it also reparents and retrieves tagged objects
3
+
3
4
  @class TaggedTemplateProvider
4
5
  ]=]
5
6
 
6
7
  local require = require(script.Parent.loader).load(script)
7
8
 
8
- local CollectionService = game:GetService("CollectionService")
9
- local RunService = game:GetService("RunService")
10
-
11
9
  local TemplateProvider = require("TemplateProvider")
10
+ local RxCollectionServiceUtils = require("RxCollectionServiceUtils")
12
11
 
13
- local TaggedTemplateProvider = setmetatable({}, TemplateProvider)
14
- TaggedTemplateProvider.ClassName = "TaggedTemplateProvider"
15
- TaggedTemplateProvider.__index = TaggedTemplateProvider
16
-
17
- function TaggedTemplateProvider.new(containerTagName)
18
- local self = setmetatable(TemplateProvider.new(), TaggedTemplateProvider)
19
-
20
- assert(type(containerTagName) == "string", "Bad containerTagName")
21
-
22
- -- We prefer a default tag name so test scripts can still read assets for testing
23
- self._tagsToInitializeSet = { [containerTagName] = true }
24
-
25
- return self
26
- end
27
-
28
- function TaggedTemplateProvider:Init()
29
- assert(not self._maid, "Should not have a maid")
30
-
31
- getmetatable(TaggedTemplateProvider).Init(self)
32
-
33
- assert(self._maid, "Should have a maid")
34
-
35
- for tag, _ in pairs(self._tagsToInitializeSet) do
36
- self:AddContainersFromTag(tag)
37
- end
38
- end
39
-
40
- function TaggedTemplateProvider:AddContainersFromTag(containerTagName)
41
- assert(self._maid, "Not initialized")
42
- assert(type(containerTagName) == "string", "Bad containerTagName")
43
-
44
- if RunService:IsRunning() then
45
- self._maid:GiveTask(CollectionService:GetInstanceAddedSignal(containerTagName):Connect(function(inst)
46
- self:AddContainer(inst)
47
- end))
12
+ local TaggedTemplateProvider = {}
48
13
 
49
- self._maid:GiveTask(CollectionService:GetInstanceRemovedSignal(containerTagName):Connect(function(inst)
50
- self:RemoveContainer(inst)
51
- end))
52
- end
14
+ function TaggedTemplateProvider.new(providerName, tagName)
15
+ assert(type(providerName) == "string", "bad providerName")
16
+ assert(type(tagName) == "string", "Bad tagName")
53
17
 
54
- for _, inst in pairs(CollectionService:GetTagged(containerTagName)) do
55
- self:AddContainer(inst)
56
- end
18
+ return TemplateProvider.new(providerName, RxCollectionServiceUtils.observeTaggedBrio(tagName))
57
19
  end
58
20
 
59
21
  return TaggedTemplateProvider
@@ -1,19 +1,27 @@
1
1
  --[=[
2
- Base of a template retrieval system. Templates can be retrieved from Roblox, or from the cloud,
3
- and then retrieved by name. Folders are ignored, so assets may be organized however you want.
2
+ Base of a template retrieval system. Templates can be retrieved from Roblox and then retrieved by name. If a folder is used
3
+ all of their children are also included as templates, which allows for flexible organization by artists.
4
4
 
5
- Templates can repliate to client if desired.
5
+ Additionally, you can provide template overrides as the last added template will always be used.
6
6
 
7
7
  ```lua
8
- -- shared/Templates.lua
8
+ -- shared/CarTemplates.lua
9
9
 
10
- return TemplateProvider.new(182451181, script) -- Load from Roblox cloud
10
+ return TemplateProvider.new(script.Name, script) -- Load locally
11
11
  ```
12
12
 
13
+ :::tip
14
+ If the TemplateProvider is initialized on the server, the the templates will be hidden from the client until the
15
+ client requests them.
16
+
17
+ This prevents large amounts of templates from being rendered to the client, taking up memory on the client. This especially
18
+ affects meshes, but can also affect sounds and other similar templates.
19
+ :::
20
+
13
21
  ```lua
14
22
  -- Server
15
23
  local serviceBag = ServiceBag.new()
16
- local templates = serviceBag:GetService(require("Templates"))
24
+ local templates = serviceBag:GetService(require("CarTemplates"))
17
25
  serviceBag:Init()
18
26
  serviceBag:Start()
19
27
  ```
@@ -21,12 +29,12 @@
21
29
  ```lua
22
30
  -- Client
23
31
  local serviceBag = ServiceBag.new()
24
- local templates = serviceBag:GetService(require("Templates"))
32
+ local templates = serviceBag:GetService(require("CarTemplates"))
25
33
  serviceBag:Init()
26
34
  serviceBag:Start()
27
35
 
28
- templates:PromiseClone("Crate"):Then(function(crate)
29
- print("Got crate from the cloud!")
36
+ templates:PromiseCloneTemplate("CopCar"):Then(function(crate)
37
+ print("Got crate!")
30
38
  end)
31
39
  ```
32
40
 
@@ -35,13 +43,27 @@
35
43
 
36
44
  local require = require(script.Parent.loader).load(script)
37
45
 
38
- local RunService = game:GetService("RunService")
39
- local StarterGui = game:GetService("StarterGui")
46
+ local ReplicatedStorage = game:GetService("ReplicatedStorage")
47
+ local HttpService = game:GetService("HttpService")
40
48
 
49
+ local Brio = require("Brio")
50
+ local DuckTypeUtils = require("DuckTypeUtils")
41
51
  local Maid = require("Maid")
42
- local String = require("String")
43
- local InsertServiceUtils = require("InsertServiceUtils")
52
+ local Observable = require("Observable")
53
+ local ObservableCountingMap = require("ObservableCountingMap")
54
+ local ObservableMapList = require("ObservableMapList")
44
55
  local Promise = require("Promise")
56
+ local PromiseMaidUtils = require("PromiseMaidUtils")
57
+ local Remoting = require("Remoting")
58
+ local Rx = require("Rx")
59
+ local RxInstanceUtils = require("RxInstanceUtils")
60
+ local String = require("String")
61
+ local TemplateReplicationModes = require("TemplateReplicationModes")
62
+ local TemplateReplicationModesUtils = require("TemplateReplicationModesUtils")
63
+
64
+ local TOMBSTONE_ID_ATTRIBUTE = "UnreplicatedTemplateId"
65
+ local TOMBSTONE_NAME_POSTFIX_UNLOADED = "_Unloaded"
66
+ local TOMBSTONE_NAME_POSTFIX_LOADED = "_Loaded"
45
67
 
46
68
  local TemplateProvider = {}
47
69
  TemplateProvider.ClassName = "TemplateProvider"
@@ -49,144 +71,220 @@ TemplateProvider.ServiceName = "TemplateProvider"
49
71
  TemplateProvider.__index = TemplateProvider
50
72
 
51
73
  --[=[
52
- Constructs a new [TemplateProvider].
53
- @param container Instance | table | number -- Value
54
- @param replicationParent Instance? -- Place to replicate instances to.
74
+ @type Template Instance | Observable<Brio<Instance>> | table
75
+ @within TemplateProvider
55
76
  ]=]
56
- function TemplateProvider.new(container, replicationParent)
57
- local self = setmetatable({}, TemplateProvider)
58
-
59
- self._replicationParent = replicationParent
60
- self._containersToInitializeSet = {}
61
- self._containersToInitializeList = {}
62
-
63
- if typeof(container) == "Instance" or type(container) == "number" then
64
- self:_registerContainer(container)
65
- elseif typeof(container) == "table" then
66
- for _, item in pairs(container) do
67
- assert(typeof(item) == "Instance" or type(item) == "number", "Bad item in initialization set")
68
77
 
69
- self:_registerContainer(item)
78
+ --[=[
79
+ Constructs a new [TemplateProvider].
70
80
 
71
- -- For easy debugging/iteration loop
72
- if typeof(item) == "Instance"
73
- and item:IsDescendantOf(StarterGui)
74
- and item:IsA("ScreenGui")
75
- and RunService:IsRunning() then
81
+ @param providerName string
82
+ @param initialTemplates Template
83
+ ]=]
84
+ function TemplateProvider.new(providerName, initialTemplates)
85
+ assert(type(providerName) == "string", "Bad providerName")
86
+ local self = setmetatable({}, TemplateProvider)
76
87
 
77
- item.Enabled = false
78
- end
79
- end
80
- end
88
+ self.ServiceName = assert(providerName, "No providerName")
89
+ self._initialTemplates = initialTemplates
81
90
 
82
- -- Make sure to replicate our parent
83
- if self._replicationParent then
84
- self:_registerContainer(self._replicationParent)
91
+ if not (self:_isValidTemplate(self._initialTemplates) or self._initialTemplates == nil) then
92
+ error(string.format("[TemplateProvider.%s] - Bad initialTemplates of type %s", self.ServiceName, typeof(initialTemplates)))
85
93
  end
86
94
 
87
95
  return self
88
96
  end
89
97
 
90
- function TemplateProvider:_registerContainer(container)
91
- assert(typeof(container) == "Instance" or type(container) == "number", "Bad container")
98
+ --[=[
99
+ Returns if the value is a template provider
92
100
 
93
- if not self._containersToInitializeSet[container] then
94
- self._containersToInitializeSet[container] = true
95
- table.insert(self._containersToInitializeList, container)
96
- end
101
+ @param value any
102
+ @return boolean
103
+ ]=]
104
+
105
+ function TemplateProvider.isTemplateProvider(value)
106
+ return DuckTypeUtils.isImplementation(TemplateProvider, value)
97
107
  end
98
108
 
99
109
  --[=[
100
110
  Initializes the container provider. Should be done via [ServiceBag].
101
- ]=]
102
- function TemplateProvider:Init()
103
- assert(not self._initialized, "Already initialized")
104
111
 
112
+ @param serviceBag ServiceBag
113
+ ]=]
114
+ function TemplateProvider:Init(serviceBag)
115
+ assert(not self._serviceBag, "Already initialized")
116
+ self._serviceBag = assert(serviceBag, "No serviceBag")
105
117
  self._maid = Maid.new()
106
- self._initialized = true
107
- self._registry = {} -- [name] = rawTemplate
108
- self._containersSet = {} -- [parentOrAssetId] = true
109
118
 
110
- self._promises = {} -- [name] = Promise
119
+ self._replicationMode = TemplateReplicationModesUtils.inferReplicationMode()
111
120
 
112
- for _, container in pairs(self._containersToInitializeList) do
113
- self:AddContainer(container)
114
- end
121
+ -- There can be multiple templates for a given name
122
+ self._templateMapList = self._maid:Add(ObservableMapList.new())
123
+ self._unreplicatedTemplateMapList = self._maid:Add(ObservableMapList.new())
124
+
125
+ self._containerRootCountingMap = self._maid:Add(ObservableCountingMap.new())
126
+ self._pendingTemplatePromises = {} -- [templateName] = Promise
127
+
128
+ self:_setupTemplateCache()
115
129
  end
116
130
 
117
- --[=[
118
- Promises to clone the template as soon as it exists.
119
- @param templateName string
120
- @return Promise<Instance>
121
- ]=]
122
- function TemplateProvider:PromiseClone(templateName)
123
- assert(type(templateName) == "string", "templateName must be a string")
131
+ function TemplateProvider:_setupTemplateCache()
132
+ if self._replicationMode == TemplateReplicationModes.SERVER then
133
+ self._tombstoneLookup = {}
134
+ self._remoting = self._maid:Add(Remoting.Server.new(ReplicatedStorage, self.ServiceName .. "TemplateProvider"))
135
+
136
+ -- TODO: Maybe de-duplicate and use a centralized service
137
+ self._maid:GiveTask(self._remoting.ReplicateTemplate:Bind(function(player, tombstoneId)
138
+ assert(type(tombstoneId) == "string", "Bad tombstoneId")
139
+ assert(self._tombstoneLookup[tombstoneId], "Not a valid tombstone")
140
+
141
+ -- Stuff doesn't replicate in the PlayerGui
142
+ local playerGui = player:FindFirstChildWhichIsA("PlayerGui")
143
+ if not playerGui then
144
+ return Promise.rejected("No playerGui")
145
+ end
146
+
147
+ -- Just group stuff to simplify things
148
+ local replicationParent = playerGui:FindFirstChild("TemplateProviderReplication")
149
+ if not replicationParent then
150
+ replicationParent = Instance.new("Folder")
151
+ replicationParent.Name = "TemplateProviderReplication"
152
+ replicationParent.Archivable = false
153
+ replicationParent.Parent = playerGui
154
+ end
155
+
156
+ local copy = self._tombstoneLookup[tombstoneId]:Clone()
157
+ copy.Parent = playerGui
158
+
159
+ task.delay(0.1, function()
160
+ copy:Remove()
161
+ end)
124
162
 
125
- self:_verifyInit()
163
+ return copy
164
+ end))
165
+ elseif self._replicationMode == TemplateReplicationModes.CLIENT then
166
+ self._pendingTombstoneRequests = {}
126
167
 
127
- local template = self._registry[templateName]
128
- if template then
129
- return Promise.resolved(self:Clone(templateName))
168
+ self._remoting = self._maid:Add(Remoting.Client.new(ReplicatedStorage, self.ServiceName .. "TemplateProvider"))
130
169
  end
131
170
 
132
- if not self._promises[templateName] then
133
- local promise = Promise.new()
134
- self._promises[templateName] = promise
171
+ if self._initialTemplates then
172
+ self._maid:GiveTask(self:AddTemplates(self._initialTemplates))
173
+ end
135
174
 
136
- -- Make sure to clean up the promise afterwards
137
- self._maid[promise] = promise
138
- promise:Then(function()
139
- self._maid[promise] = nil
140
- end)
175
+ -- Recursively adds roots, but also de-duplicates them as necessary
176
+ self._maid:GiveTask(self._containerRootCountingMap:ObserveKeysBrio():Subscribe(function(containerBrio)
177
+ if containerBrio:IsDead() then
178
+ return
179
+ end
180
+
181
+ local containerMaid, container = containerBrio:ToMaidAndValue()
182
+ self:_handleContainer(containerMaid, container)
183
+ end))
184
+ end
185
+
186
+ function TemplateProvider:_handleContainer(containerMaid, container)
187
+ if self._replicationMode == TemplateReplicationModes.SERVER
188
+ and not container:IsA("Camera")
189
+ and not container:FindFirstAncestorWhichIsA("Camera") then
190
+ -- Prevent replication to client immediately
141
191
 
142
- task.delay(5, function()
143
- if promise:IsPending() then
144
- warn(string.format("[TemplateProvider.PromiseClone] - May fail to replicate template %q from cloud. %s", templateName, self:_getReplicationHint()))
192
+ local camera = containerMaid:Add(Instance.new("Camera"))
193
+ camera.Name = "PreventReplication"
194
+ camera.Parent = container
195
+
196
+ local function handleChild(child)
197
+ if child == camera then
198
+ return
145
199
  end
146
- end)
200
+ if child:GetAttribute(TOMBSTONE_ID_ATTRIBUTE) then
201
+ return
202
+ end
203
+
204
+ child.Parent = camera
205
+ end
206
+
207
+ containerMaid:GiveTask(container.ChildAdded:Connect(handleChild))
208
+
209
+ for _, child in pairs(container:GetChildren()) do
210
+ handleChild(child)
211
+ end
212
+
213
+ self:_replicateTombstones(containerMaid, camera, container)
214
+
215
+ return
147
216
  end
148
217
 
149
- return self._promises[templateName]
150
- :Then(function()
151
- -- Get a new copy
152
- return self:Clone(templateName)
153
- end)
218
+ containerMaid:GiveTask(RxInstanceUtils.observeChildrenBrio(container):Subscribe(function(brio)
219
+ if brio:IsDead() then
220
+ return
221
+ end
222
+
223
+ local maid, child = brio:ToMaidAndValue()
224
+ self:_addInstanceTemplate(maid, child)
225
+ end))
154
226
  end
155
227
 
156
- function TemplateProvider:_getReplicationHint()
157
- local hint = ""
228
+ function TemplateProvider:_replicateTombstones(topMaid, unreplicatedParent, replicatedParent)
229
+ assert(self._replicationMode == TemplateReplicationModes.SERVER, "Only should be invoked on server")
158
230
 
159
- if RunService:IsClient() then
160
- hint = "Make sure the template provider is initialized on the server."
161
- end
231
+ -- Tombstone each child so the client knows what is replicated
232
+ topMaid:GiveTask(RxInstanceUtils.observeChildrenBrio(unreplicatedParent):Subscribe(function(brio)
233
+ if brio:IsDead() then
234
+ return
235
+ end
236
+
237
+ local maid, child = brio:ToMaidAndValue()
238
+ self:_addInstanceTemplate(maid, child)
239
+
240
+ local tombstoneId = HttpService:GenerateGUID(false)
241
+
242
+ -- Tell the client something exists here
243
+ local tombstone = maid:Add(Instance.new("Folder"))
244
+ tombstone.Name = child.Name .. TOMBSTONE_NAME_POSTFIX_UNLOADED
245
+ tombstone:SetAttribute(TOMBSTONE_ID_ATTRIBUTE, tombstoneId)
246
+
247
+ -- Recursively replicate other tombstones
248
+ if self:_shouldAddChildrenAsTemplates(child) then
249
+ self:_replicateTombstones(maid, child, tombstone)
250
+ end
251
+
252
+ self._tombstoneLookup[tombstoneId] = child
253
+
254
+ maid:GiveTask(function()
255
+ self._tombstoneLookup[tombstoneId] = nil
256
+ end)
162
257
 
163
- return hint
258
+ tombstone.Parent = replicatedParent
259
+ end))
164
260
  end
165
261
 
166
262
  --[=[
167
- Clones the template.
168
-
169
- :::info
170
- If the template name has a prefix of "Template" then it will remove it on the cloned instance.
171
- :::
263
+ Observes the given template by name
172
264
 
173
265
  @param templateName string
174
- @return Instance?
266
+ @return Observable<Instance>
175
267
  ]=]
176
- function TemplateProvider:Clone(templateName)
268
+ function TemplateProvider:ObserveTemplate(templateName)
177
269
  assert(type(templateName) == "string", "templateName must be a string")
178
270
 
179
- self:_verifyInit()
271
+ return self._templateMapList:ObserveList(templateName):Pipe({
272
+ Rx.switchMap(function(list)
273
+ if not list then
274
+ return Rx.of(nil);
275
+ end
180
276
 
181
- local template = self._registry[templateName]
182
- if not template then
183
- error(string.format("[TemplateProvider.Clone] - Cannot provide %q", tostring(templateName)))
184
- return nil
185
- end
277
+ return list:ObserveAtIndex(-1)
278
+ end);
279
+ })
280
+ end
186
281
 
187
- local newItem = template:Clone()
188
- newItem.Name = String.removePostfix(templateName, "Template")
189
- return newItem
282
+ function TemplateProvider:ObserveTemplateNamesBrio()
283
+ return self._templateMapList:ObserveKeysBrio()
284
+ end
285
+
286
+ function TemplateProvider:ObserveUnreplicatedTemplateNamesBrio()
287
+ return self._unreplicatedTemplateMapList:ObserveKeysBrio()
190
288
  end
191
289
 
192
290
  --[=[
@@ -195,197 +293,330 @@ end
195
293
  @param templateName string
196
294
  @return Instance?
197
295
  ]=]
198
- function TemplateProvider:Get(templateName)
296
+ function TemplateProvider:GetTemplate(templateName)
199
297
  assert(type(templateName) == "string", "templateName must be a string")
200
- self:_verifyInit()
201
298
 
202
- return self._registry[templateName]
299
+ return self._templateMapList:GetItemForKeyAtIndex(templateName, -1)
203
300
  end
204
301
 
205
302
  --[=[
206
- Adds a new container to the provider for provision of assets.
303
+ Promises to clone the template as soon as it exists
207
304
 
208
- @param container Instance | number
209
- ]=]
210
- function TemplateProvider:AddContainer(container)
211
- assert(typeof(container) == "Instance" or type(container) == "number", "Bad container")
212
- self:_verifyInit()
213
-
214
- if not self._containersSet[container] then
215
- self._containersSet[container] = true
216
- if type(container) == "number" then
217
- self._maid[container] = self:_loadCloudAsset(container)
218
- elseif typeof(container) == "Instance" then
219
- self._maid[container] = self:_loadFolder(container)
220
- else
221
- error("Unknown container type to load")
222
- end
223
- end
224
- end
225
-
226
- --[=[
227
- Removes a container from the provisioning set.
228
-
229
- @param container Instance | number
305
+ @param templateName string
306
+ @return Promise<Instance>
230
307
  ]=]
231
- function TemplateProvider:RemoveContainer(container)
232
- assert(typeof(container) == "Instance", "Bad container")
233
- self:_verifyInit()
308
+ function TemplateProvider:PromiseCloneTemplate(templateName)
309
+ assert(type(templateName) == "string", "templateName must be a string")
234
310
 
235
- self._containersSet[container] = nil
236
- self._maid[container] = nil
311
+ return self:PromiseTemplate(templateName)
312
+ :Then(function(template)
313
+ return self:_cloneTemplate(template)
314
+ end)
237
315
  end
238
316
 
239
317
  --[=[
240
- Returns whether or not a template is registered at the time
318
+ Promise to resolve the raw template as soon as it exists
319
+
241
320
  @param templateName string
242
- @return boolean
321
+ @return Promise<Instance>
243
322
  ]=]
244
- function TemplateProvider:IsAvailable(templateName)
323
+ function TemplateProvider:PromiseTemplate(templateName)
245
324
  assert(type(templateName) == "string", "templateName must be a string")
246
- self:_verifyInit()
247
325
 
248
- return self._registry[templateName] ~= nil
326
+ local foundTemplate = self._templateMapList:GetItemForKeyAtIndex(templateName, -1)
327
+ if foundTemplate then
328
+ return Promise.resolved(foundTemplate)
329
+ end
330
+
331
+ if self._pendingTemplatePromises[templateName] then
332
+ return self._pendingTemplatePromises[templateName]
333
+ end
334
+
335
+ local promiseTemplate = Promise.new()
336
+
337
+ -- Observe thet template
338
+ PromiseMaidUtils.whilePromise(promiseTemplate, function(topMaid)
339
+ topMaid:GiveTask(self:ObserveTemplate(templateName):Subscribe(function(template)
340
+ if template then
341
+ promiseTemplate:Resolve(template)
342
+ end
343
+ end))
344
+
345
+ if self._replicationMode == TemplateReplicationModes.SERVER then
346
+ -- There's a chance an external process will stream in our template
347
+
348
+ topMaid:GiveTask(task.delay(5, function()
349
+ warn(string.format("[TemplateProvider.%s.PromiseTemplate] - Missing template %q", self.ServiceName, templateName))
350
+ end))
351
+ elseif self._replicationMode == TemplateReplicationModes.CLIENT then
352
+ -- Replicate from the unfound area
353
+ topMaid:GiveTask(self._unreplicatedTemplateMapList:ObserveAtListIndexBrio(templateName, -1):Subscribe(function(brio)
354
+ if brio:IsDead() then
355
+ return
356
+ end
357
+
358
+ local maid, templateTombstone = brio:ToMaidAndValue()
359
+
360
+ local originalName = templateTombstone.Name
361
+
362
+ maid:GivePromise(self:_promiseReplicateTemplateFromTombstone(templateTombstone))
363
+ :Then(function(template)
364
+ -- Cache the template here which then loads it into the known templates naturally
365
+ templateTombstone.Name = String.removePostfix(originalName, TOMBSTONE_NAME_POSTFIX_UNLOADED) .. TOMBSTONE_NAME_POSTFIX_LOADED
366
+ template.Parent = templateTombstone
367
+
368
+ promiseTemplate:Resolve(template)
369
+ end)
370
+ end))
371
+
372
+ topMaid:GiveTask(task.delay(5, function()
373
+ if self._unreplicatedTemplateMapList:GetListForKey(templateName) then
374
+ warn(string.format("[TemplateProvider.%s.PromiseTemplate] - Failed to replicate template %q from server to client", self.ServiceName, templateName))
375
+ else
376
+ warn(string.format("[TemplateProvider.%s.PromiseTemplate] - Template %q is not a known template", self.ServiceName, templateName))
377
+ end
378
+ end))
379
+ elseif self._replicationMode == TemplateReplicationModes.SHARED then
380
+ -- There's a chance an external process will stream in our template
381
+
382
+ topMaid:GiveTask(task.delay(5, function()
383
+ warn(string.format("[TemplateProvider.%s.PromiseTemplate] - Missing template %q", self.ServiceName, templateName))
384
+ end))
385
+ else
386
+ error("Bad replicationMode")
387
+ end
388
+ end)
389
+
390
+ self._maid[promiseTemplate] = promiseTemplate
391
+ self._pendingTemplatePromises[templateName] = promiseTemplate
392
+
393
+ promiseTemplate:Finally(function()
394
+ self._maid[promiseTemplate] = nil
395
+ self._pendingTemplatePromises[templateName] = nil
396
+ end)
397
+
398
+ return promiseTemplate
249
399
  end
250
400
 
251
- --[=[
252
- Returns all current registered items.
401
+ function TemplateProvider:_promiseReplicateTemplateFromTombstone(templateTombstone)
402
+ assert(self._replicationMode == TemplateReplicationModes.CLIENT, "Bad replicationMode")
403
+ assert(typeof(templateTombstone) == "Instance", "Bad templateTombstone")
253
404
 
254
- @return { Instance }
255
- ]=]
256
- function TemplateProvider:GetAll()
257
- self:_verifyInit()
405
+ local tombstoneId = templateTombstone:GetAttribute(TOMBSTONE_ID_ATTRIBUTE)
406
+ if type(tombstoneId) ~= "string" then
407
+ return Promise.rejected("tombstoneId must be a string")
408
+ end
258
409
 
259
- local list = {}
260
- for _, item in pairs(self._registry) do
261
- table.insert(list, item)
410
+ if self._pendingTombstoneRequests[tombstoneId] then
411
+ return self._pendingTombstoneRequests[tombstoneId]
262
412
  end
263
413
 
264
- return list
414
+ local promiseTemplate = Promise.new()
415
+
416
+ PromiseMaidUtils.whilePromise(promiseTemplate, function(topMaid)
417
+ topMaid:GivePromise(self._remoting.ReplicateTemplate:PromiseInvokeServer(tombstoneId))
418
+ :Then(function(tempTemplate)
419
+ if not tempTemplate then
420
+ return Promise.rejected("Failed to get any template")
421
+ end
422
+
423
+ -- This tempTemplate will get destroyed by the server soon to free up server memory
424
+ -- TODO: cache on client
425
+ local copy = tempTemplate:Clone()
426
+ promiseTemplate:Resolve(copy)
427
+ end, function(...)
428
+ promiseTemplate:Reject(...)
429
+ end)
430
+ end)
431
+
432
+ self._maid[promiseTemplate] = promiseTemplate
433
+ self._pendingTombstoneRequests[tombstoneId] = promiseTemplate
434
+
435
+ promiseTemplate:Finally(function()
436
+ self._maid[promiseTemplate] = nil
437
+ self._pendingTombstoneRequests[tombstoneId] = nil
438
+ end)
439
+
440
+ return promiseTemplate
265
441
  end
266
442
 
267
443
  --[=[
268
- Gets all current the containers.
444
+ Clones the template.
445
+
446
+ :::info
447
+ If the template name has a prefix of "Template" then it will remove it on the cloned instance.
448
+ :::
269
449
 
270
- @return { Instance | number }
450
+ @param templateName string
451
+ @return Instance?
271
452
  ]=]
272
- function TemplateProvider:GetContainers()
273
- self:_verifyInit()
453
+ function TemplateProvider:CloneTemplate(templateName)
454
+ assert(type(templateName) == "string", "templateName must be a string")
274
455
 
275
- local list = {}
276
- for parent, _ in pairs(self._containersSet) do
277
- table.insert(list, parent)
278
- end
279
- return list
280
- end
456
+ local template = self._templateMapList:GetItemForKeyAtIndex(templateName, -1)
457
+ if not template then
458
+ local unreplicated = self._unreplicatedTemplateMapList:GetListForKey(templateName)
281
459
 
282
- function TemplateProvider:_verifyInit()
283
- if not RunService:IsRunning() then
284
- if not self._initialized then
285
- -- Initialize for hoarcecat!
286
- self:Init()
460
+ if unreplicated then
461
+ error(string.format("[TemplateProvider.%s.CloneTemplate] - Template %q is not replicated. Use PromiseCloneTemplate instead", self.ServiceName, tostring(templateName)))
462
+ else
463
+ error(string.format("[TemplateProvider.%s.CloneTemplate] - Cannot provide template %q", self.ServiceName, tostring(templateName)))
287
464
  end
465
+
466
+ return nil
288
467
  end
289
468
 
290
- assert(self._initialized, "TemplateProvider is not initialized")
469
+ return self:_cloneTemplate(template)
291
470
  end
292
471
 
293
- function TemplateProvider:_loadCloudAsset(assetId)
294
- assert(type(assetId) == "number", "Bad assetId")
295
- local maid = Maid.new()
296
-
297
- -- Load on server
298
- if RunService:IsServer() or not RunService:IsRunning() then
299
- maid:GivePromise(InsertServiceUtils.promiseAsset(assetId)):Then(function(result)
300
- if RunService:IsRunning() then
301
- for _, item in pairs(result:GetChildren()) do
302
- -- Replicate in children
303
- item.Parent = self._replicationParent
304
- end
472
+ --[=[
473
+ Adds a new container to the provider for provision of assets. The initial container
474
+ is considered a template. Additionally, we will include any children that are in a folder
475
+ as a potential root
476
+
477
+ :::tip
478
+ The last template with a given name added will be considered the canonical template.
479
+ :::
480
+
481
+ @param container Template
482
+ @return MaidTask
483
+ ]=]
484
+ function TemplateProvider:AddTemplates(container)
485
+ assert(self:_isValidTemplate(container), "Bad container")
486
+
487
+ if typeof(container) == "Instance" then
488
+ -- Always add this instance as we explicitly asked for it to be added as a root. This could be a
489
+ -- module script, or other component.
490
+ return self._containerRootCountingMap:Add(container)
491
+ elseif Observable.isObservable(container) then
492
+ local topMaid = Maid.new()
493
+
494
+ self:_addObservableTemplates(topMaid, container)
495
+
496
+ self._maid[topMaid] = topMaid
497
+ topMaid:GiveTask(function()
498
+ self._maid[topMaid] = nil
499
+ end)
500
+
501
+ return topMaid
502
+ elseif type(container) == "table" then
503
+ local topMaid = Maid.new()
504
+
505
+ for _, value in pairs(container) do
506
+ if typeof(value) == "Instance" then
507
+ -- Always add these as we explicitly ask for this to be a root too.
508
+ topMaid:GiveTask(self._containerRootCountingMap:Add(value))
509
+ elseif Observable.isObservable(value) then
510
+ self:_addObservableTemplates(topMaid, value)
305
511
  else
306
- -- Load without parenting
307
- maid:GiveTask(self:_loadFolder(result))
512
+ error(string.format("[TemplateProvider.%s] - Bad value of type %q in container table", self.ServiceName, typeof(value)))
308
513
  end
309
- end)
310
- end
514
+ end
311
515
 
312
- return maid
313
- end
516
+ self._maid[topMaid] = topMaid
517
+ topMaid:GiveTask(function()
518
+ self._maid[topMaid] = nil
519
+ end)
314
520
 
315
- function TemplateProvider:_transformParent(getParent)
316
- if typeof(getParent) == "Instance" then
317
- return getParent
318
- elseif type(getParent) == "function" then
319
- local container = getParent()
320
- assert(typeof(container) == "Instance", "Bad container")
321
- return container
521
+ return topMaid
322
522
  else
323
- error("Bad getParent type")
523
+ error(string.format("[TemplateProvider.%s] - Bad container of type %s", self.ServiceName, typeof(container)))
324
524
  end
325
525
  end
326
526
 
327
- function TemplateProvider:_loadFolder(parent)
328
- local maid = Maid.new()
527
+ function TemplateProvider:_addObservableTemplates(topMaid, observable)
528
+ topMaid:GiveTask(observable:Subscribe(function(result)
529
+ if Brio.isBrio(result) then
530
+ if result:IsDead() then
531
+ return
532
+ end
329
533
 
330
- -- Only connect events if we're running
331
- if RunService:IsRunning() then
332
- maid:GiveTask(parent.ChildAdded:Connect(function(child)
333
- self:_handleChildAdded(maid, child)
334
- end))
335
- maid:GiveTask(parent.ChildRemoved:Connect(function(child)
336
- self:_handleChildRemoved(maid, child)
337
- end))
534
+ local maid, template = result:ToMaidAndValue()
535
+ if typeof(template) == "Instance" then
536
+ self:_addInstanceTemplate(maid, template)
537
+ else
538
+ error("Cannot add non-instance from observable template")
539
+ end
540
+ else
541
+ error("Cannot add non Brio<Instance> from observable")
542
+ end
543
+ end))
544
+ end
545
+
546
+ function TemplateProvider:_addInstanceTemplate(topMaid, template)
547
+ if self:_shouldAddChildrenAsTemplates(template) then
548
+ topMaid:GiveTask(self._containerRootCountingMap:Add(template))
338
549
  end
339
550
 
340
- for _, child in pairs(parent:GetChildren()) do
341
- self:_handleChildAdded(maid, child)
551
+ if template:GetAttribute(TOMBSTONE_ID_ATTRIBUTE) then
552
+ topMaid:GiveTask(self._unreplicatedTemplateMapList:Push(RxInstanceUtils.observeProperty(template, "Name"):Pipe({
553
+ Rx.map(function(name)
554
+ if String.endsWith(name, TOMBSTONE_NAME_POSTFIX_UNLOADED) then
555
+ return String.removePostfix(name, TOMBSTONE_NAME_POSTFIX_UNLOADED)
556
+ elseif String.endsWith(name, TOMBSTONE_NAME_POSTFIX_LOADED) then
557
+ return String.removePostfix(name, TOMBSTONE_NAME_POSTFIX_LOADED)
558
+ else
559
+ return name
560
+ end
561
+ end);
562
+ Rx.distinct();
563
+ }), template))
564
+ else
565
+ topMaid:GiveTask(self._templateMapList:Push(RxInstanceUtils.observeProperty(template, "Name"), template))
342
566
  end
567
+ end
343
568
 
344
- maid:GiveTask(function()
345
- maid:DoCleaning()
569
+ --[=[
570
+ Returns whether or not a template is registered at the time
346
571
 
347
- -- Deregister children
348
- for _, child in pairs(parent:GetChildren()) do
349
- self:_handleChildRemoved(maid, child)
350
- end
351
- end)
572
+ @param templateName string
573
+ @return boolean
574
+ ]=]
575
+ function TemplateProvider:IsTemplateAvailable(templateName)
576
+ assert(type(templateName) == "string", "templateName must be a string")
352
577
 
353
- return maid
578
+ return self._templateMapList:GetItemForKeyAtIndex(templateName, -1) ~= nil
354
579
  end
355
580
 
356
- function TemplateProvider:_handleChildRemoved(maid, child)
357
- maid[child] = nil
358
- self:_removeFromRegistry(child)
359
- end
581
+ --[=[
582
+ Returns all current registered items.
360
583
 
361
- function TemplateProvider:_handleChildAdded(maid, child)
362
- if child:IsA("Folder") then
363
- maid[child] = self:_loadFolder(child)
364
- else
365
- self:_addToRegistery(child)
366
- end
584
+ @return { Instance }
585
+ ]=]
586
+ function TemplateProvider:GetTemplateList()
587
+ return self._templateMapList:GetListOfValuesAtListIndex(-1)
367
588
  end
368
589
 
369
- function TemplateProvider:_addToRegistery(child)
370
- local childName = child.Name
371
- -- if self._registry[childName] then
372
- -- warn(string.format("[TemplateProvider._addToRegistery] - Duplicate %q in registery. Overridding", childName))
373
- -- end
590
+ --[=[
591
+ Gets all current the containers.
592
+
593
+ @return { Instance }
594
+ ]=]
595
+ function TemplateProvider:GetContainerList()
596
+ return self._containerRootCountingMap:GetList()
597
+ end
374
598
 
375
- self._registry[childName] = child
599
+ -- Backwards compatibility
600
+ TemplateProvider.IsAvailable = assert(TemplateProvider.IsTemplateAvailable, "Missing method")
601
+ TemplateProvider.Get = assert(TemplateProvider.GetTemplate, "Missing method")
602
+ TemplateProvider.Clone = assert(TemplateProvider.CloneTemplate, "Missing method")
603
+ TemplateProvider.PromiseClone = assert(TemplateProvider.PromiseCloneTemplate, "Missing method")
604
+ TemplateProvider.GetAllTemplates = assert(TemplateProvider.GetTemplateList, "Missing method")
376
605
 
377
- if self._promises[childName] then
378
- self._promises[childName]:Resolve(child)
379
- self._promises[childName] = nil
380
- end
606
+ function TemplateProvider:_cloneTemplate(template)
607
+ local newItem = template:Clone()
608
+ newItem.Name = String.removePostfix(template.Name, "Template")
609
+ return newItem
381
610
  end
382
611
 
383
- function TemplateProvider:_removeFromRegistry(child)
384
- local childName = child.Name
612
+ function TemplateProvider:_shouldAddChildrenAsTemplates(container)
613
+ return container:IsA("Folder")
614
+ end
385
615
 
386
- if self._registry[childName] == child then
387
- self._registry[childName] = nil
388
- end
616
+ function TemplateProvider:_isValidTemplate(container)
617
+ return typeof(container) == "Instance"
618
+ or Observable.isObservable(container)
619
+ or type(container) == "table"
389
620
  end
390
621
 
391
622
  --[=[
@@ -1,39 +0,0 @@
1
- --[=[
2
- Utility functions for the TemplateProvider
3
- @class TemplateContainerUtils
4
- ]=]
5
-
6
- local Workspace = game:GetService("Workspace")
7
- local RunService = game:GetService("RunService")
8
-
9
- local TemplateContainerUtils = {}
10
-
11
- function TemplateContainerUtils.reparentFromWorkspaceIfNeeded(parent, name)
12
- assert(typeof(parent) == "Instance", "Bad parent")
13
- assert(type(name) == "string", "Bad name")
14
-
15
- local workspaceContainer = Workspace:FindFirstChild(name)
16
- local parentedContainer = parent:FindFirstChild(name)
17
- if workspaceContainer then
18
- if parentedContainer then
19
- error(string.format("Duplicate container in %q and %q",
20
- workspaceContainer:GetFullName(),
21
- parentedContainer:GetFullName()))
22
- end
23
-
24
- -- Reparent
25
- if RunService:IsRunning() then
26
- workspaceContainer.Parent = parent
27
- end
28
-
29
- return workspaceContainer
30
- end
31
-
32
- if not parentedContainer then
33
- error(string.format("No template container with name %q in %q", parent:GetFullName(), name))
34
- end
35
-
36
- return parentedContainer
37
- end
38
-
39
- return TemplateContainerUtils