-
Notifications
You must be signed in to change notification settings - Fork 0
/
bagProt.ttslua
401 lines (347 loc) · 13.8 KB
/
bagProt.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
-- urgent followup: this bag does not guard against duplicate guids! if you copy/paste shit into it repeatedly it will break!
--forward declarations for utility functions (defined at the end)
local deepcopy, round
local protTable = require("lib/objectprot/mainProt") -- this call to require() also sets a couple global functions. followup on if this is a good pattern
local objectprot = protTable.mainProt
-- local protList = protTable.protList
local instanceList = protTable.instanceList -- remember, each instance is in instanceList[protName][instance.guid]
local bagprot = { -- reference bag prot with default configuration values
protName = "MemoryBag",
blinkColor = "Red",
selfHighlightColor = "Blue",
blinkDuration = 0.5,
isRelative = false,
smoothTake = true, -- objects resting on top of each other sometimes clip through each other when taken from the bag smoothly. override it if you run into that issue.
printMessages = true,
}
bagprot = objectprot:newProt(bagprot) -- bind to parent
function bagprot:init()
-- objectprot:init(obj) -- parent's init can be called directly. yay for inheritance. not needed here however, it's just for show.
local realBag = self() -- todo: remove all realBag type vars after adding binser for proper serialization
self.memoryList = {}
local ss = tostring(realBag.script_state)
if ss ~= nil and ss ~= "" then
self:actionMode()
else
self:startMode()
end
return self
end
function bagprot:playerPrint(str) -- overrides parent function to add a check.
if self.printMessages then
objectprot:playerPrint(str)
end
end
-- reference of self.memoryList entry structure: self.memoryList[guid] = {pos = Vector3 , rot = Vector3}
function bagprot:readMemory() -- only reads save if memoryList is still empty.
if not self.memoryList or next(self.memoryList) == nil then -- or not next(self.memoryList) then
self.memoryList = self:readSave()
end
end
-- this function is sort of expensive, it loops over self.memoryList, self.getObjects, and then trackedObjects
function bagprot:getOutsideObjects() -- checks which of memoryList's objects we can "see" and returns what we can and can't
self:readMemory()
-- objects can be inside self, out on the table, or missing (neither inside or outside, e.g. in another bag).
local trackedObjects = {} -- set of objects on the table
local missingObjects = {} -- set of for missing objects
local missingObjectsArray = {} -- array for concatting
for guid, entry in pairs(self.memoryList) do -- first make a shallowcopy of all self.memoryList
trackedObjects[guid] = true
end
for _,entry in ipairs(self().getObjects()) do -- account for all objects inside self, leaving only outside and missing.
trackedObjects[entry.guid] = nil
end
for guid,_ in pairs(trackedObjects) do -- account for all objects outside on the table
local obj = getObjectFromGUID(guid)
if obj == nil then
trackedObjects[guid] = nil
missingObjects[guid] = true
missingObjectsArray[#missingObjectsArray + 1] = guid
else
trackedObjects[guid] = obj
end
end
if next(missingObjectsArray) then -- still objects unaccounted for
self:playerPrint("Lost track of some objects! Here they are in a lua table:\n{\n \"" .. table.concat(missingObjectsArray, "\",\n \"") .. "\"\n}")
end
-- remember that trackedObjects points to direct object refs but missingObjects is just guids
return trackedObjects, missingObjects
end
-- called when bag first loads:
function bagprot:startMode() -- called when bag is empty, creates absolute/relative picker buttons
self().clearButtons()
self().clearContextMenu()
self().highlightOff()
self:startMenu()
end
function bagprot:startMenu() -- ui creation is separated its own function for easier overriding
function setup_r(player)
self.isRelative = true
self:playerPrint("Using relative coords")
self:Setup(player)
end
function setup_a(player)
self.isRelative = false
self:playerPrint("Using absolute coords")
self:Setup(player)
end
self().addContextMenuItem("Setup (relative)", setup_r)
self().addContextMenuItem("Setup (absolute)", setup_a)
end
function bagprot:actionMode() -- creates recall and place buttons
self().clearButtons()
self().clearContextMenu()
self().highlightOff()
self:actionMenu()
end
function bagprot:actionMenu()
self:makeContextMenu("Place")
self:makeContextMenu("Recall")
self:makeContextMenu("Setup")
end
-- the "Setup" button:
function bagprot:Setup(player)
self.mlBackup = deepcopy(self.memoryList)
Player[player].clearSelectedObjects()
local outsideObjects, missingObjects = self:getOutsideObjects() -- dam this function is expensive, i don't want to call it twice in one setup phase.
for guid,obj in pairs(outsideObjects) do
obj.addToPlayerSelection(player)
-- Player[player].pingTable(obj.getPosition()) -- followup: pinging may be a bit too loud lol, consider highligting instead?
-- obj.highlightOn(blinkColor, blinkDuration)
end
self().highlightOn(self.selfHighlightColor)
self().clearButtons()
self().clearContextMenu()
self:setupMenu(outsideObjects, missingObjects)
end
function bagprot:setupMenu(outsideObjects, missingObjects)
-- self().createButton({
-- label="Toggle Selected", click_function="global_sendSelected", function_owner=Global,
-- position={0,0.3,-2}, rotation={0,180,0}, height=350, width=1700,
-- font_size=250, color={0,0,0}, font_color={1,1,1}
-- })
-- self().createButton({
-- label="Cancel", click_function="global_Cancel", function_owner=Global,
-- position={0,0.3,-2.8}, rotation={0,180,0}, height=350, width=1700,
-- font_size=250, color={0,0,0}, font_color={1,1,1}
-- })
-- self().createButton({
-- label="Submit", click_function="global_Submit", function_owner=Global,
-- position={0,0.3,-3.6}, rotation={0,180,0}, height=350, width=1700,
-- font_size=250, color={0,0,0}, font_color={1,1,1}
-- })
-- self.createButton({
-- label="Reset", click_function="global_Reset", function_owner=Global,
-- position={-2,0.3,0}, rotation={0,270,0}, height=350, width=800,
-- font_size=250, color={0,0,0}, font_color={1,1,1}
-- })
-- self:makeContextMenu("sendSelected") -- this doesn't work, TTS selection interface resets on right click :( so we use a hotkey instead
if next(self.memoryList) then
self:makeContextMenu("Re-Select Objects", self.Setup)
end
-- followup: also this doesn't handle a tracked object leaving a container during setup. maybe above my paygrade.
self:makeContextMenu("Cancel")
self:makeContextMenu("Submit")
if next(missingObjects) then
local function forgetMissing()
for guid, _ in pairs(missingObjects) do
self.memoryList[guid] = nil
end
end
self().makeContextMenu("Forget Missing Objects", forgetMissing)
end
self:makeContextMenu("")
self:makeContextMenu("Reset")
self:makeContextMenu("")
end
-- you can override/extend this to also record other fields e.g. name/description, lock status, color tint, variables from memory, etc.
function bagprot:recordObject(obj)
local realBag = self()
local pos,rot
if self.isRelative then
pos = realBag.positionToLocal(obj.getPosition())
rot = obj.getRotation() - realBag.getRotation()
else
pos = obj.getPosition()
rot = obj.getRotation()
end
--I need to add it like this or it won't save due to indexing issue
local entry = {
pos={x=round(pos.x,4), y=round(pos.y,4), z=round(pos.z,4)},
rot={x=round(rot.x,4), y=round(rot.y,4), z=round(rot.z,4)},
}
return entry
end
-- in Setup Mode
function bagprot:sendSelected(player, realBag) -- this is a global function because it needs to attach to a createButton()
local outsideObjects = self:getOutsideObjects() -- we call this again because shit could have changed since we last called it during self:Setup(). as a reminder, it returns the set of externally existing tracked objects
local newObjects = {} -- set of objects that are not yet tracked to be added to self.memoryList
realBag.removeFromPlayerSelection(player) -- we avoid selecting ourselves because for some insane reason the tts api allows you to putobject() bags into themselves, which makes the bag disappear. lol
for i, obj in ipairs(Player[player].getSelectedObjects()) do
local objGUID = obj.getGUID()
self.memoryList[objGUID] = self:recordObject(obj) -- add/update selected obj in the memory list.
if outsideObjects[objGUID] then
outsideObjects[objGUID] = nil -- if it's already tracked, we don't care about it after updating its entry.
else
newObjects[#newObjects + 1] = obj -- if it's not tracked yet, add it to new object set
obj.highlightOn({0,1,0}, self.blinkDuration)
end
end
local removedObjects = {}
for guid, obj in pairs(outsideObjects) do -- now we go through everything that was previously tracked but wasn't selected and remove it.
self.memoryList[guid] = nil
removedObjects[#removedObjects + 1] = obj
obj.highlightOn({1,0,0}, self.blinkDuration)
end
do
local changed = false
if next(newObjects) then
changed = true
self:playerPrint("Saved " .. #newObjects .. " new object" .. (#newObjects > 1 and "s" or ""), player)
end
if next(removedObjects) then
changed = true
self:playerPrint("Removed " .. #removedObjects .. " objects" .. (#newObjects > 1 and "s" or ""), player)
end
if not changed then
self:playerPrint("No changes to record!", player)
end
end
return newObjects, removedObjects
end
local function blinkIfAble(obj, color, duration)
return obj ~= nil and obj.highlightOn(color, duration)
end
function bagprot:blinkAll(player) -- blinks self and all outside objects with player color
local realBag = self()
realBag.removeFromPlayerSelection(player)
realBag.highlightOff()
local duration = self.blinkDuration
local player = player
local function blinkIfAble(obj)
if obj ~= nil then
obj.highlightOn(player, duration) -- yay upvalues
end
end
local function blinkSelf()
blinkIfAble(realBag)
end
-- do return end
local objs = self:getOutsideObjects()
for _,obj in pairs(objs) do
blinkIfAble(obj) -- t0
Wait.time(function() blinkIfAble(obj) end, duration * 2) -- t2
Wait.time(function() blinkIfAble(obj) end, duration * 4) -- t4
end
Wait.time(blinkSelf, duration) -- t1
Wait.time(blinkSelf, duration * 3) -- t3
end
function bagprot:Cancel(player) -- Cancels selection process
if self.mlBackup then
self.memoryList = self.mlBackup
self.mlBackup = nil
end
self:blinkAll(player)
do return end
self:playerPrint("Selection Cancelled", player)
if next(self.memoryList) then
self:actionMode()
else
self:startMode()
end
end
function bagprot:Submit(player) -- Saves selections with writeSave
if next(self.memoryList) == nil then
printToColor("You cannot submit without any objects recorded", player)
else
local count = 0
for guid, entry in pairs(self.memoryList) do
count = count + 1
end
self:blinkAll(player)
self:writeSave(self.memoryList)
self:playerPrint("Recorded " .. count .. " objects", player)
self:actionMode()
end
end
function bagprot:Reset(player) -- Resets bag to starting status
self.memoryList = nil
self:writeSave(nil)
self:playerPrint("Tool has been reset", player)
self:startMode()
end
-- in Action Mode:
function bagprot:Place() -- Sends objects from bag/table to their saved position/rotation
local realBag = self()
self:readMemory()
local bagObjList = {}
for k,v in ipairs(realBag.getObjects()) do
bagObjList[v.guid] = true -- build a set of all guids in the bag
end
local selfPos = realBag.getPosition()
local smooth = self.smoothTake
for guid, entry in pairs(self.memoryList) do
local obj = getObjectFromGUID(guid)
local objpos, objrot
if self.isRelative then
objpos = realBag.positionToWorld(entry.pos)
objrot = Vector(entry.rot) + realBag.getRotation()
else
objpos = entry.pos
objrot = entry.rot
end
if bagObjList[guid] then --If obj is inside of the bag, take it out
realBag.takeObject({guid = guid, position = objpos, rotation = objrot, smooth = smooth})
else
if obj ~= nil then --If obj is out on the table, move it
obj.setPositionSmooth(objpos, false, true)
obj.setRotationSmooth(objrot, false, true)
else
self:playerPrint("Object not found on the table or in the bag! its guid was " .. guid)
end
end
end
self:playerPrint("Objects placed", player)
end
function bagprot:Recall(player) -- Recalls objects to bag from table
local realBag = self()
self:readMemory()
for guid, entry in pairs(self.memoryList) do
local obj = getObjectFromGUID(guid)
if obj ~= nil then
local pos = obj.getPosition()
pos.y = pos.y + 1.5
obj.setPositionSmooth(pos, false, true)
Wait.condition(function() realBag.putObject(obj) end, function() return not obj.isSmoothMoving() end)
end
end
self:playerPrint("Objects recalled", player)
end
local function global_sendSelected(player, obj) -- followup: add a wrapper to manage hotkeys
local instance = instanceList["MemoryBag"][obj.guid]
if instance then instance:sendSelected(player, obj) end
end
addHotkey("Send selected to MemoryBag", global_sendSelected)
-- showHotkeyConfig()
do
deepcopy = function(orig)
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in next, orig, nil do
copy[deepcopy(orig_key)] = deepcopy(orig_value)
end
setmetatable(copy, deepcopy(getmetatable(orig)))
else -- number, string, boolean, etc
copy = orig
end
return copy
end
--Round number (num) to the Nth decimal (dec)
round = function(num, dec)
local mult = 10^(dec or 0)
return math.floor(num * mult + 0.5) / mult
end
end
return bagprot