-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Instance.ttslua
executable file
·604 lines (492 loc) · 22.4 KB
/
Instance.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
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
require('ge_tts.License')
local Class = require('ge_tts.Class')
local EventManager = require('ge_tts.EventManager')
local InstanceManager = require('ge_tts.InstanceManager')
local Logger = require('ge_tts.Logger')
local Object = require('ge_tts.Object')
local ObjectUtils = require('ge_tts.ObjectUtils')
local TableUtils = require('ge_tts.TableUtils')
---@class ge_tts__Instance
---@type table<string, ge_tts__Instance>
local allInstances = {}
---@type table<tts__Object, ge_tts__Instance[]>
local objectInstancesMap = {}
---@param instance ge_tts__Instance
local function destroyAssociatedObject(instance)
instance.takeObject({
callback = function(object)
object.destruct()
end
})
end
---@param instance ge_tts__Instance
---@param object tts__Object
local function associateObject(instance, object)
local associated = objectInstancesMap[object] or {}
table.insert(associated, instance)
objectInstancesMap[object] = associated
Logger.log(
'Instance ' .. instance.toString() .. ' associated with ' .. object.type .. ' (' .. tostring(object.getGUID()) .. ')',
Logger.VERBOSE
)
end
---@param instance ge_tts__Instance
---@param object tts__Object
local function disassociateObject(instance, object)
local associated = objectInstancesMap[object] or {}
local associationIndex = associated and TableUtils.find(associated, instance) or nil
if associationIndex then
table.remove(associated, --[[---@not nil]] associationIndex)
objectInstancesMap[object] = #associated > 0 and associated or nil
Logger.log(
'Instance ' .. instance.toString() .. ' disassociated from ' .. object.type .. ' ('
.. tostring(object.getGUID()) .. ')',
Logger.VERBOSE
)
else
Logger.log(
'Tried to disassociate instance ' .. instance.toString() .. ' from ' .. object.type
.. ' (' .. tostring(object.getGUID()) .. ') but they were not associated.',
Logger.WARNING
)
end
end
---@shape ge_tts__Instance_SavedState
---@field __ss 1
---@field instanceGuid string
---@field objectGuid nil | string
---@field zoneTriggerGuid nil | string
---@shape ge_tts__Instance_TakeObjectOptions
---@field callback nil | tts__ObjectCallbackFunction
---@field position nil | tts__VectorShape
---@field rotation nil | tts__VectorShape
---@field smooth nil | boolean
---@alias ge_tts__Instance_RecoverObject<T> fun(savedState: ge_tts__Instance_SavedState): T
---@class ge_tts__static_Instance
---@overload fun(savedState: ge_tts__Instance_SavedState): ge_tts__Instance
---@overload fun(object: tts__Object): ge_tts__Instance
---@overload fun(guid: string, container: tts__Container): ge_tts__Instance
---@overload fun(objectOrSavedState: tts__Object | ge_tts__Instance_SavedState): ge_tts__Instance
---@overload fun(objectOrGuidOrSavedState: tts__Object | string | ge_tts__Instance_SavedState, nilOrContainer: nil | tts__Container): ge_tts__Instance
local Instance = {}
---@type string
Instance.TYPE = 'Instance'
setmetatable(Instance, {
---@param class self
---@param objectOrGuidOrSavedState tts__Object | string | ge_tts__Instance_SavedState
---@param nilOrContainer nil | tts__Container
__call = function(class, objectOrGuidOrSavedState, nilOrContainer)
local self = --[[---@type ge_tts__Instance]] Class.rootConstructor(class)
setmetatable(self, {
__tostring = function(_)
return self.toString()
end,
})
---@type string
local instanceGuid
---@type nil | tts__Object @Only nil if the object has been destroyed. An instance cannot be *created* without an object.
local object
---@type nil | ge_tts__Zone
local zone = nil
---@type boolean
local destroyed = false
---@param container tts__Container
function self.onEnterContainer(container)
end
---@param container tts__Container
function self.onLeaveContainer(container)
end
---@return string
function self.getInstanceGuid()
return instanceGuid
end
local instanceType = (--[[---@type ge_tts__static_Instance]] class).TYPE
---A simple string identifier for this type of instance. ge_tts itself only uses this in toString().
---
---Subclasses may either set TYPE on the class (static) table, or alternatively, override this method.
---@return string
function self.getType()
return instanceType
end
function self.invalidateSavedState()
InstanceManager.invalidateSavedState(self)
end
--- Called when an instance's object is changed. This method is *not* called during instantiation when an object
--- is initially assigned.
---@param previousObject nil | tts__Object
---@param newObject nil | tts__Object
function self.onObjectChanged(previousObject, newObject)
end
--- Manually changing an instance's associated object is advanced functionality. Unless you really know what
--- you're doing, you probably don't want to call this method!
---@param obj nil | tts__Object
function self.setObject(obj)
if object ~= obj then
local previousObject = object
if previousObject ~= nil then
disassociateObject(self, --[[---@not nil]] previousObject)
if ObjectUtils.isContainerTag((--[[---@not nil]] previousObject).type) then
self.onLeaveContainer(--[[---@type tts__Container]] previousObject)
end
end
object = obj
if obj then
associateObject(self, --[[---@not nil]] obj)
if ObjectUtils.isContainerTag((--[[---@not nil]] obj).type) then
self.onEnterContainer(--[[---@type tts__Container]] obj)
end
end
self.invalidateSavedState()
self.onObjectChanged(previousObject, object)
end
end
--- Returns the associated object. If the object is nil or destroyed, raises an error.
---@return tts__Object
function self.getObject()
self.ensureValid()
return --[[---@not nil]] object
end
--- Returns the associated object irrespective of the instances' current state. Therefore,
--- the value returned may be nil.
---
--- This is mostly for internal use. Generally you should only be interacting with
--- non-destroyed instances. In which case, regular getObject() will work fine.
---@return nil | tts__Object
function self.safeGetObject()
return object
end
--- If the instance is currently associated with a container, then the instance's
--- corresponding object from within is retrieved. Otherwise, if the instance is directly
--- associated with its corresponding object (not in a container) then the callback is
--- called with that object. In either of these two cases, the orientation will be updated
--- according to the provided options.
---@param options ge_tts__Instance_TakeObjectOptions
---@return tts__Object
function self.takeObject(options)
self.ensureValid()
local sourceObject = --[[---@not nil]] object
local containerPosition = self.getContainerPosition()
if containerPosition then
return --[[---@not nil]] sourceObject.takeObject({
index = containerPosition - 1,
rotation = options.rotation,
position = options.position,
smooth = options.smooth,
callback_function = options.callback
})
else
if options.smooth then
if options.position then
ObjectUtils.setPositionSmooth(sourceObject, --[[---@not nil]] options.position)
end
if options.rotation then
ObjectUtils.setRotationSmooth(sourceObject, --[[---@not nil]] options.rotation)
end
else
if options.position then
sourceObject.setPosition(--[[---@not nil]] options.position)
end
if options.rotation then
sourceObject.setRotation(--[[---@not nil]] options.rotation)
end
end
if options.callback then
(--[[---@not nil]] options.callback)(sourceObject)
end
return sourceObject
end
end
---If the instance is presently within a container, returns the position (starting from 1) within said container, otherwise returns nil.
---@return nil | number
function self.getContainerPosition()
if object ~= nil and (--[[---@not nil]] object).guid ~= instanceGuid and ObjectUtils.isContainerTag((--[[---@not nil]] object).type) then
local correspondingObject = TableUtils.detect((--[[---@type tts__Container ]] object).getObjects(), function(objectState)
return objectState.guid == instanceGuid
end)
return correspondingObject and ((--[[---@not nil]] correspondingObject).index + 1)
end
return nil
end
---@return nil | number
function self.getContainerCount()
if object == nil then
return nil
end
local count = (--[[---@not nil]] object).getQuantity()
return count > 0 and count or nil
end
---@return boolean
function self.isTopOfContainer()
if object ~= nil and ObjectUtils.isContainerTag((--[[---@not nil]] object).type) then
return self.getContainerPosition() == (--[[---@not nil]] object).getQuantity()
end
return false
end
---@param previousZone nil | ge_tts__Zone
---@param newZone nil | ge_tts__Zone
function self.onZoneChanged(previousZone, newZone)
end
---@param newZone nil | ge_tts__Zone
function self.setZone(newZone)
if zone ~= newZone then
local previousZone = zone
zone = newZone
Logger.log('Instance ' .. self.toString() .. ' zone changed from ' .. tostring(previousZone) .. ' to ' .. tostring(zone), Logger.VERBOSE)
self.invalidateSavedState()
self.onZoneChanged(previousZone, zone)
end
end
---
---The zone this Instance is occupying, or most recently occupied.
---
---It is intentional that this is not set to nil when an Instance stops occupying (i.e. leaves) a zone. It's
---often useful to know where an Instance came from e.g. if desired you can return an Instance to its previous
---zone.
---
---If you're specifically interested in whether an instance is currently occupying a zone, this can be obtained
---with:
---
--- Zone.getObjectOccupyingZone(instance.getObject())
---
---@return nil | ge_tts__Zone
function self.getZone()
return zone
end
---The zone type of zone this Instance is occupying, or most recently occupied.
---@return nil | string
function self.getZoneType()
return zone and (--[[---@not nil]] zone).getType()
end
---@overload fun(): boolean
---@param colorName nil | tts__PlayerColor
---@return tts__Object
function self.reject(colorName)
self.ensureValid()
local pickupPosition = (--[[---@not nil]] object).pick_up_position
local pickupRotation = (--[[---@not nil]] object).pick_up_rotation
local rejectZone = self.getZone()
return self.takeObject({
callback = function(instanceObject)
if rejectZone then
(--[[---@not nil]] rejectZone).drop(colorName, instanceObject)
elseif pickupPosition:sqrMagnitude() ~= 0 then
ObjectUtils.setPositionSmooth(instanceObject, pickupPosition, false, true)
ObjectUtils.setRotationSmooth(instanceObject, pickupRotation, false, true)
end
end
})
end
---@return ge_tts__Instance_SavedState
function self.save()
local trigger = zone and (--[[---@not nil]] zone).getObject()
return {
__ss = 1,
instanceGuid = instanceGuid,
objectGuid = object and (--[[---@not nil]] object).getGUID(),
zoneTriggerGuid = trigger ~= nil and (--[[---@not nil]] trigger).getGUID() or nil,
}
end
--- Throws an error if the instance has a nil object. Typically this means it has been destroyed.
function self.ensureValid()
if object == nil then
if destroyed then
error("Instance has been destroyed")
else
error("Instance accessed whilst in invalid state. Are you using setObject()? Have you overridden destroy() and forgotten to call through to the super implementation?")
end
end
end
---@return boolean
function self.isDestroyed()
return destroyed
end
function self.destroy()
if destroyed then
return
end
allInstances[instanceGuid] = nil
if object ~= nil then
destroyAssociatedObject(self)
disassociateObject(self, --[[---@not nil]] object)
end
object = nil
destroyed = true
self.invalidateSavedState()
InstanceManager.onInstanceDestroyed(self)
Logger.log('Instance ' .. self.toString() .. ' destroyed.', Logger.DEBUG)
end
---@return string
function self.toString()
return self.getType() .. " (" .. instanceGuid .. ")"
end
function self.onSpawned()
end
--- Called when this instance's object is dropped by a player and is not being accepted by any zones.
---@param colorName tts__PlayerColor @Color of the TTS player that dropped the TTS object
---@param pendingRejection boolean @Whether the dropped object is will be rejected back to its pickup zone/location if we return false.
---@return boolean @Return true to indicate the dropped object has been handled and the object should not be rejected.
function self.onRelease(colorName, pendingRejection)
return false
end
if Instance.isSavedState(objectOrGuidOrSavedState) then
local savedState = --[[---@type ge_tts__Instance_SavedState]] objectOrGuidOrSavedState
instanceGuid = savedState.instanceGuid
if savedState.objectGuid then
object = --[[---@not nil]] getObjectFromGUID(--[[---@not nil]] savedState.objectGuid)
Logger.assert(object, 'Failed to recover object reference for Instance ' .. instanceGuid)
end
if savedState.zoneTriggerGuid then
local trigger = getObjectFromGUID(--[[---@not nil]] savedState.zoneTriggerGuid)
if trigger ~= nil then
-- NOTE: If this Instance is restored from its saved state before the trigger's Zone is restored from
-- its saved state, the zone will be nil. However, when the Zone is restored from its saved state
-- it will setZone() on all occupying instances. So the only possible information loss is due to
-- the fact our zone is the *most recent* Zone. If TTS saves when instance is not in its most
-- recent zone *and* that most recent zone restores second, we'll lose our reference.
--
-- We're requiring here to avoid top-level (illegal) cyclical requires.
zone = require('ge_tts.Zone').getScriptingTriggerZone(--[[---@type tts__ScriptingTrigger]] trigger)
end
end
elseif nilOrContainer then
Logger.assert(nilOrContainer ~= nil, 'Instance cannot be instantiated with a GUID but no valid associated object')
Logger.assert(objectOrGuidOrSavedState, 'Instance cannot be instantiated with a container but no GUID')
local container = --[[---@not nil]] nilOrContainer
local containerInstance = Instance.getInstance(container.guid)
instanceGuid = --[[---@type string]] objectOrGuidOrSavedState
object = container
zone = containerInstance and (--[[---@not nil]] containerInstance).getZone()
Logger.log(tostring(self) .. " instantiated in container's zone " .. tostring(zone), Logger.DEBUG)
else
Logger.assert(objectOrGuidOrSavedState ~= nil, 'Instance cannot be instantiated without a valid associated object')
local instanceObject = --[[---@type tts__Object]] objectOrGuidOrSavedState
object = instanceObject
instanceGuid = instanceObject.guid
end
allInstances[instanceGuid] = self
if object then
associateObject(self, --[[---@not nil]] object)
end
self.invalidateSavedState()
return self
end
})
---@param value any
---@return boolean
function Instance.isSavedState(value)
return type(value) == 'table' and (--[[---@type table]] value).__ss == 1
end
---@param guid string
---@return nil | ge_tts__Instance
function Instance.getInstance(guid)
return allInstances[guid]
end
---@param object tts__Object @TTS object for which we wish to obtain associated instances
---@return ge_tts__Instance[]
function Instance.getInstances(object)
local instances = objectInstancesMap[object]
return instances and TableUtils.copy(instances) or {}
end
---@param object tts__Object @TTS object for which we wish to obtain an associated instance
---@return nil | ge_tts__Instance
function Instance.getOneInstance(object)
local instances = objectInstancesMap[object]
return instances and instances[1] or nil
end
---@return table<string, ge_tts__Instance> @table<GUID, ge_tts__Instance>
function Instance.getAllInstances()
return TableUtils.copy(allInstances)
end
---@param container tts__Container
---@param position number @Starts at 1
---@return nil | ge_tts__Instance
function Instance.getContainerInstance(container, position)
local objectStates = container.getObjects()
if position > 0 and position <= #objectStates then
local objectState = objectStates[position]
return allInstances[objectState.guid]
end
return nil
end
---@param container tts__Container
---@return nil | ge_tts__Instance
function Instance.getContainerTopInstance(container)
return Instance.getContainerInstance(container, container.is_face_down and 1 or container.getQuantity())
end
---@param object tts__Object
---@return nil | ge_tts__Instance
function Instance.getPrimaryInstance(object)
return ObjectUtils.isContainerTag(object.type)
and Instance.getContainerTopInstance(--[[---@type tts__Container]] object)
or Instance.getOneInstance(object)
end
---@param object tts__Object
local function onObjectSpawn(object)
local instance = allInstances[object.guid]
if instance then
(--[[---@not nil]] instance).onSpawned()
end
end
---@param object tts__Object
local function onObjectDestroy(object)
local instances = objectInstancesMap[object]
if instances then
instances = TableUtils.copy(instances) -- Shallow copy, because we will modify the original table whilst iterating
for _, instance in ipairs(instances) do
Logger.log(
'Destroying instance ' .. instance.toString() .. ' as its associated ' .. object.type .. ' ('
.. tostring(object.getGUID()) .. ') is being destroyed',
Logger.DEBUG
)
instance.destroy()
end
end
end
EventManager.addHandler('onObjectSpawn', onObjectSpawn)
EventManager.addHandler('onObjectDestroy', onObjectDestroy)
-- When a card (or deck) has been inserted into a deck, we want our Instance to track the deck now.
---@param container tts__Container
---@param object tts__Object
local function onObjectEnterContainer(container, object)
Logger.log(
'Instance ' .. object.type .. ' (' .. tostring(object.getGUID()) .. ') entered ' .. container.type .. ' (' .. tostring(container.getGUID()) .. ')',
Logger.DEBUG
)
local instances = objectInstancesMap[object]
if instances then
instances = TableUtils.copy(instances) -- Shallow copy, because we will modify the original table whilst iterating
if container.type == Object.Tag.Infinite then
for _, instance in ipairs(instances) do
Logger.log(
'Instance ' .. instance.toString() .. ' is being destroyed as it entered infinite bag ('
.. tostring(container.getGUID()) .. ')',
Logger.DEBUG
)
instance.destroy()
end
else
for _, instance in ipairs(instances) do
Logger.log(
'Instance ' .. instance.toString() .. ' entered ' .. container.type .. ' ('
.. tostring(container.getGUID()) .. ')',
Logger.DEBUG
)
instance.setObject(container)
end
end
end
end
-- When an object leaves a container we want to find the associated Instance and associate them.
---@param container tts__Container
---@param object tts__Object
local function onObjectLeaveContainer(container, object)
local instance = allInstances[object.guid]
if instance then
Logger.log('Instance ' .. instance.toString() .. ' left ' .. container.type .. ' ('
.. tostring(container.getGUID()) .. ')', Logger.DEBUG)
instance.setObject(object)
end
end
EventManager.addHandler('onObjectEnterContainer', onObjectEnterContainer)
EventManager.addHandler('onObjectLeaveContainer', onObjectLeaveContainer)
return Instance