forked from Benjamin-Dobell/ge_tts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathObjectUtils.ttslua
378 lines (315 loc) · 14.1 KB
/
ObjectUtils.ttslua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
local Base64 = require('ge_tts.Base64')
local EventManager = require('ge_tts.EventManager')
local Json = require('ge_tts.Json')
local Object = require('ge_tts.Object')
local SaveManager = require('ge_tts.SaveManager')
local TableUtils = require('ge_tts.TableUtils')
local Vector3 = require('ge_tts.Vector3')
---There's a random component to our GUIDs designed to mitigate collisions is a user wants to copy objects between mods.
local GUID_PREFIX_RANDOM_BYTE_LENGTH = 3
---@type string
local guidPrefix
---@type number
local guidIndex = 0
---@type table<string, {object: tts__Object, json: nil | string, callbacks: tts__ObjectCallbackFunction[]}> @GUID key
local pendingSpawns = {}
---@param guid string
---@param object tts__Object
local function triggerPendingCallbacks(guid, object)
local pendingSpawn = pendingSpawns[guid]
if not pendingSpawn then
return
end
pendingSpawns[guid] = nil
for _, callback in ipairs(pendingSpawn.callbacks) do
callback(object)
end
end
---@class ge_tts__ObjectUtils
local ObjectUtils = {}
---@param obj tts__Object
---@return ge_tts__Vector3
function ObjectUtils.getTransformScale(obj)
local rotation = obj.getRotation()
local onesVector = Vector3(1, 1, 1).rotateZ(rotation.z).rotateX(rotation.x).rotateY(rotation.y)
local scale = Vector3(obj.positionToLocal(onesVector.add(obj.getPosition())))
return scale
end
---@param tag string
---@return boolean
function ObjectUtils.isContainerTag(tag)
return tag == Object.Tag.Deck or tag == Object.Tag.Bag
end
---@return string
function ObjectUtils.nextGuid()
guidIndex = guidIndex + 1
return guidPrefix .. tostring(guidIndex)
end
---@param objectState tts__ObjectState
---@param guid string
---@param callback_function nil | tts__ObjectCallbackFunction
local function safeSpawnObject(objectState, guid, callback_function)
objectState.GUID = guid
local json = Json.encode(objectState)
local spawningObject = spawnObjectJSON({json = json})
pendingSpawns[guid] = {
callbacks = {},
object = spawningObject,
json = json
}
if callback_function then
table.insert(pendingSpawns[guid].callbacks, --[[---@not nil]] callback_function)
end
return spawningObject
end
---
---Unlike the built-in spawnObjectJSON(), this function guarantees the object will be spawned in the scene in onLoad
---(SaveManager) callbacks i.e. if a user rewinds your mod.
---
---There is an edge-case with the built-in spawnObjectJSON() if Tabletop Simulator performs a save state immediately
---after your call to spawnObjectJSON(). When you rewind (or load) save state the object may or may not have spawned.
---Even if the object did spawn before the save state was generated (and hence exists when the user rewinds or loads),
---the GUID on the object reference returned to you from spawnObjectJSON() is not guaranteed to be the final GUID of the
---object within the scene. Thus, even if you correctly saved this GUID in onSave, your attempt to recover the object
---may fail, as it ended up with a different GUID.
---
---To avoid these pitfalls, this function does two things.
---
---Firstly, we provide a GUID for your object that is guaranteed[*] to be unique.
---
---Secondly, onLoad ge_tts looks for any objects that should exist but don't. If it discovers any, it will spawn them
---for you automatically. In your own onLoad (SaveManager) callbacks you can call ObjectUtils.recoverSafeSpawnedObject
---to recover a reference to any objects that were safe spawned.
---
---WARNING: If you provide a GUID for your object in jsonTable, it will not be used. Instead, please grab the GUID from
---the object reference returned from this function. GUIDs we generate do not match the same format as those generated
---by Tabletop Simulator. We have confirmed with Berserk that internally GUIDs are only ever treated as strings and it
---is entirely safe for us to use our own format.
---
---[*] If you're really determined you can still create GUID collisions. However, it won't happen by accident. You'd
---have to be intentionally spawning objects (not using this method) with specific GUIDs that you're specifically trying
---to make collide.
---
---@overload fun(objectState: tts__ObjectState): tts__Object
---@param objectState tts__ObjectState @Will be JSON encoded after we generate and assign a GUID.
---@param callback_function nil | tts__ObjectCallbackFunction @Callback that will be called when the object has finished spawning.
---@return tts__Object
function ObjectUtils.safeSpawnObject(objectState, callback_function)
return safeSpawnObject(objectState, ObjectUtils.nextGuid(), callback_function)
end
---
---Same as ObjectUtils.safeSpawnObject(...), except that instead of generating a unique GUID, it is your responsibility
---to provide one. If you fail to provide a unique GUID, all safety guarantees are lost.
---
---In practice, you should only call this method if you're respawning an object that was destroyed.
---
---@overload fun(objectState: tts__ObjectState, guid: string): tts__Object
---@param objectState tts__ObjectState @Will be JSON encoded after we generate and assign a GUID.
---@param guid string
---@param callback_function nil | tts__ObjectCallbackFunction @Callback that will be called when the object has finished spawning.
---@return tts__Object
function ObjectUtils.safeRespawnObject(objectState, guid, callback_function)
return safeSpawnObject(objectState, guid, callback_function)
end
---@overload fun(position: nil | tts__VectorShape): tts__ObjectState_Transform
---@overload fun(position: nil | tts__VectorShape, rotation: nil | tts__VectorShape): tts__ObjectState_Transform
---@overload fun(position: nil | tts__VectorShape, rotation: nil | tts__VectorShape, scale: nil | tts__VectorShape): tts__ObjectState_Transform
---@overload fun(transform: {position: nil | tts__VectorShape, rotation: nil | tts__VectorShape, scale: nil | tts__VectorShape}): tts__ObjectState_Transform
---@vararg ge_tts__Vector3
---@return tts__ObjectState_Transform
function ObjectUtils.transformState(...)
---@type tts__ObjectState_Transform
local state = {}
---@type nil | tts__VectorShape
local position
---@type nil | tts__VectorShape
local rotation = nil
---@type nil | tts__VectorShape
local scale = nil
if select('#', ...) == 1 then
local args = --[[---@type table]] ...
if args[1] then
position = args
else
local transform = --[[---@type {position: nil | tts__VectorShape, rotation: nil | tts__VectorShape, scale: nil | tts__VectorShape}]] args
position = transform.position
rotation = transform.rotation
scale = transform.scale
end
else
position, rotation, scale = ...
end
if position then
state.posX = (--[[---@type tts__CharVectorShape]] position).x or (--[[---@type tts__NumVectorShape]] position)[1]
state.posY = (--[[---@type tts__CharVectorShape]] position).y or (--[[---@type tts__NumVectorShape]] position)[2]
state.posZ = (--[[---@type tts__CharVectorShape]] position).z or (--[[---@type tts__NumVectorShape]] position)[3]
end
if rotation then
state.rotX = (--[[---@type tts__CharVectorShape]] rotation).x or (--[[---@type tts__NumVectorShape]] rotation)[1]
state.rotY = (--[[---@type tts__CharVectorShape]] rotation).y or (--[[---@type tts__NumVectorShape]] rotation)[2]
state.rotZ = (--[[---@type tts__CharVectorShape]] rotation).z or (--[[---@type tts__NumVectorShape]] rotation)[3]
end
if scale then
state.scaleX = (--[[---@type tts__CharVectorShape]] scale).x or (--[[---@type tts__NumVectorShape]] scale)[1]
state.scaleY = (--[[---@type tts__CharVectorShape]] scale).y or (--[[---@type tts__NumVectorShape]] scale)[2]
state.scaleZ = (--[[---@type tts__CharVectorShape]] scale).z or (--[[---@type tts__NumVectorShape]] scale)[3]
end
return state
end
---@param transformState tts__ObjectState_Transform
---@return ge_tts__Vector3
function ObjectUtils.getTransformStatePosition(transformState)
return Vector3(
transformState.posX or 0,
transformState.posY or 0,
transformState.posZ or 0
)
end
---@param transformState tts__ObjectState_Transform
---@return ge_tts__Vector3
function ObjectUtils.getTransformStateRotation(transformState)
return Vector3(
transformState.rotX or 0,
transformState.rotY or 0,
transformState.rotZ or 0
)
end
---@param transformState tts__ObjectState_Transform
---@return ge_tts__Vector3
function ObjectUtils.getTransformStateScale(transformState)
return Vector3(
transformState.scaleX or 1,
transformState.scaleY or 1,
transformState.scaleZ or 1
)
end
---
---Same as ObjectUtils.safeSpawnObject except that each entry in containerState.ContainedObjects will also be assigned a
---unique GUID.
---
---@overload fun(containerState: tts__ContainerState): tts__Container
---@param containerState tts__ContainerState @Will be JSON encoded after we generate and assign a GUID.
---@param callback_function nil | tts__Callback<fun(container: tts__Container): void> @Callback that will be called when the object has finished spawning.
---@return tts__Container
function ObjectUtils.safeSpawnContainer(containerState, callback_function)
for _, objectState in ipairs(containerState.ContainedObjects) do
objectState.GUID = ObjectUtils.nextGuid()
end
return --[[---@type tts__Container]] ObjectUtils.safeSpawnObject(containerState, --[[---@type nil | tts__ObjectCallbackFunction]] callback_function)
end
---
---Provides a mechanism to obtain a reference to safe spawning objects, as well as a mechanism to register a callback
---that will be called when an object (safe spawned or otherwise) has finished spawning.
---
---Accepts a TTS object, or a GUID (string). Any TTS object may be provided (safe spawned or otherwise), however when
---providing a GUID, this GUID should only ever correspond with an object that was safe spawned.
---
---Returns a TTS object or nil. If nil is returned, no such object exists and the provided callback will never be
---called. Aside from providing an invalid GUID, this may occur if the object spawned in the past and has since been
---deleted.
---
---If the object has already finished spawning, the provided callback will be called immediately (synchronously).
---Otherwise, the callback will be called as soon as the object has finished spawning.
---
---@overload fun(guid: string): nil | tts__Object
---@param guidOrObject string | tts__Object
---@param callback nil | fun(object: tts__Object): void
---@return nil | tts__Object
function ObjectUtils.getSpawnedObject(guidOrObject, callback)
---@type string
local guid
---@type nil | tts__Object
local existingObject = nil
if type(guidOrObject) == 'userdata' then
---@type tts__Object
local object = --[[---@type tts__Object]] guidOrObject
if object == nil then
return nil
end
if object.spawning then
existingObject = object
guid = object.guid
else
callback(object)
return object
end
else
guid = --[[---@type string]] guidOrObject
end
if not existingObject then
existingObject = getObjectFromGUID(guid)
end
if callback then
if existingObject ~= nil and (--[[---@not nil]] existingObject).spawning and not pendingSpawns[guid] then
-- For consistency, we'll handle non-safe spawned objects (e.g. those being pulled from a container) in the same fashion as we would safe spawned
-- objects i.e. we'll call the provided callback once the object has finished spawning.
pendingSpawns[guid] = {
callbacks = {},
object = (--[[---@not nil]] existingObject),
}
end
local pendingSpawn = pendingSpawns[guid]
if pendingSpawn then
table.insert(pendingSpawn.callbacks, --[[---@not nil]] callback)
return pendingSpawn.object
end
end
if callback and existingObject ~= nil then
callback(--[[---@type tts__Object]] existingObject)
end
return existingObject
end
---@param object tts__Object
local function onObjectSpawn(object)
triggerPendingCallbacks(object.guid, object)
end
EventManager.addHandler('onObjectSpawn', onObjectSpawn)
---@shape __ge_tts__ObjectUtils_SavedStateData
---@field guidPrefix string
---@field pendingSpawns table<string, string>
---@return string
local function onSave()
-- Entries can be injected into pendingSpawns for objects not spawned with safeSpawnObject. However, seems as we weren't responsible for spawning these
-- objects, we make no attempt to save (and potentially respawn) these objects.
local pendingJsonSpawns = TableUtils.map(
TableUtils.select(pendingSpawns, function(json) return json ~= nil end),
function(pendingSpawn) return --[[---@not nil]] pendingSpawn.json end
)
---@type __ge_tts__ObjectUtils_SavedStateData
local data = {
guidPrefix = guidPrefix,
pendingSpawns = pendingJsonSpawns,
}
return Json.encode(data)
end
local function onFirstLoad()
local guidRandomBytes = {}
for _ = 1, GUID_PREFIX_RANDOM_BYTE_LENGTH do
table.insert(guidRandomBytes, math.random(1, 255))
end
guidPrefix = Base64.encode(guidRandomBytes, false) .. ':'
end
---@param savedState string
local function onLoad(savedState)
if savedState == '' then
onFirstLoad()
return
end
local data = --[[---@type __ge_tts__ObjectUtils_SavedStateData]] Json.decode(savedState)
guidPrefix = data.guidPrefix
for guid, json in pairs(data.pendingSpawns) do
local existingObject = getObjectFromGUID(guid)
if not existingObject then
local spawningObject = spawnObjectJSON({json = json})
pendingSpawns[guid] = {
callbacks = {},
object = spawningObject,
json = json
}
end
end
end
local MODULE_NAME = 'ge_tts.ObjectUtils'
SaveManager.registerOnSave(MODULE_NAME, onSave)
SaveManager.registerOnLoad(MODULE_NAME, onLoad)
return ObjectUtils