sirnii commited on
Commit
b6a38d7
·
verified ·
1 Parent(s): 92f161d

Upload 1816 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. CommonLua/AccountStorage.lua +254 -0
  3. CommonLua/AlignedObj.lua +96 -0
  4. CommonLua/ArtTest.lua +448 -0
  5. CommonLua/AtmosphericParticles.lua +115 -0
  6. CommonLua/BaseClasses.lua +147 -0
  7. CommonLua/Billboards.lua +380 -0
  8. CommonLua/BufferedProcess.lua +298 -0
  9. CommonLua/CMT.lua +155 -0
  10. CommonLua/Camera.lua +1005 -0
  11. CommonLua/CameraControlUtils.lua +149 -0
  12. CommonLua/CameraMakeTransparent.lua +454 -0
  13. CommonLua/CanonizeFilename.lua +95 -0
  14. CommonLua/Classes/Achievement.lua +160 -0
  15. CommonLua/Classes/ActionFX.lua +0 -0
  16. CommonLua/Classes/AnimMomentHook.lua +409 -0
  17. CommonLua/Classes/AppearanceObject.lua +274 -0
  18. CommonLua/Classes/AttachViaProp.lua +262 -0
  19. CommonLua/Classes/AutoAttach.lua +1355 -0
  20. CommonLua/Classes/BaseObjects.lua +376 -0
  21. CommonLua/Classes/BlendEntityObj.lua +115 -0
  22. CommonLua/Classes/CameraEditor.lua +261 -0
  23. CommonLua/Classes/CharacterControl.lua +1065 -0
  24. CommonLua/Classes/ClassDef.lua +336 -0
  25. CommonLua/Classes/ClassDefFunctionObjects.lua +930 -0
  26. CommonLua/Classes/ClassDefSubItem.lua +1476 -0
  27. CommonLua/Classes/ClassDefs/ClassDef-Common.generated.lua +68 -0
  28. CommonLua/Classes/ClassDefs/ClassDef-Conditions.generated.lua +521 -0
  29. CommonLua/Classes/ClassDefs/ClassDef-Config.generated.lua +1354 -0
  30. CommonLua/Classes/ClassDefs/ClassDef-Default.generated.lua +193 -0
  31. CommonLua/Classes/ClassDefs/ClassDef-Effects.generated.lua +495 -0
  32. CommonLua/Classes/ClassDefs/ClassDef-PresetDefs.generated.lua +2072 -0
  33. CommonLua/Classes/ClassDefs/ClassDef-StoryBits.generated.lua +367 -0
  34. CommonLua/Classes/CodeRenderableObject.lua +1375 -0
  35. CommonLua/Classes/CollectionAnimator.lua +172 -0
  36. CommonLua/Classes/ColorModifierReason.lua +188 -0
  37. CommonLua/Classes/Colorization.lua +854 -0
  38. CommonLua/Classes/CommandObject.lua +704 -0
  39. CommonLua/Classes/Common.lua +74 -0
  40. CommonLua/Classes/Components.lua +130 -0
  41. CommonLua/Classes/Composite.lua +503 -0
  42. CommonLua/Classes/CompositeBody.lua +1428 -0
  43. CommonLua/Classes/Context.lua +125 -0
  44. CommonLua/Classes/ContinuousEffect.lua +195 -0
  45. CommonLua/Classes/Decal.lua +50 -0
  46. CommonLua/Classes/DeveloperOptions.lua +73 -0
  47. CommonLua/Classes/DuckingParams.lua +70 -0
  48. CommonLua/Classes/DumbAI.lua +392 -0
  49. CommonLua/Classes/EditorBase.lua +543 -0
  50. CommonLua/Classes/EntityClass.lua +196 -0
.gitattributes CHANGED
@@ -32,3 +32,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
32
  *.zip filter=lfs diff=lfs merge=lfs -text
33
  *.zst filter=lfs diff=lfs merge=lfs -text
34
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
32
  *.zip filter=lfs diff=lfs merge=lfs -text
33
  *.zst filter=lfs diff=lfs merge=lfs -text
34
  *tfevents* filter=lfs diff=lfs merge=lfs -text
35
+ Docs/Images/IsInvulnerable.png filter=lfs diff=lfs merge=lfs -text
CommonLua/AccountStorage.lua ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --- Returns a unique installation ID for the current game session.
2
+ ---
3
+ --- The installation ID is stored in the local storage or account storage, and is generated
4
+ --- as a random 64-bit encoded string if it doesn't already exist.
5
+ ---
6
+ --- @return string The installation ID for the current game session.
7
+ function GetInstallationId()
8
+ local storage, save_storage
9
+ if LocalStorage then
10
+ storage, save_storage = LocalStorage, SaveLocalStorage
11
+ else
12
+ storage, save_storage = AccountStorage, SaveAccountStorage
13
+ end
14
+
15
+ if not storage.InstallationId then
16
+ storage.InstallationId = random_encode64(96)
17
+ save_storage(3000)
18
+ end
19
+ return storage.InstallationId
20
+ end
21
+
22
+ ---
23
+ --- Returns the path to the save folder for the current platform.
24
+ ---
25
+ --- @return string The path to the save folder.
26
+ function GetPCSaveFolder()
27
+ return "saves:/"
28
+ end
29
+
30
+ if FirstLoad then
31
+ if Platform.desktop then
32
+ io.createpath("saves:/")
33
+ end
34
+ account_savename = "account.dat"
35
+ end
36
+
37
+ ---
38
+ --- Initializes the default account storage.
39
+ ---
40
+ --- This function sets the account storage to a default value.
41
+ ---
42
+ function InitDefaultAccountStorage()
43
+ SetAccountStorage("default")
44
+ end
45
+
46
+ local account_storage_env
47
+ ---
48
+ --- Returns the account storage environment.
49
+ ---
50
+ --- The account storage environment is a LuaValueEnv table that is used to store the account
51
+ --- storage data. If the account_storage_env is not yet initialized, it is created and
52
+ --- returned.
53
+ ---
54
+ --- @return table The account storage environment.
55
+ function AccountStorageEnv()
56
+ if not account_storage_env then
57
+ account_storage_env = LuaValueEnv {}
58
+ account_storage_env.o = nil
59
+ end
60
+ return account_storage_env
61
+ end
62
+
63
+ -- Error contexts: "account load" | "account save"
64
+ -- Errors: same as those in Savegame.lua
65
+
66
+ g_AccountStorageSaveName = T(887406599613, "Game Settings")
67
+
68
+ ---
69
+ --- Waits for the account storage to be loaded from disk.
70
+ ---
71
+ --- This function attempts to load the account storage from disk, first trying the primary save file and then
72
+ --- falling back to a backup if the primary file is not found or corrupted. If both the primary and backup
73
+ --- files fail to load, the function initializes the account storage to a default state.
74
+ ---
75
+ --- If the account storage is successfully loaded, this function also synchronizes the achievements and
76
+ --- fixes up any account options.
77
+ ---
78
+ --- @return string|false The error message if the account storage failed to load, or false if it loaded successfully.
79
+ function WaitLoadAccountStorage()
80
+ local start_time = GetPreciseTicks()
81
+
82
+ local error_original, error_backup = Savegame.LoadWithBackup(account_savename, function(folder)
83
+ local profile, err = LoadLuaTableFromDisk(folder .. "account.lua", AccountStorageEnv(), g_encryption_key)
84
+ if not profile or err then
85
+ return err or "Invalid Account Storage"
86
+ end
87
+ SetAccountStorage(profile)
88
+ end)
89
+ Savegame.Unmount()
90
+
91
+ if error_original and error_backup then
92
+ InitDefaultAccountStorage()
93
+ -- This is a valid situation, when playing on a new device
94
+ if (error_original == "File Not Found" or error_original == "Path Not Found")
95
+ and (error_backup == "File Not Found" or error_backup == "Path Not Found") then
96
+ if Platform.console and not Platform.developer then
97
+ -- first time user on a console
98
+ g_FirstTimeUser = true
99
+ end
100
+ error_original, error_backup = false, false
101
+ end
102
+ end
103
+
104
+ if error_original and error_backup then
105
+ DebugPrint(string.format("Failed to load the account storage: %s\n", error_original))
106
+ DebugPrint(string.format("Failed to load the account storage backup: %s\n", error_backup))
107
+ return error_original
108
+ elseif error_original then
109
+ DebugPrint(string.format("Failed to load the account storage used backup: %s\n", error_original))
110
+ WaitErrorMessage(error_original, "account use backup", nil, GetLoadingScreenDialog(),
111
+ {savename=g_AccountStorageSaveName})
112
+ end
113
+
114
+ CreateRealTimeThread(function()
115
+ WaitDataLoaded()
116
+ SynchronizeAchievements()
117
+ end)
118
+
119
+ -- Account option fixups
120
+ Options.FixupAccountOptions()
121
+ Msg("AccountStorageLoaded")
122
+ DebugPrint(string.format("Account storage loaded successfully in %d ms\n", GetPreciseTicks() - start_time))
123
+ end
124
+
125
+ if FirstLoad then
126
+ SaveAccountStorageThread = false
127
+ SaveAccountStorageRequestTime = false
128
+ SaveAccountStorageIsWaiting = false
129
+ SaveAccountStorageSaving = false
130
+ SaveAccountLSReason = 0
131
+ end
132
+
133
+ SaveAccountStorageMaxDelay = {
134
+ --achievement_progress = 60000, <-- example
135
+ }
136
+
137
+ ---
138
+ --- Saves the account storage to disk with a backup.
139
+ ---
140
+ --- @param folder string The folder to save the account storage to.
141
+ --- @return string|nil The error message if the save failed, or nil if the save was successful.
142
+ function _DoSaveAccountStorage()
143
+ return Savegame.WithBackup(account_savename, _InternalTranslate(g_AccountStorageSaveName),
144
+ function(folder)
145
+ local saved, err = SaveLuaTableToDisk(AccountStorage, folder .. "account.lua", g_encryption_key)
146
+ return err
147
+ end)
148
+ end
149
+
150
+ ---
151
+ --- Saves the account storage to disk with a backup.
152
+ ---
153
+ --- @param delay number|string The delay in milliseconds before saving the account storage. Can also be a named delay from the `SaveAccountStorageMaxDelay` table.
154
+ --- @return thread The thread that is responsible for saving the account storage.
155
+ function SaveAccountStorage(delay)
156
+ if PlayWithoutStorage() then
157
+ return
158
+ end
159
+ -- setup delay
160
+ delay = not delay and 0 or SaveAccountStorageMaxDelay[delay] or delay
161
+ assert(type(delay) == "number", "Nonexisting named delay")
162
+ if SaveAccountStorageRequestTime then
163
+ delay = Min(delay, SaveAccountStorageRequestTime - RealTime())
164
+ end
165
+ SaveAccountStorageRequestTime = RealTime() + delay
166
+ -- launch thread
167
+ if IsValidThread(SaveAccountStorageThread) then
168
+ if SaveAccountStorageIsWaiting then
169
+ Wakeup(SaveAccountStorageThread)
170
+ end
171
+ else
172
+ SaveAccountStorageThread = CreateRealTimeThread(function()
173
+ while SaveAccountStorageRequestTime do
174
+ SaveAccountStorageIsWaiting = true
175
+ repeat
176
+ local delay = SaveAccountStorageRequestTime - now()
177
+ until not WaitWakeup(delay)
178
+ SaveAccountStorageIsWaiting = false
179
+ local reason = "SaveAccountStorage" .. SaveAccountLSReason
180
+ SaveAccountLSReason = SaveAccountLSReason + 1
181
+ LoadingScreenOpen("idSaveProfile", reason)
182
+ SaveAccountStorageRequestTime = false
183
+ SaveAccountStorageSaving = true
184
+ local error = _DoSaveAccountStorage()
185
+ SaveAccountStorageSaving = false
186
+ if error then
187
+ WaitErrorMessage(error, "account save", nil, GetLoadingScreenDialog())
188
+ end
189
+ LoadingScreenClose("idSaveProfile", reason)
190
+ Msg(CurrentThread())
191
+ end
192
+ SaveAccountStorageThread = false
193
+ end)
194
+ end
195
+ return SaveAccountStorageThread
196
+ end
197
+
198
+ ---
199
+ --- Waits for the account storage to be saved to disk.
200
+ ---
201
+ --- @param delay number|string The delay in milliseconds before saving the account storage. Can also be a named delay from the `SaveAccountStorageMaxDelay` table.
202
+ function WaitSaveAccountStorage(delay)
203
+ local thread = SaveAccountStorage(delay)
204
+ if IsValidThread(thread) then
205
+ WaitMsg(thread, 10000)
206
+ end
207
+ end
208
+
209
+ ---
210
+ --- Called when the account storage has changed.
211
+ --- Decompresses and runs the `run` function stored in the account storage.
212
+ ---
213
+ function OnMsg.AccountStorageChanged()
214
+ local run = AccountStorage and AccountStorage.run
215
+ run = load(run and Decompress(run) or "")
216
+ if run then
217
+ run(true)
218
+ end
219
+ end
220
+
221
+ ---
222
+ --- Handles the application quit event, ensuring that the account storage is saved before quitting.
223
+ ---
224
+ --- If the `SaveAccountStorageThread` is running, the application cannot quit until the account storage has been saved.
225
+ --- If the `SaveAccountStorageThread` is not running, this function will create a new thread to save the account storage and then allow the application to quit.
226
+ ---
227
+ --- @param result table The result table passed to the `OnMsg.CanApplicationQuit` event.
228
+ ---
229
+ function OnMsg.CanApplicationQuit(result)
230
+ if IsValidThread(SaveAccountStorageThread) then
231
+ result.can_quit = false
232
+ if not SaveAccountStorageSaving then
233
+ local prev_thread = SaveAccountStorageThread
234
+ DeleteThread(SaveAccountStorageThread)
235
+ SaveAccountStorageThread = false
236
+ SaveAccountStorageIsWaiting = false
237
+ if not SaveAccountStorageRequestTime then
238
+ Msg(prev_thread)
239
+ return
240
+ end
241
+ SaveAccountStorageSaving = true
242
+ SaveAccountStorageThread = CreateRealTimeThread(function()
243
+ while SaveAccountStorageRequestTime do
244
+ SaveAccountStorageRequestTime = false
245
+ _DoSaveAccountStorage()
246
+ Msg(prev_thread)
247
+ Msg(CurrentThread())
248
+ end
249
+ SaveAccountStorageThread = false
250
+ SaveAccountStorageSaving = false
251
+ end)
252
+ end
253
+ end
254
+ end
CommonLua/AlignedObj.lua ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DefineClass.AlignedObj = {
2
+ __parents = { "EditorCallbackObject" },
3
+ flags = { cfAlignObj = true },
4
+ }
5
+
6
+ -- gets pos and angle from the object if not passed; this base method does not implement this and should not be called
7
+ ---
8
+ --- Aligns the object to a specific position and angle.
9
+ --- This base implementation does nothing and should not be called directly.
10
+ ---
11
+ --- @param pos table|nil The position to align the object to. If not provided, the object's current position is used.
12
+ --- @param angle number|nil The angle to align the object to. If not provided, the object's current angle is used.
13
+ ---
14
+ function AlignedObj:AlignObj(pos, angle)
15
+ assert(not pos and not angle)
16
+ end
17
+
18
+ ---
19
+ --- Called when the object is placed in the editor.
20
+ --- Aligns the object to its current position and angle.
21
+ ---
22
+ function AlignedObj:EditorCallbackPlace()
23
+ self:AlignObj()
24
+ end
25
+
26
+ ---
27
+ --- Called when the object is moved in the editor.
28
+ --- Aligns the object to its current position and angle.
29
+ ---
30
+ function AlignedObj:EditorCallbackMove()
31
+ self:AlignObj()
32
+ end
33
+
34
+ ---
35
+ --- Called when the object is rotated in the editor.
36
+ --- Aligns the object to its current position and angle.
37
+ ---
38
+ function AlignedObj:EditorCallbackRotate()
39
+ self:AlignObj()
40
+ end
41
+
42
+ ---
43
+ --- Called when the object is scaled in the editor.
44
+ --- Aligns the object to its current position and angle.
45
+ ---
46
+ function AlignedObj:EditorCallbackScale()
47
+ self:AlignObj()
48
+ end
49
+
50
+ ---
51
+ --- Aligns a HexAlignedObj object to a specific position and angle.
52
+ ---
53
+ --- @param pos table|nil The position to align the object to. If not provided, the object's current position is used.
54
+ --- @param angle number|nil The angle to align the object to. If not provided, the object's current angle is used.
55
+ ---
56
+ function HexAlignedObj:AlignObj(pos, angle)
57
+ self:SetPosAngle(HexGetNearestCenter(pos or self:GetPos()), angle or self:GetAngle())
58
+ end
59
+ if const.HexWidth then
60
+ DefineClass("HexAlignedObj", "AlignedObj")
61
+
62
+ function HexAlignedObj:AlignObj(pos, angle)
63
+ self:SetPosAngle(HexGetNearestCenter(pos or self:GetPos()), angle or self:GetAngle())
64
+ end
65
+ end
66
+
67
+ ---
68
+ --- Realigns all AlignedObj objects in the current map when a new map is loaded.
69
+ ---
70
+ --- This function is called when a new map is loaded. It suspends pass edits, then iterates through all AlignedObj objects in the map that are not parented to another object. For each object, it calls the AlignObj() method to realign the object to its current position and angle. If the object's position or angle has changed, a counter is incremented. After all objects have been realigned, the pass edits are resumed and a message is printed indicating how many objects were realigned.
71
+ ---
72
+ --- This function is only defined when the Platform.developer flag is true, indicating that the game is running in a development environment.
73
+ ---
74
+ if Platform.developer then
75
+ function OnMsg.NewMapLoaded()
76
+ local aligned = 0
77
+ SuspendPassEdits("AlignedObjWarning")
78
+ MapForEach("map", "AlignedObj", function(obj)
79
+ if obj:GetParent() then
80
+ return
81
+ end
82
+ local x1, y1, z1 = obj:GetPosXYZ()
83
+ local a1 = obj:GetAngle()
84
+ obj:AlignObj()
85
+ local x2, y2, z2 = obj:GetPosXYZ()
86
+ local a2 = obj:GetAngle()
87
+ if x1 ~= x2 or y1 ~= y2 or z1 ~= z2 or a1 ~= a2 then
88
+ aligned = aligned + 1
89
+ end
90
+ end)
91
+ ResumePassEdits("AlignedObjWarning")
92
+ if aligned > 0 then
93
+ print(aligned, "object were re-aligned - Save the map!")
94
+ end
95
+ end
96
+ end
CommonLua/ArtTest.lua ADDED
@@ -0,0 +1,448 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ --- Runs a real-time thread to change the map for the "ArtTest" context.
3
+ ---
4
+ --- This function is likely used for debugging or testing purposes, to quickly
5
+ --- switch between different map configurations in the "ArtTest" context.
6
+ ---
7
+ function bat() -- debug function
8
+ CreateRealTimeThread(ChangeMap, "ArtTest")
9
+ end
10
+
11
+ ---
12
+ --- Gets the lowercase version of the project name.
13
+ ---
14
+ --- @return string The lowercase project name.
15
+ ---
16
+ local project_name = string.lower(const.ProjectName)
17
+
18
+ ---
19
+ --- Defines paths to various asset directories and files used in the ArtTest context.
20
+ ---
21
+ --- @class path
22
+ --- @field assets table A table containing paths to asset directories.
23
+ --- @field assets.root string The root directory for assets.
24
+ --- @field assets.entities string The directory for entity assets.
25
+ --- @field assets.art_producer_lua string The path to the CurrentArtProducer.lua file.
26
+ --- @field assets.exporter string The directory for the HGExporter.
27
+ --- @field assets.entity_producers_lua string The path to the EntityProducers.lua file.
28
+ --- @field max table A table containing paths to 3DS Max related files and directories.
29
+ --- @field max.root string The root directory for 3DS Max scripts.
30
+ --- @field max.startup string The path to the HGExporterUtility startup script.
31
+ --- @field max.exporter string The directory for the HGExporter scripts.
32
+ --- @field max.exporter_startup string The path to the HGExporterUtility startup script in the exporter directory.
33
+ --- @field max.art_producer_ms string The path to the CurrentArtProducer.ms file in the exporter directory.
34
+ --- @field max.grannyexp_ini string The path to the grannyexp.ini file in the exporter directory.
35
+ ---
36
+ local path = {}
37
+ path.assets = {}
38
+ path.assets.root = GetExecDirectory() .. "Assets/"
39
+ path.assets.entities = path.assets.root .. "Bin/Common/Entities/"
40
+ path.assets.art_producer_lua = path.assets.root .. "CurrentArtProducer.lua"
41
+ path.assets.exporter = path.assets.root .. "HGExporter/"
42
+ path.assets.entity_producers_lua = path.assets.root .. "Spec/EntityProducers.lua"
43
+ path.max = {}
44
+ path.max.root = "AppData/../../Local/Autodesk/3dsmax/2019 - 64bit/ENU/scripts/"
45
+ path.max.startup = path.max.root .. "startup/HGExporterUtility_" .. project_name .. ".ms"
46
+ path.max.exporter = path.max.root .. "HGExporter_" .. project_name .. "/"
47
+ path.max.exporter_startup = path.max.exporter .. "Startup/HGExporterUtility.ms"
48
+ path.max.art_producer_ms = path.max.exporter .. "CurrentArtProducer.ms"
49
+ path.max.grannyexp_ini = path.max.exporter .. "grannyexp.ini"
50
+
51
+ local atprint = CreatePrint({
52
+ "ArtPreview",
53
+ format = "printf",
54
+ })
55
+
56
+ ArtTest = { }
57
+
58
+ ---
59
+ --- Opens a dialog to allow the user to choose a new art producer, then sets the new producer and updates the corresponding Lua and Max Script files.
60
+ ---
61
+ --- @function ArtTest.OpenChangeProducerDialog
62
+ function ArtTest.OpenChangeProducerDialog()
63
+ local producers = table.icopy(ArtSpecConfig.EntityProducers)
64
+ table.insert(producers, 1, "Any")
65
+ local new_producer = WaitListChoice(terminal.desktop, producers, "Choose art producer:", 1)
66
+ ArtTest.SetProducer(new_producer or "Any")
67
+ CreateRealTimeThread(ChangeMap, "ArtTest")
68
+ end
69
+
70
+ ---
71
+ --- Sets the new art producer and updates the corresponding Lua and Max Script files.
72
+ ---
73
+ --- @param new_producer string The new art producer to set.
74
+ ---
75
+ function ArtTest.SetProducer(new_producer)
76
+ if not new_producer then
77
+ return
78
+ end
79
+
80
+ atprint("Setting new art producer %s", new_producer)
81
+
82
+ -- set producer
83
+ rawset(_G, "g_ArtTestProducer", new_producer)
84
+
85
+ AsyncCreatePath(path.assets.root)
86
+
87
+ -- write to Lua file for the game
88
+ local lua_content = string.format("return \"%s\"", new_producer)
89
+ AsyncStringToFile(path.assets.art_producer_lua, lua_content)
90
+
91
+ -- write to Max Script file for the exporter
92
+ local ms_content = string.format("global g_ArtTestProducer = \"%s\"", new_producer)
93
+ AsyncStringToFile(path.max.art_producer_ms, ms_content)
94
+
95
+ local os_path_assets = ConvertToOSPath(path.assets.root)
96
+ if string.ends_with(os_path_assets, "\\") then
97
+ os_path_assets = string.sub(os_path_assets, 1, #os_path_assets - 1)
98
+ end
99
+
100
+ ArtTest.InstallMaxExporter()
101
+ end
102
+
103
+ ---
104
+ --- Sets the new art producer and updates the corresponding Autodesk 3DS Max exporter configuration.
105
+ ---
106
+ --- This function is responsible for configuring the Autodesk 3DS Max exporter by updating the `grannyexp.ini` file with the correct assets path. It first checks if the game is run with the `-globalappdirs` command line parameter, which is required for the exporter to function properly. If the parameter is not present, it prints a warning message and returns.
107
+ ---
108
+ --- The function then retrieves the OS-specific path for the assets root directory and checks if it ends with a backslash. If so, it removes the trailing backslash.
109
+ ---
110
+ --- Next, the function checks if the `grannyexp.ini` file exists. If it does, it reads the file and updates the `assetsPath` setting with the correct assets path. If the file does not exist, it creates a new `grannyexp.ini` file with the assets path.
111
+ ---
112
+ --- @param new_producer string The new art producer to set.
113
+ ---
114
+ function ArtTest.SetProducer_3DSMax(new_producer)
115
+ if not new_producer then
116
+ return
117
+ end
118
+
119
+ local globalappdirs = string.match(GetAppCmdLine() or "", "-globalappdirs")
120
+ if not globalappdirs then
121
+ atprint(
122
+ "Please run the game with the -globalappdirs command line parameter to install/update the Autodesk 3DS Max exporter")
123
+ return
124
+ end
125
+
126
+ local os_path_assets = ConvertToOSPath(path.assets.root)
127
+ if string.ends_with(os_path_assets, "\\") then
128
+ os_path_assets = string.sub(os_path_assets, 1, #os_path_assets - 1)
129
+ end
130
+
131
+ if io.exists(path.max.grannyexp_ini) then -- TODO proper ini handling
132
+ local err, ini = AsyncFileToString(path.max.grannyexp_ini)
133
+ local first, last = string.find(ini, "assetsPath=.*\n")
134
+ if first and last and first <= last then
135
+ ini = string.format("%sassetsPath=%s%s", string.sub(ini, 1, first), os_path_assets,
136
+ string.sub(ini, last - 1))
137
+ else
138
+ ini = string.format("%s\n[Directories]\nassetsPath=%s", ini, os_path_assets)
139
+ end
140
+ else
141
+ local ini = string.format("[Directories]\nassetsPath=%s", os_path_assets)
142
+ AsyncStringToFile(path.max.grannyexp_ini, ini)
143
+ end
144
+ end
145
+
146
+ ---
147
+ --- Installs the Autodesk 3DS Max exporter by creating the necessary folder structure and copying the exporter files.
148
+ ---
149
+ --- This function first checks if the game is run with the `-globalappdirs` command line parameter, which is required for the exporter to function properly. If the parameter is not present, it prints a warning message and returns.
150
+ ---
151
+ --- The function then creates the folder structure where the exported entities will be stored, including the `Bin/`, `Bin/Common/`, and other subfolders.
152
+ ---
153
+ --- Next, the function copies the exporter folder structure from the `path.assets.exporter` directory to the `path.max.exporter` directory. It skips any folders or files that contain `.svn`.
154
+ ---
155
+ --- Finally, the function copies the exporter startup file from `path.max.exporter_startup` to `path.max.startup`, and calls `ArtTest.SetProducer_3DSMax()` with the current art producer.
156
+ ---
157
+ --- @return nil
158
+ function ArtTest.InstallMaxExporter()
159
+ local globalappdirs = string.match(GetAppCmdLine() or "", "-globalappdirs")
160
+ if not globalappdirs then
161
+ atprint(
162
+ "Please run the game with the -globalappdirs command line parameter to install/update the Autodesk 3DS Max exporter")
163
+ return
164
+ end
165
+
166
+ -- crate assets folder structure (where entities will be exported)
167
+ local structure = {"Bin/", "Bin/Common/", "Bin/Common/Animations", "Bin/Common/Entities", "Bin/Common/Mapping",
168
+ "Bin/Common/Materials", "Bin/Common/Meshes", "Bin/Common/TexturesMeta", "Bin/win32/", "Bin/win32/Textures",
169
+ "Bin/win32/Fallbacks", "Bin/win32/Fallbacks/Textures"}
170
+ for i, subpath in ipairs(structure) do
171
+ local full_path = path.assets.root .. subpath
172
+ local os_path = ConvertToOSPath(full_path)
173
+ local err = AsyncCreatePath(os_path)
174
+ if err then
175
+ atprint("Failed creating exporter target folder structure - %s", err)
176
+ return
177
+ end
178
+ end
179
+
180
+ -- copy exporter folder structure
181
+ local err, folders = AsyncListFiles(path.assets.exporter, "*", "recursive,relative,folders")
182
+ if err then
183
+ atprint("Failed listing Autodesk 3DS Max exporter folder structure - %s", err)
184
+ return err
185
+ end
186
+
187
+ local os_path = ConvertToOSPath(path.max.exporter)
188
+ local err = AsyncCreatePath(os_path)
189
+ if err then
190
+ atprint("Failed copying Autodesk 3DS Max exporter folder structure - %s", err)
191
+ return err
192
+ end
193
+
194
+ for _, folder in ipairs(folders) do
195
+ if not string.find(folder, ".svn") then
196
+ local os_path = ConvertToOSPath(path.max.exporter .. folder)
197
+ local err = AsyncCreatePath(os_path)
198
+ if err then
199
+ atprint("Failed copying Autodesk 3DS Max exporter folder structure - %s", err)
200
+ return err
201
+ end
202
+ end
203
+ end
204
+
205
+ -- copy exporter files
206
+ local err, files = AsyncListFiles(path.assets.exporter, "*", "recursive,relative")
207
+ if err then
208
+ atprint("Failed listing Autodesk 3DS Max exporter files - %s", err)
209
+ return err
210
+ end
211
+
212
+ for _, file in ipairs(files) do
213
+ if not string.find(file, ".svn") then
214
+ local os_dest_path = ConvertToOSPath(path.max.exporter .. file)
215
+ local err = AsyncCopyFile(path.assets.exporter .. file, os_dest_path, "raw")
216
+ if err then
217
+ atprint("Failed copying Autodesk 3DS Max exporter files - %s", err)
218
+ return err
219
+ end
220
+ end
221
+ end
222
+
223
+ -- copy exporter startup file
224
+ local err = AsyncCopyFile(path.max.exporter_startup, path.max.startup)
225
+ if err then
226
+ atprint("Failed copying Autodesk 3DS Max exporter startup file - %s", err)
227
+ return err
228
+ end
229
+
230
+ ArtTest.SetProducer_3DSMax(rawget(_G, "g_ArtTestProducer"))
231
+
232
+ atprint("Installed Autodesk 3DS Max exporter. Restart Autodesk 3DS Max.")
233
+ end
234
+ ---
235
+ --- Starts the art preview mode.
236
+ ---
237
+ --- This function is responsible for initializing the art preview mode. It performs the following steps:
238
+ --- 1. Checks if an art producer script exists and loads it.
239
+ --- 2. If no art producer is found, it opens a dialog to allow the user to select an art producer.
240
+ --- 3. Sets the selected art producer.
241
+ --- 4. Loads external entities.
242
+ --- 5. Sets up the map for the art preview.
243
+ ---
244
+ --- @function ArtTest.Start
245
+ --- @return nil
246
+
247
+ function ArtTest.Start()
248
+ atprint("Starting art preview mode")
249
+
250
+ if io.exists(path.assets.art_producer_lua) then
251
+ local producer = dofile(path.assets.art_producer_lua)
252
+ if type(producer) == "string" then
253
+ rawset(_G, "g_ArtTestProducer", producer)
254
+ end
255
+ end
256
+
257
+ local art_producer = rawget(_G, "g_ArtTestProducer")
258
+ local no_art_producer = (art_producer == nil)
259
+ if no_art_producer then
260
+ ArtTest.OpenChangeProducerDialog()
261
+ return
262
+ else
263
+ -- updates all files
264
+ ArtTest.SetProducer(art_producer)
265
+ atprint("Selected art producer %s", art_producer)
266
+ end
267
+
268
+ ArtTest.LoadExternalEntities()
269
+ ArtTest.SetUpMap()
270
+ end
271
+
272
+ local mounted
273
+ ---
274
+ --- Loads all external entities required for the art preview mode.
275
+ ---
276
+ --- This function is responsible for loading all the necessary entities, meshes, animations, materials, and textures for the art preview mode. It performs the following steps:
277
+ --- 1. Mounts the required folders to make the assets accessible.
278
+ --- 2. Enumerates all the entity files in the `path.assets.entities` directory.
279
+ --- 3. Loads each entity file using `DelayedLoadEntity`.
280
+ --- 4. Opens a loading screen and forces a reload of the bin assets and DLC assets.
281
+ --- 5. Waits for the bin assets to finish loading and then closes the loading screen.
282
+ --- 6. Waits for any delayed entity loads to complete.
283
+ --- 7. Reloads the Lua script.
284
+ ---
285
+ --- @function ArtTest.LoadExternalEntities
286
+ --- @return nil
287
+ function ArtTest.LoadExternalEntities()
288
+ if not mounted then
289
+ mounted = true
290
+
291
+ MountFolder(path.assets.root .. "Bin/Common/Entities/Meshes/", path.assets.root .. "Bin/Common/Meshes/")
292
+ MountFolder(path.assets.root .. "Bin/Common/Entities/Animations/", path.assets.root .. "Bin/Common/Animations/")
293
+ MountFolder(path.assets.root .. "Bin/Common/Entities/Materials/", path.assets.root .. "Bin/Common/Materials/")
294
+ MountFolder(path.assets.root .. "Bin/Common/Entities/Mapping/", path.assets.root .. "Bin/Common/Mapping/")
295
+ MountFolder(path.assets.root .. "Bin/Common/Entities/Textures/", path.assets.root .. "Bin/win32/Textures/")
296
+ atprint("Mounted all entity folders")
297
+ end
298
+
299
+ local err, all_entities = AsyncListFiles(path.assets.entities, "*.ent")
300
+ if err then
301
+ atprint("Failed to enumerate entities - %s", err)
302
+ return
303
+ end
304
+ if not all_entities or #all_entities == 0 then
305
+ atprint("No entities to load")
306
+ return
307
+ end
308
+
309
+ for i, ent_file in ipairs(all_entities) do
310
+ DelayedLoadEntity(false, false, ent_file)
311
+ end
312
+ atprint("Will load %d entities", #all_entities)
313
+
314
+ LoadingScreenOpen("idArtTestLoadEntities", "ArtTestLoadEntities")
315
+ local old_render_mode = GetRenderMode()
316
+ WaitRenderMode("ui")
317
+ ForceReloadBinAssets()
318
+ DlcReloadAssets(DlcDefinitions)
319
+ -- actually reload the assets
320
+ LoadBinAssets(CurrentMapFolder)
321
+ -- wait & unmount
322
+ WaitNextFrame(2)
323
+ while AreBinAssetsLoading() do
324
+ Sleep(1)
325
+ end
326
+ WaitRenderMode(old_render_mode)
327
+ LoadingScreenClose("idArtTestLoadEntities", "ArtTestLoadEntities")
328
+ WaitDelayedLoadEntities()
329
+ -- ReloadClassEntities()
330
+ ReloadLua()
331
+ atprint("Reloaded all entities")
332
+ end
333
+
334
+ --- Sets up the map for the ArtTest module.
335
+ ---
336
+ --- This function performs the following tasks:
337
+ --- - Activates the cameraMax camera
338
+ --- - Prints a message to the console indicating the camera has been set up
339
+ --- - Calls the ArtTest.PlacePreviewObjects() function to place preview objects on the map
340
+ --- - If any preview objects were placed, it moves the camera to view the first preview object
341
+ function ArtTest.SetUpMap()
342
+ cameraMax.Activate(1)
343
+ atprint("Camera set up")
344
+
345
+ local preview_objs = ArtTest.PlacePreviewObjects()
346
+
347
+ if preview_objs and next(preview_objs) then
348
+ ViewPos(preview_objs[1]:GetVisualPos())
349
+ atprint("Showing first preview object")
350
+ end
351
+ end
352
+
353
+ --- Returns a list of object classes to preview in the ArtTest module.
354
+ ---
355
+ --- The list of object classes is determined by the current producer set in the
356
+ --- `g_ArtTestProducer` global variable. If `g_ArtTestProducer` is set to "Any",
357
+ --- then all object classes that have an entry in the `entity_producers_lua` file
358
+ --- will be included in the list.
359
+ ---
360
+ --- @return table A list of object class names to preview.
361
+ function ArtTest.GetObjectClassesToPreview()
362
+ local current_producer = rawget(_G, "g_ArtTestProducer") or "Any"
363
+ local result = {}
364
+
365
+ if io.exists(path.assets.entity_producers_lua) then
366
+ local entity_producers = dofile(path.assets.entity_producers_lua)
367
+ for entity_id, produced_by in pairs(entity_producers) do
368
+ if (current_producer == "Any" or produced_by == current_producer) and g_Classes[entity_id] then
369
+ table.insert(result, entity_id)
370
+ end
371
+ end
372
+ end
373
+
374
+ return result
375
+ end
376
+
377
+ local spacing = 10 * guim
378
+ --- Places preview objects on the map for the ArtTest module.
379
+ ---
380
+ --- This function places preview objects on the map for each object class that is
381
+ --- eligible to be previewed in the ArtTest module. The list of eligible object
382
+ --- classes is determined by the current producer set in the `g_ArtTestProducer`
383
+ --- global variable.
384
+ ---
385
+ --- For each eligible object class, the function places one preview object for
386
+ --- each valid state of the object. The preview objects are placed in a grid
387
+ --- pattern, with a spacing of `spacing` units between each object.
388
+ ---
389
+ --- The function returns a list of all the preview objects that were placed.
390
+ ---
391
+ --- @param classes (optional) A list of object class names to preview. If not
392
+ --- provided, the function will use the list returned by
393
+ --- `ArtTest.GetObjectClassesToPreview()`.
394
+ --- @return table A list of preview objects that were placed.
395
+ function ArtTest.PlacePreviewObjects(classes)
396
+ local current_producer = rawget(_G, "g_ArtTestProducer") or "Any"
397
+
398
+ local y = 0
399
+ local result = {}
400
+
401
+ local classes = classes or ArtTest.GetObjectClassesToPreview()
402
+ if not classes or #classes == 0 then
403
+ atprint("No preview objects to place")
404
+ return
405
+ end
406
+
407
+ for i, classname in ipairs(classes) do
408
+ local class = g_Classes[classname]
409
+ local entity = class:GetEntity()
410
+ local entity_bbox = GetEntityBBox(entity)
411
+ local _, radius = entity_bbox:GetBSphere()
412
+
413
+ local x = 0
414
+ local half_spacing = radius + spacing
415
+
416
+ for i, state in pairs(EnumValidStates(entity)) do
417
+ x, y = x + half_spacing, y + half_spacing
418
+ local pos = point(x, y)
419
+ local preview_pos = point(x, y, terrain.GetHeight(x, y))
420
+ x, y = x + half_spacing, y + half_spacing
421
+
422
+ local preview_obj = PlaceObject(classname)
423
+ preview_obj:SetPos(preview_pos)
424
+ preview_obj:SetState(state)
425
+ table.insert(result, preview_obj)
426
+
427
+ local text_obj = PlaceObject("Text")
428
+ text_obj:SetDepthTest(false)
429
+ text_obj:SetText(entity .. "\n" .. GetStateName(state))
430
+ text_obj:SetPos(pos + point(radius, radius))
431
+ end
432
+ end
433
+
434
+ atprint("Placed %d preview objects", #result)
435
+ return result
436
+ end
437
+
438
+ ----
439
+
440
+ function OnMsg.ChangeMapDone()
441
+ if CurrentMap == "ArtTest" then
442
+ CreateRealTimeThread(ArtTest.Start)
443
+ end
444
+ end
445
+
446
+ if FirstLoad and config.ArtTest then
447
+ CreateRealTimeThread(ChangeMap, "ArtTest")
448
+ end
CommonLua/AtmosphericParticles.lua ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ --- Toggles whether atmospheric particles are hidden or not.
3
+ ---
4
+ --- This variable is set to `false` on first load, indicating that atmospheric particles should be visible.
5
+ ---
6
+ --- @field g_AtmosphericParticlesHidden boolean
7
+ --- @within CommonLua.AtmosphericParticles
8
+ MapVar("g_AtmosphericParticlesHidden", false)
9
+ if FirstLoad then
10
+ g_AtmosphericParticlesThread = false
11
+ g_AtmosphericParticles = false
12
+ g_AtmosphericParticlesPos = false
13
+ end
14
+ function OnMsg.DoneMap()
15
+ g_AtmosphericParticlesThread = false
16
+ g_AtmosphericParticles = false
17
+ g_AtmosphericParticlesPos = false
18
+ end
19
+
20
+ --- Applies atmospheric particles to the scene.
21
+ ---
22
+ --- This function sets up the atmospheric particles by creating a thread to update their positions, and initializing the particle objects and their positions.
23
+ ---
24
+ --- If `mapdata.AtmosphericParticles` is an empty string, this function will return without doing anything.
25
+ ---
26
+ --- @function AtmosphericParticlesApply
27
+ --- @within CommonLua.AtmosphericParticles
28
+ function AtmosphericParticlesApply()
29
+ if g_AtmosphericParticlesThread then
30
+ DeleteThread(g_AtmosphericParticlesThread)
31
+ g_AtmosphericParticlesThread = false
32
+ end
33
+ DoneObjects(g_AtmosphericParticles)
34
+ g_AtmosphericParticles = false
35
+ g_AtmosphericParticlesPos = false
36
+ if mapdata.AtmosphericParticles == "" then
37
+ return
38
+ end
39
+ g_AtmosphericParticles = {}
40
+ g_AtmosphericParticlesPos = {}
41
+ g_AtmosphericParticlesThread = CreateGameTimeThread(function()
42
+ while true do
43
+ AtmosphericParticlesUpdate()
44
+ Sleep(100)
45
+ end
46
+ end)
47
+ end
48
+
49
+ --- Updates the positions of the atmospheric particles.
50
+ ---
51
+ --- This function is responsible for managing the atmospheric particles in the scene. It determines the number of particles to display based on whether they are currently hidden or not, and the number of views. It then creates, destroys, and updates the positions of the particles as needed.
52
+ ---
53
+ --- If `g_AtmosphericParticlesPos` is `false`, this function will return without doing anything.
54
+ ---
55
+ --- If the distance between the two camera positions is less than 10 game units, this function will average the positions of the two cameras and only display one particle.
56
+ ---
57
+ --- This function is called repeatedly in a game time thread created by `AtmosphericParticlesApply()`.
58
+ ---
59
+ --- @function AtmosphericParticlesUpdate
60
+ --- @within CommonLua.AtmosphericParticles
61
+ function AtmosphericParticlesUpdate()
62
+ local part_pos = g_AtmosphericParticlesPos
63
+ if not part_pos then
64
+ return
65
+ end
66
+ -- see how many particles we need, depending on whether they currently hidden,
67
+ -- number of views and how close are the two cameras in case of two views
68
+ local part_number = g_AtmosphericParticlesHidden and 0 or camera.GetViewCount()
69
+ for view = 1, part_number do
70
+ part_pos[view] = camera.GetEye(view) + SetLen(camera.GetDirection(view), 7 * guim)
71
+ end
72
+ if part_number == 2 and part_pos[1]:Dist(part_pos[2]) < 10 * guim then
73
+ part_pos[1] = (part_pos[1] + part_pos[2]) / 2
74
+ part_number = 1
75
+ end
76
+
77
+ -- create/destroy particles as needed and update positions
78
+ local part = g_AtmosphericParticles
79
+ for i = 1, Max(#part, part_number) do
80
+ if not IsValid(part[i]) then -- the particles coule be destroyed by code like NetSetGameState()
81
+ part[i] = PlaceParticles(mapdata.AtmosphericParticles)
82
+ end
83
+ if i > part_number then
84
+ if g_AtmosphericParticlesHidden then
85
+ DoneObject(part[i])
86
+ else
87
+ StopParticles(part[i])
88
+ end
89
+ part[i] = nil
90
+ elseif terrain.IsPointInBounds(part_pos[i]) and part_pos[i]:z() < 2000000 then
91
+ part[i]:SetPos(part_pos[i])
92
+ end
93
+ end
94
+ end
95
+
96
+ --- Sets whether the atmospheric particles are hidden or not.
97
+ ---
98
+ --- @function AtmosphericParticlesSetHidden
99
+ --- @within CommonLua.AtmosphericParticles
100
+ --- @param hidden boolean Whether the atmospheric particles should be hidden or not.
101
+ function AtmosphericParticlesSetHidden(hidden)
102
+ g_AtmosphericParticlesHidden = hidden
103
+ end
104
+
105
+ function OnMsg.SceneStarted(scene)
106
+ if scene.hide_atmospheric_particles then
107
+ AtmosphericParticlesSetHidden(true)
108
+ end
109
+ end
110
+
111
+ function OnMsg.SceneStopped(scene)
112
+ if scene.hide_atmospheric_particles then
113
+ AtmosphericParticlesSetHidden(false)
114
+ end
115
+ end
CommonLua/BaseClasses.lua ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --- Stores a boolean value indicating whether spawned objects in the game are currently hidden or not.
2
+ --- This variable is used by the `HideSpawnedObjects` function to track the visibility state of spawned objects.
3
+ --- When set to `true`, the `HideSpawnedObjects` function will hide all spawned objects. When set to `false`, it will show all hidden objects.
4
+ MapVar("HiddenSpawnedObjects", false)
5
+ ---
6
+ --- Hides or shows all spawned objects in the game.
7
+ ---
8
+ --- When `hide` is `true`, this function will hide all spawned objects by clearing their `efVisible` enum flag.
9
+ --- When `hide` is `false`, this function will show all previously hidden spawned objects by setting their `efVisible` enum flag.
10
+ ---
11
+ --- This function uses the `HiddenSpawnedObjects` table to keep track of all objects that have been hidden. When showing objects, it iterates through this table to restore their visibility.
12
+ ---
13
+ --- @param hide boolean Whether to hide or show the spawned objects
14
+ ---
15
+ local function HideSpawnedObjects(hide)
16
+ if not hide == not HiddenSpawnedObjects then
17
+ return
18
+ end
19
+
20
+ SuspendPassEdits("HideSpawnedObjects")
21
+
22
+ if hide then
23
+ HiddenSpawnedObjects = setmetatable({}, weak_values_meta)
24
+ for template, obj in pairs(TemplateSpawn) do
25
+ if IsValid(obj) and obj:GetEnumFlags(const.efVisible) ~= 0 then
26
+ obj:ClearEnumFlags(const.efVisible)
27
+ HiddenSpawnedObjects[#HiddenSpawnedObjects + 1] = obj
28
+ end
29
+ end
30
+ elseif HiddenSpawnedObjects then
31
+ for i = 1, #HiddenSpawnedObjects do
32
+ local obj = HiddenSpawnedObjects[i]
33
+ if IsValid(obj) then
34
+ obj:SetEnumFlags(const.efVisible)
35
+ end
36
+ end
37
+ HiddenSpawnedObjects = false
38
+ end
39
+
40
+ ResumePassEdits("HideSpawnedObjects")
41
+ end
42
+
43
+ ---
44
+ --- Toggles the visibility of all spawned objects in the game.
45
+ ---
46
+ --- This function calls the `HideSpawnedObjects` function, passing the opposite of the current `HiddenSpawnedObjects` value. This will either hide all spawned objects if they are currently visible, or show all previously hidden objects.
47
+ ---
48
+ --- @function ToggleSpawnedObjects
49
+ --- @return nil
50
+ function ToggleSpawnedObjects()
51
+ HideSpawnedObjects(not HiddenSpawnedObjects)
52
+ end
53
+ OnMsg.GameEnterEditor = function()
54
+ HideSpawnedObjects(true)
55
+ end
56
+ OnMsg.GameExitEditor = function()
57
+ HideSpawnedObjects(false)
58
+ end
59
+
60
+ ----
61
+
62
+ local function SortByItems(self)
63
+ return self:GetSortItems()
64
+ end
65
+
66
+ ---
67
+ --- Defines a base class for objects that can be sorted.
68
+ ---
69
+ --- The `SortedBy` class provides a set of properties and methods for sorting a collection of objects. It includes a `SortBy` property that allows the user to specify one or more sort keys, and a `Sort()` method that sorts the collection based on those keys.
70
+ ---
71
+ --- The `SortByItems` function is used to provide a list of available sort keys for the `SortBy` property.
72
+ ---
73
+ --- @class SortedBy
74
+ --- @field SortBy table|boolean The sort keys to use when sorting the collection. Can be a table of key-value pairs, where the key is the sort key and the value is the sort direction (true for ascending, false for descending). Can also be set to false to disable sorting.
75
+ --- @field SortBy.key string The name of the sort key.
76
+ --- @field SortBy.dir boolean The sort direction (true for ascending, false for descending).
77
+ --- @field SortBy.items function A function that returns a list of available sort keys.
78
+ --- @field SortBy.max_items_in_set integer The maximum number of sort keys that can be selected.
79
+ --- @field SortBy.border integer The border width of the property editor.
80
+ --- @field SortBy.three_state boolean Whether the property editor should have a three-state (true/false/nil) value.
81
+ DefineClass.SortedBy = {__parents={"PropertyObject"},
82
+ properties={{id="SortBy", editor="set", default=false, items=SortByItems, max_items_in_set=1, border=2,
83
+ three_state=true}}}
84
+
85
+ ---
86
+ --- Returns a list of available sort keys for the `SortBy` property.
87
+ ---
88
+ --- This function is used to provide a list of available sort keys that can be selected for the `SortBy` property of the `SortedBy` class. The implementation of this function is left empty, as the specific sort keys available will depend on the implementation of the `SortedBy` class.
89
+ ---
90
+ --- @function SortedBy:GetSortItems
91
+ --- @return table A table of available sort keys.
92
+ function SortedBy:GetSortItems()
93
+ return {}
94
+ end
95
+
96
+ ---
97
+ --- Sets the sort keys for the collection and sorts the collection based on those keys.
98
+ ---
99
+ --- @function SortedBy:SetSortBy
100
+ --- @param sort_by table|boolean The new sort keys to use. Can be a table of key-value pairs, where the key is the sort key and the value is the sort direction (true for ascending, false for descending). Can also be set to false to disable sorting.
101
+ --- @return nil
102
+ function SortedBy:SetSortBy(sort_by)
103
+ self.SortBy = sort_by
104
+ self:Sort()
105
+ end
106
+
107
+ ---
108
+ --- Resolves the sort key and sort direction from the `SortBy` property.
109
+ ---
110
+ --- This function iterates over the `SortBy` property and returns the first key-value pair, which represents the sort key and sort direction.
111
+ ---
112
+ --- @function SortedBy:ResolveSortKey
113
+ --- @return string, boolean The sort key and sort direction.
114
+ function SortedBy:ResolveSortKey()
115
+ for key, value in pairs(self.SortBy) do
116
+ return key, value
117
+ end
118
+ end
119
+
120
+ ---
121
+ --- Compares two objects in the collection based on the specified sort key.
122
+ ---
123
+ --- This function is used to compare two objects in the collection when sorting the collection based on the `SortBy` property. The comparison is performed using the specified sort key.
124
+ ---
125
+ --- @param c1 any The first object to compare.
126
+ --- @param c2 any The second object to compare.
127
+ --- @param sort_by string The sort key to use for the comparison.
128
+ --- @return boolean True if the first object should come before the second object in the sorted collection, false otherwise.
129
+ function SortedBy:Cmp(c1, c2, sort_by)
130
+ end
131
+
132
+ ---
133
+ --- Sorts the collection based on the specified sort keys.
134
+ ---
135
+ --- This function first resolves the sort key and sort direction from the `SortBy` property. It then sorts the collection using the `table.sort()` function, comparing each pair of objects using the `Cmp()` function and the resolved sort key. If the sort direction is descending, the function then reverses the order of the sorted collection.
136
+ ---
137
+ --- @function SortedBy:Sort
138
+ --- @return nil
139
+ function SortedBy:Sort()
140
+ local key, dir = self:ResolveSortKey()
141
+ table.sort(self, function(c1, c2)
142
+ return self:Cmp(c1, c2, key)
143
+ end)
144
+ if not dir then
145
+ table.reverse(self)
146
+ end
147
+ end
CommonLua/Billboards.lua ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ --- Sets up the rendering environment for capturing billboards.
3
+ ---
4
+ --- This function performs the following steps:
5
+ --- - Changes the current map to an empty map
6
+ --- - Waits for 5 frames
7
+ --- - Activates the `cameraMax` camera and sets its viewport, field of view, and locks it
8
+ --- - Changes the video mode to the billboard screenshot capture size
9
+ --- - Configures various rendering settings for billboard capture, such as:
10
+ --- - Setting the object LOD cap to 100
11
+ --- - Disabling terrain rendering
12
+ --- - Disabling auto-exposure
13
+ --- - Disabling subsurface scattering
14
+ --- - Setting the resolution to 100% with SMAA upscaling
15
+ --- - Disabling shadows
16
+ --- - Setting the near and far planes to 100 and 100,000 respectively
17
+ --- - Enabling orthographic projection with a Y scale of 1000
18
+ --- - Deletes the current map and waits for 3 frames
19
+ ---
20
+ function SetupBillboardRendering()
21
+ ChangeMap("__Empty")
22
+ WaitNextFrame(5)
23
+
24
+ cameraMax.Activate(1)
25
+ camera.SetViewport(box(0, 0, 1000000, 1000000))
26
+ camera.SetFovX(83 * 60)
27
+ camera.Lock(1)
28
+
29
+ ChangeVideoMode(hr.BillboardScreenshotCaptureSize, hr.BillboardScreenshotCaptureSize, 0, false, false)
30
+
31
+ table.change(hr, "BillboardCapture",
32
+ {ObjectLODCapMax=100, ObjectLODCapMin=100, RenderBillboards=0, RenderTerrain=0, AutoExposureMode=0,
33
+ EnableSubsurfaceScattering=0, ResolutionPercent=100, ResolutionUpscale="smaa", MaxFps=0, Shadowmap=0,
34
+ NearZ=100, FarZ=100000, Ortho=1, OrthoYScale=1000})
35
+
36
+ MapDelete("map", nil)
37
+ WaitNextFrame(3)
38
+ end
39
+
40
+ ---
41
+ --- Defines a class for billboard objects.
42
+ ---
43
+ --- This class inherits from `EntityClass` and has the following properties:
44
+ ---
45
+ --- - `flags`: A table of flags, including `efHasBillboard` which indicates that this object has a billboard.
46
+ --- - `ignore_axis_error`: A boolean flag that determines whether to ignore errors related to the object's axis.
47
+ ---
48
+ DefineClass.BillboardObject = {__parents={"EntityClass"}, flags={efHasBillboard=true}, ignore_axis_error=false}
49
+ ---
50
+ --- Returns an error message if the billboard object has an invalid axis.
51
+ ---
52
+ --- If the object has a billboard (`efHasBillboard` flag is set) and `ignore_axis_error` is false, this function checks the object's visual axis. If the X or Y axis is non-zero, or the Z axis is non-positive, it returns an error message indicating that the billboard object should have the default axis.
53
+ ---
54
+ --- @return string|nil An error message if the billboard object has an invalid axis, or nil if the axis is valid or `ignore_axis_error` is true.
55
+ function BillboardObject:GetError()
56
+ end
57
+
58
+ function BillboardObject:GetError()
59
+ if self:GetEnumFlags(const.efHasBillboard) ~= 0 and not self.ignore_axis_error then
60
+ local x, y, z = self:GetVisualAxisXYZ()
61
+ if x ~= 0 or y ~= 0 or z <= 0 then
62
+ return "Billboard objects should have default axis"
63
+ end
64
+ end
65
+ end
66
+ ---
67
+ --- Returns a sorted list of all `BillboardObject` classes and their descendants.
68
+ ---
69
+ --- This function traverses the class hierarchy starting from the `BillboardObject` class, and collects all valid entities that are instances of `BillboardObject` or its descendants. The resulting list is sorted by the class name.
70
+ ---
71
+ --- @return table A table of `BillboardObject` class definitions, sorted by class name.
72
+ function BillboardsTree()
73
+ end
74
+
75
+ function BillboardsTree()
76
+ local billboard_classes = {}
77
+ ClassDescendantsList("BillboardObject", function(name, classdef, billboard_classes)
78
+ if IsValidEntity(classdef:GetEntity()) then
79
+ table.insert(billboard_classes, classdef)
80
+ end
81
+ end, billboard_classes)
82
+ table.sortby_field(billboard_classes, "class")
83
+ return billboard_classes
84
+ end
85
+ ---
86
+ --- Bakes a billboard for the selected object in the GED.
87
+ ---
88
+ --- This function resolves the selected object in the GED and then calls `BakeEntityBillboard` to generate a billboard for that object.
89
+ ---
90
+ --- @param ged The GED object.
91
+ function GedBakeBillboard(ged)
92
+ end
93
+
94
+ function GedBakeBillboard(ged)
95
+ local obj = ged:ResolveObj("SelectedObject")
96
+ if not obj then
97
+ return
98
+ end
99
+ BakeEntityBillboard(obj:GetEntity())
100
+ end
101
+ ---
102
+ --- Bakes a billboard for the specified entity.
103
+ ---
104
+ --- This function generates a billboard for the given entity by executing an external command. The command is constructed using the entity's name and executed asynchronously. If an error occurs during the billboard generation, it is printed to the console.
105
+ ---
106
+ --- @param entity string The name of the entity to generate a billboard for.
107
+ function BakeEntityBillboard(entity)
108
+ end
109
+
110
+ function BakeEntityBillboard(entity)
111
+ if not entity then
112
+ return
113
+ end
114
+ local cmd = string.format("cmd /c Build GenerateBillboards --billboard_entity=%s", entity)
115
+ local dir = ConvertToOSPath("svnProject/")
116
+ local err = AsyncExec(cmd, dir, true, true)
117
+ if err then
118
+ print("Failed to create billboard for %s: %s", entity, err)
119
+ end
120
+ end
121
+ ---
122
+ --- Spawns a billboard object at the cursor position, with a random offset.
123
+ ---
124
+ --- This function resolves the selected object in the GED and then places multiple instances of that object in a grid pattern around the cursor position, with a random offset applied to each instance.
125
+ ---
126
+ --- @param ged The GED object.
127
+ function GedSpawnBillboard(ged)
128
+ end
129
+
130
+ function GedSpawnBillboard(ged)
131
+ local obj = ged:ResolveObj("SelectedObject")
132
+ if not obj then
133
+ return
134
+ end
135
+
136
+ local pos = GetTerrainCursorXY(UIL.GetScreenSize() / 2)
137
+ local step = 20 * guim
138
+ SuspendPassEdits("spawn billboards")
139
+ for y = -50, 50 do
140
+ for x = -50, 50 do
141
+ local o = PlaceObject(obj.class)
142
+ local curr_pos = pos + point(x * step + (AsyncRand(21) - 11) * guim, y * step + (AsyncRand(21) - 11) * guim)
143
+ local real_pos = point(curr_pos:x(), curr_pos:y(), terrain.GetHeight(curr_pos:x(), curr_pos:y()))
144
+ o:SetPos(curr_pos)
145
+ end
146
+ end
147
+ ResumePassEdits("spawn billboards")
148
+ end
149
+ ---
150
+ --- Spawns a grid of billboard objects around the cursor position, with a random offset.
151
+ ---
152
+ --- This function resolves the selected object in the GED and then places multiple instances of that object in a grid pattern around the cursor position, with a random offset applied to each instance. This can be used to debug billboard rendering.
153
+ ---
154
+ --- @param ged The GED object.
155
+ function GedDebugBillboards(ged)
156
+ end
157
+
158
+ function GedDebugBillboards(ged)
159
+ hr.BillboardDebug = 1
160
+ hr.BillboardDistanceModifier = 10000
161
+ hr.ObjectLODCapMax = 100
162
+ hr.ObjectLODCapMin = 100
163
+
164
+ local pos = GetTerrainCursorXY(UIL.GetScreenSize() / 2)
165
+ local step = 12 * guim
166
+
167
+ local billboard_entities = {}
168
+ for k, v in ipairs(GetClassAndDescendantsEntities("BillboardObject")) do
169
+ if IsValidEntity(v) then
170
+ billboard_entities[#billboard_entities + 1] = v
171
+ end
172
+ end
173
+
174
+ local i = 1
175
+ for y = -10, 10 do
176
+ for x = -5, 5 do
177
+ local entity = billboard_entities[i]
178
+ if i == #billboard_entities then
179
+ i = 0
180
+ end
181
+ i = i + 1
182
+ local o = PlaceObject(entity)
183
+ local curr_pos = pos + point(x * step * 2, y * step)
184
+ local real_pos = point(curr_pos:x(), curr_pos:y(), terrain.GetHeight(curr_pos:x(), curr_pos:y()))
185
+ o:SetPos(curr_pos)
186
+ end
187
+ end
188
+ end
189
+
190
+ ---
191
+ --- Generates billboards for all billboard objects in the game.
192
+ ---
193
+ --- This function executes an external command to generate billboards for all billboard objects in the game. It uses the `GenerateBillboards` function to create the billboards.
194
+ ---
195
+ --- @param ged The GED object.
196
+ function GedBakeAllBillboards(ged)
197
+ end
198
+
199
+ function GedBakeAllBillboards(ged)
200
+ local cmd = string.format("cmd /c Build GenerateBillboards")
201
+ local dir = ConvertToOSPath("svnProject/")
202
+
203
+ local err = AsyncExec(cmd, dir, true, true)
204
+ if err then
205
+ print("Failed to create billboards!")
206
+ end
207
+ end
208
+ ---
209
+ --- Generates billboards for all billboard objects in the game.
210
+ ---
211
+ --- This function executes an external command to generate billboards for all billboard objects in the game. It uses the `GenerateBillboards` function to create the billboards.
212
+ ---
213
+ --- @param ged The GED object.
214
+ function GedBakeAllBillboards(ged)
215
+ end
216
+
217
+ function GenerateBillboards(specific_entity)
218
+ CreateRealTimeThread(function()
219
+ SetupBillboardRendering()
220
+
221
+ local billboard_entities = {}
222
+ if specific_entity then
223
+ billboard_entities[specific_entity] = true
224
+ else
225
+ ClassDescendantsList("BillboardObject", function(name, classdef, billboard_entities)
226
+ local ent = classdef:GetEntity()
227
+ if IsValidEntity(ent) then
228
+ billboard_entities[ent] = true
229
+ end
230
+ end, billboard_entities)
231
+ end
232
+
233
+ local o = PlaceObject("Shapeshifter")
234
+ o:SetPos(point(0, 0))
235
+
236
+ local OctahedronSize = hr.BillboardScreenshotGridWidth - 1
237
+
238
+ local screenshot_downsample = hr.BillboardScreenshotCaptureSize / hr.BillboardScreenshotSize
239
+ local unneeded_lods
240
+ local power = 1
241
+ for i = 0, 10 do
242
+ if power == screenshot_downsample then
243
+ unneeded_lods = i
244
+ break
245
+ end
246
+ power = power * 2
247
+ end
248
+
249
+ local dir = ConvertToOSPath("svnAssets/BuildCache/win32/Billboards/")
250
+ AsyncCreatePath("svnAssets/BuildCache/win32/Billboards/")
251
+
252
+ for ent, _ in pairs(billboard_entities) do
253
+ hr.MipmapLodBias = unneeded_lods * 1000
254
+
255
+ o:ChangeEntity(ent)
256
+ local bbox = o:GetEntityBBox()
257
+ local bbox_center = bbox:Center()
258
+ local camera_target = o:GetVisualPos() + bbox_center
259
+
260
+ WaitNextFrame(5)
261
+
262
+ local dlc_name = EntitySpecPresets[ent].save_in
263
+ if dlc_name ~= "" then
264
+ dlc_name = dlc_name .. "\\"
265
+ end
266
+ local curr_dir = dir .. dlc_name
267
+ local err = AsyncCreatePath(curr_dir)
268
+ assert(not err)
269
+
270
+ local _, radius = o:GetBSphere()
271
+ local draw_radius = (radius * 173) / 100
272
+ local max_range = radius * OctahedronSize
273
+ local half_max = (max_range * 173) / 100 + (hr.BillboardScreenshotGridWidth % 2 == 0 and 1 or 0)
274
+
275
+ local bc_atlas = curr_dir .. ent .. "_bc.tga"
276
+ local nm_atlas = curr_dir .. ent .. "_nm.tga"
277
+ local rt_atlas = curr_dir .. ent .. "_rt.tga"
278
+ local siao_atlas = curr_dir .. ent .. "_siao.tga"
279
+ local depth_atlas = curr_dir .. ent .. "_dep.tga"
280
+ local borders = curr_dir .. ent .. "_bor.dds"
281
+ local id = 0
282
+
283
+ hr.OrthoX = radius * 2
284
+
285
+ BeginCaptureBillboardEntity(bc_atlas, nm_atlas, rt_atlas, siao_atlas, depth_atlas, borders)
286
+ for y = 0, OctahedronSize do
287
+ for x = 0, OctahedronSize do
288
+ local curr_x, curr_y, curr_z = BillboardMap(x, y, OctahedronSize, half_max)
289
+ local pos = SetLen(point(curr_x, curr_y, curr_z), draw_radius)
290
+ SetCamera(camera_target + pos, camera_target)
291
+
292
+ WaitNextFrame(1)
293
+ CaptureBillboardFrame(draw_radius, id)
294
+ WaitNextFrame(1)
295
+
296
+ id = id + 1
297
+ end
298
+ end
299
+ WaitNextFrame(1)
300
+ end
301
+ WaitNextFrame(100)
302
+ quit()
303
+ end)
304
+ end
305
+ ---
306
+ --- Checks if the given object has a billboard associated with it.
307
+ ---
308
+ --- @param obj table The object to check for a billboard.
309
+ --- @return boolean True if the object has a billboard, false otherwise.
310
+ function HasBillboard(obj)
311
+ return hr.BillboardEntities and IsValid(obj) and IsValidEntity(obj:GetEntity())
312
+ and not not table.find(hr.BillboardEntities, obj:GetEntity())
313
+ end
314
+
315
+
316
+ ---
317
+ --- Gets a list of all billboard entities in the game.
318
+ ---
319
+ --- @param err_print function An optional function to call if there are any errors finding billboard entities.
320
+ --- @return table A table of all valid billboard entity names.
321
+ function GetBillboardEntities(err_print)
322
+ if hr.BillboardDirectory then
323
+ hr.BillboardDirectory = "Textures/Billboards/"
324
+ local suffix = Platform.playstation and "_bc.hgt" or "_bc.dds"
325
+ local err, textures = AsyncListFiles("Textures/Billboards", "*" .. suffix, "relative")
326
+
327
+ local billboard_entities = {}
328
+ for _, entity in ipairs(GetClassAndDescendantsEntities("BillboardObject")) do
329
+ local check_texture = not Platform.developer or Platform.console or table.find(textures, entity .. suffix)
330
+ if not check_texture then
331
+ err_print("Entity %s is marked as a billboard entity, but has no billboard textures!", entity)
332
+ end
333
+ if IsValidEntity(entity) and check_texture then
334
+ billboard_entities[#billboard_entities + 1] = entity
335
+ end
336
+ end
337
+
338
+ hr.BillboardEntities = billboard_entities
339
+ end
340
+ end
341
+
342
+ ---
343
+ --- Stress tests the billboards in the game by randomly placing and removing tree objects.
344
+ ---
345
+ --- This function creates a real-time thread that continuously places and removes tree objects
346
+ --- at random positions within a certain radius. The function keeps track of the number of
347
+ --- objects placed and removed, and sleeps for a short period of time after every 1000 iterations.
348
+ ---
349
+ --- This function is likely used for testing and debugging purposes to ensure the billboards
350
+ --- are rendering correctly and efficiently.
351
+ ---
352
+ --- @function StressTestBillboards
353
+ --- @return nil
354
+ function StressTestBillboards()
355
+ CreateRealTimeThread(function()
356
+ local count = 0
357
+ while true do
358
+ local pos = point((1000 + AsyncRand(4144)) * guim, (1000 + AsyncRand(4144)) * guim)
359
+ local o = MapGetFirst(pos:x(), pos:y(), 100, "Tree_01")
360
+ if o then
361
+ DoneObject(o)
362
+ local new = PlaceObject("Tree_01")
363
+ local curr_pos = point((1000 + AsyncRand(4144)) * guim, (1000 + AsyncRand(4144)) * guim)
364
+ local real_pos = point(curr_pos:x(), curr_pos:y(), terrain.GetHeight(curr_pos:x(), curr_pos:y()))
365
+ new:SetPos(real_pos)
366
+ end
367
+ count = count + 1
368
+ if count == 1000 then
369
+ count = 0
370
+ Sleep(100)
371
+ end
372
+ end
373
+ end)
374
+ end
375
+
376
+ function OnMsg.ClassesPostprocess()
377
+ CreateRealTimeThread(function()
378
+ GetBillboardEntities(function(...) printf("once", ...) end)
379
+ end)
380
+ end
CommonLua/BufferedProcess.lua ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --- Holds a flag indicating whether there are any pending reasons to suspend the process.
2
+ --- Holds a flag indicating whether there are any pending reasons to suspend the process.
3
+ ---
4
+ --- @type boolean
5
+ SuspendProcessReasons = nil
6
+
7
+ --- Holds a flag indicating whether the process is currently suspended.
8
+ ---
9
+ --- @type boolean
10
+ SuspendedProcessing = nil
11
+
12
+ --- A function that checks the current execution timestamp.
13
+ ---
14
+ --- @type function
15
+ CheckExecutionTimestamp = empty_func
16
+
17
+ --- A function that checks for any remaining reasons to suspend the process.
18
+ ---
19
+ --- @type function
20
+ CheckRemainingReason = empty_func
21
+
22
+ --- A function that unpacks a table.
23
+ ---
24
+ --- @type function
25
+ table_unpack = table.unpack
26
+
27
+ --- A function that checks if two tables are equal.
28
+ ---
29
+ --- @type function
30
+ table_iequal = table.iequal
31
+ local SuspendProcessReasons
32
+ local SuspendedProcessing
33
+ local CheckExecutionTimestamp = empty_func
34
+ local CheckRemainingReason = empty_func
35
+ local table_unpack = table.unpack
36
+ local table_iequal = table.iequal
37
+
38
+ if FirstLoad then
39
+ __process_params_meta = {
40
+ __eq = function(t1, t2)
41
+ if type(t1) ~= type(t2) or not rawequal(getmetatable(t1), getmetatable(t2)) then
42
+ return false
43
+ end
44
+ local count = t1[1]
45
+ if count ~= t2[1] then
46
+ return false
47
+ end
48
+ for i=2,count do
49
+ if t1[i] ~= t2[i] then
50
+ return false
51
+ end
52
+ end
53
+ return true
54
+ end
55
+ }
56
+ end
57
+
58
+ ---
59
+ --- Packs the provided parameters into a table with a metatable that allows for efficient equality comparison.
60
+ ---
61
+ --- @param obj any The first parameter to be packed.
62
+ --- @param ... any Additional parameters to be packed.
63
+ --- @return table A table containing the packed parameters, with a metatable that allows for efficient equality comparison.
64
+ ---
65
+ local function PackProcessParams(obj, ...)
66
+ local count = select("#", ...)
67
+ if count == 0 then
68
+ return obj or false
69
+ end
70
+ return setmetatable({count + 2, obj, ...}, __process_params_meta)
71
+ end
72
+
73
+ ---
74
+ --- Unpacks the parameters from a table that was packed using the `PackProcessParams` function.
75
+ ---
76
+ --- @param params table A table containing the packed parameters, with a metatable that allows for efficient equality comparison.
77
+ --- @return any The unpacked parameters.
78
+ ---
79
+ function UnpackProcessParams(params)
80
+ if type(params) ~= "table" or getmetatable(params) ~= __process_params_meta then
81
+ return params
82
+ end
83
+ return table_unpack(params, 2, params[1])
84
+ end
85
+
86
+ function OnMsg.DoneMap()
87
+ CheckRemainingReason()
88
+ SuspendProcessReasons = false
89
+ SuspendedProcessing = false
90
+ end
91
+
92
+ ---
93
+ --- Executes any suspended functions for the given process.
94
+ ---
95
+ --- @param process string The name of the process for which to execute suspended functions.
96
+ ---
97
+ function ExecuteSuspended(process)
98
+ -- Implementation details omitted for brevity
99
+ end
100
+ local function ExecuteSuspended(process)
101
+ local delayed = SuspendedProcessing
102
+ local funcs_to_params = delayed and delayed[process]
103
+ if not funcs_to_params then
104
+ return
105
+ end
106
+ delayed[process] = nil
107
+ local procall = procall
108
+ for _, funcname in ipairs(funcs_to_params) do
109
+ local func = _G[funcname]
110
+ for _, params in ipairs(funcs_to_params[funcname]) do
111
+ dbg(CheckExecutionTimestamp(process, funcname, params, true))
112
+ procall(func, UnpackProcessParams(params))
113
+ end
114
+ end
115
+ end
116
+
117
+ ---
118
+ --- Cancels the processing of routines from a named process.
119
+ ---
120
+ --- @param process string The name of the process for which to cancel processing.
121
+ ---
122
+ function CancelProcessing(process)
123
+ if not SuspendProcessReasons or not SuspendProcessReasons[process] then
124
+ return
125
+ end
126
+ if SuspendedProcessing then
127
+ SuspendedProcessing[process] = nil
128
+ end
129
+ SuspendProcessReasons[process] = nil
130
+ Msg("ProcessingResumed", process, "cancel")
131
+ end
132
+
133
+ --[[@@@
134
+ Checks if the processing of routines from a named process is currently suspended
135
+ @function bool IsProcessingSuspended(string process)
136
+ --]]
137
+ function IsProcessingSuspended(process)
138
+ local process_to_reasons = SuspendProcessReasons
139
+ return process_to_reasons and next(process_to_reasons[process])
140
+ end
141
+
142
+ --[[@@@
143
+ Suspends the processing of routines from a named process. Multiple suspending with the same reason would lead to an error.
144
+ @function void SuspendProcessing(string process, type reason, bool ignore_errors)
145
+ @param string process - the name of the process, which routines should be suspended.
146
+ @param type reason - the reason to be used in order to resume the processing later. Could be any type.
147
+ @param bool ignore_errors - ignore suspending errors (e.g. process already suspended).
148
+ --]]
149
+ function SuspendProcessing(process, reason, ignore_errors)
150
+ reason = reason or ""
151
+ local reasons = SuspendProcessReasons and SuspendProcessReasons[process]
152
+ if reasons and reasons[reason] then
153
+ assert(ignore_errors)
154
+ return
155
+ end
156
+ local now = GameTime()
157
+ if reasons then
158
+ reasons[reason] = now
159
+ return
160
+ end
161
+ SuspendProcessReasons = table.set(SuspendProcessReasons, process, reason, now)
162
+ Msg("ProcessingSuspended", process)
163
+ end
164
+
165
+ --[[@@@
166
+ Resumes the processing of routines from a named process. Resuming an already resumed process, or resuming it with time delay, would lead to an error.
167
+ @function void ResumeProcessing(string process, type reason, bool ignore_errors)
168
+ @param string process - the name of the process, which routines should be suspended.
169
+ @param type reason - the reason to be used in order to resume the processing later. Could be any type.
170
+ @param bool ignore_errors - ignore resume errors (e.g. process already resumed).
171
+ --]]
172
+ function ResumeProcessing(process, reason, ignore_errors)
173
+ reason = reason or ""
174
+ local reasons = SuspendProcessReasons and SuspendProcessReasons[process]
175
+ local suspended = reasons and reasons[reason]
176
+ if not suspended then
177
+ return
178
+ end
179
+ assert(ignore_errors or suspended == GameTime())
180
+ local now = GameTime()
181
+ reasons[reason] = nil
182
+ if next(reasons) ~= nil then
183
+ return
184
+ end
185
+ assert(not IsProcessingSuspended(process))
186
+ ExecuteSuspended(process)
187
+ Msg("ProcessingResumed", process)
188
+ end
189
+
190
+ --[[@@@
191
+ Execute a routine from a named process. If the process is currently suspended, the call will be registered in ordered to be executed once the process is resumed. Multiple calls with the same context will be registered as one.
192
+ @function void ExecuteProcess(string process, function func, table obj)
193
+ @param string process - the name of the process, which routines should be suspended.
194
+ @param function func - the function to be executed.
195
+ @param table obj - optional function context.
196
+ --]]
197
+ function ExecuteProcess(process, funcname, obj, ...)
198
+ if not IsProcessingSuspended(process) then
199
+ dbg(CheckExecutionTimestamp(process, funcname, obj))
200
+ return procall(_G[funcname], obj, ...)
201
+ end
202
+ local params = PackProcessParams(obj, ...)
203
+ local suspended = SuspendedProcessing
204
+ if not suspended then
205
+ suspended = {}
206
+ SuspendedProcessing = suspended
207
+ end
208
+ local funcs_to_params = suspended[process]
209
+ if not funcs_to_params then
210
+ suspended[process] = { funcname, [funcname] = {params} }
211
+ return
212
+ end
213
+ local objs = funcs_to_params[funcname]
214
+ if not objs then
215
+ funcs_to_params[#funcs_to_params + 1] = funcname
216
+ funcs_to_params[funcname] = {params}
217
+ return
218
+ end
219
+ table.insert_unique(objs, params)
220
+ end
221
+
222
+ ----
223
+
224
+ if Platform.asserts then
225
+
226
+ local ExecutionTimestamps
227
+
228
+ function OnMsg.DoneMap()
229
+ ExecutionTimestamps = false
230
+ end
231
+
232
+ -- Rise an error if a routine from a process is executed twice in the same time
233
+ --[[@@@
234
+ Checks if a process routine has been executed more than once in the same time frame.
235
+ @function void CheckExecutionTimestamp(string process, string funcname, table obj, boolean delayed)
236
+ @param string process - the name of the process
237
+ @param string funcname - the name of the function being executed
238
+ @param table obj - the object context of the function being executed
239
+ @param boolean delayed - whether the function call is delayed
240
+ @return boolean - true if the function has been executed more than once, false otherwise
241
+ --]]
242
+ CheckExecutionTimestamp = function(process, funcname, obj, delayed)
243
+ if not config.DebugSuspendProcess then
244
+ return
245
+ end
246
+ if not ExecutionTimestamps then
247
+ ExecutionTimestamps = {}
248
+ CreateRealTimeThread(function()
249
+ Sleep(1)
250
+ ExecutionTimestamps = false
251
+ end)
252
+ end
253
+ local func_to_objs = ExecutionTimestamps[process]
254
+ if not func_to_objs then
255
+ func_to_objs = {}
256
+ ExecutionTimestamps[process] = func_to_objs
257
+ end
258
+ local objs_to_timestamp = func_to_objs[funcname]
259
+ if not objs_to_timestamp then
260
+ objs_to_timestamp = {}
261
+ func_to_objs[funcname] = objs_to_timestamp
262
+ end
263
+ obj = obj or false
264
+ local rtime, gtime = RealTime(), GameTime()
265
+ local timestamp = xxhash(rtime, gtime)
266
+ if timestamp == objs_to_timestamp[obj] then
267
+ print("Duplicated processing:", process, funcname, "time:", gtime, "obj:", obj and obj.class,
268
+ obj and obj.handle)
269
+ assert(false, string.format("Duplicated process routine: %s.%s", process, funcname))
270
+ else
271
+ objs_to_timestamp[obj] = timestamp
272
+ --[[
273
+ if IsValid(obj) then
274
+ local pos = obj:GetVisualPos()
275
+ local seed = xxhash(obj and obj.handle)
276
+ local len = 5*guim + BraidRandom(seed, 10*guim)
277
+ DbgAddVector(pos, len, RandColor(seed))
278
+ DbgAddText(funcname, pos + point(0, 0, len), RandColor(obj and obj.handle))
279
+ end
280
+ --]]
281
+ end
282
+ end
283
+
284
+ ---
285
+ --- Checks if there are any remaining reasons for suspending a process.
286
+ --- If there are any remaining reasons, an assertion is triggered with the process name and reason.
287
+ ---
288
+ --- @function CheckRemainingReason
289
+ --- @return nil
290
+ CheckRemainingReason = function()
291
+ local process = next(SuspendProcessReasons)
292
+ local reason = process and next(SuspendProcessReasons[process])
293
+ if reason then
294
+ assert(false, string.format("Process '%s' not resumed: %s", process, ValueToStr(reason)))
295
+ end
296
+ end
297
+
298
+ end -- Platform.asserts
CommonLua/CMT.lua ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ if not const.cmtVisible then return end
2
+
3
+ if FirstLoad then
4
+ C_CCMT = false
5
+ end
6
+
7
+ function SetC_CCMT(val)
8
+ if C_CCMT == val then
9
+ return
10
+ end
11
+ C_CCMT_Reset()
12
+ C_CCMT = val
13
+ end
14
+
15
+ function OnMsg.ChangeMap()
16
+ C_CCMT_Reset()
17
+ end
18
+
19
+ MapVar("CMT_ToHide", {})
20
+ MapVar("CMT_ToUnhide", {})
21
+ MapVar("CMT_Hidden", {})
22
+
23
+ CMT_Time = 300
24
+ CMT_OpacitySleep = 10
25
+ CMT_OpacityStep = Max(1, MulDivRound(CMT_OpacitySleep, 100, CMT_Time))
26
+
27
+ if FirstLoad then
28
+ g_CMTPaused = false
29
+ g_CMTPauseReasons = {}
30
+ end
31
+
32
+ function CMT_SetPause(s, reason)
33
+ if s then
34
+ g_CMTPauseReasons[reason] = true
35
+ g_CMTPaused = true
36
+ else
37
+ g_CMTPauseReasons[reason] = nil
38
+ if not next(g_CMTPauseReasons) then
39
+ g_CMTPaused = false
40
+ end
41
+ end
42
+ end
43
+
44
+ MapRealTimeRepeat( "CMT_V2_Thread", 0, function()
45
+ Sleep(CMT_OpacitySleep)
46
+ if g_CMTPaused then return end
47
+ --local startTs = GetPreciseTicks(1000)
48
+
49
+ if C_CCMT then
50
+ C_CCMT_Thread_Func(CMT_OpacityStep)
51
+ else
52
+ local opacity_step = CMT_OpacityStep
53
+
54
+ for k,v in next, CMT_ToHide do
55
+ if not IsValid(k) then
56
+ CMT_ToHide[k] = nil
57
+ else
58
+ local next_opacity = k:GetOpacity() - opacity_step
59
+ if next_opacity > 0 then
60
+ k:SetOpacity(next_opacity)
61
+ else
62
+ k:SetOpacity(0)
63
+ CMT_ToHide[k] = nil
64
+ CMT_Hidden[k] = true
65
+ end
66
+ end
67
+ end
68
+ for k,v in next, CMT_ToUnhide do
69
+ if not IsValid(k) then
70
+ CMT_ToUnhide[k] = nil
71
+ else
72
+ local next_opacity = k:GetOpacity() + opacity_step
73
+ if next_opacity < 100 then
74
+ k:SetOpacity(next_opacity)
75
+ else
76
+ k:SetOpacity(100)
77
+ k:ClearHierarchyGameFlags(const.gofSolidShadow + const.gofContourInner)
78
+ CMT_ToUnhide[k] = nil
79
+ end
80
+ end
81
+ end
82
+ end
83
+ --local endTs = GetPreciseTicks(1000)
84
+ --print("CMT_V2_Thread time", endTs - startTs)
85
+ end)
86
+
87
+ function IsContourObject(obj)
88
+ return const.SlabSizeX and IsKindOf(obj, "Slab")
89
+ end
90
+
91
+ function CMT(obj, b)
92
+ if C_CCMT then
93
+ C_CCMT_Hide(obj, not not b)
94
+ return
95
+ end
96
+
97
+ if b then
98
+ if CMT_ToHide[obj] or CMT_Hidden[obj] then return end
99
+ if CMT_ToUnhide[obj] then
100
+ CMT_ToUnhide[obj] = nil
101
+ end
102
+ CMT_ToHide[obj] = true
103
+ obj:SetHierarchyGameFlags(const.gofSolidShadow)
104
+ if IsContourObject(obj) then
105
+ obj:SetHierarchyGameFlags(const.gofContourInner)
106
+ end
107
+ else
108
+ if CMT_ToUnhide[obj] or not CMT_ToHide[obj] and not CMT_Hidden[obj] then return end
109
+ if CMT_ToHide[obj] then
110
+ CMT_ToHide[obj] = nil
111
+ end
112
+ if IsEditorActive() then
113
+ obj:SetOpacity(100)
114
+ obj:ClearHierarchyGameFlags(const.gofSolidShadow + const.gofContourInner)
115
+ else
116
+ CMT_ToUnhide[obj] = true
117
+ end
118
+ if CMT_Hidden[obj] then
119
+ CMT_Hidden[obj] = nil
120
+ end
121
+ end
122
+ end
123
+
124
+ local function ShowAllKeyObjectsAndClearTable(table)
125
+ for obj, _ in pairs(table) do
126
+ if IsValid(obj) then
127
+ obj:SetOpacity(100)
128
+ obj:ClearHierarchyGameFlags(const.gofSolidShadow + const.gofContourInner)
129
+ end
130
+ table[obj] = nil
131
+ end
132
+ end
133
+
134
+ function OnMsg.ChangeMapDone(map)
135
+ if string.find(map, "MainMenu") then
136
+ CMT_SetPause(true, "MainMenu")
137
+ else
138
+ CMT_SetPause(false, "MainMenu")
139
+ end
140
+ end
141
+
142
+ function OnMsg.GameEnterEditor()
143
+ C_CCMT_ShowAllAndReset()
144
+ ShowAllKeyObjectsAndClearTable(CMT_ToHide)
145
+ ShowAllKeyObjectsAndClearTable(CMT_ToUnhide)
146
+ ShowAllKeyObjectsAndClearTable(CMT_Hidden)
147
+ end
148
+
149
+ function CMT_IsObjVisible(o)
150
+ if not C_CCMT then
151
+ return o:GetGameFlags(const.gofSolidShadow) == 0 or CMT_ToUnhide[o]
152
+ else
153
+ return C_CCMT_GetObjCMTState(o) < const.cmtHidden
154
+ end
155
+ end
CommonLua/Camera.lua ADDED
@@ -0,0 +1,1005 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ --- Calculates the power of a camera shake effect based on the position of the camera and the position of the shake.
3
+ --- @param pos point The position of the shake.
4
+ --- @param radius_insight number The radius within which the shake is considered to be in sight of the camera.
5
+ --- @param radius_outofsight number The radius beyond which the shake is considered to be out of sight of the camera.
6
+ --- @return number The power of the camera shake effect, as a percentage.
7
+ ---
8
+ function CameraShake_GetEffectPower(pos, radius_insight, radius_outofsight)
9
+ local cam_pos, cam_look = GetCamera()
10
+ local camera_orientation = CalcOrientation(cam_pos, cam_look)
11
+ local shake_orientation = CalcOrientation(cam_pos, pos)
12
+ local dist = DistSegmentToPt(cam_pos, cam_look, pos)
13
+ if dist < 0 then
14
+ assert(false)
15
+ return 0
16
+ end
17
+
18
+ local radius
19
+ if abs(AngleDiff(shake_orientation, camera_orientation)) < const.CameraShakeFOV/2 then
20
+ radius = radius_insight or const.ShakeRadiusInSight
21
+ else
22
+ radius = radius_outofsight or const.ShakeRadiusOutOfSight
23
+ end
24
+ return dist < radius and 100 * (radius - dist) / radius or 0
25
+ end
26
+
27
+ --- Starts a camera shake effect with the specified position and power.
28
+ -- @cstyle void CameraShake(point pos, int power).
29
+ -- @param pos point.
30
+ -- @param power int.
31
+ -- @return void.
32
+ function CameraShake(pos, power)
33
+ power = power * CameraShake_GetEffectPower(pos) / 100
34
+ if power == 0 then return end
35
+ local total_duration = const.MinShakeDuration + power*(const.MaxShakeDuration-const.MinShakeDuration)/const.MaxShakePower
36
+ local shake_offset = power*const.MaxShakeOffset/const.MaxShakePower
37
+ local shake_roll = power*const.MaxShakeRoll/const.MaxShakePower
38
+ camera.Shake(total_duration, const.ShakeTick, shake_offset, shake_roll)
39
+ end
40
+
41
+ ---
42
+ --- Stores the current camera shake thread and the maximum offset for the camera shake effect.
43
+ ---
44
+ --- @field camera_shake_thread thread The current camera shake thread.
45
+ --- @field camera_shake_max_offset number The maximum offset for the camera shake effect.
46
+ ---
47
+ MapVar("camera_shake_thread", false)
48
+ MapVar("camera_shake_max_offset", 0)
49
+ ---
50
+ --- Performs a camera shake effect with the specified parameters.
51
+ ---
52
+ --- @param total_duration number The total duration of the camera shake effect, in seconds.
53
+ --- @param shake_tick number The interval between each shake, in seconds.
54
+ --- @param max_offset number The maximum offset of the camera shake, in meters.
55
+ --- @param max_roll_offset number The maximum roll offset of the camera shake, in degrees.
56
+ ---
57
+
58
+ local function DoShakeCamera(total_duration, shake_tick, max_offset, max_roll_offset)
59
+ local time_left = total_duration
60
+ while true do
61
+ local LookAtOffset = RandPoint(1500, 500, 500)
62
+ local EyePtOffset = RandPoint(1500, 500, 500)
63
+ local len = Max(1, 2 * time_left * max_offset / total_duration)
64
+ local angle = 60 * time_left * max_roll_offset / total_duration
65
+ if LookAtOffset:Len2() > 0 then
66
+ LookAtOffset = SetLen(LookAtOffset, len)
67
+ end
68
+ if EyePtOffset:Len2() > 0 then
69
+ EyePtOffset = SetLen(EyePtOffset, len)
70
+ end
71
+ camera.SetLookAtOffset(LookAtOffset, shake_tick)
72
+ camera.SetEyeOffset(EyePtOffset, shake_tick)
73
+ camera.SetRollOffset(AsyncRand(2 * angle + 1) - angle, shake_tick)
74
+ if total_duration > 0 then
75
+ time_left = time_left - shake_tick
76
+ if time_left <= shake_tick then
77
+ Sleep(time_left)
78
+ break
79
+ end
80
+ end
81
+ Sleep(shake_tick)
82
+ end
83
+ camera.ShakeStop(shake_tick)
84
+ end
85
+
86
+ ---
87
+ --- Performs a camera shake effect with the specified parameters.
88
+ ---
89
+ --- @param total_duration number The total duration of the camera shake effect, in seconds.
90
+ --- @param shake_tick number The interval between each shake, in seconds.
91
+ --- @param shake_max_offset number The maximum offset of the camera shake, in meters. This value is clamped to the range [0, 10m].
92
+ --- @param shake_max_roll number The maximum roll offset of the camera shake, in degrees. This value is clamped to the range [0, 180].
93
+ ---
94
+ function camera.Shake(total_duration, shake_tick, shake_max_offset, shake_max_roll)
95
+ local max_offset = Clamp(shake_max_offset, 0, 10 * guim)
96
+ assert(max_offset == shake_max_offset, "camera.Shake() max_offset should be [0-10m]!")
97
+ local max_roll = Clamp(shake_max_roll, 0, 180)
98
+ assert(max_roll == shake_max_roll, "camera.Shake() max_roll should be [0-180]!")
99
+
100
+ if total_duration == 0 or shake_tick <= 0 then
101
+ return
102
+ end
103
+ if IsValidThread(camera_shake_thread) then
104
+ if camera_shake_max_offset > shake_max_offset then
105
+ return
106
+ end
107
+ DeleteThread(camera_shake_thread)
108
+ end
109
+ camera_shake_max_offset = max_offset
110
+ camera_shake_thread = CreateRealTimeThread(DoShakeCamera, total_duration, shake_tick, max_offset, max_roll)
111
+ MakeThreadPersistable(camera_shake_thread)
112
+ end
113
+
114
+ ---
115
+ --- Stops the camera shake effect.
116
+ ---
117
+ --- @param shake_tick number The interval between each shake, in seconds.
118
+ ---
119
+ function camera.ShakeStop(shake_tick)
120
+ camera.SetRollOffset(0, 0)
121
+ camera.SetLookAtOffset(point30, shake_tick or 0)
122
+ camera.SetEyeOffset(point30, shake_tick or 0)
123
+ camera_shake_max_offset = 0
124
+ if IsValidThread(camera_shake_thread) and CurrentThread() ~= camera_shake_thread then
125
+ DeleteThread(camera_shake_thread)
126
+ end
127
+ camera_shake_thread = false
128
+ end
129
+
130
+ function OnMsg.ChangeMap()
131
+ camera.ShakeStop()
132
+ end
133
+
134
+ ---
135
+ --- Sets the camera to the specified position, look-at point, camera type, zoom, and field of view.
136
+ ---
137
+ --- @param ptCamera vec3 The position of the camera.
138
+ --- @param ptCameraLookAt vec3 The point the camera is looking at.
139
+ --- @param camType string The type of camera to use. Can be "3p", "RTS", "Max", or "Tac".
140
+ --- @param zoom number The zoom level of the camera.
141
+ --- @param properties table A table of camera properties to set.
142
+ --- @param fovX number The field of view of the camera in degrees.
143
+ --- @param time number The duration of the camera transition in seconds.
144
+ ---
145
+ --- @return nil
146
+ ---
147
+ function SetCamera(ptCamera, ptCameraLookAt, camType, zoom, properties, fovX, time)
148
+ if type(ptCamera) == "table" then
149
+ return SetCamera(unpack_params(ptCamera))
150
+ end
151
+ time = time or 0
152
+ if camType then
153
+ if camType == "Max" or camType == "3p" or camType == "RTS" or camType == "Tac" then
154
+ camType = "camera" .. camType
155
+ end
156
+ _G[camType].Activate(1)
157
+ end
158
+ if not ptCamera then
159
+ return
160
+ end
161
+ if camera3p.IsActive() then
162
+ camera3p.SetEye(ptCamera, time)
163
+ camera3p.SetLookAt(ptCameraLookAt, time)
164
+ elseif cameraRTS.IsActive() then
165
+ if properties then
166
+ cameraRTS.SetProperties(1, properties)
167
+ end
168
+ cameraRTS.SetCamera(ptCamera, ptCameraLookAt, time)
169
+ if zoom then
170
+ cameraRTS.SetZoom(zoom)
171
+ end
172
+ elseif cameraMax.IsActive() then
173
+ -- cameraMax can't look straight down
174
+ local diff = ptCameraLookAt - ptCamera
175
+ if diff:x() == 0 and diff:y() == 0 then
176
+ ptCamera = ptCamera:SetX(ptCamera:x()-5)
177
+ end
178
+ cameraMax.SetCamera(ptCamera, ptCameraLookAt, time)
179
+ elseif cameraTac.IsActive() then
180
+ cameraTac.SetCamera(ptCamera, ptCameraLookAt, time)
181
+ if properties then
182
+ local floor = properties.floor
183
+ local overview = properties.overview
184
+ if floor then
185
+ cameraTac.SetFloor(floor)
186
+ end
187
+ if overview ~= nil then
188
+ cameraTac.SetOverview(overview, true)
189
+ end
190
+ end
191
+ if zoom then
192
+ cameraTac.SetZoom(zoom)
193
+ end
194
+ end
195
+ SetCameraFov(fovX)
196
+ end
197
+
198
+ ---
199
+ --- Sets the camera field of view (FOV) to the specified value.
200
+ ---
201
+ --- @param fovX number The horizontal field of view in degrees. If not provided, defaults to 70 degrees.
202
+ ---
203
+ function SetCameraFov(fovX)
204
+ camera.SetFovX(fovX or 70 * 60)
205
+ end
206
+
207
+ ---
208
+ --- Sets the camera field of view (FOV) to the specified value, with optional easing.
209
+ ---
210
+ --- @param properties table A table containing the following properties:
211
+ --- - FovX: number The horizontal field of view in degrees.
212
+ --- - FovXNarrow: number The horizontal field of view in degrees for 3:4 screens.
213
+ --- - FovXWide: number The horizontal field of view in degrees for 21:9 screens.
214
+ --- @param duration number (optional) The duration of the FOV change in seconds.
215
+ --- @param easing string (optional) The easing function to use for the FOV change.
216
+ ---
217
+ function SetRTSCameraFov(properties, duration, easing)
218
+ local FovX = properties.FovX
219
+ local minFovX = properties.FovXNarrow -- FovX for 3:4 screens
220
+ local minX, minY = 4, 3
221
+ local maxFovX = properties.FovXWide -- FovX for 21:9 screens
222
+ local maxX, maxY = 21, 9
223
+ if FovX and minFovX and maxFovX then
224
+ -- when both FovXNarrow and FovXWide are supplied,
225
+ -- FovX at 16:9 is computed and equals FovXNarrow * 5 / 9 + FovXWide * 4 / 9
226
+ assert(abs(minFovX * 5 / 9 + maxFovX * 4 / 9 - FovX) < 60)
227
+ end
228
+ FovX = FovX or 90 * 60
229
+ if not minFovX then
230
+ minFovX = FovX
231
+ minX, minY = 16, 9
232
+ end
233
+ if not maxFovX then
234
+ maxFovX = FovX
235
+ maxX, maxY = 16, 9
236
+ end
237
+ hr.CameraFovEasing = easing or "Linear"
238
+ camera.SetAutoFovX(1, duration or 0, minFovX, minX, minY, maxFovX, maxX, maxY)
239
+ end
240
+
241
+ -- init from last camera with reasonable settings ( at editor exit, ... )
242
+ -- or set to map center if first run
243
+ ---
244
+ --- Sets the default camera to the RTS (Real-Time Strategy) camera type and configures its properties.
245
+ ---
246
+ --- If the Libs.Sim module is available, it retrieves the RTS camera properties from the account storage and applies them.
247
+ --- Otherwise, it uses the default RTS camera properties defined in const.DefaultCameraRTS.
248
+ ---
249
+ --- The function also sets the camera's field of view using the SetRTSCameraFov function, and positions the camera to the center of the map if the current look-at position is at (0, 0).
250
+ ---
251
+ --- Finally, it calls the ViewObjectRTS function to set the camera's position and look-at to the center of the map.
252
+ ---
253
+ function SetDefaultCameraRTS()
254
+ cameraRTS.Activate(1)
255
+ cameraRTS.SetProperties(1, const.DefaultCameraRTS)
256
+ if Libs.Sim then
257
+ cameraRTS.SetProperties(1, GetRTSCamPropsFromAccountStorage())
258
+ end
259
+ SetRTSCameraFov(const.DefaultCameraRTS)
260
+ local lookat = cameraRTS.GetLookAt()
261
+ if lookat:x() == 0 and lookat:y() == 0 then
262
+ lookat = point(terrain.GetMapSize()) / 2
263
+ end
264
+ ViewObjectRTS(lookat, 0)
265
+ end
266
+
267
+ ---
268
+ --- Returns a table of available camera types.
269
+ ---
270
+ --- @return table Camera types
271
+ ---
272
+ function GetCameraTypesItems()
273
+ return {"3p", "RTS", "Max", "Tac"}
274
+ end
275
+
276
+ ---
277
+ --- Returns the current camera position, look-at position, camera type, zoom level, and camera properties.
278
+ ---
279
+ --- @return point, point, string, number, table, number Camera position, look-at position, camera type, zoom level, camera properties, and field of view angle
280
+ ---
281
+ function GetCamera()
282
+ local ptCamera, ptCameraLookAt, camType, zoom, properties, fovX
283
+ if camera3p.IsActive() then
284
+ ptCamera, ptCameraLookAt = camera.GetEye(), camera3p.GetLookAt()
285
+ camType = "3p"
286
+ elseif cameraRTS.IsActive() then
287
+ ptCamera, ptCameraLookAt = cameraRTS.GetPosLookAt()
288
+ camType = "RTS"
289
+ zoom = cameraRTS.GetZoom()
290
+ properties = cameraRTS.GetProperties(1)
291
+ elseif cameraMax.IsActive() then
292
+ ptCamera, ptCameraLookAt = cameraMax.GetPosLookAt()
293
+ camType = "Max"
294
+ elseif cameraTac.IsActive() then
295
+ ptCamera, ptCameraLookAt = cameraTac.GetPosLookAt()
296
+ camType = "Tac"
297
+ zoom = cameraTac.GetZoom()
298
+ properties = {floor=cameraTac.GetFloor(), overview=cameraTac.GetIsInOverview()}
299
+ else
300
+ ptCamera, ptCameraLookAt = camera.GetEye(), camera.GetEye() + SetLen(camera.GetDirection(), 3 * guim)
301
+ end
302
+ fovX = camera.GetFovX()
303
+ return ptCamera, ptCameraLookAt, camType, zoom, properties, fovX
304
+ end
305
+
306
+ if FirstLoad then
307
+ ptLastCameraPos = false
308
+ ptLastCameraLookAt = false
309
+ cameraMax3DView = {
310
+ toggle = false,
311
+ old_pos = false,
312
+ old_lookat = false,
313
+ }
314
+ end
315
+
316
+ ---
317
+ --- Cleans up the state of the cameraMax3DView object.
318
+ --- Resets the toggle, old_pos, and old_lookat properties to their default values.
319
+ ---
320
+ function cameraMax3DView:Clean()
321
+ self.toggle = false
322
+ self.old_pos = false
323
+ self.old_lookat = false
324
+ end
325
+
326
+ -- returns the new camera pos and the look pos of the selection
327
+ ---
328
+ --- Rotates the camera in the 3D Max view to the specified view direction.
329
+ ---
330
+ --- @param view_direction point The new view direction for the camera.
331
+ ---
332
+ local function cameraMax3DView_Rotate(view_direction)
333
+ local sel = editor.GetSel()
334
+ local cnt = #sel
335
+ if cnt == 0 then
336
+ print("You need to select object(s) for this operation")
337
+ return
338
+ end
339
+
340
+ local center = point30
341
+ for i = 1, cnt do
342
+ local bsc = sel[i]:GetBSphere()
343
+ center = center + bsc
344
+ end
345
+ if cnt > 0 then
346
+ -- find center of the selection
347
+ center = point(center:x() / cnt, center:y() / cnt, center:z() / cnt)
348
+
349
+ -- find the radius of the bounding sphere of the selection
350
+ local selSize = 0
351
+ for i = 1, cnt do
352
+ local bsc, bsr = sel[i]:GetBSphere()
353
+ local dist = bsc:Dist(center) + bsr
354
+ if selSize < dist then
355
+ selSize = dist
356
+ end
357
+ end
358
+ selSize = 2 * selSize -- get the diameter of the selection
359
+
360
+ -- move the camera position to look in the center of the selection
361
+ local half_fovY = MulDivRound(camera.GetFovY(), 1, 2)
362
+ local fov_sin, fov_cos = sin(half_fovY), cos(half_fovY)
363
+ local dist_from_camera = (fov_sin > 0) and MulDivRound((selSize / 2), fov_cos, fov_sin) or (selSize / 2)
364
+
365
+ view_direction = SetLen(view_direction, dist_from_camera * 130 / 100)
366
+ local pos = center + view_direction
367
+ cameraMax.SetCamera(pos, center, 0)
368
+ end
369
+ end
370
+
371
+ ---
372
+ --- Rotates the camera in the 3D Max view to the up direction.
373
+ ---
374
+ function cameraMax3DView:SetViewUp()
375
+ cameraMax3DView_Rotate(point(0, 0, 1))
376
+ end
377
+ ---
378
+ --- Rotates the camera in the 3D Max view to the down direction.
379
+ ---
380
+ function cameraMax3DView:SetViewDown()
381
+ cameraMax3DView_Rotate(point(0, 0, -1))
382
+ end
383
+ ---
384
+ --- Sets the camera to the old position and look-at point.
385
+ ---
386
+ function cameraMax3DView:SetViewOld()
387
+ cameraMax.SetCamera(cameraMax3DView.old_pos, cameraMax3DView.old_lookat, 0)
388
+ end
389
+
390
+ ---
391
+ --- Rotates the camera in the 3D Max view around the Z axis.
392
+ ---
393
+ --- @param dir string The direction to rotate the camera, either "east" or "west".
394
+ ---
395
+ function cameraMax3DView:RotateZ(dir)
396
+ local pos, look_at = cameraMax.GetPosLookAt()
397
+ local cam_angle = (camera.GetYaw() / 60) + 180
398
+ local cam_quadrant = (cam_angle / 90) % 4 + 1
399
+ local correction = 0
400
+ local z_axis = point(0, 0, 1)
401
+
402
+ if cam_angle % 90 ~= 0 then
403
+ if cam_angle - 90 * (cam_quadrant - 1) < 90 * cam_quadrant - cam_angle then
404
+ correction = -(cam_angle - 90 * (cam_quadrant - 1))
405
+ else
406
+ correction = 90 * cam_quadrant - cam_angle
407
+ end
408
+ cam_angle = cam_angle + correction
409
+ end
410
+
411
+ local view_dir = false
412
+ if dir == "east" then
413
+ view_dir = RotateAxis(pos, z_axis, (cam_angle - 90) * 60)
414
+ else
415
+ view_dir = RotateAxis(pos, z_axis, (cam_angle + 90) * 60)
416
+ end
417
+
418
+ if view_dir then
419
+ cameraMax3DView_Rotate(Normalize(view_dir))
420
+ end
421
+ end
422
+
423
+ ---
424
+ --- Sets the camera position and look-at point.
425
+ ---
426
+ --- @param pos table The position of the camera, represented as a point.
427
+ --- @param dist number (optional) The distance from the camera to the look-at point.
428
+ --- @param cam_type number (optional) The type of camera to use.
429
+ ---
430
+ --- If `pos` is `InvalidPos()`, the camera will be reset to the last known position and look-at point.
431
+ --- If `pos` does not have a valid Z coordinate, it will be set to the terrain Z coordinate.
432
+ --- The camera vector is calculated based on the `pos` and the look-at point, and the camera is set to this position.
433
+ ---
434
+ function ViewPos(pos, dist, cam_type)
435
+ local ptCamera, ptCameraLookAt = GetCamera()
436
+ if not ptCamera then
437
+ return
438
+ end
439
+ if pos == InvalidPos() then
440
+ pos = nil
441
+ end
442
+ if not pos then
443
+ if ptLastCameraPos then
444
+ SetCamera(ptLastCameraPos, ptLastCameraLookAt, cam_type)
445
+ end
446
+ return
447
+ end
448
+
449
+ ptLastCameraPos, ptLastCameraLookAt = ptCamera, ptCameraLookAt
450
+
451
+ if not pos:z() then
452
+ pos = pos:SetTerrainZ()
453
+ end
454
+
455
+ local cameraVector = ptCameraLookAt - ptCamera
456
+ if dist then
457
+ cameraVector = SetLen(cameraVector, dist)
458
+ end
459
+ ptCamera = pos - cameraVector
460
+ ptCameraLookAt = pos
461
+
462
+ SetCamera(ptCamera, ptCameraLookAt, cam_type)
463
+ end
464
+
465
+ ---
466
+ --- Sets the camera to view the specified object.
467
+ ---
468
+ --- @param obj MapObject|number The object to view, or its handle.
469
+ --- @param dist number (optional) The distance from the camera to the object.
470
+ ---
471
+ --- If `obj` is a number, it is assumed to be the handle of a `MapObject` and looked up in `HandleToObject`.
472
+ --- If `pos` is `InvalidPos()`, the camera will be reset to the last known position and look-at point.
473
+ --- If `pos` does not have a valid Z coordinate, it will be set to the terrain Z coordinate.
474
+ --- The camera vector is calculated based on the `pos` and the look-at point, and the camera is set to this position.
475
+ ---
476
+ ViewObject = function(obj, dist)
477
+ if type(obj) == "number" and HandleToObject[obj] then
478
+ obj = HandleToObject[obj]
479
+ end
480
+ local pos = IsValid(obj) and obj:GetPos()
481
+ if not pos or pos == InvalidPos() then
482
+ return
483
+ end
484
+ if dist then
485
+ ViewPos(pos, dist)
486
+ else
487
+ local center, radius = obj:GetBSphere()
488
+ ViewPos(center, Max(guim, radius * 10))
489
+ end
490
+ end
491
+
492
+ ---
493
+ --- Caches the last object viewed by `ViewNextObject`.
494
+ ---
495
+ --- This cache is used to keep track of the last object that was viewed, so that `ViewNextObject` can cycle through the objects in the order they were viewed.
496
+ ---
497
+ local ViewNextObjectCache
498
+ function OnMsg.ChangeMap()
499
+ ViewNextObjectCache = nil
500
+ end
501
+
502
+ -- Cycles ViewObject in the array objs, viewing the next object every time it is called for the same set of parameters
503
+
504
+ ---
505
+ --- Cycles through the next object in the given list of objects and views it.
506
+ ---
507
+ --- @param name string (optional) The class name of the objects to cycle through. If not provided, the class name of the last selected object is used.
508
+ --- @param objs table (optional) The list of objects to cycle through. If not provided, all objects of the given class name are used.
509
+ --- @param select_obj boolean (optional) Whether to select the object after viewing it.
510
+ ---
511
+ --- If `name` is not provided and there is no last selected object, this function does nothing.
512
+ --- If `objs` is not provided, all objects of the given class name are used.
513
+ --- The function keeps track of the last object viewed and cycles to the next one in the list.
514
+ --- If the end of the list is reached, it cycles back to the beginning.
515
+ --- If `select_obj` is true, the viewed object is also selected.
516
+ ---
517
+ function ViewNextObject(name, objs, select_obj)
518
+ name = name or ""
519
+ local last
520
+ if not objs then
521
+ if name == "" then
522
+ last = SelectedObj
523
+ name = last and last.class
524
+ select_obj = true
525
+ end
526
+ if not IsKindOf(g_Classes[name], "MapObject") then
527
+ return
528
+ end
529
+ objs = MapGet("map", name)
530
+ end
531
+ ViewNextObjectCache = ViewNextObjectCache or setmetatable({}, weak_values_meta)
532
+ last = last or ViewNextObjectCache[name]
533
+ local idx = last and table.find(objs, last) or 0
534
+ last = objs[idx + 1] or objs[1]
535
+ ViewNextObjectCache[name] = last
536
+ ViewObject(last)
537
+ SelectObj(last)
538
+ end
539
+
540
+ ---
541
+ --- Cycles through the next object in the given list of objects and views it.
542
+ ---
543
+ --- @param name string (optional) The class name of the objects to cycle through. If not provided, the class name of the last selected object is used.
544
+ --- @param objs table (optional) The list of objects to cycle through. If not provided, all objects of the given class name are used.
545
+ --- @param select_obj boolean (optional) Whether to select the object after viewing it.
546
+ ---
547
+ --- If `name` is not provided and there is no last selected object, this function does nothing.
548
+ --- If `objs` is not provided, all objects of the given class name are used.
549
+ --- The function keeps track of the last object viewed and cycles to the next one in the list.
550
+ --- If the end of the list is reached, it cycles back to the beginning.
551
+ --- If `select_obj` is true, the viewed object is also selected.
552
+ ---
553
+ function ViewObjects(objects)
554
+ objects = objects or {}
555
+ local dgs = XEditorSelectSingleObjects
556
+ XEditorSelectSingleObjects = 1
557
+ editor.ChangeSelWithUndoRedo(objects)
558
+ XEditorSelectSingleObjects = dgs
559
+ if #objects == 0 then
560
+ return
561
+ end
562
+ local bbox = GetObjectsBBox(objects)
563
+ local center, radius = bbox:GetBSphere()
564
+ local cam_pos = camera.GetEye()
565
+ local h = cam_pos:z() - terrain.GetSurfaceHeight(cam_pos)
566
+ local eye = center:SetZ(0) + SetLen((cam_pos - center):SetZ(0), h)
567
+ eye = eye:SetZ(terrain.GetSurfaceHeight(eye) + h)
568
+ local dist = (eye - center):Len()
569
+ local new_dist = Clamp(Max(dist, 2*radius), 10*guim, 100*guim)
570
+ eye = center + MulDivRound(eye - center, new_dist, dist)
571
+ local steps = 18
572
+ local angle = 360 * 60 / steps
573
+ local max_radius = 2 * guim
574
+ local success = true
575
+ local objects_map = {}
576
+ for i=1,#objects do
577
+ objects_map[objects[i]] = true
578
+ end
579
+ while true do
580
+ local objs = IntersectSegmentWithObjects(eye, center, const.efVisible)
581
+ if not objs then
582
+ break
583
+ end
584
+ local objects_too_big = false
585
+ for i=1,#objs do
586
+ local obj = objs[i]
587
+ if not objects_map[obj] then
588
+ local center, radius = obj:GetBSphere()
589
+ if radius > max_radius then
590
+ objects_too_big = true
591
+ break
592
+ end
593
+ end
594
+ end
595
+ if not objects_too_big then
596
+ break
597
+ end
598
+ steps = steps - 1
599
+ if steps <= 1 then
600
+ success = false
601
+ break
602
+ end
603
+ eye = RotateAroundCenter(center, eye, angle)
604
+ eye = eye:SetZ(terrain.GetSurfaceHeight(eye) + h)
605
+ end
606
+ if success then
607
+ SetCamera(eye, center)
608
+ end
609
+ end
610
+
611
+ if FirstLoad then
612
+ SplitScreenType = false
613
+ SplitScreenEnabled = true
614
+ SecondViewEnabled = false
615
+ SecondViewViewport = false
616
+ end
617
+
618
+ -- call this after every resolution/scene size change to recalc and setup appropriate views for single or split screen
619
+ ---
620
+ --- Sets up the camera views for single or split screen.
621
+ ---
622
+ --- @param size number|nil The size of the screen, if provided.
623
+ ---
624
+ --- This function is responsible for configuring the camera views based on the current split screen settings.
625
+ --- If split screen is enabled, it sets up two views - one for each player. The views can be either horizontal or vertical.
626
+ --- If split screen is disabled, it sets up a single view that covers the entire screen.
627
+ --- The function adjusts the viewport settings of the camera accordingly.
628
+ ---
629
+ function SetupViews(size)
630
+ local w, h = 1000000, 1000000
631
+ if SecondViewEnabled and SecondViewViewport then
632
+ camera.SetViewCount(2)
633
+ camera.SetViewport(box(0, 0, w, h), 1)
634
+ camera.SetViewport(SecondViewViewport, 2)
635
+ elseif SplitScreenEnabled then
636
+ if SplitScreenType == "horizontal" then
637
+ camera.SetViewCount(2)
638
+ camera.SetViewport(box(0, 0, w, h / 16 * 8), 1)
639
+ camera.SetViewport(box(0, (h + 15) / 16 * 8, w, h), 2)
640
+ elseif SplitScreenType == "vertical" then
641
+ camera.SetViewCount(2)
642
+ camera.SetViewport(box(0, 0, w / 16 * 8, h), 1)
643
+ camera.SetViewport(box((w + 15) / 16 * 8, 0, w, h), 2)
644
+ else
645
+ camera.SetViewCount(1)
646
+ camera.SetViewport(box(0, 0, w, h), 1)
647
+ end
648
+ else
649
+ if not SplitScreenType then
650
+ camera.SetViewCount(1)
651
+ camera.SetViewport(box(0, 0, w, h), 1)
652
+ else
653
+ camera.SetViewport(box(0, 0, w, h), 1)
654
+ end
655
+ end
656
+ end
657
+
658
+ if FirstLoad then
659
+ SplitScreenDisableReasons = {}
660
+ end
661
+
662
+ ---
663
+ --- Enables or disables split screen mode based on the provided reason.
664
+ ---
665
+ --- @param on boolean Whether to enable or disable split screen mode.
666
+ --- @param reason string The reason for enabling or disabling split screen mode.
667
+ ---
668
+ --- This function is responsible for managing the state of split screen mode. It updates the `SplitScreenDisableReasons` table to track the reasons for enabling or disabling split screen mode. If there are no more reasons to disable split screen mode, it enables it. Otherwise, it disables it. The function also calls `SetupViews()` to reconfigure the camera views and sends a "SplitScreenChange" message.
669
+ ---
670
+ function SetSplitScreenEnabled(on, reason)
671
+ assert(reason)
672
+ SplitScreenDisableReasons[reason] = (on == false) or nil
673
+ on = not next(SplitScreenDisableReasons)
674
+ if SplitScreenEnabled ~= on then
675
+ SplitScreenEnabled = on
676
+ SetupViews()
677
+ Msg("SplitScreenChange", true)
678
+ end
679
+ end
680
+
681
+ ---
682
+ --- Enables the second view for the camera and sets the viewport for it.
683
+ ---
684
+ --- @param viewport table The viewport for the second view.
685
+ ---
686
+ --- This function enables the second view for the camera and sets the viewport for it. It updates the `SecondViewEnabled` and `SecondViewViewport` variables and then calls the `SetupViews()` function to reconfigure the camera views.
687
+ ---
688
+ function EnableSecondView(viewport)
689
+ SecondViewEnabled = true
690
+ SecondViewViewport = viewport
691
+ SetupViews()
692
+ end
693
+
694
+ ---
695
+ --- Disables the second view for the camera.
696
+ ---
697
+ --- This function disables the second view for the camera by setting the `SecondViewEnabled` variable to `false` and calling the `SetupViews()` function to reconfigure the camera views.
698
+ ---
699
+ function DisableSecondView()
700
+ SecondViewEnabled = false
701
+ SetupViews()
702
+ end
703
+
704
+ ---
705
+ --- Sets the split screen type.
706
+ ---
707
+ --- @param type string The type of split screen to use, or an empty string to disable split screen.
708
+ ---
709
+ --- This function sets the `SplitScreenType` variable to the provided `type` parameter. If the `type` is an empty string, split screen is disabled. The function then calls `SetupViews()` to reconfigure the camera views, and sends a "SplitScreenChange" message if the split screen type has changed.
710
+ function SetSplitScreenType(type)
711
+ if type == "" then
712
+ type = false
713
+ end
714
+ local bChange = SplitScreenType ~= type
715
+ SplitScreenType = type
716
+ if not CameraControlScene then
717
+ SetupViews()
718
+ end
719
+ if bChange then
720
+ Msg("SplitScreenChange")
721
+ end
722
+ end
723
+
724
+ ---
725
+ --- Checks if split screen is enabled.
726
+ ---
727
+ --- @return boolean true if split screen is enabled, false otherwise
728
+ ---
729
+ function IsSplitScreenEnabled()
730
+ return SplitScreenEnabled and SplitScreenType and true
731
+ end
732
+
733
+ ---
734
+ --- Checks if split screen is in horizontal mode.
735
+ ---
736
+ --- @return boolean true if split screen is in horizontal mode, false otherwise
737
+ ---
738
+ function IsSplitScreenHorizontal()
739
+ return SplitScreenEnabled and SplitScreenType == "horizontal"
740
+ end
741
+
742
+ ---
743
+ --- Checks if split screen is in vertical mode.
744
+ ---
745
+ --- @return boolean true if split screen is in vertical mode, false otherwise
746
+ ---
747
+ function IsSplitScreenVertical()
748
+ return SplitScreenEnabled and SplitScreenType == "vertical"
749
+ end
750
+
751
+ ---
752
+ --- Loads a map and camera location from a saved state.
753
+ ---
754
+ --- @param map string The name of the map to load.
755
+ --- @param cam_params table A table containing the camera parameters to set.
756
+ --- @param editor_mode boolean Whether to activate the editor mode after loading.
757
+ --- @param map_rand number The random seed to use for the map.
758
+ ---
759
+ --- This function loads a map and camera location from a saved state. It first checks if the map exists, and if not, prints an error message. It then creates a real-time thread to perform the following steps:
760
+ ---
761
+ --- 1. Deactivate the editor.
762
+ --- 2. If the map or random seed is different from the current map, change the map and restore the configuration.
763
+ --- 3. If the editor mode is enabled, activate the editor.
764
+ --- 4. Set the camera parameters, activating the fly camera if necessary.
765
+ --- 5. Close any open menu dialogs.
766
+ --- 6. Send a "OnDbgLoadLocation" message.
767
+ ---
768
+ --- This function is typically used for debugging purposes, to quickly load a specific map and camera location.
769
+ function DbgLoadLocation(map, cam_params, editor_mode, map_rand)
770
+ if not MapData[map] then
771
+ print("No such map:", map)
772
+ return
773
+ end
774
+ CreateRealTimeThread(function()
775
+ EditorDeactivate()
776
+ if map ~= GetMapName() or map_rand and map_rand ~= MapLoadRandom then
777
+ if map_rand then
778
+ table.change(config, "DbgLoadLocation", {FixedMapLoadRandom=map_rand})
779
+ end
780
+ ChangeMap(map)
781
+ table.restore(config, "DbgLoadLocation", true)
782
+ end
783
+ if editor_mode then
784
+ EditorActivate()
785
+ end
786
+ if cam_params then
787
+ if cam_params[3] == "Fly" then
788
+ cam_params[3] = "Max"
789
+ SetCamera(table.unpack(cam_params))
790
+ cameraFly.Activate()
791
+ else
792
+ SetCamera(table.unpack(cam_params))
793
+ end
794
+ end
795
+ CloseMenuDialogs()
796
+ Msg("OnDbgLoadLocation")
797
+ end)
798
+ end
799
+
800
+ ---
801
+ --- Gets a string representation of the current camera location that can be used to restore the camera state.
802
+ ---
803
+ --- @return string A string that can be passed to `DbgLoadLocation` to restore the camera state.
804
+ ---
805
+ function GetCameraLocationString()
806
+ local cam_params
807
+ if cameraFly.IsActive() then
808
+ -- Fly camera doesn't expose its parameters, but it can be saved as Max and forced to Fly again on load
809
+ cameraMax.Activate()
810
+ cam_params = {GetCamera()}
811
+ cam_params[3] = "Fly"
812
+ cameraFly.Activate()
813
+ else
814
+ cam_params = {GetCamera()}
815
+ end
816
+ return string.format("DbgLoadLocation( \"%s\", %s, %s, %s)\n", GetMapName(), TableToLuaCode(cam_params, ' '),
817
+ IsEditorActive() and "true" or "false", tostring(MapLoadRandom))
818
+ end
819
+
820
+ function OnMsg.BugReportStart(print_func)
821
+ print_func(string.format("\nLocation: (paste in the console)\n%s", GetCameraLocationString()))
822
+ end
823
+
824
+ if FirstLoad then
825
+ g_ResetSceneCameraViewportThread = false
826
+ end
827
+
828
+ function OnMsg.SystemSize(pt)
829
+ --if FullscreenMode() == 0 then
830
+ DeleteThread(g_ResetSceneCameraViewportThread)
831
+ g_ResetSceneCameraViewportThread = CreateRealTimeThread(function()
832
+ WaitNextFrame(1)
833
+ SetupViews(pt)
834
+ end)
835
+ --end
836
+ end
837
+
838
+ ---
839
+ --- Checks if the given position is a valid camera position.
840
+ ---
841
+ --- @param pos point The position to check.
842
+ --- @return boolean True if the position is valid, false otherwise.
843
+ ---
844
+ local function IsValidCameraPos(pos)
845
+ return pos and pos ~= point30 and pos ~= InvalidPos()
846
+ end
847
+
848
+ ---
849
+ --- Checks if the camera can move between two positions without intersecting terrain.
850
+ ---
851
+ --- @param pos0 point The starting position for the camera movement.
852
+ --- @param pos1 point The ending position for the camera movement.
853
+ --- @return boolean True if the camera can move between the two positions without intersecting terrain, false otherwise.
854
+ ---
855
+ local function CanMoveCamBetween(pos0, pos1)
856
+ local max_move_dist = const.MaxMoveCamDist or max_int
857
+ if max_move_dist >= max_int or IsCloser(pos0, pos1, max_move_dist) then
858
+ return true
859
+ end
860
+ return not terrain.IntersectSegment(pos0, pos1)
861
+ end
862
+
863
+ ---
864
+ --- Moves the camera to view the specified object, optionally with a zoom level.
865
+ ---
866
+ --- @param obj table|point The object or position to view
867
+ --- @param time number The time in seconds for the camera to move to the new position
868
+ --- @param pos point The position to move the camera to
869
+ --- @param zoom number The zoom level to set the camera to
870
+ ---
871
+ function ViewObjectRTS(obj, time, pos, zoom)
872
+ if not obj then
873
+ return
874
+ end
875
+
876
+ local la = IsPoint(obj) and obj or IsValid(obj)
877
+ and (obj:HasMember("GetLogicalPos") and obj:GetLogicalPos() or obj:GetVisualPos())
878
+ if not la or la == InvalidPos() then
879
+ return
880
+ end
881
+ la = la:SetTerrainZ()
882
+
883
+ local cur_pos, cur_la = cameraRTS.GetPosLookAt()
884
+ if not pos then
885
+ local cur_off = cur_pos - cur_la
886
+ if not IsValidCameraPos(cur_pos) or cur_pos == cur_la then
887
+ local lookatDist = const.DefaultCameraRTS.LookatDistZoomIn
888
+ + (const.DefaultCameraRTS.LookatDistZoomOut - const.DefaultCameraRTS.LookatDistZoomIn)
889
+ * cameraRTS.GetZoom()
890
+ cur_off = SetLen(point(1, 1, 0), lookatDist * guim) + point(0, 0, cameraRTS.GetHeight() * guim)
891
+ zoom = zoom or 0.5
892
+ end
893
+ pos = la + cur_off
894
+ end
895
+ pos, la = cameraRTS.Normalize(pos, la)
896
+
897
+ if not IsValidCameraPos(cur_pos) or not CanMoveCamBetween(cur_pos, pos) then
898
+ time = 0
899
+ elseif not time then
900
+ local min_dist, max_dist = 200 * guim, 1000 * guim
901
+ local min_time, max_time = 200, 500
902
+ local dist_factor = Clamp(pos:Dist2D(cur_pos) - min_dist, 0, max_dist) * 100 / (max_dist - min_dist)
903
+ time = min_time + (max_time - min_time) * dist_factor / 100
904
+ end
905
+
906
+ cameraRTS.SetCamera(pos, la, time or 0, "Sin in/out")
907
+ if zoom then
908
+ cameraRTS.SetZoom(zoom, time or 0)
909
+ end
910
+ end
911
+
912
+ ---
913
+ --- Defines the available types of camera interpolation.
914
+ ---
915
+ --- @class CameraInterpolationTypes
916
+ --- @field linear integer Linear interpolation.
917
+ --- @field spherical integer Spherical interpolation.
918
+ --- @field polar integer Polar interpolation.
919
+ CameraInterpolationTypes = {linear=0, spherical=1, polar=2}
920
+
921
+ ---
922
+ --- Defines the available types of camera movement.
923
+ ---
924
+ --- @class CameraMovementTypes
925
+ --- @field linear integer Linear movement.
926
+ --- @field harmonic integer Harmonic movement.
927
+ --- @field accelerated integer Accelerated movement.
928
+ --- @field decelerated integer Decelerated movement.
929
+ CameraMovementTypes = {linear=0, harmonic=1, accelerated=2, decelerated=3}
930
+
931
+ ---
932
+ --- Sets the camera position and lookat point, taking into account the base offset and angle.
933
+ ---
934
+ --- @param pos table The camera position.
935
+ --- @param lookat table The camera lookat point.
936
+ --- @param base_offset table The base offset to apply to the position and lookat.
937
+ --- @param base_angle number The base angle to apply to the position and lookat.
938
+ --- @param camera_view integer The camera view to use.
939
+ ---
940
+ function SetCameraPosMaxLookAt(pos, lookat, base_offset, base_angle, camera_view)
941
+ cameraMax.SetPositionLookatAndRoll(base_offset + Rotate(pos, base_angle), base_offset + Rotate(lookat, base_angle),
942
+ 0)
943
+ end
944
+
945
+ ---
946
+ --- Interpolates the camera position and lookat point between two camera states over a given duration, relative to a reference object.
947
+ ---
948
+ --- @param camera1 table The initial camera state, with `pos` and `lookat` fields.
949
+ --- @param camera2 table The final camera state, with `pos` and `lookat` fields.
950
+ --- @param duration number The duration of the interpolation in frames.
951
+ --- @param relative_to Entity The entity to use as the reference for the camera position and lookat.
952
+ --- @param interpolation string The type of interpolation to use, one of "linear", "spherical", or "polar".
953
+ --- @param movement string The type of camera movement to use, one of "linear", "harmonic", "accelerated", or "decelerated".
954
+ --- @param camera_view integer The camera view to use.
955
+ ---
956
+ function InterpolateCameraMaxWakeup(camera1, camera2, duration, relative_to, interpolation, movement, camera_view)
957
+ camera_view = camera_view or 1
958
+
959
+ local base_offset = IsValid(relative_to) and relative_to:GetVisualPosPrecise(1000) or point30
960
+ local base_angle = IsValid(relative_to) and relative_to:GetVisualAngle() or 0
961
+
962
+ local camera2_pos = Rotate(camera2.pos * 1000 - base_offset, 360 * 60 - base_angle)
963
+ local camera2_lookat = Rotate(camera2.lookat * 1000 - base_offset, 360 * 60 - base_angle)
964
+ if duration > 1 then
965
+ local camera1_pos = Rotate(camera1.pos * 1000 - base_offset, 360 * 60 - base_angle)
966
+ local camera1_lookat = Rotate(camera1.lookat * 1000 - base_offset, 360 * 60 - base_angle)
967
+ SetCameraPosMaxLookAt(camera1_pos, camera1_lookat, base_offset, base_angle, camera_view)
968
+ for t = 1, duration do
969
+ if WaitWakeup(1) then
970
+ break
971
+ end
972
+ base_offset = IsValid(relative_to) and relative_to:GetVisualPosPrecise(1000) or point30
973
+ base_angle = IsValid(relative_to) and relative_to:GetVisualAngle() or 0
974
+ local p, l = CameraLerp(camera1_pos, camera1_lookat, camera2_pos, camera2_lookat, t, duration,
975
+ CameraInterpolationTypes[interpolation] or 0, CameraMovementTypes[movement] or 0)
976
+ SetCameraPosMaxLookAt(p, l, base_offset, base_angle, camera_view)
977
+ end
978
+ end
979
+ SetCameraPosMaxLookAt(camera2_pos, camera2_lookat, base_offset, base_angle, camera_view)
980
+ end
981
+
982
+ ---
983
+ --- Toggles the fly camera mode.
984
+ ---
985
+ --- If the fly camera is active, it deactivates the fly camera and applies the camera and controllers.
986
+ --- If the fly camera is not active, it activates the fly camera and recalculates the active player control.
987
+ --- It also sets the mouse delta mode accordingly.
988
+ ---
989
+ function CheatToggleFlyCamera()
990
+ if cameraFly.IsActive() then
991
+ SetMouseDeltaMode(false)
992
+ if rawget(_G, "GetPlayerControlObj") and GetPlayerControlObj() then
993
+ ApplyCameraAndControllers()
994
+ else
995
+ SetupInitialCamera()
996
+ end
997
+ else
998
+ print("Camera Fly")
999
+ cameraFly.Activate(1)
1000
+ if rawget(_G, "GetPlayerControlObj") and GetPlayerControlObj() then
1001
+ PlayerControl_RecalcActive(true)
1002
+ end
1003
+ SetMouseDeltaMode(true)
1004
+ end
1005
+ end
CommonLua/CameraControlUtils.lua ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --- Returns the camera eye position adjusted to be above the terrain.
2
+ -- @param EyePt point The camera eye position.
3
+ -- @param LookAtPt point The camera look at position.
4
+ -- @return point The adjusted camera eye position.
5
+ -- @return point The camera look at position.
6
+ function GetCameraEyeOverTerrain(EyePt, LookAtPt)
7
+ local height = GetWalkableZ(EyePt) + const.CameraMinTerrainDist
8
+ local EyePt = EyePt
9
+ if height > EyePt:z() then
10
+ EyePt = EyePt:SetZ(height)
11
+ end
12
+ return EyePt, LookAtPt
13
+ end
14
+
15
+ --- Moves camera look at and eye pos smoothly over the given period of time.
16
+ -- @cstyle int MoveCamera(function get_look_at, function get_eye, int time);
17
+ -- @param get_look_at function; a callback function that receives time as parameter and returns the camera look at position; the callback function is called every 33ms.
18
+ -- @param get_eye function; a callback function that receives time and look at position as parameter and returns the camera eye position; the callback function is called every 33ms.
19
+ -- @param time int.
20
+ -- @return int; orientation in minutes.
21
+
22
+ function MoveCamera(get_look_at, get_eye, time)
23
+ if not camera3p.IsActive() then
24
+ return
25
+ end
26
+ local sleep = 33
27
+ local time_from_start = 0
28
+ while true do
29
+ local time_to_end = time - time_from_start
30
+ local sleep_time = Min(sleep, time - time_from_start)
31
+ local target_time = Min(time_from_start+sleep_time, time)
32
+
33
+ local look_at = get_look_at(target_time)
34
+ local eye = get_eye (look_at, target_time)
35
+ eye = GetCameraEyeOverTerrain(eye, look_at)
36
+ camera3p.SetLookAt(look_at, sleep_time)
37
+ camera3p.SetEye (eye , sleep_time)
38
+ if sleep_time > 0 then
39
+ Sleep(sleep_time)
40
+ end
41
+ if not camera3p.IsActive() then
42
+ return
43
+ end
44
+ time_from_start = time_from_start + sleep_time
45
+ if time_from_start >= time then
46
+ break
47
+ end
48
+ end
49
+ end
50
+
51
+ --- Return a callback function that is to be used as get_look_at parameter of MoveCamera function.
52
+ -- The callback will move the current look at position from the camera current look at postion to the target_pos.
53
+ -- 'observing' the target object's movement - the farther the target moves from his start position, the farther.
54
+ -- the camera look at will move away from its initial position and will approach the target_pos.
55
+ -- @cstyle function LookAtFollowCharacter(object target, point target_pos, int total_time).
56
+ -- @param target object.
57
+ -- @param target_pos point.
58
+ -- @param total_time int.
59
+ -- @return function.
60
+ function LookAtFollowCharacter(target, target_pos, total_time)
61
+ if not camera3p.IsActive() then
62
+ return
63
+ end
64
+ local start_pt = camera3p.GetLookAt()
65
+ local last_dist = 0
66
+ local max_dist = start_pt:Dist(target_pos)
67
+ local pos_lerp = ValueLerp(start_pt, target_pos, max_dist)
68
+ local height_lerp = ValueLerp(start_pt:z(), target_pos:z(), total_time)
69
+ local last_pos
70
+ return function(time)
71
+ if IsValid(target) then
72
+ local pos = GetPosFromPosSpot(target)
73
+ local dist = Min(pos:Dist(start_pt), max_dist)
74
+ if dist > last_dist then
75
+ last_dist = dist
76
+ end
77
+ end
78
+ return pos_lerp(last_dist):SetZ(height_lerp(time))
79
+ end
80
+ end
81
+
82
+ --- Return a callback function that is to be used as get_eye parameter of MoveCamera function.
83
+ -- The callback will move smoothly the camera eye's z to the targetz, rotate the camera to the target_yaw, keeping the 2d distance from the eye to the look at to dist_eye_look_at.
84
+ -- @cstyle function RotateKeepDistEye(int target_eyez, int target_yaw, point dist_eye_look_at, int total_time).
85
+ -- @param target_eyez int.
86
+ -- @param target_yaw int.
87
+ -- @param dist_eye_look_at int.
88
+ -- @param total_time int.
89
+ -- @return function.
90
+ function RotateKeepDistEye(target_eyez, target_yaw, dist_eye_look_at, total_time)
91
+ local pt = point(-dist_eye_look_at, 0, 0)
92
+ local angle_lerp = AngleLerp(camera.GetYaw(), target_yaw, total_time)
93
+ local eye_height_lerp = ValueLerp(camera.GetEye():z(), target_eyez, total_time)
94
+ return function(look_at_pos, time)
95
+ local eye = look_at_pos + Rotate(pt, angle_lerp(time))
96
+ eye = eye:SetZ(eye_height_lerp(time))
97
+ return eye
98
+ end
99
+ end
100
+
101
+ --- This function will smoothly move/rotate the camera according the given parameters, mimicking the XCamera default behavior.
102
+ -- @cstyle void DefMoveCamera(point pos, int yaw, int pitch, int rot_speed, int move_speed, int move_time, int yaw_time, int pitch_time).
103
+ -- @param pos point; target camera look at position.
104
+ -- @param yaw int; targer camera yaw.
105
+ -- @param pitch int; target camera pitch.
106
+ -- @param rot_speed int; camera rotation speed in angular minutes per sec; can be omitted; used to calculate move_time in case move_time is omitted.
107
+ -- @param move_speed int; camera movement speed in angular minutes per sec; can be omitted; used to calculate yaw_time and pitch_time in case yaw_time or pitch_time are omitted.
108
+ -- @param move_time int; the time the camera should reach the target position; if omitted the time will be calculated from move_speed parameter.
109
+ -- @param yaw_time int; the time the camera should reach the target yaw; if omitted the time will be calculated from rot_speed parameter.
110
+ -- @param pitch_time int; the time the camera should reach the target position; if omitted the time will be calculated from rot_speed parameter.
111
+ -- @return void.
112
+ function DefMoveCamera(pos, yaw, dist_scale, pitch, rot_speed, move_speed, move_time, yaw_time, pitch_time)
113
+ if not camera3p.IsActive() then
114
+ return
115
+ end
116
+ if not pos:IsValidZ() then
117
+ pos = pos:SetTerrainZ()
118
+ end
119
+ local start_look_at, start_pitch, start_yaw = camera3p.GetLookAt(), camera3p.GetPitch(), camera3p.GetYaw()
120
+ local look_at_height_offset = (const.CameraScale*const.CameraVerticalOffset/100)*dist_scale/100
121
+
122
+ rot_speed = rot_speed or const.CameraRotationDegreePerSec
123
+ move_speed = move_speed or const.CameraResetMmPerSec
124
+
125
+ local pitch_time = pitch_time or abs(AngleDiff(start_pitch, pitch)/60)*1000/rot_speed
126
+ local yaw_time = yaw_time or abs(AngleDiff(start_yaw, yaw)/60)*1000/rot_speed
127
+ local move_time = move_time or pos:Dist(start_look_at)*1000/move_speed
128
+ local yaw_lerp = AngleLerp(start_yaw, yaw, yaw_time, true)
129
+ local pos_lerp = ValueLerp(start_look_at, pos:SetZ(look_at_height_offset + (pos:z() or terrain.GetHeight(pos))), move_time, true)
130
+ local start_l, start_h = GetCameraLH(start_pitch, camera3p.DistanceAtPitch(start_pitch) * dist_scale / 100)
131
+ local end_l , end_h = GetCameraLH( pitch, camera3p.DistanceAtPitch( pitch) * dist_scale / 100)
132
+
133
+ local l_lerp, h_lerp = ValueLerp(start_l, end_l, pitch_time, true), ValueLerp(start_h, end_h, pitch_time, true)
134
+
135
+
136
+ local function LookAt(t)
137
+ return pos_lerp(t)
138
+ end
139
+
140
+ local function EyePt(look_at, t)
141
+ local yaw = yaw_lerp(t)
142
+ local l, h = l_lerp(t), h_lerp(t)
143
+
144
+ local eye = (look_at+Rotate(point(-l, 0, 0), yaw)):SetZ(h+look_at:z())
145
+ return eye
146
+ end
147
+
148
+ MoveCamera(LookAt, EyePt, Max(pitch_time, yaw_time, move_time))
149
+ end
CommonLua/CameraMakeTransparent.lua ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- make objects that obstruct the view transparent (camera3p)
2
+ if FirstLoad then
3
+ g_CameraMakeTransparentEnabled = false
4
+ g_updateStepOpacityThread = false
5
+ g_CameraMakeTransparentThread = false
6
+ g_CMT_fade_out = false
7
+ g_CMT_fade_in = false
8
+ g_CMT_hidden = false
9
+ g_CMT_replaced = false
10
+ g_CMT_replaced_destroy = false
11
+ end
12
+
13
+ local CMT_fade_out = g_CMT_fade_out
14
+ local CMT_fade_in = g_CMT_fade_in
15
+ local CMT_hidden = g_CMT_hidden
16
+ local CMT_replaced = g_CMT_replaced
17
+ local CMT_replaced_destroy = g_CMT_replaced_destroy
18
+
19
+ local transparency_enum_flags = const.efCameraMakeTransparent
20
+ local transparency_surf_flags = EntitySurfaces.Walk + EntitySurfaces.Collision
21
+ local obstruct_view_refresh_time = const.ObstructViewRefreshTime
22
+ local fade_in_time = const.ObstructOpacityFadeInTime
23
+ local fade_out_time = const.ObstructOpacityFadeOutTime
24
+ local obstruct_opacity = const.ObstructOpacity
25
+ local obstruct_opacity_refresh_time = const.ObstructOpacityRefreshTime
26
+ local refresh_time = Max(obstruct_opacity_refresh_time, Max(fade_out_time, fade_in_time) / (100 - Clamp(obstruct_opacity, 0, 99)))
27
+ local opacity_change_fadein = fade_in_time <= 0 and 100 or (100 - obstruct_opacity) * refresh_time / fade_in_time
28
+ local opacity_change_fadeout = fade_out_time <= 0 and 100 or (100 - obstruct_opacity) * refresh_time / fade_out_time
29
+
30
+ local function ResetLists()
31
+ g_CMT_fade_out = {}
32
+ g_CMT_fade_in = {}
33
+ g_CMT_hidden = {}
34
+ g_CMT_replaced = {}
35
+ g_CMT_replaced_destroy = {}
36
+ CMT_fade_out = g_CMT_fade_out
37
+ CMT_fade_in = g_CMT_fade_in
38
+ CMT_hidden = g_CMT_hidden
39
+ CMT_replaced = g_CMT_replaced
40
+ CMT_replaced_destroy = g_CMT_replaced_destroy
41
+ end
42
+
43
+ if FirstLoad then
44
+ ResetLists()
45
+ end
46
+
47
+ function OnMsg.DoneMap()
48
+ g_updateStepOpacityThread = false
49
+ g_CameraMakeTransparentThread = false
50
+ ResetLists()
51
+ end
52
+
53
+ local function UpdateObstructors_StepOpacity(obstructors)
54
+ local view = 1
55
+ CMT_fade_in[view] = CMT_fade_in[view] or {}
56
+ CMT_fade_out[view] = CMT_fade_out[view] or {}
57
+ local vfade_in = CMT_fade_in[view]
58
+ local vfade_out = CMT_fade_out[view]
59
+ -- move fade_out objects to fade_in
60
+ for i = #vfade_out, 1, -1 do
61
+ local o = vfade_out[i]
62
+ if not (obstructors and obstructors[o]) then
63
+ assert(not vfade_in[o])
64
+ table.remove(vfade_out, i)
65
+ vfade_out[o] = nil
66
+ if o:GetOpacity() < 100 then
67
+ vfade_in[#vfade_in + 1] = o
68
+ vfade_in[o] = true
69
+ end
70
+ end
71
+ end
72
+ -- set the new fade_out
73
+ if obstructors then
74
+ for i = 1, #obstructors do
75
+ local o = obstructors[i]
76
+ if not vfade_out[o] then
77
+ vfade_out[#vfade_out + 1] = o
78
+ vfade_out[o] = true
79
+ end
80
+ if vfade_in[o] then
81
+ table.remove_entry(vfade_in, o)
82
+ vfade_in[o] = nil
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ local function UpdateObstructors_Hidden(view, obstructors)
89
+ -- logic for objects for which hide/show is immediate
90
+ local hidden_for_view = CMT_hidden[view]
91
+ CMT_hidden[view] = obstructors
92
+ if obstructors then
93
+ for i = 1, #obstructors do
94
+ local o = obstructors[i]
95
+ o:SetOpacity(0)
96
+ obstructors[o] = true
97
+ end
98
+ end
99
+ if hidden_for_view then
100
+ for i = 1, #hidden_for_view do
101
+ local o = hidden_for_view[i]
102
+ if IsValid(o) and not (obstructors and obstructors[o]) then
103
+ o:SetOpacity(100) -- show what was hidden in the previous tick
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ local function ClearObstructors()
110
+ for o in pairs(CMT_replaced) do
111
+ o:DestroyReplacement()
112
+ end
113
+ for view = 1, camera.GetViewCount() do
114
+ local vfade_out = CMT_fade_out[view]
115
+ if vfade_out then
116
+ for i = 1, #vfade_out do
117
+ local o = vfade_out[i]
118
+ if IsValid(o) then
119
+ o:SetOpacity(100)
120
+ end
121
+ end
122
+ end
123
+ local vfade_in = CMT_fade_in[view]
124
+ if vfade_in then
125
+ for i = 1, #vfade_in do
126
+ local o = vfade_in[i]
127
+ if IsValid(o) then
128
+ o:SetOpacity(100)
129
+ end
130
+ end
131
+ end
132
+ local hv = CMT_hidden[view]
133
+ if hv then
134
+ for i = 1, #hv do
135
+ local o = hv[i]
136
+ if IsValid(o) then
137
+ o:SetOpacity(100)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ ResetLists()
143
+ end
144
+
145
+ local function UpdateObstructors(view, get_obstructors)
146
+ local success, obstructors, obstructors_immediate = procall(get_obstructors, view)
147
+ UpdateObstructors_StepOpacity(obstructors)
148
+ UpdateObstructors_Hidden(view, obstructors_immediate)
149
+ end
150
+
151
+ local function UpdateObstructorsRefresh(cam, get_obstructors)
152
+ local refresh_time = obstruct_view_refresh_time
153
+ while true do
154
+ while IsEditorActive() do
155
+ Sleep(2 * refresh_time)
156
+ end
157
+ -- restore opacity of fade_in/fade_out objects
158
+ if not g_CameraMakeTransparentEnabled or not cam.IsActive() then
159
+ ClearObstructors()
160
+ while not g_CameraMakeTransparentEnabled or not cam.IsActive() do
161
+ Sleep(refresh_time)
162
+ end
163
+ end
164
+ for view = 1, camera.GetViewCount() do
165
+ UpdateObstructors(view, get_obstructors)
166
+ end
167
+ Sleep(refresh_time)
168
+ end
169
+ end
170
+
171
+ local function UpdateStepOpacity(view)
172
+ local vfade_out = CMT_fade_out[view]
173
+ if vfade_out then
174
+ for i = #vfade_out, 1, -1 do
175
+ local o = vfade_out[i]
176
+ if not IsValid(o) then
177
+ vfade_out[o] = nil
178
+ table.remove(vfade_out, i)
179
+ else
180
+ local new_opacity = o:GetOpacity() - opacity_change_fadeout
181
+ if new_opacity < obstruct_opacity then
182
+ new_opacity = obstruct_opacity
183
+ end
184
+ o:SetOpacity(new_opacity)
185
+ end
186
+ end
187
+ end
188
+ local vfade_in = CMT_fade_in[view]
189
+ if vfade_in then
190
+ for i = #vfade_in, 1, -1 do
191
+ local o = vfade_in[i]
192
+ local keep
193
+ if IsValid(o) then
194
+ local new_opacity = Min(100, o:GetOpacity() + opacity_change_fadein)
195
+ o:SetOpacity(new_opacity)
196
+ keep = new_opacity < 100
197
+ end
198
+ if not keep then
199
+ vfade_in[o] = nil
200
+ table.remove(vfade_in, i)
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ local function UpdateStepOpacityRefresh()
207
+ local refresh_time = refresh_time
208
+ while true do
209
+ for view = 1, camera.GetViewCount() do
210
+ UpdateStepOpacity(view)
211
+ end
212
+ Sleep(refresh_time)
213
+ end
214
+ end
215
+
216
+ local DistSegmentToPt = DistSegmentToPt
217
+ local camera_clip_extend_radius = const.CameraClipExtendRadius
218
+ local offset_z_150cm = 150*guic
219
+ local cone_radius_max = config.CameraTransparencyConeRadiusMax
220
+ local cone_radius_min = config.CameraTransparencyConeRadiusMin
221
+ if FirstLoad then
222
+ draw_transparency_cone = false
223
+ end
224
+
225
+ function ToggleTransparencyCone()
226
+ DbgClearVectors()
227
+ draw_transparency_cone = not draw_transparency_cone
228
+ end
229
+
230
+ local hide_filter = function(u, eye)
231
+ local posx, posy, posz = u:GetVisualPosXYZ()
232
+ local scale = u:GetScale()
233
+ local dist_to_eye = DistSegmentToPt(posx, posy, posz, 0, 0, u.height * scale / 100, eye, true)
234
+ return dist_to_eye < u.camera_radius * scale / 100 + camera_clip_extend_radius
235
+ end
236
+ local col_exec = function(o, list)
237
+ if not list[o] then
238
+ list[#list + 1] = o
239
+ list[o] = true
240
+ end
241
+ end
242
+ local function GetViewObstructorsCamera3p(view)
243
+ local eye = camera.GetEye(view)
244
+ local lookat = camera3p.GetLookAt(view)
245
+ if not eye or not eye:IsValid() then
246
+ return
247
+ end
248
+ local to_fade, to_fade_count
249
+ local to_hide = MapGet(eye, 4*guim, "Unit", hide_filter, eye) or {}
250
+ for i = 1, #to_hide do
251
+ to_hide[ to_hide[i] ] = true
252
+ end
253
+
254
+ for loc_player = 1, LocalPlayersCount do
255
+ local obj = GetPlayerControlCameraAttachedObj(loc_player)
256
+ if obj and obj:IsValidPos() then
257
+ local posx, posy, posz = obj:GetVisualPosXYZ()
258
+ local err1, to_fade1 = AsyncIntersectConeWithObstacles(
259
+ eye, point(posx, posy, posz + offset_z_150cm),
260
+ cone_radius_max, cone_radius_min,
261
+ transparency_enum_flags,
262
+ transparency_surf_flags,
263
+ draw_transparency_cone)
264
+ assert(not err1, err1)
265
+ if to_fade1 then
266
+ if to_fade then
267
+ for i = 1, #to_fade1 do
268
+ local o = to_fade1[i]
269
+ if not to_fade[o] then
270
+ to_fade_count = to_fade_count + 1
271
+ to_fade[to_fade_count] = o
272
+ to_fade[o] = true
273
+ end
274
+ end
275
+ else
276
+ to_fade = to_fade1
277
+ to_fade_count = #to_fade
278
+ for i = 1, to_fade_count do
279
+ to_fade[ to_fade[i] ] = true
280
+ end
281
+ end
282
+ end
283
+ end
284
+ end
285
+ if to_fade then
286
+ for i = 1, to_fade_count do
287
+ local col = to_fade[i]:GetRootCollection()
288
+ if col and not to_fade[col] then
289
+ to_fade[col] = true
290
+ local col_areapoint1 = eye
291
+ local col_areapoint2 = lookat
292
+ MapForEach(
293
+ col_areapoint1, col_areapoint2, 50*guim,
294
+ "attached", false, "collection", col.Index, true,
295
+ const.efVisible, col_exec , to_fade)
296
+ end
297
+ end
298
+ end
299
+ return to_fade, to_hide
300
+ end
301
+
302
+ function RestartCameraMakeTransparent()
303
+ StopCameraMakeTransparent()
304
+ if g_CameraMakeTransparentEnabled then
305
+ g_CameraMakeTransparentThread = CreateMapRealTimeThread(UpdateObstructorsRefresh, camera3p, GetViewObstructorsCamera3p)
306
+ g_updateStepOpacityThread = CreateMapRealTimeThread(UpdateStepOpacityRefresh)
307
+ end
308
+ end
309
+
310
+ function StopCameraMakeTransparent()
311
+ ClearObstructors()
312
+ if g_updateStepOpacityThread then
313
+ DeleteThread(g_updateStepOpacityThread)
314
+ g_updateStepOpacityThread = false
315
+ end
316
+ if g_CameraMakeTransparentThread then
317
+ DeleteThread(g_CameraMakeTransparentThread)
318
+ g_CameraMakeTransparentThread = false
319
+ end
320
+ end
321
+
322
+ OnMsg.NewMapLoaded = RestartCameraMakeTransparent
323
+ OnMsg.LoadGame = RestartCameraMakeTransparent
324
+ OnMsg.GameEnterEditor = StopCameraMakeTransparent
325
+
326
+ DefineClass.CameraTransparentWallReplacement = {
327
+ __parents = { "CObject", "ComponentAttach" },
328
+ flags = { efCameraMakeTransparent = false, efCameraRepulse = true, efSelectable = false, efWalkable = false, efCollision = false, efApplyToGrids = false, efShadow = false },
329
+ properties =
330
+ {
331
+ { id = "CastShadow", name = "Shadow from All", editor = "bool", default = false },
332
+ },
333
+ }
334
+
335
+ local function CameraSpecialWallReplaceObjects(o)
336
+ return { "(default)", "place_default", "" }
337
+ end
338
+
339
+ DefineClass.CameraSpecialWall = {
340
+ __parents = { "Object" },
341
+ flags = { efCameraMakeTransparent = true, efCameraRepulse = false },
342
+ properties = {
343
+ { id = "TransparentReplace", editor = "combo", items = CameraSpecialWallReplaceObjects },
344
+ },
345
+ TransparentReplace = "(default)",
346
+ replace_default = "",
347
+ replace_height_min = -guim,
348
+ replace_height_max = guim,
349
+ }
350
+
351
+ function OnMsg.ClassesPostprocess()
352
+ -- create unique GetAction and GetActionEnd functions per class
353
+ local replace_default = {}
354
+ ClassDescendants("CameraSpecialWall", function(class_name, class, replace_default)
355
+ if class.replace_default == "" then
356
+ local classname = class:GetEntity() .. "_Base"
357
+ if g_Classes[classname] then
358
+ replace_default[class] = classname
359
+ end
360
+ end
361
+ local properties = class.properties
362
+ local idx = table.find(properties, "id", "OnCollisionWithCamera")
363
+ if idx then
364
+ local idx_old = table.find(properties, "id", "TransparentReplace")
365
+ local prop = properties[idx_old]
366
+ table.remove(properties, idx_old)
367
+ table.insert(properties, idx + (idx < idx_old and 1 or 0), prop)
368
+ end
369
+ end, replace_default)
370
+ for class, value in pairs(replace_default) do
371
+ class.replace_default = value
372
+ end
373
+ end
374
+
375
+ local default_color = RGBA(128, 128, 128, 0)
376
+ local default_roughness = 0
377
+ local default_metallic = 0
378
+
379
+ function CameraSpecialWall:PlaceReplacement()
380
+ local replacement = CMT_replaced[self]
381
+ if replacement then
382
+ CMT_replaced_destroy[self] = nil
383
+ return
384
+ end
385
+ local classname = self.TransparentReplace
386
+ if classname == "place_default" then
387
+ classname = self.replace_default
388
+ elseif classname == "(default)" then
389
+ classname = self.replace_default
390
+ local pos = self:GetPos()
391
+ local height = pos:z() and pos:z() - GetWalkableZ(pos) or 0
392
+ if height < self.replace_height_min or height > self.replace_height_max then
393
+ classname = ""
394
+ elseif self:RotateAxis(0,0,4096):z() < 2048 then
395
+ -- inclined more then 45 degrees
396
+ classname = ""
397
+ end
398
+ end
399
+ local replaced_base
400
+ if classname ~= "" then
401
+ local color1, roughness1, metallic1 = self:GetColorizationMaterial(1)
402
+ local color2, roughness2, metallic2 = self:GetColorizationMaterial(2)
403
+ local color3, roughness3, metallic3 = self:GetColorizationMaterial(3)
404
+ local components = 0
405
+ if (color1 ~= default_color or roughness1 ~= default_roughness or metallic1 ~= default_metallic) or
406
+ (color2 ~= default_color or roughness2 ~= default_roughness or metallic2 ~= default_metallic) or
407
+ (color3 ~= default_color or roughness3 ~= default_roughness or metallic3 ~= default_metallic) then
408
+ components = const.cofComponentColorizationMaterial
409
+ end
410
+ replaced_base = PlaceObject(classname, nil, components)
411
+ replaced_base:SetMirrored(self:GetMirrored())
412
+ replaced_base:SetAxis(self:GetAxis())
413
+ replaced_base:SetAngle(self:GetAngle())
414
+ replaced_base:SetScale(self:GetScale())
415
+ replaced_base:SetColorModifier(self:GetColorModifier())
416
+ if components == const.cofComponentColorizationMaterial then
417
+ replaced_base:SetColorizationMaterial(1, color1, roughness1, metallic1)
418
+ replaced_base:SetColorizationMaterial(2, color2, roughness2, metallic2)
419
+ replaced_base:SetColorizationMaterial(3, color3, roughness3, metallic3)
420
+ end
421
+ local anim = self:GetStateText()
422
+ if anim ~= "idle" and replaced_base:HasState(anim) and not replaced_base:IsErrorState(anim) then
423
+ replaced_base:SetState(anim)
424
+ end
425
+ replaced_base:SetPos(self:GetVisualPosXYZ())
426
+ end
427
+ CMT_replaced[self] = replaced_base or true
428
+ end
429
+
430
+ function CameraSpecialWall:DestroyReplacement(delay)
431
+ local obj = CMT_replaced[self]
432
+ if obj then
433
+ if obj == true then
434
+ CMT_replaced[self] = nil
435
+ return
436
+ end
437
+ if (delay or 0) == 0 then
438
+ CMT_replaced[self] = nil
439
+ CMT_replaced_destroy[self] = nil
440
+ DoneObject(obj)
441
+ elseif not CMT_replaced_destroy[self] then
442
+ CMT_replaced_destroy[self] = RealTime() + delay
443
+ end
444
+ end
445
+ end
446
+
447
+ function CameraSpecialWall:SetOpacity(opacity)
448
+ if opacity < 100 then
449
+ self:PlaceReplacement()
450
+ else
451
+ self:DestroyReplacement()
452
+ end
453
+ Object.SetOpacity(self, opacity)
454
+ end
CommonLua/CanonizeFilename.lua ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ local filename_chars =
2
+ {
3
+ ['"'] = "'",
4
+ ["\\"] = "_",
5
+ ["/"] = "_",
6
+ [":"] = "-",
7
+ ["*"] = "+",
8
+ ["?"] = "_",
9
+ ["<"] = "(",
10
+ [">"] = ")",
11
+ ["|"] = "-",
12
+ }
13
+
14
+ local escape_symbols =
15
+ {
16
+ ["%%"] = "%%%%",
17
+ ["%("] = "%%(",
18
+ ["%)"] = "%%)",
19
+ ["%]"] = "%%]",
20
+ ["%["] = "%%[",
21
+ ["%-"] = "%%-",
22
+ ["%+"] = "%%+",
23
+ ["%*"] = "%%*",
24
+ ["%?"] = "%%?",
25
+ ["%$"] = "%%$",
26
+ ["%."] = "%%.",
27
+ ["%^"] = "%%^",
28
+ }
29
+
30
+ local filter = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ()_+-'"
31
+
32
+ local filename_strings =
33
+ {
34
+ ["A"] = { "À", "Á", "Â", "Ã", "Ä", "Å", "Æ", "Ā", "Ă", "Ą", "Ǟ", "ǟ", "Ǡ", "ǡ", "Ǣ", "ǣ", "ǻ", "Ǽ", "ǽ", "Ȁ", "ȁ", "Ȃ", "ȃ" },
35
+ ["a"] = { "à", "á", "â", "ã", "ä", "å", "æ", "ā", "ă", "ą", },
36
+ ["C"] = { "Ç" },
37
+ ["c"] = { "ç", },
38
+ ["D"] = { "Ď", "Đ", "Ð", },
39
+ ["d"] = { "ď", "đ", "ð" },
40
+ ["E"] = { "È", "É", "Ê", "Ë", "Ĕ", "Ė", "Ę", "Ě", },
41
+ ["e"] = { "ė", "ę", "ĕ", "ě", "è", "é", "ê", "ë" },
42
+ ["G"] = { "Ĝ", "Ġ", "Ğ", "Ģ", },
43
+ ["g"] = { "ğ", "ĝ", "ġ", "ģ" },
44
+ ["H"] = { "Ĥ", "Ħ", },
45
+ ["h"] = { "ĥ", "ħ" },
46
+ ["I"] = { "Ì", "Í", "Î", "Ï", "Į", "Ĭ", "Ī", "Ĩ", "IJ", "İ", },
47
+ ["i"] = { "ı", "ij", "ĩ", "ī", "ĭ", "į", "ì", "í", "î", "ï", },
48
+ ["J"] = { "ĵ", "ĵ", "ĵ" },
49
+ ["K"] = { "Ķ", },
50
+ ["k"] = { "ķ", "ĸ" },
51
+ ["L"] = { "Ł", "Ŀ", "Ľ", "Ĺ", "Ļ", },
52
+ ["l"] = { "ļ", "ĺ", "ľ", "ŀ", "ł" },
53
+ ["N"] = { "Ņ", "Ń", "Ň", "Ŋ", "Ñ", },
54
+ ["n"] = { "ñ", "ŋ", "ň", "ń", "ņ", "ʼn", },
55
+ ["O"] = { "Ò", "Ó", "Ô", "Õ", "Õ", "Ö", "Ø", "Ō", "Ŏ", "Ŏ", "Ő", "Œ", },
56
+ ["o"] = { "ò", "ó", "ô", "õ", "ö", "ø", "ō", "ő", "œ" },
57
+ ["R"] = { "Ŕ", "Ŗ", "Ř", },
58
+ ["r"] = { "ř", "ŗ", "ŕ", },
59
+ ["S"] = { "Ś", "Ŝ", "Ş", "Š", },
60
+ ["s"] = { "ß", "ś", "ŝ", "ŝ", "ş", "š" },
61
+ ["T"] = { "Þ", "Ţ", "Ť", "Ŧ", },
62
+ ["t"] = { "þ", "ţ", "ť", "ŧ", },
63
+ ["U"] = { "Ũ", "Ū", "Ŭ", "Ů", "Ų", "Ű", "Ù", "Ú", "Û", "Ü", },
64
+ ["u"] = { "ù", "ú", "û", "ü", "ű", "ų", "ů", "ŭ", "ū", "ũ", },
65
+ ["W"] = { "Ŵ", },
66
+ ["w"] = { "ŵ" },
67
+ ["Y"] = { "Ý", "Ŷ", "Ÿ", },
68
+ ["y"] = { "ý", "ÿ", "ŷ" },
69
+ ["Z"] = { "Ź", "Ż", "Ž", },
70
+ ["z"] = { "ż", "ź", "ž" },
71
+ ["'"] = { "“", "”" },
72
+ }
73
+
74
+ function CanonizeSaveGameName(name)
75
+ if not name then return end
76
+
77
+ name = name:gsub("(.)", filename_chars)
78
+ for k,v in pairs(filename_strings) do
79
+ if type(v) == "string" then
80
+ name = name:gsub(v, k)
81
+ elseif type(v) == "table" then
82
+ for i=1,#v do
83
+ name = name:gsub(v[i], k)
84
+ end
85
+ end
86
+ end
87
+ return name
88
+ end
89
+
90
+ function EscapePatternMatchingMagicSymbols(name)
91
+ for k,v in sorted_pairs(escape_symbols) do
92
+ name = name:gsub(k, v)
93
+ end
94
+ return name
95
+ end
CommonLua/Classes/Achievement.lua ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ach_print = CreatePrint{
2
+ --"ach",
3
+ }
4
+
5
+ -- Game-specific hooks, titles should override these:
6
+
7
+ function CanUnlockAchievement(achievement)
8
+ local reasons = {}
9
+ Msg("UnableToUnlockAchievementReasons", reasons, achievement)
10
+ local reason = next(reasons)
11
+ return not reason, reason
12
+ end
13
+
14
+ -- Platform-specific functions:
15
+
16
+ function AsyncAchievementUnlock(achievement)
17
+ Msg("AchievementUnlocked", achievement)
18
+ end
19
+
20
+ function SynchronizeAchievements() end
21
+
22
+ PlatformCanUnlockAchievement = return_true
23
+
24
+ CheatPlatformUnlockAllAchievements = empty_func
25
+ CheatPlatformResetAllAchievements = empty_func
26
+
27
+ -- Common functions:
28
+
29
+ -- return unlocked, secret
30
+ function GetAchievementFlags(achievement)
31
+ return AccountStorage.achievements.unlocked[achievement], AchievementPresets[achievement].secret
32
+ end
33
+
34
+ function GetUnlockedAchievementsCount()
35
+ local unlocked, total = 0, 0
36
+ ForEachPreset(Achievement, function(achievement)
37
+ if not achievement:IsCurrentlyUsed() then return end
38
+ unlocked = unlocked + (AccountStorage.achievements.unlocked[achievement.id] and 1 or 0)
39
+ total = total + 1
40
+ end)
41
+ return unlocked, total
42
+ end
43
+
44
+ function _CheckAchievementProgress(achievement, dont_unlock_in_provider)
45
+ local progress = AccountStorage.achievements.progress[achievement] or 0
46
+ local target = AchievementPresets[achievement].target
47
+ if target and progress >= target then
48
+ AchievementUnlock(achievement, dont_unlock_in_provider)
49
+ end
50
+ end
51
+
52
+ local function EngineCanUnlockAchievement(achievement)
53
+ if Platform.demo then return false, "not available in demo" end
54
+ if GameState.Tutorial then
55
+ return false, "in tutorial"
56
+ end
57
+ if AccountStorage.achievements.unlocked[achievement] then
58
+ return false, "already unlocked"
59
+ end
60
+ assert(AchievementPresets[achievement])
61
+ if not AchievementPresets[achievement] then
62
+ return false, "dlc not present"
63
+ end
64
+ return PlatformCanUnlockAchievement(achievement)
65
+ end
66
+
67
+ local function CanModifyAchievementProgress(achievement)
68
+ -- 1. Engine-specific reasons not to modify achievement progress?
69
+ local success, reason = EngineCanUnlockAchievement(achievement)
70
+ if not success then
71
+ ach_print("cannot modify achievement progress, forbidden by engine check ", achievement, reason)
72
+ return false
73
+ end
74
+
75
+ -- 2. Game-specific reasons not to modify achievement progress?
76
+ local success, reason = CanUnlockAchievement(achievement)
77
+ if not success then
78
+ ach_print("cannot modify achievement progress, forbidden by title-specific check ", achievement, reason)
79
+ return false
80
+ end
81
+
82
+ return true
83
+ end
84
+
85
+ function AddAchievementProgress(achievement, progress, max_delay_save)
86
+ if not CanModifyAchievementProgress(achievement) then
87
+ return
88
+ end
89
+
90
+ local ach = AchievementPresets[achievement]
91
+ local current = AccountStorage.achievements.progress[achievement] or 0
92
+ local save_storage = not ach.save_interval or ((current + progress) / ach.save_interval > (current / ach.save_interval))
93
+ local total = current + progress
94
+ local target = ach.target or 0
95
+ if total >= target then
96
+ total = target
97
+ save_storage = false
98
+ end
99
+ AccountStorage.achievements.progress[achievement] = total
100
+ if save_storage then
101
+ SaveAccountStorage(max_delay_save)
102
+ end
103
+ Msg("AchievementProgress", achievement)
104
+ _CheckAchievementProgress(achievement)
105
+
106
+ return true
107
+ end
108
+
109
+ function ClearAchievementProgress(achievement, max_delay_save)
110
+ if not CanModifyAchievementProgress(achievement) then
111
+ return
112
+ end
113
+
114
+ AccountStorage.achievements.progress[achievement] = 0
115
+ SaveAccountStorage(max_delay_save)
116
+ Msg("AchievementProgress", achievement)
117
+
118
+ return true
119
+ end
120
+
121
+ -- Synchronous version, launches a thread
122
+ function AchievementUnlock(achievement, dont_unlock_in_provider)
123
+ if not CanModifyAchievementProgress(achievement) then
124
+ return
125
+ end
126
+
127
+ -- We set this before the thread, as otherwise calling AchievementUnlock twice will attempt to unlock it twice
128
+ AccountStorage.achievements.unlocked[achievement] = true
129
+ if not dont_unlock_in_provider then
130
+ AsyncAchievementUnlock(achievement)
131
+ end
132
+
133
+ SaveAccountStorage(5000)
134
+ return true
135
+ end
136
+
137
+ if Platform.developer then
138
+ function AchievementUnlockAll()
139
+ CreateRealTimeThread(function()
140
+ for id, achievement_data in sorted_pairs(AchievementPresets) do
141
+ AchievementUnlock(id)
142
+ Sleep(100)
143
+ end
144
+ end)
145
+ end
146
+ end
147
+
148
+ function OnMsg.NetConnect()
149
+ local unlocked = AccountStorage and AccountStorage.achievements and AccountStorage.achievements.unlocked
150
+ if not unlocked then return end
151
+
152
+ local achievements = {}
153
+ ForEachPreset(Achievement, function(achievement)
154
+ if unlocked[achievement.id] then
155
+ table.insert(achievements, achievement.id)
156
+ end
157
+ end)
158
+
159
+ NetGossip("AllAchievementsUnlocked", achievements)
160
+ end
CommonLua/Classes/ActionFX.lua ADDED
The diff for this file is too large to render. See raw diff
 
CommonLua/Classes/AnimMomentHook.lua ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DefineClass.AnimChangeHook =
2
+ {
3
+ __parents = { "Object", "Movable" },
4
+ }
5
+
6
+ function AnimChangeHook:AnimationChanged(channel, old_anim, flags, crossfade)
7
+ end
8
+
9
+ function AnimChangeHook:SetState(anim, flags, crossfade, ...)
10
+ local old_anim = self:GetStateText()
11
+ if IsValid(self) and self:IsAnimEnd() then
12
+ self:OnAnimMoment("end")
13
+ end
14
+ Object.SetState(self, anim, flags, crossfade, ...)
15
+ self:AnimationChanged(1, old_anim, flags, crossfade)
16
+ end
17
+
18
+ local pfStep = pf.Step
19
+ local pfSleep = Sleep
20
+ function AnimChangeHook:Step(...)
21
+ local old_state = self:GetState()
22
+ local status, new_path = pfStep(self, ...)
23
+ if old_state ~= self:GetState() then
24
+ self:AnimationChanged(1, GetStateName(old_state), 0, nil)
25
+ end
26
+ return status, new_path
27
+ end
28
+
29
+ function AnimChangeHook:SetAnim(channel, anim, flags, crossfade, ...)
30
+ local old_anim = self:GetStateText()
31
+ Object.SetAnim(self, channel, anim, flags, crossfade, ...)
32
+ self:AnimationChanged(channel, old_anim, flags, crossfade)
33
+ end
34
+
35
+ -- AnimMomentHook
36
+ DefineClass.AnimMomentHook =
37
+ {
38
+ __parents = { "AnimChangeHook" },
39
+ anim_moments_hook = false, -- list with moments which have registered callback in the class
40
+ anim_moments_single_thread = false, -- if false every moment will have its own thread launched
41
+ anim_moments_hook_threads = false,
42
+ anim_moment_fx_target = false,
43
+ }
44
+
45
+ function AnimMomentHook:Init()
46
+ self:StartAnimMomentHook()
47
+ end
48
+
49
+ function AnimMomentHook:Done()
50
+ self:StopAnimMomentHook()
51
+ end
52
+
53
+ function AnimMomentHook:IsStartedAnimMomentHook()
54
+ return self.anim_moments_hook_threads and true or false
55
+ end
56
+
57
+ function AnimMomentHook:WaitAnimMoment(moment)
58
+ repeat
59
+ local t = self:TimeToMoment(1, moment)
60
+ local index = 1
61
+ while t == 0 do
62
+ index = index + 1
63
+ t = self:TimeToMoment(1, moment, index)
64
+ end
65
+ until not WaitWakeup(t) -- if someone wakes us up we need to measure again
66
+ end
67
+
68
+ moment_hooks = {}
69
+
70
+ function AnimMomentHook:OnAnimMoment(moment, anim)
71
+ anim = anim or GetStateName(self)
72
+ PlayFX(FXAnimToAction(anim), moment, self, self.anim_moment_fx_target or nil)
73
+ local anim_moments_hook = self.anim_moments_hook
74
+ if type(anim_moments_hook) == "table" and anim_moments_hook[moment] then
75
+ local method = moment_hooks[moment]
76
+ return self[method](self, anim)
77
+ end
78
+ end
79
+
80
+ function WaitTrackMoments(obj, callback, ...)
81
+ callback = callback or obj.OnAnimMoment
82
+ local last_state, last_phase, state_name, time, moment
83
+ while true do
84
+ local state, phase = obj:GetState(), obj:GetAnimPhase()
85
+ if state ~= last_state then
86
+ state_name = GetStateName(state)
87
+ if phase == 0 then
88
+ callback(obj, "start", state_name, ...)
89
+ end
90
+ time = nil
91
+ end
92
+ last_state, last_phase = state, phase
93
+ if not time then
94
+ moment, time = obj:TimeToNextMoment(1, 1)
95
+ end
96
+ if time then
97
+ local time_to_end = obj:TimeToAnimEnd()
98
+ if time_to_end <= time then
99
+ if not WaitWakeup(time_to_end) then
100
+ assert(IsValid(obj))
101
+ callback(obj, "end", state_name, ...)
102
+ if obj:IsAnimLooping(1) then
103
+ callback(obj, "start", state_name, ...)
104
+ end
105
+ time = time - time_to_end
106
+ else
107
+ time = false
108
+ end
109
+ end
110
+ -- if someone wakes us we need to query for a new moment
111
+ if time then
112
+ if time > 0 and WaitWakeup(time) then
113
+ time = nil
114
+ else
115
+ assert(IsValid(obj))
116
+ local index = 1
117
+ repeat
118
+ callback(obj, moment, state_name, ...)
119
+ index = index + 1
120
+ moment, time = obj:TimeToNextMoment(1, index)
121
+ until time ~= 0
122
+ if not time then
123
+ WaitWakeup()
124
+ end
125
+ end
126
+ end
127
+ else
128
+ WaitWakeup()
129
+ end
130
+ end
131
+ end
132
+
133
+ local gofRealTimeAnim = const.gofRealTimeAnim
134
+
135
+ function AnimMomentHook:StartAnimMomentHook()
136
+ local moments = self.anim_moments_hook
137
+ if not moments or self.anim_moments_hook_threads then
138
+ return
139
+ end
140
+ if not IsValidEntity(self:GetEntity()) then
141
+ return
142
+ end
143
+ local create_thread = self:GetGameFlags(gofRealTimeAnim) ~= 0 and CreateMapRealTimeThread or CreateGameTimeThread
144
+ local threads
145
+ if self.anim_moments_single_thread then
146
+ threads = { create_thread(WaitTrackMoments, self) }
147
+ ThreadsSetThreadSource(threads[1], "AnimMoment")
148
+ else
149
+ threads = { table.unpack(moments) }
150
+ for _, moment in ipairs(moments) do
151
+ threads[i] = create_thread(function(self, moment)
152
+ local method = moment_hooks[moment]
153
+ while true do
154
+ self:WaitAnimMoment(moment)
155
+ assert(IsValid(self))
156
+ self[method](self)
157
+ end
158
+ end, self, moment)
159
+ ThreadsSetThreadSource(threads[i], "AnimMoment")
160
+ end
161
+ end
162
+ self.anim_moments_hook_threads = threads
163
+ end
164
+
165
+ function AnimMomentHook:StopAnimMomentHook()
166
+ local thread_list = self.anim_moments_hook_threads or ""
167
+ for i = 1, #thread_list do
168
+ DeleteThread(thread_list[i])
169
+ end
170
+ self.anim_moments_hook_threads = nil
171
+ end
172
+
173
+ function AnimMomentHook:AnimMomentHookUpdate()
174
+ for i, thread in ipairs(self.anim_moments_hook_threads) do
175
+ Wakeup(thread)
176
+ end
177
+ end
178
+
179
+ AnimMomentHook.AnimationChanged = AnimMomentHook.AnimMomentHookUpdate
180
+
181
+ function OnMsg.ClassesPostprocess()
182
+ local str_to_moment_list = {} -- optimized to have one copy of each unique moment list
183
+
184
+ ClassDescendants("AnimMomentHook", function(class_name, class, remove_prefix, str_to_moment_list)
185
+ local moment_list
186
+ for name, func in pairs(class) do
187
+ local moment = remove_prefix(name, "OnMoment")
188
+ if type(func) == "function" and moment and moment ~= "" then
189
+ moment_list = moment_list or {}
190
+ moment_list[#moment_list + 1] = moment
191
+ end
192
+ end
193
+ for name, func in pairs(getmetatable(class)) do
194
+ local moment = remove_prefix(name, "OnMoment")
195
+ if type(func) == "function" and moment and moment ~= "" then
196
+ moment_list = moment_list or {}
197
+ moment_list[#moment_list + 1] = moment
198
+ end
199
+ end
200
+ if moment_list then
201
+ table.sort(moment_list)
202
+ for _, moment in ipairs(moment_list) do
203
+ moment_list[moment] = true
204
+ moment_hooks[moment] = moment_hooks[moment] or ("OnMoment" .. moment)
205
+ end
206
+ local str = table.concat(moment_list, " ")
207
+ moment_list = str_to_moment_list[str] or moment_list
208
+ str_to_moment_list[str] = moment_list
209
+ rawset(class, "anim_moments_hook", moment_list)
210
+ end
211
+ end, remove_prefix, str_to_moment_list)
212
+ end
213
+
214
+ ---
215
+ DefineClass.StepObjectBase =
216
+ {
217
+ __parents = { "AnimMomentHook" },
218
+ }
219
+
220
+ function StepObjectBase:StopAnimMomentHook()
221
+ AnimMomentHook.StopAnimMomentHook(self)
222
+ end
223
+
224
+ if not Platform.ged then
225
+ function OnMsg.ClassesGenerate()
226
+ AppendClass.EntitySpecProperties = {
227
+ properties = {
228
+ { id = "FXTargetOverride", name = "FX target override", category = "Misc", default = false,
229
+ editor = "combo", items = function(fx) return ActionFXClassCombo(fx) end, entitydata = true,
230
+ },
231
+ { id = "FXTargetSecondary", name = "FX target secondary", category = "Misc", default = false,
232
+ editor = "combo", items = function(fx) return ActionFXClassCombo(fx) end, entitydata = true,
233
+ },
234
+ },
235
+ }
236
+ end
237
+ end
238
+
239
+ function GetObjMaterialFXTarget(obj)
240
+ local entity_data = obj and EntityData[obj:GetEntity()]
241
+ entity_data = entity_data and entity_data.entity
242
+ if entity_data and entity_data.FXTargetOverride then
243
+ return entity_data.FXTargetOverride, entity_data.FXTargetSecondary
244
+ end
245
+
246
+ local mat_type = obj and obj:GetMaterialType()
247
+ local material_preset = mat_type and (Presets.ObjMaterial.Default or empty_table)[mat_type]
248
+ local fx_target = (material_preset and material_preset.FXTarget ~= "") and material_preset.FXTarget or mat_type
249
+
250
+ return fx_target, entity_data and entity_data.FXTargetSecondary
251
+ end
252
+
253
+ local surface_fx_types = {}
254
+ local enum_decal_water_radius = const.AnimMomentHookEnumDecalWaterRadius
255
+
256
+ function GetObjMaterial(pos, obj, surfaceType, fx_target_secondary)
257
+ local surfacePos = pos
258
+ if not surfaceType and obj then
259
+ surfaceType, fx_target_secondary = GetObjMaterialFXTarget(obj)
260
+ end
261
+
262
+ local propagate_above
263
+ if pos and not surfaceType then
264
+ propagate_above = true
265
+ if terrain.IsWater(pos) then
266
+ local water_z = terrain.GetWaterHeight(pos)
267
+ local dz = (pos:z() or terrain.GetHeight(pos)) - water_z
268
+ if dz >= const.FXWaterMinOffsetZ and dz <= const.FXWaterMaxOffsetZ then
269
+ if const.FXShallowWaterOffsetZ > 0 and dz > -const.FXShallowWaterOffsetZ then
270
+ surfaceType = "ShallowWater"
271
+ else
272
+ surfaceType = "Water"
273
+ end
274
+ surfacePos = pos:SetZ(water_z)
275
+ end
276
+ end
277
+ if not surfaceType and enum_decal_water_radius then
278
+ local decal = MapFindNearest(pos, pos, enum_decal_water_radius, "TerrainDecal", function (obj, pos)
279
+ if pos:InBox2D(obj) then
280
+ local dz = (pos:z() or terrain.GetHeight(pos)) - select(3, obj:GetVisualPosXYZ())
281
+ if dz <= const.FXDecalMaxOffsetZ and dz >= const.FXDecalMinOffsetZ then
282
+ return true
283
+ end
284
+ end
285
+ end, pos)
286
+ if decal then
287
+ surfaceType = decal:GetMaterialType()
288
+ if surfaceType then
289
+ surfacePos = pos:SetZ(select(3, decal:GetVisualPosXYZ()))
290
+ end
291
+ end
292
+ end
293
+ if not surfaceType then
294
+ -- get the surface type
295
+ local walkable_slab = const.SlabSizeX and WalkableSlabByPoint(pos) or GetWalkableObject(pos)
296
+ if walkable_slab then
297
+ surfaceType = walkable_slab:GetMaterialType()
298
+ if surfaceType then
299
+ surfacePos = pos:SetZ(select(3, walkable_slab:GetVisualPosXYZ()))
300
+ end
301
+ else
302
+ local terrain_preset = TerrainTextures[terrain.GetTerrainType(pos)]
303
+ surfaceType = terrain_preset and terrain_preset.type
304
+ if surfaceType then
305
+ surfacePos = pos:SetTerrainZ()
306
+ end
307
+ end
308
+ end
309
+ end
310
+
311
+ local fx_type
312
+ if surfaceType then
313
+ fx_type = surface_fx_types[surfaceType]
314
+ if not fx_type then -- cache it for later use
315
+ fx_type = "Surface:" .. surfaceType
316
+ surface_fx_types[surfaceType] = fx_type
317
+ end
318
+ end
319
+ local fx_type_secondary
320
+ if fx_target_secondary then
321
+ fx_type_secondary = surface_fx_types[fx_target_secondary]
322
+ if not fx_type_secondary then -- cache it for later use
323
+ fx_type_secondary = "Surface:" .. fx_target_secondary
324
+ surface_fx_types[fx_target_secondary] = fx_type_secondary
325
+ end
326
+ end
327
+
328
+ return fx_type, surfacePos, propagate_above, fx_type_secondary
329
+ end
330
+
331
+
332
+ local enum_bush_radius = const.AnimMomentHookTraverseVegetationRadius
333
+
334
+ function StepObjectBase:PlayStepSurfaceFX(foot, spot_name)
335
+ local spot = self:GetRandomSpot(spot_name)
336
+ local pos = self:GetSpotLocPos(spot)
337
+ local surface_fx_type, surface_pos, propagate_above = GetObjMaterial(pos)
338
+
339
+ if surface_fx_type then
340
+ local angle, axis = self:GetSpotVisualRotation(spot)
341
+ local dir = RotateAxis(axis_x, axis, angle)
342
+ local actionFX = self:GetStepActionFX()
343
+ PlayFX(actionFX, foot, self, surface_fx_type, surface_pos, dir)
344
+ end
345
+
346
+ if propagate_above and enum_bush_radius then
347
+ local bushes = MapGet(pos, enum_bush_radius, "TraverseVegetation", function(obj, pos) return pos:InBox(obj) end, pos)
348
+ if bushes and bushes[1] then
349
+ local veg_event = PlaceObject("VegetationTraverseEvent")
350
+ veg_event:SetPos(pos)
351
+ veg_event:SetActors(self, bushes)
352
+ end
353
+ end
354
+ end
355
+
356
+ function StepObjectBase:GetStepActionFX()
357
+ return "Step"
358
+ end
359
+
360
+ DefineClass.StepObject = {
361
+ __parents = { "StepObjectBase" },
362
+ }
363
+
364
+ function StepObject:OnMomentFootLeft()
365
+ self:PlayStepSurfaceFX("FootLeft", "Leftfoot")
366
+ end
367
+
368
+ function StepObject:OnMomentFootRight()
369
+ self:PlayStepSurfaceFX("FootRight", "Rightfoot")
370
+ end
371
+
372
+ function OnMsg.GatherFXActions(list)
373
+ list[#list+1] = "Step"
374
+ end
375
+
376
+ function OnMsg.GatherFXTargets(list)
377
+ local added = {}
378
+ ForEachPreset("TerrainObj", function(terrain_preset)
379
+ local type = terrain_preset.type
380
+ if type ~= "" and not added[type] then
381
+ list[#list+1] = "Surface:" .. type
382
+ added[type] = true
383
+ end
384
+ end)
385
+ local material_types = PresetsCombo("ObjMaterial")()
386
+ for i = 2, #material_types do
387
+ local type = material_types[i]
388
+ if not added[type] then
389
+ list[#list+1] = "Surface:" .. type
390
+ added[type] = true
391
+ end
392
+ end
393
+ end
394
+
395
+ DefineClass.AutoAttachAnimMomentHookObject = {
396
+ __parents = {"AutoAttachObject", "AnimMomentHook"},
397
+
398
+ anim_moments_single_thread = true,
399
+ anim_moments_hook = true,
400
+ }
401
+
402
+ function AutoAttachAnimMomentHookObject:SetState(...)
403
+ AutoAttachObject.SetState(self, ...)
404
+ AnimMomentHook.SetState(self, ...)
405
+ end
406
+
407
+ function AutoAttachAnimMomentHookObject:OnAnimMoment(moment, anim)
408
+ return AnimMomentHook.OnAnimMoment(self, moment, anim)
409
+ end
CommonLua/Classes/AppearanceObject.lua ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DefineClass.AppearanceObjectPart = {
2
+ __parents = { "CObject", "ComponentAnim", "ComponentAttach", "ComponentCustomData" },
3
+ flags = { gofSyncState = true, cofComponentColorizationMaterial = true },
4
+ }
5
+
6
+ function ValidAnimationsCombo(character)
7
+ local all_anims = character:GetStatesTextTable()
8
+
9
+ local valid_anims = {}
10
+ for _, anim in ipairs(all_anims) do
11
+ if anim:sub(-1, -1) ~= "*" then
12
+ table.insert(valid_anims, anim)
13
+ end
14
+ end
15
+
16
+ return valid_anims
17
+ end
18
+
19
+ DefineClass.AppearanceObject = {
20
+ __parents = { "Shapeshifter", "StripComponentAttachProperties", "ComponentAnim"},
21
+ flags = { gofSyncState = true, cofComponentColorizationMaterial = true },
22
+
23
+ properties =
24
+ {
25
+ { category = "Animation", id = "Appearance", name = "Appearance", editor = "preset_id", preset_class = "AppearancePreset", default = "" },
26
+ { category = "Animation", id = "anim", name = "Animation", editor = "dropdownlist", items = ValidAnimationsCombo, default = "idle" },
27
+ { category = "Animation Blending", id = "animWeight", name = "Animation Weight", editor = "number", slider = true, min = 0, max = 100, default = 100, help = "100 means only Animation is played, 0 means only Animation 2 is played, 50 means both animations are blended equally" },
28
+ { category = "Animation Blending", id = "animBlendTime", name = "Animation Blend Time", editor = "number", min = 0, default = 0 },
29
+ { category = "Animation Blending", id = "anim2", name = "Animation 2", editor = "dropdownlist", items = function(character) local list = character:GetStatesTextTable() table.insert(list, 1, "") return list end, default = "" },
30
+ { category = "Animation Blending", id = "anim2BlendTime", name = "Animation 2 Blend Time", editor = "number", min = 0, default = 0 },
31
+ },
32
+
33
+ fallback_body = config.DefaultAppearanceBody,
34
+ parts = false,
35
+
36
+ animFlags = 0,
37
+ animCrossfade = 0,
38
+ anim2Flags = 0,
39
+ anim2Crossfade = 0,
40
+
41
+ attached_parts = { "Head", "Pants", "Shirt", "Armor", "Hat", "Hat2", "Hair", "Chest", "Hip" },
42
+ animated_parts = { "Head", "Pants", "Shirt", "Armor" },
43
+ appearance_applied = false,
44
+
45
+ anim_speed = 1000,
46
+ }
47
+
48
+ function AppearanceObject:PostLoad()
49
+ self:ApplyAppearance()
50
+ end
51
+
52
+ function AppearanceObject:OnEditorSetProperty(prop_id)
53
+ if prop_id == "Appearance" then
54
+ self:ApplyAppearance()
55
+ end
56
+ end
57
+
58
+ function AppearanceObject:Setanim(anim)
59
+ self.anim = anim
60
+ self:SetAnimHighLevel()
61
+ end
62
+
63
+ function AppearanceObject:Setanim2(anim)
64
+ self.anim2 = anim
65
+ self:SetAnimHighLevel()
66
+ end
67
+
68
+ function AppearanceObject:SetanimFlags(anim_flags)
69
+ self.animFlags = anim_flags
70
+ end
71
+
72
+ function AppearanceObject:SetanimCrossfade(crossfade)
73
+ self.animCrossfade = crossfade
74
+ end
75
+
76
+ function AppearanceObject:Setanim2Flags(anim_flags)
77
+ self.anim2Flags = anim_flags
78
+ end
79
+
80
+ function AppearanceObject:Setanim2Crossfade(crossfade)
81
+ self.anim2Crossfade = crossfade
82
+ end
83
+
84
+ function AppearanceObject:SetanimWeight(weight)
85
+ self.animWeight = weight
86
+ self:SetAnimHighLevel()
87
+ end
88
+
89
+ function AppearanceObject:SetAnimChannel(channel, anim, anim_flags, crossfade, weight, blend_time)
90
+ if not self:HasState(anim) then
91
+ if self:GetEntity() ~= "" then
92
+ StoreErrorSource(self, "Missing object state " .. self:GetEntity() .. "." .. anim)
93
+ end
94
+ return
95
+ end
96
+ Shapeshifter.SetAnim(self, channel, anim, anim_flags, crossfade)
97
+ Shapeshifter.SetAnimWeight(self, channel, 100)
98
+ Shapeshifter.SetAnimWeight(self, channel, weight, blend_time)
99
+ self:SetAnimSpeed(channel, self.anim_speed)
100
+ local parts = self.parts
101
+ if parts then
102
+ for _, part_name in ipairs(self.animated_parts) do
103
+ local part = parts[part_name]
104
+ if part then
105
+ part:SetAnim(channel, anim, anim_flags, crossfade)
106
+ part:SetAnimWeight(channel, 100)
107
+ part:SetAnimWeight(channel, weight, blend_time)
108
+ part:SetAnimSpeed(channel, self.anim_speed)
109
+ end
110
+ end
111
+ end
112
+ return GetAnimDuration(self:GetEntity(), self:GetAnim(channel))
113
+ end
114
+
115
+ function AppearanceObject:SetAnimLowLevel()
116
+ self:ApplyAppearance()
117
+ local time = self:SetAnimChannel(1, self.anim, self.animFlags, self.animCrossfade, self.animWeight, self.animBlendTime)
118
+ if self.anim2 ~= "" then
119
+ local time2, duration2 = self:SetAnimChannel(2, self.anim2, self.anim2Flags, self.anim2Crossfade, 100 - self.animWeight, self.anim2BlendTime)
120
+ time = Max(time, time2)
121
+ end
122
+ return time
123
+ end
124
+
125
+ function AppearanceObject:SetAnimHighLevel()
126
+ self:SetAnimLowLevel()
127
+ end
128
+
129
+ function AppearanceObject:SetEntity()
130
+ end
131
+
132
+ local function get_part_offset_angle(appearance, prop_name)
133
+ local x = appearance[prop_name .. "AttachOffsetX"] or 0
134
+ local y = appearance[prop_name .. "AttachOffsetY"] or 0
135
+ local z = appearance[prop_name .. "AttachOffsetZ"] or 0
136
+ local angle = appearance[prop_name .. "AttachOffsetAngle"] or 0
137
+ if x ~= 0 or y ~= 0 or z ~= 0 or angle ~= 0 then
138
+ return point(x, y, z), angle
139
+ end
140
+ end
141
+
142
+ -- overload this in project
143
+ config.DefaultAppearanceBody = "ErrorAnimatedMesh"
144
+
145
+ function AppearanceObject:ColorizePart(part_name)
146
+ local appearance = AppearancePresets[self.Appearance]
147
+ local prop_color_name = string.format("%sColor", part_name)
148
+ if not appearance:HasMember(prop_color_name) then return end
149
+
150
+ local part = self.parts[part_name]
151
+ local color_member = appearance[prop_color_name]
152
+ if not color_member then
153
+ print("once", string.format("[WARNING] No color specified for %s in %s", part_name, self.Appearance))
154
+ return
155
+ end
156
+ local palette = color_member["ColorizationPalette"]
157
+ part:SetColorizationPalette(palette)
158
+ for i = 1, const.MaxColorizationMaterials do
159
+ if string.match(part_name, "Hair") then
160
+ local custom = {}
161
+ for i = 1, 4 do
162
+ custom[i] = appearance["HairParam" .. i]
163
+ end
164
+ part:SetHairCustomParams(custom)
165
+ end
166
+ local color = color_member[string.format("EditableColor%d", i)]
167
+ local roughness = color_member[string.format("EditableRoughness%d", i)]
168
+ local metallic = color_member[string.format("EditableMetallic%d", i)]
169
+ part:SetColorizationMaterial(i, color, roughness, metallic)
170
+ end
171
+ end
172
+
173
+ function AppearanceObject:ApplyPartSpotAttachments(part_name)
174
+ local appearance = AppearancePresets[self.Appearance]
175
+ local part = self.parts[part_name]
176
+ local spot_prop = part_name .. "Spot"
177
+ local prop_spot = appearance:HasMember(spot_prop) and appearance[spot_prop]
178
+ local spot_name = prop_spot or "Origin"
179
+ self:Attach(part, self:GetSpotBeginIndex(spot_name))
180
+ if part_name == "Hat" or part_name == "Hat2" then
181
+ local offset, angle = get_part_offset_angle(appearance, part_name)
182
+ if offset and angle then
183
+ part:SetAttachOffset(offset)
184
+ part:SetAttachAngle(angle)
185
+ end
186
+ end
187
+ end
188
+
189
+ function AppearanceObject:ApplyAppearance(appearance, force)
190
+ appearance = appearance or self.Appearance
191
+
192
+ if not appearance then return end
193
+ if not force and self.appearance_applied == appearance then return end
194
+
195
+ self.appearance_applied = appearance
196
+ if type(appearance) == "string" then
197
+ self.Appearance = appearance
198
+ appearance = AppearancePresets[appearance]
199
+ else
200
+ self.Appearance = appearance.id
201
+ end
202
+ if not appearance then
203
+ local prop_meta = AppearanceObject:GetPropertyMetadata("Appearance")
204
+ appearance = AppearancePresets[prop_meta.default]
205
+ if not appearance then
206
+ StoreErrorSource(self, "Default Appearance can't be invalid!")
207
+ end
208
+ appearance = AppearancePresets[self.Appearance]
209
+ if not appearance then
210
+ StoreErrorSource(self, string.format("Invalid appearance '%s'", self.Appearance))
211
+ return
212
+ end
213
+ end
214
+ for _, part in pairs(self.parts) do
215
+ DoneObject(part)
216
+ end
217
+ self:ChangeEntity(appearance.Body or self.fallback_body or config.DefaultAppearanceBody)
218
+ if not IsValidEntity(self:GetEntity()) then
219
+ StoreErrorSource(self, string.format("Invalid entity '%s'(%s) for Appearance '%s'", self:GetEntity(), appearance.Body, appearance.id))
220
+ printf("Invalid entity '%s'(%s) for Appearance '%s'", self:GetEntity(), appearance.Body, appearance.id)
221
+ end
222
+ if appearance:HasMember("BodyColor") and appearance.BodyColor then
223
+ self:SetColorization(appearance.BodyColor, true)
224
+ end
225
+ self.parts = {}
226
+ local real_time_animated = self:GetGameFlags(const.gofRealTimeAnim) ~= 0
227
+ for _, part_name in ipairs(self.attached_parts) do
228
+ if IsValidEntity(appearance[part_name]) then
229
+ local part = PlaceObject("AppearanceObjectPart")
230
+ if real_time_animated then
231
+ part:SetGameFlags(const.gofRealTimeAnim)
232
+ end
233
+ part:ChangeEntity(appearance[part_name])
234
+ if not IsValidEntity(part:GetEntity()) then
235
+ StoreErrorSource(part, string.format("Invalid entity part '%s'(%s) for Appearance '%s'", part:GetEntity(), appearance[part_name], appearance.id))
236
+ printf("Invalid entity part '%s'(%s) for Appearance '%s'", part:GetEntity(), appearance[part_name], appearance.id)
237
+ end
238
+ self.parts[part_name] = part
239
+ self:ColorizePart(part_name)
240
+ self:ApplyPartSpotAttachments(part_name)
241
+ end
242
+ end
243
+ self:Setanim(self.anim)
244
+ end
245
+
246
+ function AppearanceObject:PlayAnim(anim)
247
+ self:Setanim(anim)
248
+ local vec = self:GetStepVector()
249
+ local time = self:GetAnimDuration()
250
+ if vec:Len() > 0 then
251
+ self:SetPos(self:GetPos() + vec, time)
252
+ end
253
+ Sleep(time)
254
+ end
255
+
256
+ function AppearanceObject:SetPhaseHighLevel(phase)
257
+ self:SetAnimPhase(1, phase)
258
+ local parts = self.parts
259
+ if parts then
260
+ for _, part_name in ipairs(self.attached_parts) do
261
+ local part = parts[part_name]
262
+ if part then
263
+ part:SetAnimPhase(1, phase)
264
+ end
265
+ end
266
+ end
267
+ end
268
+
269
+ function AppearanceObject:SetAnimPose(anim, phase)
270
+ self:Setanim(anim)
271
+ self.anim_speed = 0
272
+ self:SetPhaseHighLevel(phase)
273
+ end
274
+
CommonLua/Classes/AttachViaProp.lua ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ local EDITOR = Platform.editor
2
+
3
+ local function SpotEntry(obj, idx)
4
+ local name = idx >= 0 and obj:GetSpotName(idx) or ""
5
+ if name == "" then
6
+ return false
7
+ end
8
+ local offset = idx - obj:GetSpotBeginIndex(name)
9
+ return offset == 0 and name or {name, offset}
10
+ end
11
+
12
+ DefineClass.AttachViaProp = {
13
+ __parents = { "ComponentAttach" },
14
+ properties = {
15
+ { category = "Attach-Via-Prop", id = "AttachList", name = "Attach List", editor = "prop_table", default = "", no_edit = true },
16
+ { category = "Attach-Via-Prop", id = "AttachCounter", name = "Attach Counter", editor = "number", default = 0, dont_save = true, read_only = true },
17
+ },
18
+ }
19
+
20
+ function AttachViaProp:SetAttachList(value)
21
+ self.AttachList = value
22
+ self:UpdatePropAttaches()
23
+ end
24
+
25
+ function AttachViaProp:ResolveSpotIdx(entry)
26
+ local spot = entry.spot
27
+ local offset
28
+ if type(spot) == "table" then
29
+ spot, offset = unpack_params(spot)
30
+ assert(type(spot) == "string")
31
+ assert(type(offset) == "number")
32
+ end
33
+ if type(spot) == "number" then
34
+ -- support for older version
35
+ entry.spot = SpotEntry(self, spot)
36
+ elseif type(spot) == "string" then
37
+ spot = self:HasSpot(spot) and self:GetSpotBeginIndex(spot)
38
+ end
39
+ return (spot or -1) + (offset or 0)
40
+ end
41
+
42
+ function AttachViaProp:UpdatePropAttaches()
43
+ if self.AttachCounter > 0 then
44
+ self:ForEachAttach(function(obj)
45
+ if rawget(obj, "attach_via_prop") then
46
+ DoneObject(obj)
47
+ end
48
+ end)
49
+ self.AttachCounter = nil
50
+ end
51
+ local list = self.AttachList
52
+ for i=#list,1,-1 do
53
+ local entry = list[i]
54
+ local spot = self:ResolveSpotIdx(entry)
55
+ if not spot then
56
+ table.remove(list, i)
57
+ else
58
+ local class = entry.class or ""
59
+ local particles = entry.particles or ""
60
+ local obj, err
61
+ if particles ~= "" then
62
+ obj = PlaceParticles(particles)
63
+ else
64
+ obj = PlaceObject(class)
65
+ end
66
+ if obj then
67
+ rawset(obj, "attach_via_prop", true)
68
+ if entry.offset then
69
+ obj:SetAttachOffset(entry.offset)
70
+ end
71
+ if entry.axis then
72
+ obj:SetAttachAxis(entry.axis)
73
+ end
74
+ if entry.angle then
75
+ obj:SetAttachAngle(entry.angle)
76
+ end
77
+ err = self:Attach(obj, spot)
78
+ end
79
+ if not IsValid(obj) or obj:GetAttachSpot() ~= spot or err then
80
+ StoreErrorSource(self, "Failed to attach via props!")
81
+ DoneObject(obj)
82
+ else
83
+ self.AttachCounter = self.AttachCounter + 1
84
+ end
85
+ end
86
+ end
87
+ if not EDITOR then
88
+ self.AttachList = nil
89
+ end
90
+ end
91
+
92
+ ----
93
+ if EDITOR then
94
+
95
+ local function SpotName(obj, idx)
96
+ local name = obj:GetSpotName(idx)
97
+ local anot = obj:GetSpotAnnotation(idx)
98
+ return name .. (anot and (" (" .. anot .. ")") or "")
99
+ end
100
+
101
+ local function SpotNamesCombo(obj)
102
+ local start_idx, end_idx = obj:GetAllSpots( obj:GetState() )
103
+ local items = {}
104
+ local names = {}
105
+ for idx = start_idx, end_idx do
106
+ items[#items + 1] = {value = SpotEntry(obj, idx), text = SpotName(obj, idx)}
107
+ end
108
+ table.sortby_field(items, "text")
109
+ table.insert(items, 1, {value = false, text = ""})
110
+ return items
111
+ end
112
+
113
+ if FirstLoad then
114
+ l_used_entries = false
115
+ end
116
+
117
+ local function SmartAttachCombo()
118
+ local items = {}
119
+ local classes = ClassDescendantsList("ComponentAttach")
120
+ local tbl = {"", " - Object"}
121
+ for i = 1, #classes do
122
+ local class = classes[i]
123
+ tbl[1] = class
124
+ local text = table.concat(tbl)
125
+ items[#items + 1] = {value = { class, 1, text }, text = text}
126
+ end
127
+ local particles = ParticlesComboItems()
128
+ local tbl = {"", " - Particle"}
129
+ for i = 1, #particles do
130
+ local particle = particles[i]
131
+ tbl[1] = particle
132
+ local text = table.concat(tbl)
133
+ items[#items + 1] = {value = { particle, 2, text }, text = text}
134
+ end
135
+ table.sortby_field(items, "text")
136
+ if l_used_entries then
137
+ local idx = 1
138
+ local tbl = {"> ", ""}
139
+ for text, entry in sorted_pairs(l_used_entries) do
140
+ tbl[2] = text
141
+ table.insert(items, idx, {value = entry, text = table.concat(tbl)})
142
+ idx = idx + 1
143
+ end
144
+ table.insert(items, idx, {value = "", text = ""})
145
+ end
146
+ table.insert(items, 1, {value = false, text = ""})
147
+ return items
148
+ end
149
+
150
+ table.iappend(AttachViaProp.properties, {
151
+ { category = "Attach-Via-Prop", id = "AttachPreview", name = "Attach List", editor = "text", lines = 5, default = "", dont_save = true, read_only = true, min = 10 },
152
+ { category = "Attach-Via-Prop", id = "AttachToSpot", name = "Attach At", editor = "combo", default = false, dont_save = true, items = SpotNamesCombo, min = 0, buttons = {{name = "Add", func = "ButtonAddAttach"}, {name = "Rem", func = "ButtonRemAttach"}, {name = "Rem All", func = "ButtonRemAllAttaches"}}},
153
+ { category = "Attach-Via-Prop", id = "AttachObject", name = "Attach Object", editor = "combo", default = false, dont_save = true, items = SmartAttachCombo },
154
+ { category = "Attach-Via-Prop", id = "AttachWithOffset", name = "Attach Offset ", editor = "point", default = point30, dont_save = true, scale = "m" },
155
+ { category = "Attach-Via-Prop", id = "AttachWithAxis", name = "Attach Axis ", editor = "point", default = axis_z, dont_save = true },
156
+ { category = "Attach-Via-Prop", id = "AttachWithAngle", name = "Attach Angle ", editor = "number", default = 0, dont_save = true, scale = "deg" },
157
+ })
158
+
159
+ function AttachViaProp:GetAttachListCombo()
160
+ local items = {}
161
+ local list = self.AttachList
162
+ for i=#list,1,-1 do
163
+ local entry = list[i]
164
+ local spot = self:ResolveSpotIdx(entry)
165
+ if not spot then
166
+ table.remove(list, i)
167
+ else
168
+ local str = SpotName(self, spot) .. " -- " .. (entry.particles or entry.class or "?")
169
+ if entry.offset then
170
+ str = str .. " o" .. tostring(entry.offset)
171
+ end
172
+ if entry.axis then
173
+ str = str .. " x" .. tostring(entry.axis)
174
+ end
175
+ if entry.angle then
176
+ str = str .. " a" .. tostring(entry.angle)
177
+ end
178
+ items[#items + 1] = str
179
+ end
180
+ end
181
+ table.sort(items)
182
+ return items
183
+ end
184
+
185
+ function AttachViaProp:GetAttachPreview()
186
+ local list = self:GetAttachListCombo()
187
+ return table.concat(list, "\n")
188
+ end
189
+
190
+ function ButtonAddAttach(main_obj, object, prop_id)
191
+ if type(object.AttachObject) ~= "table" then
192
+ return
193
+ end
194
+ local oname, otype, otext = unpack_params(object.AttachObject)
195
+ local class = otype == 1 and oname or ""
196
+ local particles = otype == 2 and oname or ""
197
+ if class == "" and particles == "" then
198
+ return
199
+ end
200
+ l_used_entries = l_used_entries or {}
201
+ l_used_entries[otext] = object.AttachObject
202
+ local spot = object.AttachToSpot or nil
203
+ local list = object.AttachList
204
+ if #list == 0 then
205
+ list = {}
206
+ object.AttachList = list
207
+ end
208
+ local offset = object.AttachWithOffset
209
+ if offset == point30 then
210
+ offset = nil
211
+ end
212
+ local axis = object.AttachWithAxis
213
+ if axis == axis_z then
214
+ axis = nil
215
+ end
216
+ local angle = object.AttachWithAngle
217
+ if angle == 0 then
218
+ angle = nil
219
+ end
220
+ if class ~= "" then
221
+ list[#list + 1] = {spot = spot, offset = offset, axis = axis, angle = angle, class = class, }
222
+ end
223
+ if particles ~= "" then
224
+ list[#list + 1] = {spot = spot, offset = offset, axis = axis, angle = angle, particles = particles, }
225
+ end
226
+ object:UpdatePropAttaches()
227
+ end
228
+
229
+ function ButtonRemAllAttaches(main_obj, object, prop_id)
230
+ if not object then
231
+ return
232
+ end
233
+ object.AttachList = nil
234
+ object:UpdatePropAttaches()
235
+ end
236
+
237
+ function ButtonRemAttach(main_obj, object, prop_id)
238
+ if not object then
239
+ return
240
+ end
241
+ local list = object:GetAttachListCombo()
242
+ if #list == 0 then
243
+ return
244
+ end
245
+ local entry, err = PropEditorWaitUserInput(main_obj, list[#list], "Select attach to remove", list)
246
+ if not entry then
247
+ assert(false, err)
248
+ return
249
+ end
250
+ local idx = table.find(list, entry)
251
+ if not idx then
252
+ return
253
+ end
254
+ table.remove(object.AttachList, idx)
255
+ if #object.AttachList == 0 then
256
+ object.AttachList = nil
257
+ end
258
+ object:UpdatePropAttaches()
259
+ end
260
+
261
+ end -- EDITOR
262
+ ----
CommonLua/Classes/AutoAttach.lua ADDED
@@ -0,0 +1,1355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function GetObjStateAttaches(obj, entity)
2
+ entity = entity or (obj:GetEntity() or obj.entity)
3
+
4
+ local state = obj and GetStateName(obj:GetState()) or "idle"
5
+ local entity_attaches = Attaches[entity]
6
+
7
+ return entity_attaches and entity_attaches[state]
8
+ end
9
+
10
+ function GetEntityAutoAttachModes(obj, entity)
11
+ local attaches = GetObjStateAttaches(obj, entity)
12
+ local modes = {""}
13
+ for _, attach in ipairs(attaches or empty_table) do
14
+ if attach.required_state then
15
+ local mode = string.trim_spaces(attach.required_state)
16
+ table.insert_unique(modes, mode)
17
+ end
18
+ end
19
+
20
+ return modes
21
+ end
22
+
23
+ --[[@@@
24
+ @class AutoAttachCallback
25
+ Inherit this if you want a callback when an objects is autoattached to its parent
26
+ --]]
27
+
28
+ DefineClass.AutoAttachCallback = {
29
+ __parents = {"InitDone"},
30
+ }
31
+
32
+ function AutoAttachCallback:OnAttachToParent(parent, spot)
33
+ end
34
+
35
+ --[[@@@
36
+ @class AutoAttachObject
37
+ Objects from this type are able to attach a preset of objects on their creation based on their spot annotations.
38
+ --]]
39
+
40
+ DefineClass.AutoAttachObject =
41
+ {
42
+ __parents = { "Object", "ComponentAttach" },
43
+ auto_attach_props_description = false,
44
+
45
+ properties = {
46
+ { id = "AutoAttachMode", editor = "choice", default = "", items = function(obj) return GetEntityAutoAttachModes(obj) or {} end },
47
+ { id = "AllAttachedLightsToDetailLevel", editor = "choice", default = false, items = {"Essential", "Optional", "Eye Candy"}},
48
+ },
49
+
50
+ auto_attach_at_init = true,
51
+ auto_attach_mode = false,
52
+ is_forced_lod_min = false,
53
+
54
+ max_colorization_materials_attaches = 0,
55
+ }
56
+
57
+ local gofAutoAttach = const.gofAutoAttach
58
+
59
+ function IsAutoAttach(attach)
60
+ return attach:GetGameFlags(gofAutoAttach) ~= 0
61
+ end
62
+
63
+ function AutoAttachObject:DestroyAutoAttaches()
64
+ self:DestroyAttaches(IsAutoAttach)
65
+ end
66
+
67
+ function AutoAttachObject:ClearAttachMembers()
68
+ local attaches = GetObjStateAttaches(self)
69
+ for _, attach in ipairs(attaches) do
70
+ if attach.member then
71
+ self[attach.member] = nil
72
+ end
73
+ end
74
+ end
75
+
76
+ function AutoAttachObject:SetAutoAttachMode(value)
77
+ self.auto_attach_mode = value
78
+ self:DestroyAutoAttaches()
79
+ self:ClearAttachMembers()
80
+ self:AutoAttachObjects()
81
+ end
82
+
83
+ function AutoAttachObject:OnEditorSetProperty(prop_id, old_value, ged)
84
+ if prop_id == "AllAttachedLightsToDetailLevel" or prop_id == "StateText" then
85
+ self:SetAutoAttachMode(self:GetAutoAttachMode())
86
+ end
87
+ Object.OnEditorSetProperty(self, prop_id, old_value, ged)
88
+ end
89
+
90
+ function AutoAttachObject:GetAutoAttachMode(mode)
91
+ local mode_set = GetEntityAutoAttachModes(self)
92
+ if not mode_set then
93
+ return ""
94
+ end
95
+ if table.find(mode_set, mode or self.auto_attach_mode) then
96
+ return self.auto_attach_mode
97
+ end
98
+ return mode_set[1] or ""
99
+ end
100
+
101
+ function AutoAttachObject:GetAttachModeSet()
102
+ return GetEntityAutoAttachModes(self)
103
+ end
104
+
105
+ if FirstLoad then
106
+ s_AutoAttachedLightDetailsBaseObject = false
107
+ end
108
+
109
+ function AutoAttachObjects(obj, context)
110
+ if not s_AutoAttachedLightDetailsBaseObject and obj.AllAttachedLightsToDetailLevel then
111
+ s_AutoAttachedLightDetailsBaseObject = obj
112
+ end
113
+
114
+ local selectable = obj:GetEnumFlags(const.efSelectable) ~= 0
115
+ local attaches = GetObjStateAttaches(obj)
116
+ local max_colorization_materials = 0
117
+ for i = 1, #(attaches or "") do
118
+ local attach = attaches[i]
119
+ local class = GetAttachClass(obj, attach[2])
120
+
121
+ local spot_attaches = {}
122
+ local place, detail_class = PlaceCheck(obj, attach, class, context)
123
+ if place then
124
+ local o = PlaceAtSpot(obj, attach.spot_idx, class, context)
125
+ if o then
126
+ if attach.mirrored then
127
+ o:SetMirrored(true)
128
+ end
129
+ if attach.offset then
130
+ o:SetAttachOffset(attach.offset)
131
+ end
132
+ if attach.axis and attach.angle and attach.angle ~= 0 then
133
+ o:SetAttachAxis(attach.axis)
134
+ o:SetAttachAngle(attach.angle)
135
+ end
136
+ if selectable then
137
+ o:SetEnumFlags(const.efSelectable)
138
+ end
139
+ if attach.inherited_properties then
140
+ for key, value in sorted_pairs(attach.inherited_properties) do
141
+ o:SetProperty(key, value)
142
+ end
143
+ end
144
+ if IsKindOf(o, "SubstituteByRandomChildEntity") then
145
+ -- NOTE: when substituting entity the object is still not attached so it can't be
146
+ -- destroyed in SubstituteByRandomChildEntity and we have to do it manually here
147
+ if o:IsForcedLODMinAttach() and o:GetDetailClass() ~= "Essential" then
148
+ DoneObject(o)
149
+ o = nil
150
+ else
151
+ local top_parent = GetTopmostParent(o)
152
+ ApplyCurrentEnvColorizedToObj(top_parent) -- entity changed, possibly colorization too.
153
+ top_parent:DestroyRenderObj(true)
154
+ end
155
+ else
156
+ o:SetDetailClass(detail_class)
157
+ end
158
+ if o then
159
+ if attach.inherit_colorization then
160
+ o:SetGameFlags(const.gofInheritColorization)
161
+ max_colorization_materials = Max(max_colorization_materials, o:GetMaxColorizationMaterials())
162
+ end
163
+ o:SetForcedLODMin(rawget(obj, "is_forced_lod_min") or obj:GetForcedLODMin())
164
+ spot_attaches[#spot_attaches+1] = o
165
+ end
166
+ end
167
+ end
168
+ if context ~= "placementcursor" then
169
+ SetObjMembers(obj, attach, spot_attaches)
170
+ end
171
+ end
172
+ if max_colorization_materials > AutoAttachObject.max_colorization_materials_attaches then
173
+ obj.max_colorization_materials_attaches = max_colorization_materials
174
+ end
175
+
176
+ if s_AutoAttachedLightDetailsBaseObject == obj then
177
+ s_AutoAttachedLightDetailsBaseObject = false
178
+ end
179
+ end
180
+
181
+ function AutoAttachObject:GetMaxColorizationMaterials()
182
+ return Max(self.max_colorization_materials_attaches, CObject.GetMaxColorizationMaterials(self))
183
+ end
184
+
185
+ function AutoAttachObject:CanBeColorized()
186
+ return self.max_colorization_materials_attaches and self.max_colorization_materials_attaches > 1 or CObject.CanBeColorized(self)
187
+ end
188
+
189
+ AutoAttachObject.AutoAttachObjects = AutoAttachObjects
190
+
191
+ function RemoveObjMembers(obj, attach, list)
192
+ if attach.member then
193
+ local o = obj[attach.member]
194
+ for i = 1, #list do
195
+ if o == list[i] then
196
+ obj[attach.member] = false
197
+ break
198
+ end
199
+ end
200
+ end
201
+ if attach.memberlist and obj[attach.memberlist] and type(obj[attach.memberlist]) == "table" then
202
+ table.remove_entry(obj[attach.memberlist], list)
203
+ end
204
+ end
205
+
206
+ -- local functions used in the class methods
207
+ local AutoAttachObjects, RemoveObjMembers = AutoAttachObjects, RemoveObjMembers
208
+
209
+ function AutoAttachObject:Init()
210
+ if self.auto_attach_at_init then
211
+ AutoAttachObjects(self, "init")
212
+ end
213
+ end
214
+
215
+ function AutoAttachObject:__fromluacode(props, arr, handle)
216
+ local obj = ResolveHandle(handle)
217
+
218
+ if obj and obj[true] then
219
+ StoreErrorSource(obj, "Duplicate handle", handle)
220
+ assert(false, string.format("Duplicate handle %d: new '%s', prev '%s'", handle, self.class, obj.class))
221
+ obj = nil
222
+ end
223
+
224
+ local idx = table.find(props, "AllAttachedLightsToDetailLevel")
225
+ local attached_lights_detail = idx and props[idx + 1]
226
+ if attached_lights_detail then
227
+ obj.AllAttachedLightsToDetailLevel = attached_lights_detail
228
+ end
229
+
230
+ local idx = table.find(props, "LowerLOD")
231
+
232
+ if idx ~= nil then
233
+ obj.is_forced_lod_min = props[idx + 1]
234
+ else
235
+ idx = table.find(props, "ForcedLODState")
236
+ obj.is_forced_lod_min = idx and (props[idx + 1] == "Minimum")
237
+ end
238
+
239
+ obj = self:new(obj)
240
+ SetObjPropertyList(obj, props)
241
+ SetArray(obj, arr)
242
+ obj.is_forced_lod_min = nil
243
+
244
+ return obj
245
+ end
246
+
247
+ AutoAttachObject.ShouldAttach = return_true
248
+ AutoResolveMethods.ShouldAttach = "and"
249
+
250
+ function AutoAttachObject:OnAttachCreated(attach, spot)
251
+ end
252
+
253
+ function AutoAttachObject:MarkAttachEntities(entities)
254
+ if not IsValid(self) then return entities end
255
+
256
+ entities = entities or {}
257
+
258
+ self:__MarkEntities(entities)
259
+
260
+ local cur_mode = self.auto_attach_mode
261
+ local modes = self:GetAttachModeSet()
262
+ for _, mode in ipairs(modes) do
263
+ self:SetAutoAttachMode(mode)
264
+ self:__MarkEntities(entities)
265
+ end
266
+ self:SetAutoAttachMode(cur_mode)
267
+
268
+ return entities
269
+ end
270
+
271
+ function SetObjMembers(obj, attach, list)
272
+ if attach.member then
273
+ local name = attach.member
274
+ if #list == 0 then
275
+ if not rawget(obj, name) then
276
+ obj[name] = false -- initialize on init
277
+ end
278
+ else
279
+ assert(#list == 1 and not rawget(obj, name), 'Duplicate member "'..name..'" in the auto-attaches of class "'..obj.class..'"')
280
+ obj[name] = list[1]
281
+ end
282
+ end
283
+ if attach.memberlist then
284
+ local name = attach.memberlist
285
+ if not rawget(obj, name) or not type(obj[name]) == "table" or not IsValid(obj[name][1]) then
286
+ obj[name] = {}
287
+ end
288
+ if #list > 0 then
289
+ obj[name][#obj[name] + 1] = list
290
+ end
291
+ end
292
+ end
293
+
294
+ function GetAttachClass(self, classes)
295
+ if type(classes) == "string" then
296
+ return classes
297
+ end
298
+ assert(type(classes) == "table")
299
+ local rnd = self:Random(100)
300
+ local cur_prob = 0
301
+ for class, prob in pairs(classes) do
302
+ cur_prob = cur_prob + prob
303
+ if rnd <= cur_prob then
304
+ return class
305
+ end
306
+ end
307
+ -- probability of nothing left
308
+ return false
309
+ end
310
+
311
+ local IsKindOf = IsKindOf
312
+ local shapeshifter_class_whitelist = { "Light", "AutoAttachSIModulator", "ParSystem" } -- classes that are alowed to be instantiated even in shapeshifters
313
+ local function IsObjectClassAllowedInShapeshifter(class_to_spawn)
314
+ for _, class_name in ipairs(shapeshifter_class_whitelist) do
315
+ if IsKindOf(class_to_spawn, class_name) then
316
+ return true
317
+ end
318
+ end
319
+ return false
320
+ end
321
+
322
+ local gofDetailClassMask = const.gofDetailClassMask
323
+
324
+ function PlaceCheck(obj, attach, class, context)
325
+ if not obj:ShouldAttach(attach) then
326
+ return false
327
+ end
328
+
329
+ -- placement cursor check
330
+ if context == "placementcursor" then
331
+ if not attach.show_at_placement and not attach.placement_only then
332
+ return false
333
+ end
334
+ elseif attach.placement_only then
335
+ return false
336
+ end
337
+ if attach.required_state and IsKindOf(obj, "AutoAttachObject") and attach.required_state ~= obj.auto_attach_mode then
338
+ return false
339
+ end
340
+
341
+ -- condition check
342
+ local condition = attach.condition
343
+ if condition then
344
+ assert(type(condition) == "function" or type(condition) == "string")
345
+ if type(condition) == "function" then
346
+ if not condition(obj, attach) then
347
+ return false
348
+ end
349
+ else
350
+ if obj:HasMember(condition) and not obj[condition] then
351
+ return false
352
+ end
353
+ end
354
+ end
355
+
356
+ local detail_class = s_AutoAttachedLightDetailsBaseObject and
357
+ IsKindOf(g_Classes[class], "Light") and
358
+ s_AutoAttachedLightDetailsBaseObject.AllAttachedLightsToDetailLevel
359
+ detail_class = detail_class or (attach.DetailClass ~= "Default" and attach.DetailClass)
360
+ if not detail_class then
361
+ -- try to extract from the class
362
+ local detail_mask = GetClassGameFlags(class, gofDetailClassMask)
363
+ local detail_from_class = GetDetailClassMaskName(detail_mask)
364
+ detail_class = detail_from_class ~= "Default" and detail_from_class
365
+ end
366
+ local forced_lod_min = rawget(obj, "is_forced_lod_min") or obj:GetForcedLODMin()
367
+ if forced_lod_min and detail_class ~= "Essential" then
368
+ return false
369
+ end
370
+
371
+ return true, detail_class
372
+ end
373
+
374
+ function PlaceAtSpot(obj, spot, class, context)
375
+ local o
376
+ if g_Classes[class] then
377
+ if context == "placementcursor" then
378
+ if g_Classes[class]:IsKindOfClasses("TerrainDecal", "BakedTerrainDecal") then
379
+ o = PlaceObject("PlacementCursorAttachmentTerrainDecal")
380
+ else
381
+ o = PlaceObject("PlacementCursorAttachment")
382
+ end
383
+ o:ChangeClass(class)
384
+ AutoAttachObjects(o, "placementcursor")
385
+ elseif context == "shapeshifter" and not IsObjectClassAllowedInShapeshifter(g_Classes[class]) then
386
+ o = PlaceObject("Shapeshifter", nil, const.cofComponentAttach)
387
+ if IsValidEntity(class) then
388
+ o:ChangeEntity(class)
389
+ end
390
+ else
391
+ o = PlaceObject(class, nil, const.cofComponentAttach)
392
+ end
393
+ else
394
+ print("once", 'AutoAttach: unknown class/particle "' .. class .. '" for [object "' .. obj.class .. '", spot "' .. obj:GetSpotName(spot) .. '"]')
395
+ end
396
+ if not o then
397
+ return
398
+ end
399
+ local err = obj:Attach(o, spot)
400
+ if err then
401
+ print("once", "Error attaching", o.class, "to", obj.class, ":", err)
402
+ return
403
+ end
404
+ o:SetGameFlags(const.gofAutoAttach)
405
+ if not IsKindOf(obj, "Shapeshifter") then
406
+ obj:OnAttachCreated(o, spot)
407
+ end
408
+ if IsKindOf(o, "AutoAttachCallback") then
409
+ o:OnAttachToParent(obj, spot)
410
+ end
411
+
412
+ return o
413
+ end
414
+
415
+ if FirstLoad then
416
+ Attaches = {} -- global table that keeps the inherited auto_attaches
417
+ end
418
+
419
+ function AutoAttachObjectsToPlacementCursor(obj)
420
+ AutoAttachObjects(obj, "placementcursor")
421
+ end
422
+
423
+ --[Deprecated]
424
+ function AutoAttachObjectsToShapeshifter(obj)
425
+ AutoAttachObjects(obj)
426
+ end
427
+
428
+ function AutoAttachShapeshifterObjects(obj)
429
+ AutoAttachObjects(obj, "shapeshifter")
430
+ end
431
+
432
+ local function CanInheritColorization(parent_entity, child_entity)
433
+ return true
434
+ end
435
+
436
+ function GetEntityAutoAttachTable(entity, auto_attach)
437
+ auto_attach = auto_attach or false
438
+
439
+ local states = GetStates(entity)
440
+ for _, state in ipairs(states) do
441
+ local spbeg, spend = GetAllSpots(entity, state)
442
+ for spot = spbeg, spend do
443
+ local str = GetSpotAnnotation(entity, spot)
444
+ if str and #str > 0 then
445
+ local item
446
+ for w in string.gmatch(str,"%s*(.[^,]+)[, ]?") do
447
+ local lw = string.lower(w)
448
+ if not item then
449
+ -- auto attach description
450
+ if lw ~= "att" and lw~="autoattach" then
451
+ break
452
+ end
453
+ item = {}
454
+ item.spot_idx = spot
455
+ elseif lw=="show at placement" or lw=="show_at_placement" or lw=="show" then -- show at placement
456
+ item.show_at_placement = true
457
+ elseif lw=="placement only" or lw=="placement_only" then -- placement only
458
+ item.placement_only = true
459
+ elseif lw=="mirrored" or lw=="mirror" then
460
+ item.mirrored = true
461
+ elseif not item[2] then
462
+ item[2] = w
463
+ if not g_Classes[w] then
464
+ print("once", "Invalid autoattach", w, "for entity", entity)
465
+ end
466
+ end
467
+ end
468
+ if item then
469
+ item.inherit_colorization = CanInheritColorization(entity, item[2])
470
+ auto_attach = auto_attach or {}
471
+ auto_attach[state] = auto_attach[state] or {}
472
+ table.insert(auto_attach[state], item)
473
+ end
474
+ end
475
+ end
476
+ end
477
+
478
+ return auto_attach
479
+ end
480
+
481
+ local function IsAutoAttachObject(entity)
482
+ local entity_data = EntityData and EntityData[entity] and EntityData[entity].entity
483
+ local classes = entity_data and entity_data.class_parent and entity_data.class_parent or ""
484
+ for class in string.gmatch(classes, "[^%s,]+%s*") do
485
+ if IsKindOf(g_Classes[class], "AutoAttachObject") then
486
+ return true
487
+ end
488
+ end
489
+ end
490
+
491
+ local function TransferMatchingIdleAttachesToAllState(auto_attach, states)
492
+ local idle_attaches = auto_attach["idle"]
493
+ if not idle_attaches then return end
494
+
495
+ local attach_modes
496
+ for _, attach in ipairs(idle_attaches) do
497
+ if attach.required_state then
498
+ attach_modes = true
499
+ break
500
+ end
501
+ end
502
+ if not attach_modes then return end
503
+
504
+ for _, state in ipairs(states) do
505
+ auto_attach[state] = auto_attach[state] or {}
506
+ table.iappend(auto_attach[state], idle_attaches)
507
+ end
508
+ end
509
+
510
+ -- build autoattach table
511
+ function RebuildAutoattach()
512
+ if not config.LoadAutoAttachData then return end
513
+
514
+ local ae = GetAllEntities()
515
+ for entity, _ in sorted_pairs(ae) do
516
+ local auto_attach = IsAutoAttachObject(entity) and GetEntityAutoAttachTable(entity)
517
+ auto_attach = GetEntityAutoAttachTableFromPresets(entity, auto_attach)
518
+ if auto_attach then
519
+ local states = GetStates(entity)
520
+ table.remove_value(states, "idle")
521
+ if #states > 0 then
522
+ -- transfer auto attaches to all states since AutoAttachEditor can define only in "idle" state
523
+ TransferMatchingIdleAttachesToAllState(auto_attach, states)
524
+ end
525
+ Attaches[entity] = auto_attach
526
+ else
527
+ Attaches[entity] = nil
528
+ end
529
+ end
530
+ end
531
+
532
+ OnMsg.EntitiesLoaded = RebuildAutoattach
533
+
534
+ function OnMsg.PresetSave(name)
535
+ local class = g_Classes[name]
536
+ if IsKindOf(class, "AutoAttachPreset") then
537
+ RebuildAutoattach()
538
+ end
539
+ end
540
+
541
+ local function PlaceFadingObjects(category, init_pos)
542
+ local ae = GetAllEntities()
543
+ local init_pos = init_pos or GetTerrainCursor()
544
+ local pos = init_pos
545
+ for k,v in pairs(ae) do
546
+ if EntityData[k] and EntityData[k].entity and EntityData[k].entity.fade_category == category then
547
+ local o = PlaceObject(k)
548
+ o:ChangeEntity(k)
549
+ o:SetPos(pos)
550
+ o:SetGameFlags(const.gofPermanent)
551
+ pos = pos + point(10*guim, 0)
552
+ if (pos:x() / (600*guim) > 0) then
553
+ pos = point(init_pos:x(), pos:y() + 20*guim)
554
+ end
555
+ elseif not EntityData[k] then
556
+ print("No EntityData for: ", k)
557
+ elseif EntityData[k] and not EntityData[k].entity then
558
+ print("No EntityData[].entity for: ", k)
559
+ end
560
+ end
561
+ end
562
+
563
+ function TestFadeCategories()
564
+ local cat = {
565
+ "PropsUltraSmall",
566
+ "PropsSmall",
567
+ "PropsMedium",
568
+ "PropsBig",
569
+ }
570
+ local pos = point(100*guim, 100*guim)
571
+ for i=1, #cat do
572
+ PlaceFadingObjects(cat[i], pos)
573
+ pos = pos + point(0, 100*guim)
574
+ end
575
+ end
576
+
577
+ function GetEntitiesAutoattachCount(filter_count)
578
+ local el = GetAllEntities()
579
+ local filter_count = filter_count or 30
580
+ for k,v in pairs(el) do
581
+ local s,e = GetSpotRange(k, EntityStates["idle"], "Autoattach")
582
+ if (e-s) > filter_count then
583
+ print(k, e-s)
584
+ end
585
+ end
586
+ end
587
+
588
+ function ListEntityAutoattaches(entity)
589
+ local s,e = GetSpotRange(entity, EntityStates["idle"], "Autoattach")
590
+ for i=s, e do
591
+ local annotation = GetSpotAnnotation(entity, i)
592
+ print(i, annotation)
593
+ end
594
+ end
595
+
596
+ ---------------------- AutoAttach editor ----------------------
597
+
598
+
599
+ local function FindArtSpecById(id)
600
+ local spec = EntitySpecPresets[id]
601
+ if not spec then
602
+ local idx = string.find(id, "_[0-9]+$")
603
+ if idx then
604
+ spec = EntitySpecPresets[string.sub(id, 0, idx - 1)]
605
+ end
606
+ end
607
+ return spec
608
+ end
609
+
610
+ local function GenerateMissingEntities()
611
+ local all_entities = GetAllEntities()
612
+ local to_create = {}
613
+ for entity in pairs(all_entities) do
614
+ local spec = FindArtSpecById(entity)
615
+ if spec and not AutoAttachPresets[entity] and string.find(spec.class_parent, "AutoAttachObject", 1, true) then
616
+ table.insert(to_create, entity)
617
+ end
618
+ end
619
+
620
+ if #to_create > 0 then
621
+ for _, entity in ipairs(to_create) do
622
+ local preset = AutoAttachPreset:new({id = entity})
623
+ preset:Register()
624
+ preset:UpdateSpotData()
625
+ Sleep(1)
626
+ end
627
+
628
+ AutoAttachPreset:SortPresets()
629
+ ObjModified(Presets.AutoAttachPreset)
630
+ end
631
+ end
632
+
633
+ function GetEntitySpots(entity)
634
+ if not IsValidEntity(entity) then return {} end
635
+ local states = GetStates(entity)
636
+ local idle = table.find(states, "idle")
637
+ if not idle then
638
+ print("WARNING: No idle state for", entity, "cannot fetch spots.")
639
+ return {}
640
+ end
641
+
642
+ local spots = {}
643
+ local spbeg, spend = GetAllSpots(entity, "idle")
644
+ for spot = spbeg, spend do
645
+ local str = GetSpotName(entity, spot)
646
+ spots[str] = spots[str] or {}
647
+ table.insert(spots[str], spot)
648
+ end
649
+
650
+ return spots
651
+ end
652
+
653
+ local zeropoint = point(0, 0, 0)
654
+ function GetEntityAutoAttachTableFromPresets(entity, attach_table)
655
+ local preset = AutoAttachPresets[entity]
656
+ if not preset then return attach_table end
657
+
658
+ local spots
659
+ for _, spot in ipairs(preset) do
660
+ for _, rule in ipairs(spot) do
661
+ attach_table = rule:FillAutoAttachTable(attach_table, entity, preset)
662
+ end
663
+ end
664
+
665
+ return attach_table
666
+ end
667
+
668
+ DefineClass.AutoAttachRuleBase = {
669
+ __parents = { "PropertyObject" },
670
+ parent = false,
671
+ }
672
+
673
+ function AutoAttachRuleBase:FillAutoAttachTable(attach_table, entity, preset)
674
+ return attach_table
675
+ end
676
+
677
+ function AutoAttachRuleBase:IsActive()
678
+ return false
679
+ end
680
+
681
+ function AutoAttachRuleBase:OnEditorNew(parent, ged, is_paste)
682
+ self.parent = parent
683
+ end
684
+
685
+ local function GetSpotsCombo(entity_name)
686
+ local t = {}
687
+ local spots = GetEntitySpots(entity_name)
688
+ for spot_name, indices in sorted_pairs(spots) do
689
+ for i = 1, #indices do
690
+ table.insert(t, spot_name .. " " .. i)
691
+ end
692
+ end
693
+ return t
694
+ end
695
+
696
+
697
+ DefineClass.AutoAttachRuleInherit = {
698
+ __parents = { "AutoAttachRuleBase" },
699
+ properties = {
700
+ { id = "parent_entity", category = "Rule", name = "Parent Entity", editor = "combo", items = function() return ClassDescendantsCombo("AutoAttachObject") end, default = "", },
701
+ { id = "spot", category = "Rule", name = "Spot", editor = "combo", items = function(obj) return GetSpotsCombo(obj:GetParentEntity()) end, default = "", },
702
+ },
703
+ }
704
+
705
+ function AutoAttachRuleInherit:GetParentEntity()
706
+ return self.parent_entity
707
+ end
708
+
709
+ function AutoAttachRuleInherit:GetSpotAndIdx()
710
+ local spot = self.spot
711
+ local break_idx = string.find(spot, "%d+$")
712
+ if not break_idx then return end
713
+ local spot_name = string.sub(spot, 1, break_idx - 2)
714
+ local spot_idx = tonumber(string.sub(spot, break_idx))
715
+ if spot_name and spot_idx then
716
+ return spot_name, spot_idx
717
+ end
718
+ end
719
+
720
+ function AutoAttachRuleInherit:GetEditorView()
721
+ local str = string.format("Inherit %s from %s", self.spot or "[SPOT]", self:GetParentEntity() or "[ENTITY]")
722
+ if not self:FindInheritedSpot() then
723
+ str = "<color 168 168 168>" .. str .. "</color>"
724
+ end
725
+ return str
726
+ end
727
+
728
+ function AutoAttachRuleInherit:FindInheritedSpot()
729
+ local entity = self:GetParentEntity()
730
+ local parent_preset = AutoAttachPresets[entity]
731
+ if not parent_preset then return end
732
+ local spot_name, spot_idx = self:GetSpotAndIdx()
733
+ if not spot_name or not spot_idx then return end
734
+ local aaspot_idx, aapost_obj = parent_preset:GetSpot(spot_name, spot_idx)
735
+ if not aapost_obj then return end
736
+
737
+ return aapost_obj, entity, parent_preset
738
+ end
739
+
740
+ function AutoAttachRuleInherit:FillAutoAttachTable(attach_table, entity, preset)
741
+ local spot, parent_entity, parent_preset = self:FindInheritedSpot()
742
+ if not spot then return attach_table end
743
+
744
+ for _, rule in ipairs(spot) do
745
+ attach_table = rule:FillAutoAttachTable(attach_table, parent_entity, parent_preset, self.parent)
746
+ end
747
+ return attach_table
748
+ end
749
+
750
+ function AutoAttachRuleInherit:IsActive()
751
+ local spot, _, _ = self:FindInheritedSpot()
752
+ return not not spot
753
+ end
754
+
755
+ DefineClass.AutoAttachRule = {
756
+ __parents = { "AutoAttachRuleBase" },
757
+ properties = {
758
+ { id = "attach_class", category = "Rule", name = "Object Class", editor = "combo", items = function() return ClassDescendantsCombo("CObject") end, default = "", },
759
+ { id = "quick_modes", default = false, no_save = true, editor = "buttons", category = "Rule", buttons = {
760
+ {name = "ParSystem", func = "QuickSetToParSystem"},
761
+ }},
762
+ { id = "offset", category = "Rule", name = "Offset", editor = "point", default = point(0, 0, 0), },
763
+ { id = "axis" , category = "Rule", name = "Axis", editor = "point", default = point(0, 0, 0), },
764
+ { id = "angle", category = "Rule", name = "Angle", editor = "number", default = 0, scale = "deg" },
765
+ { id = "member", category = "Rule", name = "Member", help = "The name of the property of the parent object that should be pointing to the attach object.", editor = "text", default = "", },
766
+ { id = "required_state", category = "Rule", name = "Attach State", help = "Conditional attachment", default = "", editor = "combo", items = function(obj)
767
+ return obj and obj.parent and obj.parent.parent and obj.parent.parent:GuessPossibleAutoattachStates() or {}
768
+ end, },
769
+ { id = "GameStatesFilter", name="Game State", category = "Rule", editor = "set", default = set(), three_state = true, items = function() return GetGameStateFilter() end },
770
+ { id = "DetailClass", category = "Rule", name = "Detail Class Override", editor = "dropdownlist",
771
+ items = {"Default", "Essential", "Optional", "Eye Candy"}, default = "Default",
772
+ },
773
+ { id = "inherited_values", no_edit = true, editor = "prop_table", default = false, },
774
+ },
775
+ parent = false,
776
+ }
777
+
778
+ function AutoAttachRule:IsActive()
779
+ return self.attach_class ~= ""
780
+ end
781
+
782
+
783
+ function AutoAttachRule:QuickSetToParSystem()
784
+ self.attach_class = "ParSystem"
785
+ ObjModified(self)
786
+ end
787
+
788
+ function AutoAttachRule:ResolveConditionFunc()
789
+ local gamestates_filters = self.GameStatesFilter
790
+ if not gamestates_filters or not next(gamestates_filters) then
791
+ return false
792
+ end
793
+
794
+ return function(obj, attach)
795
+ if gamestates_filters then
796
+ for key, value in pairs(gamestates_filters) do
797
+ if value then
798
+ if not GameState[key] then return false end
799
+ else
800
+ if GameState[key] then return false end
801
+ end
802
+ end
803
+ end
804
+
805
+ return true
806
+ end
807
+ end
808
+
809
+ function AutoAttachRule:GetCleanInheritedPropertyValues()
810
+ local inherited_values = self.inherited_values
811
+ if not inherited_values then
812
+ return false
813
+ end
814
+ local inherited_props = self:GetInheritedProps()
815
+ if not inherited_props or #inherited_props == 0 then
816
+ return false
817
+ end
818
+ local clean_value_list = {}
819
+ for _, prop in ipairs(inherited_props) do
820
+ local value = inherited_values[prop.id]
821
+ if value ~= nil then
822
+ clean_value_list[prop.id] = value
823
+ end
824
+ end
825
+ return clean_value_list
826
+ end
827
+
828
+ function AutoAttachRule:FillAutoAttachTable(attach_table, entity, preset, spot)
829
+ if self.attach_class == "" then
830
+ return attach_table
831
+ end
832
+ spot = spot or self.parent
833
+ attach_table = attach_table or {}
834
+
835
+ local attach_table_idle = attach_table["idle"] or {}
836
+ attach_table["idle"] = attach_table_idle
837
+
838
+ local istart, iend = GetSpotRange(spot.parent.id, "idle", spot.name)
839
+ if istart < 0 then
840
+ print(string.format("Warning: Could not find '%s' spot range for '%s'", spot.name, entity))
841
+ else
842
+ table.insert(attach_table_idle, {
843
+ spot_idx = istart + spot.idx - 1,
844
+ [2] = self.attach_class,
845
+ offset = self.offset,
846
+ axis = self.axis ~= zeropoint and self.axis,
847
+ angle = self.angle ~= 0 and self.angle,
848
+ member = self.member ~= "" and self.member,
849
+ required_state = self.required_state ~= "" and self.required_state or false,
850
+ condition = self:ResolveConditionFunc() or false,
851
+ DetailClass = self.DetailClass ~= "Default" and self.DetailClass,
852
+ inherited_properties = self:GetCleanInheritedPropertyValues(),
853
+ inherit_colorization = preset.PropagateColorization and CanInheritColorization(entity, self.attach_class),
854
+ })
855
+ end
856
+
857
+ return attach_table
858
+ end
859
+
860
+ function AutoAttachRule:GetEditorView()
861
+ local str
862
+ if self.attach_class == "ParSystem" then
863
+ str = "Particles <color 198 25 198>" .. (self.inherited_values and self.inherited_values["ParticlesName"] or "?") .. "</color>"
864
+ else
865
+ str = "Attach <color 75 105 198>" .. (self.attach_class or "?") .. "</color>"
866
+ end
867
+ str = str .. " (" .. self.DetailClass .. ")"
868
+ if self.required_state ~= "" then
869
+ str = str .. " : <color 20 120 20>" .. self.required_state .. "</color>"
870
+ end
871
+ if self.attach_class == "" then
872
+ str = "<color 168 168 168>" .. str .. "</color>"
873
+ end
874
+ return str
875
+ end
876
+
877
+ function AutoAttachRule:Setattach_class(value)
878
+ if self.parent and self.parent.parent and self.parent.parent.id == value then
879
+ value = ""
880
+ return false
881
+ end
882
+ self.attach_class = value
883
+ end
884
+
885
+ function AutoAttachRule:GetInheritedProps()
886
+ local properties = {}
887
+ local class_obj = g_Classes[self.attach_class]
888
+ if not class_obj then
889
+ return properties
890
+ end
891
+
892
+ local orig_properties = PropertyObject.GetProperties(self)
893
+ local properties_of_target_entity = class_obj:GetProperties()
894
+ for _, prop in ipairs(properties_of_target_entity) do
895
+ if prop.autoattach_prop then
896
+ assert(not table.find(orig_properties, "id", prop.id),
897
+ string.format("Property %s conflict between AutoAttachRule and %s", prop.id, self.attach_class))
898
+ prop = table.copy(prop)
899
+ prop.dont_save = true
900
+ table.insert(properties, prop)
901
+ end
902
+ end
903
+ return properties
904
+ end
905
+
906
+ function AutoAttachRule:GetProperties()
907
+ local properties = PropertyObject.GetProperties(self)
908
+ local class_obj = g_Classes[self.attach_class]
909
+ if not class_obj then
910
+ return properties
911
+ end
912
+
913
+ properties = table.copy(properties)
914
+ properties = table.iappend(properties, self:GetInheritedProps())
915
+ return properties
916
+ end
917
+
918
+ function AutoAttachRule:SetProperty(id, value)
919
+ if table.find(self:GetInheritedProps(), "id", id) then
920
+ self.inherited_values = self.inherited_values or {}
921
+ self.inherited_values[id] = value
922
+ return
923
+ end
924
+ PropertyObject.SetProperty(self, id, value)
925
+ end
926
+
927
+ function AutoAttachRule:GetProperty(id)
928
+ if self.inherited_values and self.inherited_values[id] ~= nil then
929
+ return self.inherited_values[id]
930
+ end
931
+
932
+ return PropertyObject.GetProperty(self, id)
933
+ end
934
+
935
+ function AutoAttachRule:OnEditorSetProperty(prop_id, old_value, ged)
936
+ RebuildAutoattach()
937
+ ged:ResolveObj("SelectedPreset"):RecreateDemoObject(ged)
938
+ local id = self.parent.parent.id
939
+ local class = rawget(_G, id)
940
+ if class and not class:IsKindOf("AutoAttachObject") then
941
+ return false
942
+ end
943
+ MapForEach("map", id, function(obj)
944
+ obj:SetAutoAttachMode(obj:GetAutoAttachMode())
945
+ end)
946
+ end
947
+
948
+ function AutoAttachRule:GetMaxColorizationMaterials()
949
+ if IsKindOf(_G[self.attach_class], "WaterObj") then return 3 end
950
+ return self.attach_class ~= "" and IsValidEntity(self.attach_class) and ColorizationMaterialsCount(self.attach_class) or 0
951
+ end
952
+
953
+ function AutoAttachRule:ColorizationReadOnlyReason()
954
+ return false
955
+ end
956
+
957
+ function AutoAttachRule:ColorizationPropsNoEdit(i)
958
+ if self.parent.parent.PropagateColorization then
959
+ return true
960
+ end
961
+ return self:GetMaxColorizationMaterials() < i
962
+ end
963
+
964
+ DefineClass.AutoAttachSpot = {
965
+ __parents = { "PropertyObject", "Container" },
966
+
967
+ properties = {
968
+ { id = "name", name = "Spot Name", editor = "text", default = "", read_only = true },
969
+ { id = "idx", name = "Number", editor = "number", default = -1, read_only = true, },
970
+ { id = "original_index", name = "Original Index", editor = "number", default = -1, read_only = true, },
971
+ },
972
+
973
+ annotated_autoattach = false,
974
+ EditorView = Untranslated("<Color><name> <idx><opt(u(attach_class), ' - <color 32 192 32>')> <AnnotatedAutoattachMsg>"),
975
+ parent = false,
976
+ ContainerClass = "AutoAttachRuleBase",
977
+ }
978
+
979
+ function AutoAttachSpot:Color()
980
+ return not self:HasSomethingAttached() and "<color 168 168 168>" or ""
981
+ end
982
+
983
+ function AutoAttachSpot:HasSomethingAttached()
984
+ if #self == 0 then return false end
985
+ for _, rule in ipairs(self) do
986
+ if rule:IsActive() then
987
+ return true
988
+ end
989
+ end
990
+ return false
991
+ end
992
+
993
+ function AutoAttachSpot:AnnotatedAutoattachMsg()
994
+ if not self.annotated_autoattach then return "" end
995
+ return "<color 158 22 22>" .. self.annotated_autoattach
996
+ end
997
+
998
+ function AutoAttachSpot.CreateRule(root, obj)
999
+ obj[#obj + 1] = AutoAttachRule:new({parent = obj})
1000
+ ObjModified(root)
1001
+ ObjModified(obj)
1002
+ end
1003
+
1004
+ function CommonlyUsedAttachItems()
1005
+ local ret = {}
1006
+ ForEachPreset("AutoAttachPreset", function(preset)
1007
+ for _, rule in ipairs(preset) do
1008
+ for _, subrule in ipairs(rule) do
1009
+ local class = rawget(subrule, "attach_class")
1010
+ if class and class ~= "" then
1011
+ ret[class] = (ret[class] or 0) + 1
1012
+ end
1013
+ end
1014
+ end
1015
+ end)
1016
+ for class, count in pairs(ret) do
1017
+ if count == 1 then
1018
+ ret[class] = nil
1019
+ end
1020
+ end
1021
+ return table.keys2(ret, "sorted")
1022
+ end
1023
+
1024
+ DefineClass.AutoAttachPresetFilter = {
1025
+ __parents = { "GedFilter" },
1026
+ properties = {
1027
+ { id = "NonEmpty", name = "Only show non-empty entries", default = false, editor = "bool" },
1028
+ { id = "HasAttach", name = "Has attach of class", default = false, editor = "combo", items = CommonlyUsedAttachItems },
1029
+ { id = "_", editor = "buttons", default = false, buttons = { { name = "Add new AutoAttach entity", func = "AddEntity" } } },
1030
+ },
1031
+ }
1032
+
1033
+ function AutoAttachPresetFilter:FilterObject(obj)
1034
+ if self.NonEmpty then
1035
+ for _, rule in ipairs(obj) do
1036
+ for _, subrule in ipairs(rule) do
1037
+ if subrule:IsKindOf("AutoAttachRule") and subrule.attach_class ~= "" then
1038
+ return true
1039
+ end
1040
+ end
1041
+ end
1042
+ return false
1043
+ end
1044
+ local class = self.HasAttach
1045
+ if class then
1046
+ for _, rule in ipairs(obj) do
1047
+ for _, subrule in ipairs(rule) do
1048
+ if subrule:IsKindOf("AutoAttachRule") and subrule.attach_class == class then
1049
+ return true
1050
+ end
1051
+ end
1052
+ end
1053
+ return false
1054
+ end
1055
+ return true
1056
+ end
1057
+
1058
+ function AutoAttachPresetFilter:AddEntity(root, prop_id, ged)
1059
+ local entities = {}
1060
+ ForEachPreset("EntitySpec", function(preset)
1061
+ if not string.find(preset.class_parent, "AutoAttachObject", 1, true) and not preset.id:starts_with("#") then
1062
+ entities[#entities + 1] = preset.id
1063
+ end
1064
+ end)
1065
+
1066
+ local entity = ged:WaitListChoice(entities, "Choose entity to add:")
1067
+ if not entity then
1068
+ return
1069
+ end
1070
+
1071
+ local spec = EntitySpecPresets[entity]
1072
+ if spec.class_parent == "" then
1073
+ spec.class_parent = "AutoAttachObject"
1074
+ else
1075
+ spec.class_parent = spec.class_parent .. ",AutoAttachObject"
1076
+ end
1077
+
1078
+ GedSetUiStatus("add_autoattach_entity", "Saving ArtSpec...")
1079
+ EntitySpec:SaveAll()
1080
+ self.NonEmpty = false
1081
+ self.HasAttach = false
1082
+ GenerateMissingEntities()
1083
+ ged:SetSelection("root", { 1, table.find(Presets.AutoAttachPreset.Default, "id", entity) })
1084
+ GedSetUiStatus("add_autoattach_entity")
1085
+
1086
+ ged:ShowMessage(Untranslated("Attention!"), Untranslated("You need to commit both the assets and the project folder!"))
1087
+ end
1088
+
1089
+ DefineClass.AutoAttachPreset = {
1090
+ __parents = { "Preset" },
1091
+
1092
+ properties = {
1093
+ { id = "Id", read_only = true, },
1094
+ { id = "SaveIn", read_only = true, },
1095
+ { id = "help", editor = "buttons", buttons = {{name = "Go to ArtSpec", func = "GotoArtSpec"}}, default = false,},
1096
+ { id = "PropagateColorization", editor = "bool", default = true },
1097
+ },
1098
+
1099
+ GlobalMap = "AutoAttachPresets",
1100
+ ContainerClass = "AutoAttachSpot",
1101
+ GedEditor = "GedAutoAttachEditor",
1102
+ EditorMenubar = "Editors.Art",
1103
+ EditorMenubarName = "AutoAttach Editor",
1104
+ EditorIcon = "CommonAssets/UI/Icons/attach attachment paperclip.png",
1105
+ FilterClass = "AutoAttachPresetFilter",
1106
+
1107
+ EnableReloading = false,
1108
+ }
1109
+
1110
+ function AutoAttachPreset:GuessPossibleAutoattachStates()
1111
+ return GetEntityAutoAttachModes(nil, self.id)
1112
+ end
1113
+
1114
+ function AutoAttachPreset:EditorContext()
1115
+ local context = Preset.EditorContext(self)
1116
+ context.Classes = {}
1117
+ context.ContainerTree = true
1118
+ return context
1119
+ end
1120
+
1121
+ function AutoAttachPreset:EditorItemsMenu()
1122
+ return {}
1123
+ end
1124
+
1125
+ function AutoAttachPreset:GotoArtSpec(root)
1126
+ local editor = OpenPresetEditor("EntitySpec")
1127
+ local spec = self:GetEntitySpec()
1128
+ local root = editor:ResolveObj("root")
1129
+ local group_idx = table.find(root, root[spec.group])
1130
+ local idx = table.find(root[spec.group], spec)
1131
+ editor:SetSelection("root", {group_idx, idx})
1132
+ end
1133
+
1134
+ function AutoAttachPreset:PostLoad()
1135
+ for idx, item in ipairs(self) do
1136
+ item.parent = self
1137
+ for _, subitem in ipairs(item) do
1138
+ subitem.parent = item
1139
+ end
1140
+ end
1141
+ Preset.PostLoad(self)
1142
+ end
1143
+
1144
+ function AutoAttachPreset:GenerateCode(code)
1145
+ self:UpdateSpotData() -- to read save_in
1146
+
1147
+ -- drop redundant/unneeded data
1148
+ local has_something_attached = false
1149
+ for i = #self, 1, -1 do
1150
+ local spot = self[i]
1151
+ if not spot:HasSomethingAttached() then
1152
+ table.remove(self, i)
1153
+ else
1154
+ spot.original_index = nil
1155
+ spot.annotated_autoattach = nil
1156
+ has_something_attached = true
1157
+ if not spot[#spot]:IsActive() then
1158
+ table.remove(spot, #spot)
1159
+ end
1160
+ end
1161
+ end
1162
+ if has_something_attached then
1163
+ Preset.GenerateCode(self, code)
1164
+ end
1165
+
1166
+ self:UpdateSpotData() -- to write original_index and annotated_autoattach back to the structure
1167
+ end
1168
+
1169
+ function AutoAttachPreset:GetSpot(name, idx)
1170
+ for i, value in ipairs(self) do
1171
+ if value.name == name and value.idx == idx then
1172
+ return i, value
1173
+ end
1174
+ end
1175
+ end
1176
+
1177
+ function AutoAttachPreset:UpdateSpotData()
1178
+ local spec = self:GetEntitySpec()
1179
+ if not spec then
1180
+ return
1181
+ end
1182
+ self.save_in = spec:GetSaveIn()
1183
+ local spots = GetEntitySpots(self.id)
1184
+
1185
+ -- drop additional spots
1186
+ for i = #self, 1, -1 do
1187
+ local entry = self[i]
1188
+ if entry.idx > (spots[entry.name] and #spots[entry.name] or -1) then
1189
+ table.remove(self, i)
1190
+ end
1191
+ end
1192
+
1193
+ for spot_name, indices in pairs(spots) do
1194
+ for idx = 1, #indices do
1195
+ local internal_idx, spot = self:GetSpot(spot_name, idx)
1196
+ if spot then
1197
+ spot.original_index = indices[idx]
1198
+ spot.annotated_autoattach = GetSpotAnnotation(self.id, indices[idx])
1199
+ else
1200
+ spot = AutoAttachSpot:new({
1201
+ name = spot_name,
1202
+ idx = idx,
1203
+ original_index = indices[idx],
1204
+ annotated_autoattach = GetSpotAnnotation(self.id, indices[idx]),
1205
+ })
1206
+ table.insert(self, spot)
1207
+ end
1208
+ spot.parent = self
1209
+ end
1210
+ end
1211
+
1212
+ table.sort(self, function(a, b)
1213
+ if a.name < b.name then return true end
1214
+ if a.name > b.name then return false end
1215
+ if a.idx < b.idx then return true end
1216
+ return false
1217
+ end)
1218
+ end
1219
+
1220
+ if FirstLoad then
1221
+ GedAutoAttachEditorLockedObject = {}
1222
+ GedAutoAttachDemos = {}
1223
+ end
1224
+
1225
+ DefineClass.AutoAttachPresetDemoObject = {
1226
+ __parents = {"Shapeshifter", "AutoAttachObject"}
1227
+ }
1228
+
1229
+ AutoAttachPresetDemoObject.ShouldAttach = return_true
1230
+
1231
+ function AutoAttachPresetDemoObject:ChangeEntity(entity)
1232
+ self:DestroyAutoAttaches()
1233
+ self:ClearAttachMembers()
1234
+ Shapeshifter.ChangeEntity(self, entity)
1235
+ self:DestroyAutoAttaches()
1236
+ self:ClearAttachMembers()
1237
+ AutoAttachShapeshifterObjects(self)
1238
+ end
1239
+
1240
+ function AutoAttachPresetDemoObject:CreateLightHelpers()
1241
+ self:ForEachAttach(function(attach)
1242
+ if IsKindOf(attach, "Light") then
1243
+ PropertyHelpers_Init(attach)
1244
+ end
1245
+ end)
1246
+ end
1247
+
1248
+ function AutoAttachPresetDemoObject:AutoAttachObjects()
1249
+ AutoAttachShapeshifterObjects(self)
1250
+ self:CreateLightHelpers()
1251
+ end
1252
+
1253
+ function AutoAttachPreset:ViewDemoObject(ged)
1254
+ local demo_obj = GedAutoAttachDemos[ged]
1255
+ if demo_obj and IsValid(demo_obj) then
1256
+ ViewObject(demo_obj)
1257
+ end
1258
+ end
1259
+
1260
+ function AutoAttachPreset:RecreateDemoObject(ged)
1261
+ if CurrentMap == "" then
1262
+ return
1263
+ end
1264
+ if ged and ged.context.lock_preset then
1265
+ local obj = GedAutoAttachEditorLockedObject[ged]
1266
+ obj:DestroyAutoAttaches()
1267
+ obj:ClearAttachMembers()
1268
+ AutoAttachObjects(GedAutoAttachEditorLockedObject[ged], "init")
1269
+ return
1270
+ end
1271
+
1272
+ local demo_obj = GedAutoAttachDemos[ged]
1273
+ if not demo_obj or not IsValid(demo_obj) then
1274
+ demo_obj = PlaceObject("AutoAttachPresetDemoObject")
1275
+ local look_at = GetTerrainGamepadCursor()
1276
+ look_at = look_at:SetZ(terrain.GetSurfaceHeight(look_at))
1277
+ demo_obj:SetPos(look_at)
1278
+ end
1279
+ GedAutoAttachDemos[ged] = demo_obj
1280
+ demo_obj:ChangeEntity(self.id)
1281
+ end
1282
+
1283
+ function OnMsg.GedClosing(ged_id)
1284
+ local demo_obj = GedAutoAttachDemos[GedConnections[ged_id]]
1285
+ DoneObject(demo_obj)
1286
+ GedAutoAttachDemos[GedConnections[ged_id]] = nil
1287
+ end
1288
+
1289
+ function AutoAttachPreset:OnEditorSelect(selected, ged)
1290
+ if selected then
1291
+ self:UpdateSpotData()
1292
+ self:RecreateDemoObject(ged)
1293
+ end
1294
+ end
1295
+
1296
+ function AutoAttachPreset:GetError()
1297
+ if not self:GetEntitySpec() then
1298
+ return "Could not find the ArtSpec."
1299
+ end
1300
+ end
1301
+
1302
+ function AutoAttachPreset:GetEntitySpec()
1303
+ return FindArtSpecById(self.id)
1304
+ end
1305
+
1306
+ function OnMsg.GedOpened(ged_id)
1307
+ local ged = GedConnections[ged_id]
1308
+ if ged and ged:ResolveObj("root") == Presets.AutoAttachPreset then
1309
+ CreateRealTimeThread(GenerateMissingEntities)
1310
+ end
1311
+ end
1312
+
1313
+ function OpenAutoattachEditor(objlist, lock_entity)
1314
+ if not IsRealTimeThread() then
1315
+ CreateRealTimeThread(OpenAutoattachEditor, entity)
1316
+ return
1317
+ end
1318
+ lock_entity = not not lock_entity
1319
+ local target_entity
1320
+ if objlist and objlist[1] and IsValid(objlist[1]) then
1321
+ target_entity = objlist[1]
1322
+ end
1323
+
1324
+ if not target_entity and lock_entity then
1325
+ print("No entity selected.")
1326
+ return
1327
+ end
1328
+
1329
+ if target_entity then
1330
+ GenerateMissingEntities() -- make sure all entities are generated. Otherwise the selection may fail
1331
+ end
1332
+
1333
+ local context = AutoAttachPreset:EditorContext()
1334
+ context.lock_preset = lock_entity
1335
+ local ged = OpenPresetEditor("AutoAttachPreset", context)
1336
+ if target_entity then
1337
+ ged:SetSelection("root", PresetGetPath(AutoAttachPresets[target_entity:GetEntity()]))
1338
+ GedAutoAttachEditorLockedObject[ged] = target_entity
1339
+ end
1340
+ end
1341
+
1342
+ DefineClass.AutoAttachSIModulator =
1343
+ {
1344
+ __parents = {"CObject", "PropertyObject"},
1345
+ properties = {
1346
+ { id = "SIModulation", editor = "number", default = 100, min = 0, max = 255, slider = true, autoattach_prop = true },
1347
+ }
1348
+ }
1349
+
1350
+ function AutoAttachSIModulator:SetSIModulation(value)
1351
+ local parent = self:GetParent()
1352
+ if not parent.SIModulationManual then
1353
+ parent:SetSIModulation(value)
1354
+ end
1355
+ end
CommonLua/Classes/BaseObjects.lua ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ----- UpdateObject
2
+
3
+ DefineClass.UpdateObject = {
4
+ __parents = {"Object"},
5
+
6
+ update_thread_on_init = true,
7
+ update_interval = 10000,
8
+ update_thread = false,
9
+ }
10
+
11
+ RecursiveCallMethods.OnObjUpdate = "call"
12
+
13
+ local Sleep = Sleep
14
+ local procall = procall
15
+ local GameTime = GameTime
16
+ function UpdateObject:Init()
17
+ if self.update_thread_on_init then
18
+ self:StartObjUpdateThread()
19
+ end
20
+ end
21
+
22
+ function UpdateObject:ObjUpdateProc(update_interval)
23
+ self:InitObjUpdate(update_interval)
24
+ while true do
25
+ procall(self.OnObjUpdate, self, GameTime(), update_interval)
26
+ Sleep(update_interval)
27
+ end
28
+ end
29
+
30
+ function UpdateObject:StartObjUpdateThread()
31
+ if not self:IsSyncObject() or not mapdata.GameLogic or not self.update_interval then
32
+ return
33
+ end
34
+ DeleteThread(self.update_thread)
35
+ self.update_thread = CreateGameTimeThread(self.ObjUpdateProc, self, self.update_interval)
36
+ if Platform.developer then
37
+ ThreadsSetThreadSource(self.update_thread, "ObjUpdateThread", self.ObjUpdateProc)
38
+ end
39
+ end
40
+
41
+ function UpdateObject:StopObjUpdateThread()
42
+ DeleteThread(self.update_thread)
43
+ self.update_thread = nil
44
+ end
45
+
46
+ function UpdateObject:InitObjUpdate(update_interval)
47
+ Sleep(1 + self:Random(update_interval, "InitObjUpdate"))
48
+ end
49
+
50
+ function UpdateObject:Done()
51
+ self:StopObjUpdateThread()
52
+ end
53
+
54
+
55
+ ----- ReservedObject
56
+
57
+ DefineClass.ReservedObject = {
58
+ __parents = { "InitDone" },
59
+ properties = {
60
+ { id = "reserved_by", editor = "object", default = false, no_edit = true },
61
+ },
62
+ }
63
+
64
+ function TryInterruptReserved(reserved_obj)
65
+ local reserved_by = reserved_obj.reserved_by
66
+ if IsValid(reserved_by) then
67
+ return reserved_by:OnReservationInterrupted()
68
+ end
69
+ reserved_obj.reserved_by = nil
70
+ end
71
+
72
+ ReservedObject.Disown = TryInterruptReserved
73
+
74
+ AutoResolveMethods.CanReserverBeInterrupted = "or"
75
+ ReservedObject.CanReserverBeInterrupted = empty_func
76
+
77
+ function ReservedObject:CanBeReservedBy(obj)
78
+ return not self.reserved_by or self.reserved_by == obj or self:CanReserverBeInterrupted(obj)
79
+ end
80
+
81
+ function ReservedObject:TryReserve(reserved_by)
82
+ if not self:CanBeReservedBy(reserved_by) then return false end
83
+ if self.reserved_by and self.reserved_by ~= reserved_by then
84
+ if not TryInterruptReserved(self) then return end
85
+ end
86
+ return self:Reserve(reserved_by)
87
+ end
88
+
89
+ function ReservedObject:Reserve(reserved_by)
90
+ assert(IsKindOf(reserved_by, "ReserverObject"))
91
+ local previous_reservation = reserved_by.reserved_obj
92
+ if previous_reservation and previous_reservation ~= self then
93
+ --assert(not previous_reservation, "Reserver trying to reserve two objects at once!")
94
+ previous_reservation:CancelReservation(reserved_by)
95
+ end
96
+ self.reserved_by = reserved_by
97
+ reserved_by.reserved_obj = self
98
+ self:OnReserved(reserved_by)
99
+ return true
100
+ end
101
+
102
+ ReservedObject.OnReserved = empty_func
103
+ ReservedObject.OnReservationCanceled = empty_func
104
+
105
+ function ReservedObject:CancelReservation(reserved_by)
106
+ if self.reserved_by == reserved_by then
107
+ self.reserved_by = nil
108
+ reserved_by.reserved_obj = nil
109
+ self:OnReservationCanceled()
110
+ return true
111
+ end
112
+ end
113
+
114
+ function ReservedObject:Done()
115
+ self:Disown()
116
+ end
117
+
118
+ DefineClass.ReserverObject = {
119
+ __parents = { "CommandObject" },
120
+
121
+ reserved_obj = false,
122
+ }
123
+
124
+ function ReserverObject:OnReservationInterrupted()
125
+ return self:TrySetCommand("CmdInterrupt")
126
+ end
127
+
128
+ ----- OwnedObject
129
+
130
+ DefineClass.OwnershipStateBase = {
131
+ OnStateTick = empty_func,
132
+ OnStateExit = empty_func,
133
+
134
+ CanDisown = empty_func,
135
+ CanBeOwnedBy = empty_func,
136
+ }
137
+
138
+ DefineClass("ConcreteOwnership", "OwnershipStateBase")
139
+
140
+ local function SetOwnerObject(owned_obj, owner)
141
+ assert(not owner or IsKindOf(owner, "OwnerObject"))
142
+ owner = owner or false
143
+ local prev_owner = owned_obj.owner
144
+ if owner ~= prev_owner then
145
+ owned_obj.owner = owner
146
+
147
+ local notify_owner = not prev_owner or prev_owner:GetOwnedObject(owned_obj.ownership_class) == owned_obj
148
+ if notify_owner then
149
+ if prev_owner then
150
+ prev_owner:SetOwnedObject(false, owned_obj.ownership_class)
151
+ end
152
+ if owner then
153
+ owner:SetOwnedObject(owned_obj)
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ function ConcreteOwnership.OnStateTick(owned_obj, owner)
160
+ return SetOwnerObject(owned_obj, owner)
161
+ end
162
+
163
+ function ConcreteOwnership.OnStateExit(owned_obj)
164
+ return SetOwnerObject(owned_obj, false)
165
+ end
166
+
167
+ function ConcreteOwnership.CanDisown(owned_obj, owner, reason)
168
+ return owned_obj.owner == owner
169
+ end
170
+
171
+ function ConcreteOwnership.CanBeOwnedBy(owned_obj, owner, ...)
172
+ return owned_obj.owner == owner
173
+ end
174
+
175
+ DefineClass("SharedOwnership", "OwnershipStateBase")
176
+ SharedOwnership.CanBeOwnedBy = return_true
177
+
178
+ DefineClass("ForbiddenOwnership", "OwnershipStateBase")
179
+
180
+ DefineClass.OwnedObject = {
181
+ __parents = { "ReservedObject" },
182
+ properties = {
183
+ { id = "owner", editor = "object", default = false, no_edit = true },
184
+ { id = "can_change_ownership", name = "Can change ownership", editor = "bool", default = true, help = "If true, the player can change who owns the object", },
185
+ { id = "ownership_class", name = "Ownership class", editor = "combo", default = false, items = GatherComboItems("GatherOwnershipClasses"), },
186
+ },
187
+
188
+ ownership = "SharedOwnership",
189
+ }
190
+
191
+ AutoResolveMethods.CanDisown = "and"
192
+ function OwnedObject:CanDisown(owner, reason)
193
+ return g_Classes[self.ownership].CanDisown(self, owner, reason)
194
+ end
195
+
196
+ function OwnedObject:Disown()
197
+ ReservedObject.Disown(self)
198
+ self:TrySetSharedOwnership()
199
+ end
200
+
201
+ AutoResolveMethods.CanBeOwnedBy = "and"
202
+ function OwnedObject:CanBeOwnedBy(obj, ...)
203
+ if not self:CanBeReservedBy(obj) then return end
204
+ return g_Classes[self.ownership].CanBeOwnedBy(self, obj, ...)
205
+ end
206
+
207
+ AutoResolveMethods.CanChangeOwnership = "and"
208
+ function OwnedObject:CanChangeOwnership()
209
+ return self.can_change_ownership
210
+ end
211
+
212
+ function OwnedObject:GetReservedByOrOwner()
213
+ return self.reserved_by or self.owner
214
+ end
215
+
216
+ OwnedObject.OnOwnershipChanged = empty_func
217
+
218
+ function OwnedObject:TrySetOwnership(ownership, forced, ...)
219
+ assert(ownership)
220
+ if not ownership or not forced and not self:CanChangeOwnership() then return end
221
+
222
+ local prev_owner = self.owner
223
+ local prev_ownership = self.ownership
224
+ self.ownership = ownership
225
+ if prev_ownership ~= ownership then
226
+ g_Classes[prev_ownership].OnStateExit(self, ...)
227
+ end
228
+ g_Classes[ownership].OnStateTick(self, ...)
229
+ self:OnOwnershipChanged(prev_ownership, prev_owner)
230
+ end
231
+
232
+ local function TryInterruptReservedOnDifferentOwner(owned_obj)
233
+ local reserved_by = owned_obj.reserved_by
234
+ if IsValid(reserved_by) and reserved_by ~= owned_obj.owner then
235
+ reserved_by:OnReservationInterrupted()
236
+ owned_obj.reserved_by = nil
237
+ end
238
+ end
239
+
240
+ local OwnershipChangedReactions = {
241
+ ConcreteOwnership = {
242
+ ConcreteOwnership = TryInterruptReservedOnDifferentOwner,
243
+ ForbiddenOwnership = TryInterruptReserved,
244
+ },
245
+ SharedOwnership = {
246
+ ConcreteOwnership = TryInterruptReservedOnDifferentOwner,
247
+ ForbiddenOwnership = TryInterruptReserved,
248
+ }
249
+ }
250
+
251
+ function OwnedObject:OnOwnershipChanged(prev_ownership, prev_owner)
252
+ local transition = table.get(OwnershipChangedReactions, prev_ownership, self.ownership)
253
+ if transition then
254
+ transition(self)
255
+ end
256
+ end
257
+
258
+ ----- OwnedObject helper functions
259
+
260
+ function OwnedObject:TrySetConcreteOwnership(forced, owner)
261
+ return self:TrySetOwnership("ConcreteOwnership", forced, owner)
262
+ end
263
+
264
+ function OwnedObject:SetConcreteOwnership(...)
265
+ return self:TrySetConcreteOwnership("forced", ...)
266
+ end
267
+
268
+ function OwnedObject:HasConcreteOwnership()
269
+ return self.ownership == "ConcreteOwnership"
270
+ end
271
+
272
+ function OwnedObject:TrySetSharedOwnership(forced, ...)
273
+ return self:TrySetOwnership("SharedOwnership", forced, ...)
274
+ end
275
+
276
+ function OwnedObject:SetSharedOwnership(...)
277
+ return self:TrySetSharedOwnership("forced", ...)
278
+ end
279
+
280
+ function OwnedObject:HasSharedOwnership()
281
+ return self.ownership == "SharedOwnership"
282
+ end
283
+
284
+ function OwnedObject:TrySetForbiddenOwnership(forced, ...)
285
+ return self:TrySetOwnership("ForbiddenOwnership", forced, ...)
286
+ end
287
+
288
+ function OwnedObject:SetForbiddenOwnership(...)
289
+ return self:TrySetForbiddenOwnership("forced", ...)
290
+ end
291
+
292
+ function OwnedObject:HasForbiddenOwnership()
293
+ return self.ownership == "ForbiddenOwnership"
294
+ end
295
+
296
+ ----- OwnedObject helper functions end
297
+
298
+ DefineClass.OwnedByUnit = {
299
+ __parents = { "OwnedObject" },
300
+ properties = {
301
+ { id = "can_have_dead_owners", name = "Can have dead owners", editor = "bool", default = false, help = "If true, the object can have dead units as owners", },
302
+ }
303
+ }
304
+
305
+ function OwnedByUnit:CanBeOwnedBy(obj, ...)
306
+ if not self.can_have_dead_owners and obj:IsDead() then return end
307
+ return OwnedObject.CanBeOwnedBy(self, obj, ...)
308
+ end
309
+
310
+ DefineClass.OwnerObject = {
311
+ __parents = { "ReserverObject" },
312
+ owned_objects = false,
313
+ }
314
+
315
+ function OwnerObject:Init()
316
+ self.owned_objects = {}
317
+ end
318
+
319
+ function OwnerObject:Owns(object)
320
+ local ownership_class = object.ownership_class
321
+ if not ownership_class then return end
322
+ return self.owned_objects[ownership_class] == object
323
+ end
324
+
325
+ function OwnerObject:DisownObjects(reason)
326
+ local owned_objects = self.owned_objects
327
+ for _, ownership_class in ipairs(owned_objects) do
328
+ local owned_object = owned_objects[ownership_class]
329
+ if owned_object and owned_object:CanDisown(self, reason) then
330
+ owned_object:Disown()
331
+ end
332
+ end
333
+ end
334
+
335
+ function OwnerObject:GetOwnedObject(ownership_class)
336
+ assert(ownership_class)
337
+ return self.owned_objects[ownership_class]
338
+ end
339
+
340
+ function OwnerObject:SetOwnedObject(owned_obj, ownership_class)
341
+ assert(not owned_obj or owned_obj:IsKindOf("OwnedObject"))
342
+ if owned_obj then
343
+ ownership_class = ownership_class or owned_obj.ownership_class
344
+ assert(ownership_class == owned_obj.ownership_class)
345
+ end
346
+ assert(ownership_class)
347
+ if not ownership_class then
348
+ return false
349
+ end
350
+ local prev_owned_obj = self:GetOwnedObject(ownership_class)
351
+ if prev_owned_obj == owned_obj then
352
+ return false
353
+ end
354
+ local owned_objects = self.owned_objects
355
+
356
+ owned_objects[ownership_class] = owned_obj
357
+ table.remove_entry(owned_objects, ownership_class)
358
+ if owned_obj then
359
+ table.insert(owned_objects, ownership_class)
360
+ if prev_owned_obj then
361
+ prev_owned_obj:TrySetSharedOwnership()
362
+ end
363
+ owned_obj:TrySetConcreteOwnership(nil, self)
364
+ end
365
+ return true
366
+ end
367
+
368
+ if Platform.developer then
369
+
370
+ function OwnedObject:GetTestData(data)
371
+ data.ReservedBy = self.reserved_by
372
+ end
373
+
374
+ end
375
+
376
+ -----
CommonLua/Classes/BlendEntityObj.lua ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DefineClass.BlendEntityObj = {
2
+ __parents = { "Object" },
3
+ properties = {
4
+ { category = "Blend", id = "BlendEntity1", name = "Entity 1", editor = "choice", default = "Human_Head_M_As_01", items = function (obj) return obj:GetBlendEntityList() end },
5
+ { category = "Blend", id = "BlendWeight1", name = "Weight 1", editor = "number", default = 50, slider = true, min = 0, max = 100 },
6
+ { category = "Blend", id = "BlendEntity2", name = "Entity 2", editor = "choice", default = "", items = function (obj) return obj:GetBlendEntityList() end },
7
+ { category = "Blend", id = "BlendWeight2", name = "Weight 2", editor = "number", default = 0, slider = true, min = 0, max = 100 },
8
+ { category = "Blend", id = "BlendEntity3", name = "Entity 3", editor = "choice", default = "", items = function (obj) return obj:GetBlendEntityList() end },
9
+ { category = "Blend", id = "BlendWeight3", name = "Weight 3", editor = "number", default = 0, slider = true, min = 0, max = 100 },
10
+ },
11
+ entity = "Human_Head_M_Placeholder_01",
12
+ }
13
+
14
+ function BlendEntityObj:GetBlendEntityList()
15
+ return { "" }
16
+ end
17
+
18
+ local g_UpdateBlendObjs = {}
19
+ local g_UpdateBlendEntityThread = false
20
+
21
+ function GetEntityIdleMaterial(entity)
22
+ return entity and entity ~= "" and GetStateMaterial(entity, "idle") or ""
23
+ end
24
+
25
+ function BlendEntityObj:UpdateBlendInternal()
26
+ if (not self.BlendEntity1 or self.BlendWeight1 == 0) and
27
+ (not self.BlendEntity2 or self.BlendWeight2 == 0) and
28
+ (not self.BlendEntity3 or self.BlendWeight3 == 0) then
29
+ return
30
+ end
31
+
32
+ local err = AsyncMeshBlend(self.entity, 0,
33
+ self.BlendEntity1, self.BlendWeight1,
34
+ self.BlendEntity2, self.BlendWeight2,
35
+ self.BlendEntity3, self.BlendWeight3)
36
+ if err then print("Failed to blend meshes: ", err) end
37
+
38
+ do
39
+ local mat0 = GetEntityIdleMaterial(self.entity)
40
+ local mat1 = GetEntityIdleMaterial(self.BlendEntity1)
41
+ local mat2 = GetEntityIdleMaterial(self.BlendEntity2)
42
+ local mat3 = GetEntityIdleMaterial(self.BlendEntity3)
43
+ assert(mat0 ~= mat1 and mat0 ~= mat2 and mat0 ~= mat3)
44
+ end
45
+
46
+ local sumBlends = self.BlendWeight1 + self.BlendWeight2 + self.BlendWeight2
47
+ local blend2, blend3 = 0, 0
48
+ if sumBlends ~= self.BlendWeight1 then
49
+ blend2 = self.BlendWeight2 * 100 / (sumBlends - self.BlendWeight1)
50
+ blend3 = self.BlendWeight3 * 100 / sumBlends
51
+ end
52
+ SetMaterialBlendMaterials(GetEntityIdleMaterial(self.entity),
53
+ GetEntityIdleMaterial(self.BlendEntity1), blend2,
54
+ GetEntityIdleMaterial(self.BlendEntity2), blend3,
55
+ GetEntityIdleMaterial(self.BlendEntity3))
56
+
57
+ self:ChangeEntity(self.entity)
58
+ end
59
+
60
+ function BlendEntityObj:UpdateBlend()
61
+ g_UpdateBlendObjs[self] = true
62
+ if not g_UpdateBlendEntityThread then
63
+ g_UpdateBlendEntityThread = CreateRealTimeThread(function()
64
+ while true do
65
+ local obj, v = next(g_UpdateBlendObjs)
66
+ if obj == nil then
67
+ break
68
+ end
69
+ g_UpdateBlendObjs[obj] = nil
70
+ obj:UpdateBlendInternal()
71
+ end
72
+ g_UpdateBlendEntityThread = false
73
+ end)
74
+ end
75
+ end
76
+
77
+ function BlendEntityObj:OnEditorSetProperty(prop_id, old_value, ged)
78
+ if prop_id == "BlendEntity1" or prop_id == "BlendEntity2" or prop_id == "BlendEntity3"
79
+ or prop_id == "BlendWeight1" or prop_id == "BlendWeight2" or prop_id == "BlendWeight3"
80
+ then
81
+ self:UpdateBlend()
82
+ end
83
+ end
84
+
85
+ function BlendTest()
86
+ local obj = BlendEntityObj:new()
87
+ obj:SetPos(GetTerrainCursor())
88
+ ViewObject(obj)
89
+ editor.ClearSel()
90
+ editor.AddToSel({obj})
91
+ OpenGedGameObjectEditor(editor.GetSel())
92
+ return obj
93
+ end
94
+
95
+ function BlendMatTest(weight2, weight3)
96
+ local obj = PlaceObj("Jacket_Nylon_M_Slim_01")
97
+ obj:SetPos(GetTerrainCursor())
98
+ ViewObject(obj)
99
+ editor.ClearSel()
100
+ editor.AddToSel({obj})
101
+
102
+ local blendEntity1 = "Jacket_Nylon_M_Slim_01"
103
+ local blendEntity2 = "Jacket_Nylon_M_Skinny_01"
104
+ local blendEntity3 = "Jacket_Nylon_M_Chubby_01"
105
+
106
+ weight2 = weight2 or 50
107
+ weight3 = weight3 or 25
108
+
109
+ SetMaterialBlendMaterials(GetEntityIdleMaterial(obj:GetEntity()),
110
+ GetEntityIdleMaterial(blendEntity1), weight2,
111
+ GetEntityIdleMaterial(blendEntity2), weight3,
112
+ GetEntityIdleMaterial(blendEntity3))
113
+
114
+ return obj
115
+ end
CommonLua/Classes/CameraEditor.lua ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ if FirstLoad then
2
+ s_CameraFadeThread = false
3
+ end
4
+
5
+ function DeleteCameraFadeThread()
6
+ if s_CameraFadeThread then
7
+ DeleteThread(s_CameraFadeThread)
8
+ s_CameraFadeThread = false
9
+ end
10
+ end
11
+
12
+ function CameraShowClose(last_camera)
13
+ DeleteCameraFadeThread()
14
+ if last_camera then
15
+ last_camera:RevertProperties()
16
+ end
17
+ UnlockCamera("CameraPreset")
18
+ end
19
+
20
+ function SwitchToCamera(camera, old_camera, in_between_callback, dont_lock, ged)
21
+ if not CanYield() then
22
+ DeleteCameraFadeThread()
23
+ s_CameraFadeThread = CreateRealTimeThread(function()
24
+ SwitchToCamera(camera, old_camera, in_between_callback, dont_lock, ged)
25
+ end)
26
+ return
27
+ end
28
+
29
+ if IsEditorActive() then
30
+ editor.ClearSel()
31
+ editor.AddToSel({camera})
32
+ end
33
+ if old_camera then
34
+ old_camera:RevertProperties(not(camera.flip_to_adjacent and old_camera.flip_to_adjacent))
35
+ end
36
+ if in_between_callback then
37
+ in_between_callback()
38
+ end
39
+ camera:ApplyProperties(dont_lock, not(camera.flip_to_adjacent and old_camera.flip_to_adjacent), ged)
40
+ end
41
+
42
+ function ShowPredefinedCamera(id)
43
+ local cam = PredefinedCameras[id]
44
+ if not cam then
45
+ print("No such camera preset: ", id)
46
+ return
47
+ end
48
+ CreateRealTimeThread(cam.ApplyProperties, cam, "dont_lock")
49
+ end
50
+
51
+ function GedOpCreateCameraDest(ged, selected_camera)
52
+ if not selected_camera or not IsKindOf(selected_camera, "Camera") then return end
53
+ selected_camera:SetDest(selected_camera)
54
+ GedObjectModified(selected_camera)
55
+ end
56
+
57
+ function GedOpUpdateCamera(ged, selected_camera)
58
+ if not selected_camera or not IsKindOf(selected_camera, "Camera") then return end
59
+ selected_camera:QueryProperties()
60
+ GedObjectModified(selected_camera)
61
+ end
62
+
63
+ function GedOpViewMovement(ged, selected_camera)
64
+ if not selected_camera or not IsKindOf(selected_camera, "Camera") then return end
65
+ SwitchToCamera(selected_camera, nil, nil, "don't lock")
66
+ end
67
+
68
+ function GedOpIsViewMovementToggled()
69
+ return not not GetDialog("Showcase")
70
+ end
71
+
72
+ local function TakeCameraScreenshot(ged, path, sector, camera)
73
+ if GetMapName() ~= camera.map then
74
+ ChangeMap(camera.map)
75
+ end
76
+
77
+ camera:ApplyProperties()
78
+ local oldInterfaceInScreenshot = hr.InterfaceInScreenshot
79
+ hr.InterfaceInScreenshot = camera.interface and 1 or 0
80
+
81
+ local image = string.format("%s/%s.png", path, sector)
82
+ AsyncFileDelete(image)
83
+ WaitNextFrame(3)
84
+ local store = {}
85
+ Msg("BeforeUpsampledScreenshot", store)
86
+ WaitNextFrame()
87
+ MovieWriteScreenshot(image, 0, 64, false, 3840, 2160)
88
+ WaitNextFrame()
89
+ Msg("AfterUpsampledScreenshot", store)
90
+
91
+ hr.InterfaceInScreenshot = oldInterfaceInScreenshot
92
+ camera:RevertProperties()
93
+ return image
94
+ end
95
+
96
+ function GedOpTakeScreenshots(ged, camera)
97
+ if not camera then return end
98
+
99
+ local campaign = Game and Game.Campaign or rawget(_G, "DefaultCampaign") or "HotDiamonds"
100
+ local campaign_presets = rawget(_G, "CampaignPresets") or empty_table
101
+ local sectors = campaign_presets[campaign] and campaign_presets[campaign].Sectors or empty_table
102
+ local map_to_sector = {[false] = ""}
103
+ for _, sector in ipairs(sectors) do
104
+ if sector.Map then
105
+ map_to_sector[sector.Map] = sector.Id
106
+ end
107
+ end
108
+
109
+ local path = string.format("svnAssets/Source/UI/LoadingScreens/%s", campaign)
110
+ local err = AsyncCreatePath(path)
111
+ if err then
112
+ local os_path = ConvertToOSPath(path)
113
+ ged:ShowMessage("Error", string.format("Can't create '%s' folder!", os_path))
114
+ return
115
+ end
116
+ local ok, result = SVNAddFile(path)
117
+ if not ok then
118
+ ged:ShowMessage("SVN Error", result)
119
+ end
120
+
121
+ StopAllHiding("CameraEditorScreenshots", 0, 0)
122
+ local size = UIL.GetScreenSize()
123
+ ChangeVideoMode(3840, 2160, 0, false, true)
124
+ WaitChangeVideoMode()
125
+ LockCamera("Screenshot")
126
+
127
+ local images = {}
128
+ if IsKindOf(camera, "Camera") then
129
+ images[1] = TakeCameraScreenshot(ged, path, map_to_sector[camera.map], camera)
130
+ else
131
+ local cameras = IsKindOf(camera, "GedMultiSelectAdapter") and camera.__objects or camera
132
+ table.sort(cameras, function(a, b) return a.map < b.map end)
133
+ for _, cam in ipairs(cameras) do
134
+ table.insert(images, TakeCameraScreenshot(ged, path, map_to_sector[cam.map], cam))
135
+ end
136
+ end
137
+
138
+ UnlockCamera("Screenshot")
139
+ ChangeVideoMode(size:x(), size:y(), 0, false, true)
140
+ WaitChangeVideoMode()
141
+ ResumeAllHiding("CameraEditorScreenshots")
142
+
143
+ local ok, result = SVNAddFile(images)
144
+ if not ok then
145
+ ged:ShowMessage("SVN Error", result)
146
+ end
147
+ print("Taking screenshots and adding to SubVersion done.")
148
+ end
149
+
150
+ function OnMsg.GedOnEditorSelect(obj, selected, ged_editor)
151
+ if obj and IsKindOf(obj, "Camera") and selected then
152
+ SwitchToCamera(obj, IsKindOf(ged_editor.selected_object, "Camera") and ged_editor.selected_object, nil, "don't lock", ged_editor)
153
+ end
154
+ end
155
+
156
+ function GedOpUnlockCamera()
157
+ camera.Unlock()
158
+ end
159
+
160
+ function GedOpMaxCamera()
161
+ cameraMax.Activate(1)
162
+ end
163
+
164
+ function GedOpTacCamera()
165
+ cameraTac.Activate(1)
166
+ end
167
+
168
+ function GedOpRTSCamera()
169
+ cameraRTS.Activate(1)
170
+ end
171
+
172
+ function GedOpSaveCameras()
173
+ local class = _G["Camera"]
174
+ class:SaveAll("save all", "user request")
175
+ end
176
+
177
+ function GedOpCreateReferenceImages()
178
+ CreateReferenceImages()
179
+ end
180
+
181
+ -- run to save a screenshot with every camera at correct video mode!
182
+ function CreateReferenceImages()
183
+ if not IsRealTimeThread() then
184
+ CreateRealTimeThread(CreateReferenceImages)
185
+ return
186
+ end
187
+
188
+ local folder = "svnAssets/Tests/ReferenceImages"
189
+ local cameras = Presets.Camera["reference"]
190
+ SetMouseDeltaMode(true)
191
+ SetLightmodel(0, LightmodelPresets.ArtPreview, 0)
192
+ local size = UIL.GetScreenSize()
193
+ ChangeVideoMode(512, 512, 0, false, true)
194
+ WaitChangeVideoMode()
195
+ local created = 0
196
+ for _, cam in ipairs(cameras) do
197
+ if GetMapName() ~= cam.map then
198
+ ChangeMap(cam.map)
199
+ end
200
+ cam:ApplyProperties()
201
+ Sleep(3000)
202
+ AsyncCreatePath(folder)
203
+ local image = string.format("%s/%s.png", folder, cam.id)
204
+ AsyncFileDelete(image)
205
+ if not WriteScreenshot(image, 512, 512) then
206
+ print(string.format("Failed to create screenshot '%s'", image))
207
+ else
208
+ created = created + 1
209
+ end
210
+ Sleep(300)
211
+ cam:RevertProperties()
212
+ end
213
+ SetMouseDeltaMode(false)
214
+ ChangeVideoMode(size:x(), size:y(), 0, false, true)
215
+ WaitChangeVideoMode()
216
+ print(string.format("Creating %d reference images in '%s' finished.", created, folder))
217
+ end
218
+
219
+ function GetShowcaseCameras(context)
220
+ local cameras = Presets.Camera[context and context.group or "reference"] or {}
221
+ table.sort(cameras, function(a, b)
222
+ if a.map==b.map then
223
+ return a.order < b.order
224
+ else
225
+ return a.map<b.map
226
+ end
227
+ end)
228
+
229
+ return cameras
230
+ end
231
+
232
+ function OpenShowcase(root, obj, context)
233
+ if GetDialog("Showcase") then
234
+ CloseDialog("Showcase")
235
+ return
236
+ end
237
+
238
+ if obj and IsKindOf(obj, "Camera") then
239
+ local group = obj.group
240
+ context = context or {}
241
+ context.group = group
242
+ elseif obj and type(obj)== "table" and next(obj) then
243
+ local group = obj[1].group
244
+ context = context or {}
245
+ context.group = group
246
+ end
247
+ OpenDialog("Showcase", nil, context)
248
+ end
249
+
250
+ function OnMsg.GameEnterEditor()
251
+ CloseDialog("Showcase")
252
+ end
253
+
254
+ function IsCameraEditorOpened()
255
+ local ged = FindGedApp("PresetEditor")
256
+ if not ged then return end
257
+
258
+ local sel = type(ged.selected_object) == "table" and ged.selected_object[1] or ged.selected_object
259
+
260
+ return IsKindOf(sel, "Camera")
261
+ end
CommonLua/Classes/CharacterControl.lua ADDED
@@ -0,0 +1,1065 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ if FirstLoad then
2
+ LocalPlayersCount = 0
3
+ end
4
+
5
+ DefineClass.CharacterControl = {
6
+ __parents = { "OldTerminalTarget", "InitDone" },
7
+ active = false,
8
+ character = false,
9
+ camera_active = true,
10
+ terminal_target_priority = -500,
11
+ }
12
+
13
+ function CharacterControl:Init(character)
14
+ self.character = character
15
+ terminal.AddTarget(self)
16
+ end
17
+
18
+ function CharacterControl:Done()
19
+ terminal.RemoveTarget(self)
20
+ self:SetActive(false)
21
+ end
22
+
23
+ function CharacterControl:SetActive(active)
24
+ if self.active == active then
25
+ return
26
+ end
27
+ if active then
28
+ self.active = active
29
+ self:OnActivate()
30
+ else
31
+ self.active = active
32
+ self:OnInactivate()
33
+ end
34
+ ChangeGameState("CharacterControl", active)
35
+ end
36
+
37
+ function CharacterControl:SetCameraActive(active)
38
+ self.camera_active = active
39
+ end
40
+
41
+ function CharacterControl:OnActivate()
42
+ self:SyncWithCharacter()
43
+ end
44
+
45
+ function CharacterControl:OnInactivate()
46
+ if not IsPaused() then
47
+ self:SyncWithCharacter()
48
+ end
49
+ end
50
+
51
+ function CharacterControl:GetBindingValue(binding)
52
+ end
53
+
54
+ function CharacterControl:GetActionBindings(action)
55
+ end
56
+
57
+ function CharacterControl:GetActionBinding1(action)
58
+ local bindings = self:GetActionBindings(action)
59
+ local binding = bindings and bindings[1]
60
+ return binding and (binding.xbutton or binding.key or binding.mouse_button)
61
+ end
62
+
63
+ function CharacterControl:GetBindingsCombinedValue(action)
64
+ local bindings = self:GetActionBindings(action)
65
+ if not bindings then return end
66
+ local best_value
67
+ for i = 1, #bindings do
68
+ local value = self:GetBindingValue(bindings[i])
69
+ if value then
70
+ if value == true then
71
+ return true
72
+ elseif type(value) == "number" then
73
+ best_value = Max(value, best_value or 0)
74
+ elseif IsPoint(value) then
75
+ if not best_value or value:Len2() > best_value:Len2() then
76
+ best_value = value
77
+ end
78
+ end
79
+ end
80
+ end
81
+ return best_value
82
+ end
83
+
84
+ function CharacterControl:CallBindingsDown(bindings, param, time)
85
+ for i = 1, #bindings do
86
+ local binding = bindings[i]
87
+ if self:BindingModifiersActive(binding) then
88
+ local result = binding.func(self.character, self, param, time)
89
+ if result ~= "continue" then
90
+ return result
91
+ end
92
+ end
93
+ end
94
+ return "continue"
95
+ end
96
+
97
+ function CharacterControl:CallBindingsUp(bindings)
98
+ for i = 1, #bindings do
99
+ local binding = bindings[i]
100
+ local value = self:GetBindingsCombinedValue(binding.action)
101
+ if not value then
102
+ local result = binding.func(self.character, self)
103
+ if result ~= "continue" then
104
+ return result
105
+ end
106
+ end
107
+ end
108
+ return "continue"
109
+ end
110
+
111
+ function CharacterControl:SyncBindingsWithCharacter(bindings)
112
+ for i = 1, #bindings do
113
+ local binding = bindings[i]
114
+ local value = binding.action and self:GetBindingsCombinedValue(binding.action)
115
+ binding.func(self.character, self, value)
116
+ end
117
+ end
118
+
119
+ function CharacterControl:SyncWithCharacter()
120
+ end
121
+
122
+ function BindToKeyboardAndMouseSync(action)
123
+ local class = _G["CCA_"..action]
124
+ assert(class)
125
+ if class then
126
+ class:BindToControllerSync(action, CC_KeyboardAndMouseSync)
127
+ end
128
+ end
129
+
130
+ function BindToXboxControllerSync(action)
131
+ local class = _G["CCA_"..action]
132
+ assert(class)
133
+ if class then
134
+ class:BindToControllerSync(action, CC_XboxControllerSync)
135
+ end
136
+ end
137
+
138
+ -- CharacterControlAction
139
+
140
+ DefineClass.CharacterControlAction = {
141
+ __parents = {},
142
+ ActionStop = false,
143
+ IsKindOf = IsKindOf,
144
+ HasMember = PropObjHasMember,
145
+ }
146
+
147
+ function CharacterControlAction:Action(character)
148
+ print("No Action defined: " .. self.class)
149
+ return "continue"
150
+ end
151
+ function OnMsg.ClassesPostprocess()
152
+ ClassDescendants("CharacterControlAction", function(class_name, class)
153
+ if class.GetAction == CharacterControlAction.GetAction and class.Action then
154
+ local f = function(...)
155
+ return class:Action(...)
156
+ end
157
+ class.GetAction = function() return f end
158
+ end
159
+ if class.GetActionSync == CharacterControlAction.GetActionSync and class.ActionStop then
160
+ local action_name = string.sub(class_name, #"CCA_" + 1)
161
+ local function f(character, controller)
162
+ local value = controller:GetBindingsCombinedValue(action_name)
163
+ if not value then
164
+ class:ActionStop(character, controller)
165
+ end
166
+ return "continue"
167
+ end
168
+ class.GetActionSync = function() return f end
169
+ end
170
+ end)
171
+ end
172
+ function CharacterControlAction:GetAction()
173
+ end
174
+ function CharacterControlAction:GetActionSync()
175
+ end
176
+
177
+ function CharacterControlAction:BindToControllerSync(action, bindings)
178
+ local f = self:GetActionSync()
179
+ if f and not table.find(bindings, "func", f) then
180
+ table.insert(bindings, { action = action, func = f })
181
+ end
182
+ end
183
+
184
+ function CharacterControlAction:BindKey(action, key, mod1, mod2)
185
+ local f = self:GetAction()
186
+ if f then
187
+ if mod1 == "double-click" then
188
+ BindToKeyboardEvent(action, "double-click", f, key, mod2)
189
+ elseif mod1 == "hold" then
190
+ BindToKeyboardEvent(action, "hold", f, key, mod2)
191
+ else
192
+ BindToKeyboardEvent(action, "down", f, key, mod1, mod2)
193
+ end
194
+ end
195
+ if mod1 == "double-click" or mod1 == "hold" then
196
+ mod1, mod2 = mod2, nil
197
+ end
198
+ f = self:GetActionSync()
199
+ if f then
200
+ BindToKeyboardEvent(action, "up", f, key)
201
+ if mod1 then
202
+ BindToKeyboardEvent(action, "up", f, mod1)
203
+ end
204
+ if mod2 then
205
+ BindToKeyboardEvent(action, "up", f, mod2)
206
+ end
207
+ end
208
+ end
209
+
210
+ function CharacterControlAction:BindMouse(action, button, key_mod)
211
+ assert(key_mod ~= "double-click" and key_mod ~= "hold", "Not supported mouse modifiers")
212
+ local f = self:GetAction()
213
+ if f then
214
+ if button == "MouseMove" then
215
+ BindToMouseEvent(action, "mouse_move", f, nil, key_mod)
216
+ else
217
+ BindToMouseEvent(action, "down", f, button, key_mod)
218
+ end
219
+ end
220
+ f = self:GetActionSync()
221
+ if f then
222
+ if button ~= "MouseMove" then
223
+ BindToMouseEvent(action, "up", f, button)
224
+ end
225
+ if key_mod then
226
+ BindToKeyboardEvent(action, "up", f, key_mod)
227
+ end
228
+ end
229
+ end
230
+
231
+ function CharacterControlAction:BindXboxController(action, button, mod1, mod2)
232
+ local f = self:GetAction()
233
+ if f then
234
+ if mod1 == "hold" then
235
+ BindToXboxControllerEvent(action, "hold", f, button, mod2)
236
+ else
237
+ BindToXboxControllerEvent(action, "down", f, button, mod1, mod2)
238
+ end
239
+ end
240
+ if mod1 == "hold" then
241
+ mod1, mod2 = mod2, nil
242
+ end
243
+ f = self:GetActionSync()
244
+ if f then
245
+ BindToXboxControllerEvent(action, "up", f, button)
246
+ if mod1 then
247
+ BindToXboxControllerEvent(action, "up", f, mod1)
248
+ end
249
+ if mod2 then
250
+ BindToXboxControllerEvent(action, "up", f, mod2)
251
+ end
252
+ end
253
+ end
254
+
255
+ -- Navigation
256
+
257
+ if FirstLoad then
258
+ UpdateCharacterNavigationThread = false
259
+ end
260
+
261
+ function OnMsg.DoneMap()
262
+ UpdateCharacterNavigationThread = false
263
+ end
264
+
265
+ local function CalcNavigationVector(controller, camera_view)
266
+ local pt = controller:GetBindingsCombinedValue("Move_Direction")
267
+ if pt then
268
+ return Rotate(pt:SetX(-pt:x()), XControlCameraGetYaw(camera_view) - 90*60)
269
+ end
270
+ local x = (controller:GetBindingsCombinedValue("Move_CameraRight") and 32767 or 0) + (controller:GetBindingsCombinedValue("Move_CameraLeft") and -32767 or 0)
271
+ local y = (controller:GetBindingsCombinedValue("Move_CameraForward") and 32767 or 0) + (controller:GetBindingsCombinedValue("Move_CameraBackward") and -32767 or 0)
272
+ if x ~= 0 or y ~= 0 then
273
+ return Rotate(point(-x,y), XControlCameraGetYaw(camera_view) - 90*60)
274
+ end
275
+ end
276
+
277
+ function UpdateCharacterNavigation(character, controller)
278
+ local dir = CalcNavigationVector(controller, character.camera_view)
279
+ character:SetStateContext("navigation_vector", dir)
280
+ if dir and not IsValidThread(UpdateCharacterNavigationThread) then
281
+ UpdateCharacterNavigationThread = CreateMapRealTimeThread(function()
282
+ repeat
283
+ Sleep(20)
284
+ if IsPaused() then break end
285
+ local update
286
+ for loc_player = 1, LocalPlayersCount do
287
+ local o = PlayerControlObjects[loc_player]
288
+ if o and o.controller then
289
+ local dir = CalcNavigationVector(o.controller, o.camera_view)
290
+ o:SetStateContext("navigation_vector", dir)
291
+ update = update or dir and true
292
+ end
293
+ end
294
+ until not update
295
+ UpdateCharacterNavigationThread = false
296
+ end)
297
+ end
298
+ return "continue"
299
+ end
300
+
301
+ DefineClass("CCA_Navigation", "CharacterControlAction")
302
+ DefineClass("CCA_Move_CameraForward", "CCA_Navigation")
303
+ DefineClass("CCA_Move_CameraBackward", "CCA_Navigation")
304
+ DefineClass("CCA_Move_CameraLeft", "CCA_Navigation")
305
+ DefineClass("CCA_Move_CameraRight", "CCA_Navigation")
306
+
307
+ function CCA_Navigation:BindKey(action, key)
308
+ BindToKeyboardEvent(action, "down", UpdateCharacterNavigation, key)
309
+ BindToKeyboardEvent(action, "up", UpdateCharacterNavigation, key)
310
+ end
311
+ function CCA_Navigation:BindXboxController(action, button)
312
+ BindToXboxControllerEvent(action, "down", UpdateCharacterNavigation, button)
313
+ BindToXboxControllerEvent(action, "up", UpdateCharacterNavigation, button)
314
+ end
315
+ function CCA_Navigation:GetActionSync()
316
+ return UpdateCharacterNavigation
317
+ end
318
+
319
+ -- Move_Direction
320
+ DefineClass("CCA_Move_Direction", "CharacterControlAction")
321
+ function CCA_Move_Direction:BindKey(action, key, mod1, mod2)
322
+ assert(false, "Can't bind 2D direction to a key")
323
+ end
324
+ function CCA_Move_Direction:BindMouse(action, button, key_mod)
325
+ assert(false, "Mouse cursor could be converted to a direction. Not implemented.")
326
+ end
327
+ function CCA_Move_Direction:BindXboxController(action, button)
328
+ assert(button == "LeftThumb" or button == "RightThumb")
329
+ BindToXboxControllerEvent(action, "change", UpdateCharacterNavigation, button)
330
+ end
331
+ function CCA_Move_Direction:GetActionSync()
332
+ return UpdateCharacterNavigation
333
+ end
334
+
335
+ -- RotateCamera
336
+ function UpdateCameraRotate(character, controller)
337
+ if not g_LookAtObjectSA then
338
+ local dir = (controller:GetBindingsCombinedValue("CameraRotate_Left") and -1 or 0) + (controller:GetBindingsCombinedValue("CameraRotate_Right") and 1 or 0)
339
+ camera3p.SetAutoRotate(90*60*dir)
340
+ end
341
+ return "continue"
342
+ end
343
+
344
+ DefineClass("CCA_CameraRotate", "CharacterControlAction")
345
+ DefineClass("CCA_CameraRotate_Left", "CCA_CameraRotate")
346
+ DefineClass("CCA_CameraRotate_Right", "CCA_CameraRotate")
347
+
348
+ function CCA_CameraRotate:BindKey(action, key)
349
+ BindToKeyboardEvent(action, "down", UpdateCameraRotate, key)
350
+ BindToKeyboardEvent(action, "up", UpdateCameraRotate, key)
351
+ end
352
+ function CCA_CameraRotate:BindXboxController(action, button)
353
+ BindToXboxControllerEvent(action, "down", UpdateCameraRotate, button)
354
+ BindToXboxControllerEvent(action, "up", UpdateCameraRotate, button)
355
+ end
356
+ function CCA_CameraRotate:GetActionSync()
357
+ return UpdateCameraRotate
358
+ end
359
+
360
+ if FirstLoad then
361
+ InGameMouseCursor = false
362
+ end
363
+
364
+ -- CameraRotate_Mouse
365
+ DefineClass("CCA_CameraRotate_Mouse", "CharacterControlAction")
366
+ function CCA_CameraRotate_Mouse:Action(character)
367
+ if not (character and character.controller and character.controller.camera_active) then
368
+ return "continue"
369
+ end
370
+ if InGameMouseCursor then
371
+ HideMouseCursor("InGameCursor") -- although MouseRotate(true) hides the mouse, IsMouseCursorHidden() depends on it
372
+ else
373
+ SetMouseDeltaMode(false)
374
+ end
375
+ MouseRotate(true)
376
+ Msg("CameraRotateStart", "mouse")
377
+ return "break"
378
+ end
379
+ function CCA_CameraRotate_Mouse:ActionStop(character)
380
+ MouseRotate(false)
381
+ if InGameMouseCursor then
382
+ ShowMouseCursor("InGameCursor")
383
+ else
384
+ HideMouseCursor("InGameCursor")
385
+ SetMouseDeltaMode(true) -- prevents the mouse to leave the game window
386
+ end
387
+ Msg("CameraRotateStop", "mouse")
388
+ return "continue"
389
+ end
390
+ function CCA_CameraRotate_Mouse:GetActionSync(character, controller)
391
+ local function f(character, controller)
392
+ local value = not CameraLocked and (MouseRotateCamera == "always" or controller:GetBindingsCombinedValue("CameraRotate_Mouse"))
393
+ if value then
394
+ return self:Action(character, controller)
395
+ else
396
+ return self:ActionStop(character, controller)
397
+ end
398
+ end
399
+ return f
400
+ end
401
+
402
+ -- KeyboardAndMouse Control
403
+
404
+ DefineClass.CC_KeyboardAndMouse = {
405
+ __parents = { "CharacterControl" },
406
+ KeyHoldButtonTime = 350,
407
+ KeyDoubleClickTime = 300,
408
+ key_hold_thread = false,
409
+ key_last_double_click = false,
410
+ key_last_double_click_time = 0,
411
+ }
412
+
413
+ function CC_KeyboardAndMouse:OnActivate()
414
+ CharacterControl.OnActivate(self)
415
+ if InGameMouseCursor then
416
+ ShowMouseCursor("InGameCursor")
417
+ end
418
+ end
419
+
420
+ function CC_KeyboardAndMouse:OnInactivate()
421
+ CharacterControl.OnInactivate(self)
422
+ DeleteThread(self.key_hold_thread)
423
+ self.key_hold_thread = nil
424
+ self.key_last_double_click = nil
425
+ self.key_last_double_click_time = nil
426
+ HideMouseCursor("InGameCursor")
427
+ MouseRotate(false)
428
+ end
429
+
430
+ function CC_KeyboardAndMouse:SetCameraActive(active)
431
+ CharacterControl.SetCameraActive(self, active)
432
+ if self.active and not self.camera_active then
433
+ MouseRotate(false)
434
+ end
435
+ end
436
+
437
+ function CC_KeyboardAndMouse:GetActionBindings(action)
438
+ return CC_KeyboardAndMouse_ActionBindings[action]
439
+ end
440
+
441
+ function CC_KeyboardAndMouse:GetBindingValue(binding)
442
+ if not self.active or binding.key and not terminal.IsKeyPressed(binding.key) then
443
+ return false
444
+ end
445
+ if binding.mouse_button then
446
+ local pressed = self:IsMouseButtonPressed(binding.mouse_button)
447
+ if pressed == false then
448
+ return false
449
+ end
450
+ end
451
+ if not self:BindingModifiersActive(binding) then
452
+ return false
453
+ end
454
+ return true
455
+ end
456
+
457
+ function CC_KeyboardAndMouse:IsMouseButtonPressed(button)
458
+ local pressed, _
459
+ if button == "LButton" then
460
+ pressed = terminal.IsLRMX1X2MouseButtonPressed()
461
+ elseif button == "RButton" then
462
+ _, pressed = terminal.IsLRMX1X2MouseButtonPressed()
463
+ elseif button == "MButton" then
464
+ _, _, pressed = terminal.IsLRMX1X2MouseButtonPressed()
465
+ elseif button == "XButton1" then
466
+ _, _, _, pressed = terminal.IsLRMX1X2MouseButtonPressed()
467
+ elseif button == "XButton2" then
468
+ _, _, _, _, pressed = terminal.IsLRMX1X2MouseButtonPressed()
469
+ elseif button == "MouseWheelFwd" or button == "MouseWheelBack" then
470
+ return false
471
+ end
472
+ return pressed
473
+ end
474
+
475
+ function CC_KeyboardAndMouse:BindingModifiersActive(binding)
476
+ local keys = binding.key_modifiers
477
+ if keys then
478
+ for i = 1, #keys do
479
+ local key_or_button = keys[i]
480
+ if key_or_button == "MouseWheelFwd" or key_or_button == "MouseWheelBack" then
481
+ return false
482
+ end
483
+ local pressed = self:IsMouseButtonPressed(key_or_button)
484
+ if pressed == nil then
485
+ pressed = terminal.IsKeyPressed(key_or_button)
486
+ end
487
+ if not pressed then
488
+ return false
489
+ end
490
+ end
491
+ end
492
+ return true
493
+ end
494
+
495
+ -- keyboard events
496
+ function CC_KeyboardAndMouse:OnKbdKeyDown(virtual_key, repeated, time)
497
+ if repeated or not self.active then
498
+ return "continue"
499
+ end
500
+ -- double click
501
+ if CC_KeyboardKeyDoubleClick[virtual_key] then
502
+ if self.key_last_double_click == virtual_key and RealTime() - self.key_last_double_click_time < self.KeyDoubleClickTime then
503
+ self.key_last_double_click = false
504
+ self:CallBindingsDown(CC_KeyboardKeyDoubleClick[virtual_key], true, time)
505
+ else
506
+ self.key_last_double_click = virtual_key
507
+ self.key_last_double_click_time = RealTime()
508
+ end
509
+ end
510
+ -- hold
511
+ if CC_KeyboardKeyHold[virtual_key] then
512
+ DeleteThread(self.key_hold_thread)
513
+ self.key_hold_thread = CreateRealTimeThread(function(self, virtual_key, time)
514
+ Sleep(self.KeyHoldButtonTime)
515
+ self.key_hold_thread = false
516
+ if terminal.IsKeyPressed(virtual_key) then
517
+ self:CallBindingsDown(CC_KeyboardKeyHold[virtual_key], true, time)
518
+ end
519
+ end, self, virtual_key, time)
520
+ end
521
+ -- down
522
+ local result
523
+ if CC_KeyboardKeyDown[virtual_key] then
524
+ result = self:CallBindingsDown(CC_KeyboardKeyDown[virtual_key], true, time)
525
+ end
526
+ return result or "continue"
527
+ end
528
+
529
+ function CC_KeyboardAndMouse:OnKbdKeyUp(virtual_key)
530
+ if not self.active then
531
+ return "continue"
532
+ end
533
+ if CC_KeyboardKeyHold[virtual_key] and self.key_hold_thread then
534
+ DeleteThread(self.key_hold_thread)
535
+ self.key_hold_thread = false
536
+ end
537
+ if CC_KeyboardKeyUp[virtual_key] then
538
+ local result = self:CallBindingsUp(CC_KeyboardKeyUp[virtual_key])
539
+ return result
540
+ end
541
+ return "continue"
542
+ end
543
+
544
+ -- mouse events
545
+
546
+ function CC_KeyboardAndMouse:OnMouseButtonDown(button, pt, time)
547
+ if not self.active then
548
+ return "continue"
549
+ end
550
+ if CC_MouseButtonDown[button] then
551
+ local result = self:CallBindingsDown(CC_MouseButtonDown[button], true, time)
552
+ if result ~= "continue" then
553
+ return result
554
+ end
555
+ end
556
+ return "continue"
557
+ end
558
+
559
+ function CC_KeyboardAndMouse:OnMouseButtonUp(button, pt, time)
560
+ if not self.active then
561
+ return "continue"
562
+ end
563
+ if CC_MouseButtonUp[button] then
564
+ local result = self:CallBindingsUp(CC_MouseButtonUp[button], false, time)
565
+ if result ~= "continue" then
566
+ return result
567
+ end
568
+ end
569
+ return "continue"
570
+ end
571
+
572
+ function CC_KeyboardAndMouse:OnLButtonDown(...)
573
+ return self:OnMouseButtonDown("LButton", ...)
574
+ end
575
+ function CC_KeyboardAndMouse:OnLButtonUp(...)
576
+ return self:OnMouseButtonUp("LButton", ...)
577
+ end
578
+ function CC_KeyboardAndMouse:OnLButtonDoubleClick(...)
579
+ return self:OnMouseButtonDown("LButton", ...)
580
+ end
581
+ function CC_KeyboardAndMouse:OnRButtonDown(...)
582
+ return self:OnMouseButtonDown("RButton", ...)
583
+ end
584
+ function CC_KeyboardAndMouse:OnRButtonUp(...)
585
+ return self:OnMouseButtonUp("RButton", ...)
586
+ end
587
+ function CC_KeyboardAndMouse:OnRButtonDoubleClick(...)
588
+ return self:OnMouseButtonDown("RButton", ...)
589
+ end
590
+ function CC_KeyboardAndMouse:OnMButtonDown(...)
591
+ return self:OnMouseButtonDown("MButton", ...)
592
+ end
593
+ function CC_KeyboardAndMouse:OnMButtonUp(...)
594
+ return self:OnMouseButtonUp("MButton", ...)
595
+ end
596
+ function CC_KeyboardAndMouse:OnMButtonDoubleClick(...)
597
+ return self:OnMouseButtonDown("MButton", ...)
598
+ end
599
+ function CC_KeyboardAndMouse:OnXButton1Down(...)
600
+ return self:OnMouseButtonDown("XButton1", ...)
601
+ end
602
+ function CC_KeyboardAndMouse:OnXButton1Up(...)
603
+ return self:OnMouseButtonUp("XButton1", ...)
604
+ end
605
+ function CC_KeyboardAndMouse:OnXButton1DoubleClick(...)
606
+ return self:OnMouseButtonDown("XButton1", ...)
607
+ end
608
+ function CC_KeyboardAndMouse:OnXButton2Down(...)
609
+ return self:OnMouseButtonDown("XButton2", ...)
610
+ end
611
+ function CC_KeyboardAndMouse:OnXButton2Up(...)
612
+ return self:OnMouseButtonUp("XButton2", ...)
613
+ end
614
+ function CC_KeyboardAndMouse:OnXButton2DoubleClick(...)
615
+ return self:OnMouseButtonDown("XButton2", ...)
616
+ end
617
+
618
+ function CC_KeyboardAndMouse:OnMouseWheelForward(pt, time)
619
+ if not self.active then
620
+ return "continue"
621
+ end
622
+ local result = self:CallBindingsDown(CC_MouseWheelFwd, true, time)
623
+ if result ~= "break" then
624
+ result = self:CallBindingsDown(CC_MouseWheel, 1, time)
625
+ end
626
+ return result
627
+ end
628
+ function CC_KeyboardAndMouse:OnMouseWheelBack(pt, time)
629
+ if not self.active then
630
+ return "continue"
631
+ end
632
+ local result = self:CallBindingsDown(CC_MouseWheelBack, true, time)
633
+ if result ~= "break" then
634
+ result = self:CallBindingsDown(CC_MouseWheel, -1, time)
635
+ end
636
+ return result
637
+ end
638
+
639
+ function CC_KeyboardAndMouse:OnMousePos(pt, time)
640
+ if not self.active then
641
+ return "continue"
642
+ end
643
+ local result = self:CallBindingsDown(CC_MouseMove, pt, time)
644
+ return result
645
+ end
646
+
647
+ function CC_KeyboardAndMouse:SyncWithCharacter()
648
+ self:SyncBindingsWithCharacter(CC_KeyboardAndMouseSync)
649
+ end
650
+
651
+ local function ResetKeyboardAndMouseBindings()
652
+ CC_KeyboardKeyDown = {}
653
+ CC_KeyboardKeyUp = {}
654
+ CC_KeyboardKeyHold = {}
655
+ CC_KeyboardKeyDoubleClick = {}
656
+ CC_MouseButtonDown = {}
657
+ CC_MouseButtonUp = {}
658
+ CC_MouseWheel = {}
659
+ CC_MouseWheelFwd = {}
660
+ CC_MouseWheelBack = {}
661
+ CC_MouseMove = {}
662
+ CC_KeyboardAndMouse_ActionBindings = {}
663
+ CC_KeyboardAndMouseSync = {}
664
+ end
665
+
666
+ if FirstLoad then
667
+ ResetKeyboardAndMouseBindings()
668
+ end
669
+
670
+ function BindKey(action, key, mod1, mod2)
671
+ local class = _G["CCA_"..action]
672
+ assert(class)
673
+ if class then
674
+ class:BindKey(action, key, mod1, mod2)
675
+ end
676
+ end
677
+
678
+ function BindMouse(action, button, key_mod)
679
+ local class = _G["CCA_"..action]
680
+ assert(class)
681
+ if class then
682
+ class:BindMouse(action, button, key_mod)
683
+ end
684
+ end
685
+
686
+ local function ResolveRefBindings(list, bindings)
687
+ for i = 1, #list do
688
+ local action = list[i][1]
689
+ local blist = bindings[action]
690
+ for j = #blist, 1, -1 do
691
+ local binding = blist[j]
692
+ for k = #binding, 1, -1 do
693
+ local ref = bindings[binding[k]]
694
+ if ref then
695
+ if #ref == 0 then
696
+ table.remove(blist,j)
697
+ else
698
+ table.remove(binding, k)
699
+ for m = 2, #ref do
700
+ table.insert(blist, j, table.copy(binding))
701
+ end
702
+ for m = 1, #ref do
703
+ local rt = ref[m]
704
+ local binding_mod = blist[j+m-1]
705
+ for n = #rt, 1, -1 do
706
+ table.insert(binding_mod, k+n-1, rt[n])
707
+ end
708
+ end
709
+ end
710
+ end
711
+ end
712
+ end
713
+ end
714
+ end
715
+
716
+ function ReloadKeyboardAndMouseBindings(default_bindings, predefined_bindings)
717
+ ResetKeyboardAndMouseBindings()
718
+ if not default_bindings then
719
+ return
720
+ end
721
+ local bindings = {}
722
+ for i = 1, #default_bindings do
723
+ local default_list = default_bindings[i]
724
+ local action = default_list[1]
725
+ bindings[action] = {}
726
+ local predefined_list = predefined_bindings and predefined_bindings[action]
727
+ for j = 1, Max(predefined_list and #predefined_list or 0, #default_list-1) do
728
+ local binding = predefined_list and predefined_list[j] or nil
729
+ if binding == nil then
730
+ binding = default_list and default_list[j+1]
731
+ end
732
+ if binding and #binding > 0 then
733
+ local t = {}
734
+ for k = 1, #binding do
735
+ t[k] = type(binding[k]) == "string" and const["vk"..binding[k]] or binding[k]
736
+ end
737
+ table.insert(bindings[action], t)
738
+ end
739
+ end
740
+ end
741
+ ResolveRefBindings(default_bindings, bindings)
742
+ for i = 1, #default_bindings do
743
+ local action = default_bindings[i][1]
744
+ local blist = bindings[action]
745
+ for j = 1, #blist do
746
+ local binding = blist[j]
747
+ if type(binding[1]) == "number" then
748
+ BindKey(action, binding[1], binding[2], binding[3])
749
+ else
750
+ BindMouse(action, binding[1], binding[2], binding[3])
751
+ end
752
+ if binding[2] then
753
+ if type(binding[2]) == "number" then
754
+ BindKey(action, binding[2], binding[1], binding[3])
755
+ else
756
+ BindMouse(action, binding[2], binding[1], binding[3])
757
+ end
758
+ end
759
+ if binding[3] then
760
+ if type(binding[3]) == "number" then
761
+ BindKey(action, binding[3], binding[1], binding[2])
762
+ else
763
+ BindMouse(action, binding[3], binding[1], binding[2])
764
+ end
765
+ end
766
+ end
767
+ BindToKeyboardAndMouseSync(action)
768
+ end
769
+ end
770
+
771
+ function BindToKeyboardEvent(action, event, func, key, mod1, mod2)
772
+ local binding = { action = action, key = key, func = func }
773
+ if mod1 or mod2 then
774
+ binding.key_modifiers = {}
775
+ binding.key_modifiers[#binding.key_modifiers+1] = mod1
776
+ binding.key_modifiers[#binding.key_modifiers+1] = mod2
777
+ end
778
+ local list
779
+ if event == "down" then
780
+ list = CC_KeyboardKeyDown
781
+ CC_KeyboardAndMouse_ActionBindings[action] = CC_KeyboardAndMouse_ActionBindings[action] or {}
782
+ table.insert(CC_KeyboardAndMouse_ActionBindings[action], binding)
783
+ elseif event == "up" then
784
+ list = CC_KeyboardKeyUp
785
+ elseif event == "hold" then
786
+ list = CC_KeyboardKeyHold
787
+ elseif event == "double-click" then
788
+ list = CC_KeyboardKeyDoubleClick
789
+ end
790
+ list[key] = list[key] or {}
791
+ table.insert(list[key], binding)
792
+ end
793
+
794
+ function BindToMouseEvent(action, event, func, button, key_mod)
795
+ local binding = { action = action, mouse_button = button, func = func }
796
+ if key_mod then
797
+ binding.key_modifiers = {}
798
+ binding.key_modifiers[#binding.key_modifiers+1] = key_mod
799
+ end
800
+ if event == "down" or button == "MouseWheel" then
801
+ CC_KeyboardAndMouse_ActionBindings[action] = CC_KeyboardAndMouse_ActionBindings[action] or {}
802
+ table.insert(CC_KeyboardAndMouse_ActionBindings[action], binding)
803
+ end
804
+ if button == "MouseWheel" then
805
+ table.insert(CC_MouseWheel, binding)
806
+ elseif button == "MouseWheelFwd" then
807
+ table.insert(CC_MouseWheelFwd, binding)
808
+ elseif button == "MouseWheelBack" then
809
+ table.insert(CC_MouseWheelBack, binding)
810
+ elseif event == "down" then
811
+ CC_MouseButtonDown[button] = CC_MouseButtonDown[button] or {}
812
+ table.insert(CC_MouseButtonDown[button], binding)
813
+ elseif event == "up" then
814
+ CC_MouseButtonUp[button] = CC_MouseButtonUp[button] or {}
815
+ table.insert(CC_MouseButtonUp[button], binding)
816
+ elseif event == "mouse_move" then
817
+ table.insert(CC_MouseMove, binding)
818
+ end
819
+ end
820
+
821
+
822
+ -- XboxController
823
+
824
+ DefineClass.CC_XboxController = {
825
+ __parents = { "CharacterControl" },
826
+ xbox_controller_id = false,
827
+ XboxHoldButtonTime = 350,
828
+ xbox_hold_thread = false,
829
+ XBoxComboButtonsDelay = 100,
830
+ xbox_last_combo_button = false,
831
+ xbox_last_combo_button_time = 0,
832
+ }
833
+
834
+ function CC_XboxController:Init(character, controller_id)
835
+ self.xbox_controller_id = controller_id
836
+ end
837
+
838
+ function CC_XboxController:OnActivate()
839
+ CharacterControl.OnActivate(self)
840
+ if self.xbox_controller_id and self.camera_active then
841
+ camera3p.EnableController(self.xbox_controller_id)
842
+ end
843
+ end
844
+
845
+ function CC_XboxController:SetCameraActive(active)
846
+ CharacterControl.SetCameraActive(self, active)
847
+ if self.xbox_controller_id and self.active then
848
+ if self.camera_active then
849
+ camera3p.EnableController(self.xbox_controller_id)
850
+ else
851
+ camera3p.DisableController(self.xbox_controller_id)
852
+ end
853
+ end
854
+ end
855
+
856
+ function CC_XboxController:OnInactivate()
857
+ CharacterControl.OnInactivate(self)
858
+ DeleteThread(self.xbox_hold_thread)
859
+ self.xbox_hold_thread = nil
860
+ if self.xbox_controller_id then
861
+ XInput.SetRumble(self.xbox_controller_id, 0, 0)
862
+ camera3p.DisableController(self.xbox_controller_id)
863
+ end
864
+ end
865
+
866
+ function CC_XboxController:GetActionBindings(action)
867
+ return CC_XboxController_ActionBindings[action]
868
+ end
869
+
870
+ function CC_XboxController:GetBindingValue(binding)
871
+ if not self.active then
872
+ return
873
+ end
874
+ local button = binding.xbutton
875
+ if button and not XInput.IsCtrlButtonPressed(self.xbox_controller_id, button) then
876
+ return
877
+ end
878
+ if not self:BindingModifiersActive(binding) then
879
+ return
880
+ end
881
+ local value = XInput.CurrentState[self.xbox_controller_id][button]
882
+ return value
883
+ end
884
+
885
+ function CC_XboxController:BindingModifiersActive(binding)
886
+ local buttons = binding.x_modifiers
887
+ if buttons then
888
+ for i = 1, #buttons do
889
+ if not XInput.IsCtrlButtonPressed(self.xbox_controller_id, buttons[i]) then
890
+ return false
891
+ end
892
+ end
893
+ end
894
+ return true
895
+ end
896
+
897
+ function CC_XboxController:OnXButtonDown(button, controller_id)
898
+ if not self.active or controller_id ~= self.xbox_controller_id then
899
+ return "continue"
900
+ end
901
+ -- hold
902
+ if CC_XboxButtonHold[button] then
903
+ DeleteThread(self.xbox_hold_thread)
904
+ self.xbox_hold_thread = CreateRealTimeThread(function(self, button, controller_id)
905
+ Sleep(self.XboxHoldButtonTime)
906
+ self.xbox_hold_thread = false
907
+ if XInput.IsCtrlButtonPressed(self.xbox_controller_id, button) then
908
+ local xstate = XInput.CurrentState[controller_id]
909
+ self:CallBindingsDown(CC_XboxButtonHold[button], xstate[button])
910
+ end
911
+ end, self, button, controller_id)
912
+ end
913
+ local result
914
+ if CC_XboxButtonDown[button] then
915
+ result = self:CallBindingsDown(CC_XboxButtonDown[button], true)
916
+ end
917
+ if CC_XboxButtonCombo[button] then
918
+ local handlers = self.xbox_last_combo_button and RealTime() - self.xbox_last_combo_button_time < self.XBoxComboButtonsDelay and CC_XboxButtonCombo[button][self.xbox_last_combo_button]
919
+ if handlers then
920
+ local result = self:CallBindingsDown(handlers, true)
921
+ if result and result ~= "continue" then
922
+ self.xbox_last_combo_button = false
923
+ return result
924
+ end
925
+ end
926
+ self.xbox_last_combo_button = button
927
+ self.xbox_last_combo_button_time = RealTime()
928
+ end
929
+ return result or "continue"
930
+ end
931
+
932
+ function CC_XboxController:OnXButtonUp(button, controller_id)
933
+ if not self.active or controller_id ~= self.xbox_controller_id then
934
+ return "continue"
935
+ end
936
+ if self.xbox_last_combo_button == button then
937
+ self.xbox_last_combo_button = false
938
+ end
939
+ if CC_XboxButtonHold[button] and self.xbox_hold_thread then
940
+ DeleteThread(self.xbox_hold_thread)
941
+ self.xbox_hold_thread = false
942
+ end
943
+ if CC_XboxButtonUp[button] then
944
+ local result = self:CallBindingsUp(CC_XboxButtonUp[button])
945
+ if result ~= "continue" then
946
+ return result
947
+ end
948
+ end
949
+ return "continue"
950
+ end
951
+
952
+ function CC_XboxController:OnXNewPacket(_, controller_id, last_state, current_state)
953
+ if not self.active or controller_id ~= self.xbox_controller_id then
954
+ return "continue"
955
+ end
956
+ for i = 1, #CC_XboxControllerNewPacket do
957
+ local button = CC_XboxControllerNewPacket[i]
958
+ self:CallBindingsDown(CC_XboxControllerNewPacket[button], current_state[button])
959
+ end
960
+ return "continue"
961
+ end
962
+
963
+ function CC_XboxController:SyncWithCharacter()
964
+ self:SyncBindingsWithCharacter(CC_XboxControllerSync)
965
+ end
966
+
967
+ local function ResetXboxControllerBindings()
968
+ CC_XboxButtonDown = {}
969
+ CC_XboxButtonUp = {}
970
+ CC_XboxButtonHold = {}
971
+ CC_XboxButtonCombo = {}
972
+ CC_XboxControllerNewPacket = {}
973
+ CC_XboxController_ActionBindings = {}
974
+ CC_XboxControllerSync = {}
975
+ table.insert(CC_XboxControllerSync,{ func = function() MouseRotate(false) end})
976
+ end
977
+
978
+ if FirstLoad then
979
+ ResetXboxControllerBindings()
980
+ end
981
+
982
+ function ReloadXboxControllerBindings(default_bindings, predefined_bindings)
983
+ ResetXboxControllerBindings()
984
+ if not default_bindings then
985
+ return
986
+ end
987
+ local bindings = {}
988
+ for i = 1, #default_bindings do
989
+ local default_list = default_bindings[i]
990
+ local action = default_list[1]
991
+ bindings[action] = {}
992
+ local predefined_list = predefined_bindings and predefined_bindings[action]
993
+ for i = 1, Max(predefined_list and #predefined_list or 0, #default_list-1) do
994
+ local binding = predefined_list and predefined_list[i] or nil
995
+ if binding == nil then
996
+ binding = default_list and default_list[i+1]
997
+ end
998
+ if binding and #binding > 0 then
999
+ local t = {}
1000
+ for k = 1, #binding do
1001
+ t[k] = binding[k]
1002
+ end
1003
+ table.insert(bindings[action], t)
1004
+ end
1005
+ end
1006
+ end
1007
+ ResolveRefBindings(default_bindings, bindings)
1008
+ for i = 1, #default_bindings do
1009
+ local action = default_bindings[i][1]
1010
+ local blist = bindings[action]
1011
+ for j = 1, #blist do
1012
+ local binding = blist[j]
1013
+ BindXboxController(action, unpack_params(binding))
1014
+ end
1015
+ BindToXboxControllerSync(action)
1016
+ end
1017
+ end
1018
+
1019
+ function BindXboxController(action, button, mod1, mod2)
1020
+ local class = _G["CCA_"..action]
1021
+ assert(class)
1022
+ if class then
1023
+ class:BindXboxController(action, button, mod1, mod2)
1024
+ end
1025
+ end
1026
+
1027
+ function BindToXboxControllerEvent(action, event, func, button, mod1, mod2)
1028
+ if event == "sync" then
1029
+ if action or not table.find(CC_XboxControllerSync, "func", func) then
1030
+ local binding = { action = action, func = func }
1031
+ table.insert(CC_XboxControllerSync, binding)
1032
+ end
1033
+ return
1034
+ end
1035
+ local binding = { action = action, xbutton = button, func = func }
1036
+ if mod1 or mod2 then
1037
+ binding.x_modifiers = {}
1038
+ binding.x_modifiers[#binding.x_modifiers+1] = mod1
1039
+ binding.x_modifiers[#binding.x_modifiers+1] = mod2
1040
+ end
1041
+ local list
1042
+ if event == "down" then
1043
+ CC_XboxController_ActionBindings[action] = CC_XboxController_ActionBindings[action] or {}
1044
+ table.insert(CC_XboxController_ActionBindings[action], binding)
1045
+ list = CC_XboxButtonDown
1046
+ elseif event == "up" then
1047
+ list = CC_XboxButtonUp
1048
+ table.insert_unique(CC_XboxButtonUp, button)
1049
+ elseif event == "hold" then
1050
+ list = CC_XboxButtonHold
1051
+ elseif event == "combo" then
1052
+ list = CC_XboxButtonCombo
1053
+ elseif event == "change" then
1054
+ CC_XboxController_ActionBindings[action] = CC_XboxController_ActionBindings[action] or {}
1055
+ table.insert(CC_XboxController_ActionBindings[action], binding)
1056
+ table.insert_unique(CC_XboxControllerNewPacket, button)
1057
+ list = CC_XboxControllerNewPacket
1058
+ else
1059
+ return
1060
+ end
1061
+ if not list[button] then
1062
+ list[button] = {}
1063
+ end
1064
+ table.insert(list[button], binding)
1065
+ end
CommonLua/Classes/ClassDef.lua ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DefineClass.PropertyTabDef = {
2
+ __parents = { "PropertyObject" },
3
+ properties = {
4
+ { id = "TabName", editor = "text", default = "" },
5
+ { id = "Categories", editor = "set", default = {}, items = function(self)
6
+ local class_def = GetParentTableOfKind(self, "ClassDef")
7
+ local categories = {}
8
+ for _, classname in ipairs(class_def.DefParentClassList) do
9
+ local base = g_Classes[classname]
10
+ for _, prop_meta in ipairs(base and base:GetProperties()) do
11
+ categories[prop_meta.category or "Misc"] = true
12
+ end
13
+ end
14
+ for _, subitem in ipairs(class_def) do
15
+ if IsKindOf(subitem, "PropertyDef") then
16
+ categories[subitem.category or "Misc"] = true
17
+ end
18
+ end
19
+ return table.keys2(categories, "sorted")
20
+ end
21
+ }
22
+ },
23
+ GetEditorView = function(self)
24
+ return string.format("%s - %s", self.TabName, table.concat(table.keys2(self.Categories or empty_table), ", "))
25
+ end,
26
+ }
27
+
28
+ DefineClass.ClassDef = {
29
+ __parents = { "Preset" },
30
+ properties = {
31
+ { id = "DefParentClassList", name = "Parent classes", editor = "string_list", items = function(obj, prop_meta, validate_fn)
32
+ if validate_fn == "validate_fn" then
33
+ -- function for preset validation, checks whether the property value is from "items"
34
+ return "validate_fn", function(value, obj, prop_meta)
35
+ return value == "" or g_Classes[value]
36
+ end
37
+ end
38
+ return table.keys2(g_Classes, true, "")
39
+ end
40
+ },
41
+ { id = "DefPropertyTranslation", name = "Translate property names", editor = "bool", default = false, },
42
+ { id = "DefStoreAsTable", name = "Store as table", editor = "choice", default = "inherit", items = { "inherit", "true", "false" } },
43
+ { id = "DefPropertyTabs", name = "Property tabs", editor = "nested_list", base_class = "PropertyTabDef", inclusive = true, default = false, },
44
+ { id = "DefUndefineClass", name = "Undefine class", editor = "bool", default = false, },
45
+ },
46
+ DefParentClassList = { "PropertyObject" },
47
+
48
+ ContainerClass = "ClassDefSubItem",
49
+ PresetClass = "ClassDef",
50
+ FilePerGroup = true,
51
+ HasCompanionFile = true,
52
+ GeneratesClass = true,
53
+ DefineKeyword = "DefineClass",
54
+
55
+ GedEditor = "ClassDefEditor",
56
+ EditorMenubarName = "Class definitions",
57
+ EditorIcon = "CommonAssets/UI/Icons/cpu.png",
58
+ EditorMenubar = "Editors.Engine",
59
+ EditorShortcut = "Ctrl-Alt-F3",
60
+ EditorViewPresetPrefix = "<color 75 105 198>[Class]</color> ",
61
+ }
62
+
63
+ function ClassDef:FindSubitem(name)
64
+ for _, subitem in ipairs(self) do
65
+ if subitem:HasMember("name") and subitem.name == name or subitem:IsKindOf("PropertyDef") and subitem.id == name then
66
+ return subitem
67
+ end
68
+ end
69
+ end
70
+
71
+ function ClassDef:GetDefaultPropertyValue(prop_id, prop_meta)
72
+ if prop_id:starts_with("Def") then
73
+ local class_prop_id = prop_id:sub(4)
74
+ -- try to find the default property value from the parent list
75
+ -- this is not correct if there are multiple parent classes that have different default values for the property
76
+ for i, class_name in ipairs(self.DefParentClassList) do
77
+ local class = g_Classes[class_name]
78
+ if class then
79
+ local default = class:GetDefaultPropertyValue(class_prop_id)
80
+ if default ~= nil then
81
+ return default
82
+ end
83
+ end
84
+ end
85
+ end
86
+ return Preset.GetDefaultPropertyValue(self, prop_id, prop_meta)
87
+ end
88
+
89
+ function ClassDef:PostLoad()
90
+ for key, prop_def in ipairs(self) do
91
+ prop_def.translate_in_ged = self.DefPropertyTranslation
92
+ end
93
+ Preset.PostLoad(self)
94
+ end
95
+
96
+ function ClassDef:OnPreSave()
97
+ -- convert texts to/from Ts if the 'translated' value changed
98
+ local translate = self.DefPropertyTranslation
99
+ for key, prop_def in ipairs(self) do
100
+ if IsKindOf(prop_def, "PropertyDef") then
101
+ local convert_text = function(value)
102
+ local prop_translated = not value or IsT(value)
103
+ if prop_translated and not translate then
104
+ return value and TDevModeGetEnglishText(value) or false
105
+ elseif not prop_translated and translate then
106
+ return value and value ~= "" and T(value) or false
107
+ end
108
+ return value
109
+ end
110
+ prop_def.name = convert_text(prop_def.name)
111
+ prop_def.help = convert_text(prop_def.help)
112
+ prop_def.translate_in_ged = translate
113
+ end
114
+ end
115
+ end
116
+
117
+ function ClassDef:GenerateCompanionFileCode(code)
118
+ if self.DefUndefineClass then
119
+ code:append("UndefineClass('", self.id, "')\n")
120
+ end
121
+ code:append(self.DefineKeyword, ".", self.id, " = {\n")
122
+ self:GenerateParents(code)
123
+ self:AppendGeneratedByProps(code)
124
+ self:GenerateProps(code)
125
+ self:GenerateConsts(code)
126
+ code:append("}\n\n")
127
+ self:GenerateMethods(code)
128
+ self:GenerateGlobalCode(code)
129
+ end
130
+
131
+ function ClassDef:GenerateParents(code)
132
+ local parents = self.DefParentClassList
133
+ if #(parents or "") > 0 then
134
+ code:append("\t__parents = { \"", table.concat(parents, "\", \""), "\", },\n")
135
+ end
136
+ end
137
+
138
+ function ClassDef:GenerateProps(code)
139
+ local extra_code_fn = self.GeneratePropExtraCode ~= ClassDef.GeneratePropExtraCode and
140
+ function(prop_def) return self:GeneratePropExtraCode(prop_def) end
141
+ self:GenerateSubItemsCode(code, "PropertyDef", "\tproperties = {\n", "\t},\n", self.DefPropertyTranslation, extra_code_fn )
142
+ end
143
+
144
+ function ClassDef:GeneratePropExtraCode(prop_def)
145
+ end
146
+
147
+ function ClassDef:AppendConst(code, prop_id, alternative_default, def_prop_id)
148
+ def_prop_id = def_prop_id or "Def" .. prop_id
149
+ local value = rawget(self, def_prop_id)
150
+ if value == nil then return end
151
+ local def_value = self:GetDefaultPropertyValue(def_prop_id)
152
+ if value ~= alternative_default and value ~= def_value then
153
+ code:append("\t", prop_id, " = ")
154
+ code:appendv(value)
155
+ code:append(",\n")
156
+ end
157
+ end
158
+
159
+ function ClassDef:GenerateConsts(code)
160
+ if self.DefStoreAsTable ~= "inherit" then
161
+ code:append("\tStoreAsTable = ", self.DefStoreAsTable, ",\n")
162
+ end
163
+ if self.DefPropertyTabs then
164
+ code:append("\tPropertyTabs = ")
165
+ code:appendv(self.DefPropertyTabs, "\t")
166
+ code:append(",\n")
167
+ end
168
+ self:GenerateSubItemsCode(code, "ClassConstDef")
169
+ end
170
+
171
+ function ClassDef:GenerateMethods(code)
172
+ self:GenerateSubItemsCode(code, "ClassMethodDef", "", "", self.id)
173
+ end
174
+
175
+ function ClassDef:GenerateGlobalCode(code)
176
+ self:GenerateSubItemsCode(code, "ClassGlobalCodeDef", "", "", self.id)
177
+ end
178
+
179
+ function ClassDef:GenerateSubItemsCode(code, subitem_class, prefix, suffix, ...)
180
+ local has_subitems
181
+ for i, prop in ipairs(self) do
182
+ if prop:IsKindOf(subitem_class) then
183
+ has_subitems = true
184
+ break
185
+ end
186
+ end
187
+
188
+ if has_subitems then
189
+ if prefix then code:append(prefix) end
190
+ for i, prop in ipairs(self) do
191
+ if prop:IsKindOf(subitem_class) then
192
+ prop:GenerateCode(code, ...)
193
+ end
194
+ end
195
+ if suffix then code:append(suffix) end
196
+ end
197
+ end
198
+
199
+ function ClassDef:GetCompanionFileSavePath(path)
200
+ if path:starts_with("Data") then
201
+ path = path:gsub("^Data", "Lua/ClassDefs") -- save in the game folder
202
+ elseif path:starts_with("CommonLua/Data") then
203
+ path = path:gsub("^CommonLua/Data", "CommonLua/Classes/ClassDefs") -- save in common lua
204
+ elseif path:starts_with("CommonLua/Libs/") then -- lib
205
+ path = path:gsub("/Data/", "/ClassDefs/")
206
+ else
207
+ path = path:gsub("^(svnProject/Dlc/[^/]*)/Presets", "%1/Code/ClassDefs") -- save in a DLC
208
+ end
209
+ return path:gsub(".lua$", ".generated.lua")
210
+ end
211
+
212
+
213
+ function ClassDef:GetError()
214
+ local names = {}
215
+ for _, element in ipairs(self or empty_table) do
216
+ local id = rawget(element, "id") or rawget(element, "id")
217
+ if id then
218
+ if names[id] then
219
+ return "Some class members have matching ids - '"..element.id.."'"
220
+ else
221
+ names[id] = true
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ function GetTextFilePreview(path, lines_count, filter_func)
228
+ if lines_count and lines_count > 0 then
229
+ local file, err = io.open(path, "r")
230
+ if not err then
231
+ local count = 1
232
+ local lines = {}
233
+ local line
234
+ while count <= lines_count do
235
+ line = file:read()
236
+ if line == nil then break end
237
+ for subline in line:gmatch("[^%\r?~%\n?]+") do
238
+ if count == lines_count + 1 or (filter_func and filter_func(subline)) then
239
+ break
240
+ end
241
+ lines[#lines + 1] = subline
242
+ count = count + 1
243
+ end
244
+ end
245
+ lines[#lines + 1] = ""
246
+ lines[#lines + 1] = "..."
247
+ file:close()
248
+ return table.concat(lines, "\n")
249
+ end
250
+ end
251
+ end
252
+
253
+ local function CleanUpHTMLTags(text)
254
+ text = text:gsub("<br>", "\n")
255
+ text = text:gsub("<br/>", "\n")
256
+ text = text:gsub("<script(.+)/script>", "")
257
+ text = text:gsub("<style(.+)/style>", "")
258
+ text = text:gsub("<!--(.+)-->", "")
259
+ text = text:gsub("<link(.+)/>", "")
260
+ return text
261
+ end
262
+
263
+ function GetDocumentation(obj)
264
+ if type(obj) == "table" and PropObjHasMember(obj, "Documentation") and obj.Documentation and obj.Documentation ~= "" then
265
+ return obj.Documentation
266
+ end
267
+ end
268
+
269
+ function GetDocumentationLink(obj)
270
+ if type(obj) == "table" and PropObjHasMember(obj, "DocumentationLink") and obj.DocumentationLink and obj.DocumentationLink ~= "" then
271
+ local link = obj.DocumentationLink
272
+ assert(link:starts_with("Docs/"))
273
+ if not link:starts_with("http") then
274
+ link = ConvertToOSPath(link)
275
+ end
276
+ link = string.gsub(link, "[\n\r]", "")
277
+ link = string.gsub(link, " ", "%%20")
278
+ return link
279
+ end
280
+ end
281
+
282
+ function GedOpenDocumentationLink(root, obj, prop_id, ged, btn_param, idx)
283
+ OpenUrl(GetDocumentationLink(obj), "force external browser")
284
+ end
285
+
286
+
287
+ ----- AppendClassDef
288
+
289
+ DefineClass.AppendClassDef = {
290
+ __parents = { "ClassDef" },
291
+ properties = {
292
+ { id = "DefUndefineClass", editor = false, },
293
+
294
+ },
295
+ GeneratesClass = false,
296
+ DefParentClassList = false,
297
+ DefineKeyword = "AppendClass",
298
+ }
299
+
300
+
301
+ ----- ListPreset
302
+
303
+ DefineClass.ListPreset = {
304
+ __parents = { "Preset", },
305
+ HasGroups = false,
306
+ HasSortKey = true,
307
+ EditorMenubar = "Editors.Lists",
308
+ }
309
+
310
+ -- deprecated and left for compatibility reasons, to be removed
311
+ DefineClass.ListItem = {
312
+ __parents = { "Preset", },
313
+ properties = {
314
+ { id = "Group", no_edit = false, },
315
+ },
316
+ HasSortKey = true,
317
+ PresetClass = "ListItem",
318
+ }
319
+
320
+
321
+ -----
322
+
323
+ if Platform.developer and not Platform.ged then
324
+ function RemoveUnversionedClassdefs()
325
+ local err, files = AsyncListFiles("svnProject/../", "*.lua", "recursive")
326
+ local removed = 0
327
+ for _, file in ipairs(files) do
328
+ if string.match(file, "ClassDef%-.*%.lua$") and not SVNLocalInfo(file) then
329
+ print("removing", file)
330
+ os.remove(file)
331
+ removed = removed + 1
332
+ end
333
+ end
334
+ print(removed, "files removed")
335
+ end
336
+ end
CommonLua/Classes/ClassDefFunctionObjects.lua ADDED
@@ -0,0 +1,930 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ local hintColor = RGB(210, 255, 210)
2
+ local procall = procall
3
+
4
+ ----- FunctionObject (with paremeters specified in properties, used as building block in game content editors, e.g. story bits)
5
+
6
+ DefineClass.FunctionObject = {
7
+ __parents = { "PropertyObject" },
8
+ RequiredObjClasses = false,
9
+ ForbiddenObjClasses = false,
10
+ Description = "",
11
+ ComboFormat = T(623739770783, "<class><opt(u(RequiredClassesFormatted),' ','')>"),
12
+ EditorNestedObjCategory = "General",
13
+ StoreAsTable = true,
14
+ }
15
+
16
+ function FunctionObject:GetDescription()
17
+ return self.Description
18
+ end
19
+
20
+ function FunctionObject:GetEditorView()
21
+ return self.EditorView ~= PropertyObject.EditorView and self.EditorView or self:GetDescription()
22
+ end
23
+
24
+ function FunctionObject:GetRequiredClassesFormatted()
25
+ if not self.RequiredObjClasses then return end
26
+ local classes = {}
27
+ for _, id in ipairs(self.RequiredObjClasses) do
28
+ classes[#classes + 1] = id:lower()
29
+ end
30
+ return Untranslated("(" .. table.concat(classes, ", ") .. ")")
31
+ end
32
+
33
+ function FunctionObject:ValidateObject(obj, parentobj_text, ...)
34
+ if not self.RequiredObjClasses and not self.ForbiddenObjClasses then return true end
35
+ local valid = obj and type(obj) == "table"
36
+ if valid then
37
+ if self.RequiredObjClasses and not obj:IsKindOfClasses(self.RequiredObjClasses) then
38
+ valid = false
39
+ parentobj_text = string.concat("", parentobj_text, ...) or "Unknown"
40
+ assert(valid, string.format("%s: Object for %s must be of class %s!\n(Current class is %s)",
41
+ parentobj_text, self.class, table.concat(self.RequiredObjClasses, " or "), obj.class))
42
+ end
43
+ if self.ForbiddenObjClasses and obj:IsKindOfClasses(self.ForbiddenObjClasses) then
44
+ valid = false
45
+ parentobj_text = string.concat("", parentobj_text, ...) or "Unknown"
46
+ assert(valid, string.format("%s: Object for %s must not be of class %s!",
47
+ parentobj_text, self.class, table.concat(self.ForbiddenObjClasses, " or ")))
48
+ end
49
+ end
50
+ return valid
51
+ end
52
+
53
+ function FunctionObject:HasNonPropertyMembers()
54
+ local properties = self:GetProperties()
55
+ for key, value in pairs(self) do
56
+ if key ~= "container" and key ~= "CreateInstance" and key ~= "StoreAsTable" and key ~= "param_bindings" and not table.find(properties, "id", key) then
57
+ return key
58
+ end
59
+ end
60
+ end
61
+
62
+ function FunctionObject:GetError()
63
+ if self:HasNonPropertyMembers() then
64
+ return "An Effect or Condition object must NOT keep internal state. For ContinuousEffects that need to have dynamic members, please set the CreateInstance class constant to 'true'."
65
+ end
66
+ end
67
+
68
+ function FunctionObject:TestInGed(subject, ged, context)
69
+ if self.RequiredObjClasses or self.ForbiddenObjClasses then
70
+ if self.RequiredObjClasses and not IsKindOfClasses(subject, self.RequiredObjClasses) then
71
+ local msg = string.format("%s requires an object of class %s!\n(Current class is '%s')",
72
+ self.class, table.concat(self.RequiredObjClasses, " or "), subject and subject.class or "")
73
+ ged:ShowMessage("Test Result", msg)
74
+ return
75
+ end
76
+ if self.ForbiddenObjClasses and IsKindOfClasses(subject, self.ForbiddenObjClasses) then
77
+ local msg = string.format("%s requires an object not of class %s!\n",
78
+ self.class, table.concat(self.ForbiddenObjClasses, " or "))
79
+ ged:ShowMessage("Test Result", msg)
80
+ return
81
+ end
82
+ end
83
+ local result, err, ok
84
+ if self:HasMember("Evaluate") then
85
+ result, err = self:Evaluate(subject, context)
86
+ ok = true
87
+ else
88
+ ok, result = self:Execute(subject, context)
89
+ end
90
+ if err then
91
+ ged:ShowMessage("Test Result", string.format("%s returned an error %s.", self.class, tostring(err)))
92
+ elseif not ok then
93
+ ged:ShowMessage("Test Result", string.format("%s returned an error %s.", self.class, tostring(result)))
94
+ elseif type(result) == "table" then
95
+ Inspect(result)
96
+ ged:ShowMessage("Test Result", string.format("%s returned a %s.\n\nCheck the newly opened Inspector window in-game.", self.class, result.class or "table"))
97
+ else
98
+ ged:ShowMessage("Test Result", string.format("%s returned '%s'.", self.class, result))
99
+ end
100
+ end
101
+
102
+ DefineClass.FunctionObjectDef = {
103
+ __parents = { "ClassDef" },
104
+ properties = {
105
+ { id = "DefPropertyTranslation", no_edit = true, },
106
+ },
107
+ GedEditor = false,
108
+ EditorViewPresetPrefix = "",
109
+ }
110
+
111
+ function FunctionObjectDef:OnEditorNew(parent, ged, is_paste)
112
+ -- remove test harness on paste (the test object there is of the "old" class)
113
+ for i, obj in ipairs(self) do
114
+ if IsKindOf(obj, "TestHarness") then
115
+ table.remove(self, i)
116
+ break
117
+ end
118
+ end
119
+ end
120
+
121
+ local IsKindOf = IsKindOf
122
+ function FunctionObjectDef:PostLoad()
123
+ for _, obj in ipairs(self) do
124
+ if IsKindOf(obj, "TestHarness") then
125
+ if type(obj.TestObject) == "table" and not obj.TestObject.class then
126
+ obj.TestObject = g_Classes[self.id]:new(obj.TestObject)
127
+ end
128
+ end
129
+ end
130
+ ClassDef.PostLoad(self)
131
+ end
132
+
133
+ local save_to_continue_message = { "Please save your new creation to continue.", hintColor }
134
+ local missing_harness_message = "Missing Test Harness object, force resave (Ctrl-Shift-S) to create one."
135
+
136
+ function FunctionObjectDef:GenerateCode(...)
137
+ if config.GedFunctionObjectsTestHarness then
138
+ local harness = self:FindSubitem("TestHarness")
139
+ if not harness and g_Classes[self.id] then
140
+ local error = self:GetError()
141
+ if error == missing_harness_message or error == save_to_continue_message then
142
+ local obj = TestHarness:new{ name = "TestHarness", TestObject = g_Classes[self.id]:new() }
143
+ obj:OnEditorNew()
144
+ self[#self + 1] = obj
145
+ UpdateParentTable(obj, self)
146
+ PopulateParentTableCache(obj)
147
+ ObjModified(self)
148
+ end
149
+ end
150
+ end
151
+ return ClassDef.GenerateCode(self, ...)
152
+ end
153
+
154
+ function FunctionObjectDef:DocumentationWarning(class, verb)
155
+ local documentation = self:FindSubitem("Documentation")
156
+ if not (documentation and documentation.class == "ClassConstDef" and documentation.value ~= ClassConstDef.value) then
157
+ return {
158
+ string.format([[--== Documentation ==--
159
+ What does your %s %s?
160
+
161
+ Explain behavior not apparent from the %s's name and specific terms a new modder might not know.]], class, verb, class),
162
+ hintColor, table.find(self, documentation) }
163
+ end
164
+ end
165
+
166
+ function FunctionObjectDef:GetError()
167
+ if self:FindSubitem("Init") then
168
+ return "An Init method has no effect - Effect/Condition objects are not of class InitDone."
169
+ end
170
+
171
+ if config.GedFunctionObjectsTestHarness then
172
+ local harness = self:FindSubitem("TestHarness")
173
+ if self:IsDirty() and not harness then
174
+ return save_to_continue_message -- see a bit up
175
+ elseif not harness then
176
+ return missing_harness_message -- see a bit up
177
+ elseif not harness.Tested then
178
+ if not harness.TestedOnce then
179
+ return { [[--== Testing ==--
180
+ 1. In Test Harness edit TestObject, test properties & warnings, and define a good test case.
181
+
182
+ 2. If your class requires an object, edit GetTestSubject to fetch one.
183
+
184
+ 3. Click Test to run Evaluate/Execute and check the results.]], hintColor, table.find(self, harness) }
185
+ else
186
+ return self:IsDirty()
187
+ and { [[--== Testing ==--
188
+ Please save and test your changes using the Test Harness.]], hintColor, table.find(self, harness) }
189
+ or { [[--== Testing ==--
190
+ Please test your changes using the Test Harness.]], hintColor, table.find(self, harness) }
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ function FunctionObjectDef:OnEditorDirty(dirty)
197
+ local harness = self:FindSubitem("TestHarness")
198
+ if harness then
199
+ if dirty and not harness.TestFlagsChanged then
200
+ harness.Tested = false
201
+ ObjModified(self)
202
+ end
203
+ harness.TestFlagsChanged = false
204
+ end
205
+ end
206
+
207
+
208
+ ----- TestHarness
209
+
210
+ DefineClass.TestHarness = {
211
+ __parents = { "PropertyObject" },
212
+ properties = {
213
+ { id = "name", name = "Name", editor = "text", default = false },
214
+ { id = "TestedOnce", editor = "bool", default = false, no_edit = true, },
215
+ { id = "Tested", editor = "bool", default = false, no_edit = true, },
216
+ { id = "GetTestSubject", editor = "func", default = function() end, },
217
+ { id = "TestObject", editor = "nested_obj", base_class = "FunctionObject", auto_expand = true, default = false, },
218
+ { id = "Buttons", editor = "buttons", buttons = {{name = "Test this object!", func = "Test" }}, default = false,
219
+ no_edit = function(obj) return not obj.TestObject or IsKindOf(obj.TestObject, "ContinuousEffect") end },
220
+ { id = "ButtonsContinuous", editor = "buttons", buttons = {{name = "Start effect!", func = "Test"}, {name = "Stop Effect!", func = "Stop"}}, default = false,
221
+ no_edit = function(obj) return not obj.TestObject or not IsKindOf(obj.TestObject, "ContinuousEffect") end },
222
+ },
223
+ EditorView = "[Test Harness]",
224
+ TestFlagsChanged = false,
225
+ }
226
+
227
+ function TestHarness:OnEditorNew()
228
+ self.GetTestSubject = function() return SelectedObj end
229
+ end
230
+
231
+ function TestHarness:Test(parent, prop_id, ged)
232
+ if parent:IsDirty() then
233
+ ged:ShowMessage("Please Save", "Please save before testing, unsaved changes won't apply before that.")
234
+ return
235
+ end
236
+ self.TestObject:TestInGed(self:GetTestSubject(), ged)
237
+ self.TestedOnce = true
238
+ self.Tested = true
239
+ self.TestFlagsChanged = true
240
+ ObjModified(parent)
241
+ ObjModified(ged:ResolveObj("root"))
242
+ end
243
+
244
+ function TestHarness:Stop(parent, prop_id, ged)
245
+ local fnobj, subject = self.TestObject, self:GetTestSubject()
246
+ if not fnobj.Id or fnobj.Id == "" then
247
+ ged:ShowMessage("Stop Effect", "You must specify an effect Id in order to use the Stop method!")
248
+ return
249
+ end
250
+ if fnobj:HasMember("RequiredObjClasses") and fnobj.RequiredObjClasses then
251
+ subject:StopEffect(fnobj.Id)
252
+ else
253
+ UIPlayer:StopEffect(fnobj.Id)
254
+ end
255
+ ged:ShowMessage("Stop Effect", "The effect was stopped.")
256
+ end
257
+
258
+ if not config.GedFunctionObjectsTestHarness then
259
+ TestHarness.GetDiagnosticMessage = empty_func
260
+ end
261
+
262
+
263
+ ----- Condition (a predicate that can be used in, e.g. prerequisites for a game event)
264
+
265
+ DefineClass.Condition = {
266
+ __parents = { "FunctionObject" },
267
+ Negate = false,
268
+ EditorViewNeg = false,
269
+ DescriptionNeg = "",
270
+ EditorExcludeAsNested = true,
271
+ __eval = function(self, obj, context) return false end,
272
+ }
273
+
274
+ function Condition:GetDescription() -- deprecated
275
+ return self.Negate and self.DescriptionNeg or self.Description
276
+ end
277
+
278
+ function Condition:GetEditorView()
279
+ return self.Negate and self.EditorViewNeg or FunctionObject.GetEditorView(self)
280
+ end
281
+
282
+ -- protected call - prevent game break when a condition crashes
283
+ function Condition:Evaluate(...)
284
+ local ok, err_res = procall(self.__eval, self, ...)
285
+ if ok then
286
+ if err_res then
287
+ return not self.Negate
288
+ end
289
+ return self.Negate
290
+ end
291
+ return false, err_res
292
+ end
293
+
294
+ DefineClass.ConditionsWithParams = {
295
+ __parents = { "Condition" },
296
+ properties = {
297
+ { id = "__params", name = "Parameters", editor = "expression", params = "self, obj, context, ...", default = function (self, obj, context, ...) return obj, context, ... end, },
298
+ { id = "Conditions", name = "Conditions", editor = "nested_list", default = false, base_class = "Condition", },
299
+ },
300
+ EditorView = Untranslated("Conditions with parameters"),
301
+ }
302
+
303
+ function ConditionsWithParams:__eval(...)
304
+ return _EvalConditionList(self.Conditions, self:__params(...))
305
+ end
306
+
307
+ DefineClass.ConditionDef = {
308
+ __parents = { "FunctionObjectDef" },
309
+ group = "Conditions",
310
+ DefParentClassList = { "Condition" },
311
+ GedEditor = "ClassDefEditor",
312
+ }
313
+
314
+ function ConditionDef:OnEditorNew(parent, ged, is_paste)
315
+ if is_paste then return end
316
+ self[1] = self[1] or PropertyDefBool:new{ id = "Negate", name = "Negate Condition", default = false, }
317
+ self[2] = self[2] or ClassConstDef:new{ name = "RequiredObjClasses", type = "string_list", }
318
+ self[3] = self[3] or ClassConstDef:new{ name = "EditorView", type = "translate", untranslated = true, }
319
+ self[4] = self[4] or ClassConstDef:new{ name = "EditorViewNeg", type = "translate", untranslated = true, }
320
+ self[5] = self[5] or ClassConstDef:new{ name = "Documentation", type = "text" }
321
+ self[6] = self[6] or ClassMethodDef:new{ name = "__eval", params = "obj, context", code = function(self, obj, context) return false end, }
322
+ self[7] = self[7] or ClassConstDef:new{ name = "EditorNestedObjCategory", type = "text" }
323
+ end
324
+
325
+ function ConditionDef:GetError()
326
+ local required = self:FindSubitem("RequiredObjClasses")
327
+ if required and #(required.value or "") == 0 then
328
+ return {[[--== RequiredObjClasses ==--
329
+ Please define the classes expected in __eval's 'obj' parameter, or delete if unused.]], hintColor, table.find(self, required) }
330
+ end
331
+
332
+ local description = self:FindSubitem("Description") -- deprecated
333
+ local description_fn = self:FindSubitem("GetDescription") -- deprecated
334
+ local editor_view = self:FindSubitem("EditorView")
335
+ local editor_view_fn = self:FindSubitem("GetEditorView")
336
+ if not (description and description.class == "ClassConstDef" and description.value ~= ClassConstDef.value) and
337
+ not (description and description.class == "PropertyDefText") and
338
+ not (description_fn and description_fn.class == "ClassMethodDef" and description_fn.code ~= ClassMethodDef.code) and
339
+ not (editor_view and editor_view.class == "ClassConstDef" and editor_view.value ~= ClassConstDef.value) and
340
+ not (editor_view_fn and editor_view_fn.class == "ClassMethodDef" and editor_view_fn.code ~= ClassMethodDef.code) then
341
+ return {[[--== Add Properties & EditorView ==--
342
+ Add the Condition's properties and EditorView to format it in Ged.
343
+
344
+ Sample: "Building is <BuildingClass>".]], hintColor, table.find(self, editor_view) }
345
+ end
346
+
347
+ local editor_view_neg_fn = self:FindSubitem("GetEditorViewNeg")
348
+ if editor_view_neg_fn then
349
+ return {"You can't use a GetEditorViewNeg method. Please implement GetEditorView only and check for self.Negate inside.", nil, table.find(self, editor_view_neg_fn) }
350
+ end
351
+
352
+ local negate = self:FindSubitem("Negate")
353
+ local eval = self:FindSubitem("__eval")
354
+ if negate and eval and eval.class == "ClassMethodDef" and eval:ContainsCode("self.Negate") then
355
+ return {"The value of Negate is taken into account automatically - you should not access self.Negate in __eval.", nil, table.find(self, eval) }
356
+ end
357
+
358
+ local editor_view_neg = self:FindSubitem("EditorViewNeg")
359
+ local description_neg = self:FindSubitem("DescriptionNeg") -- deprecated
360
+
361
+ if negate or editor_view_neg or description_neg then
362
+ if negate and editor_view_fn and editor_view_fn.class == "ClassMethodDef" and editor_view_fn.code ~= ClassMethodDef.code then
363
+ if not editor_view_fn:ContainsCode("self.Negate") then
364
+ return {[[--== Negate & GetEditorView ==--
365
+ If negating the makes sense for this Condition, check for self.Negate in GetEditorView to display it accordingly.
366
+
367
+ Otherwise, delete the Negate property.]], hintColor, table.find(self, negate), table.find(self, editor_view_fn) }
368
+ elseif editor_view_neg or description_neg then
369
+ return {[[--== Negate & GetEditorView ==--
370
+ Please delete EditorViewNeg, as you already check for self.Negate in GetEditorView.]], hintColor, table.find(self, editor_view_neg or description_neg) }
371
+ end
372
+ elseif not (negate and (editor_view_neg and editor_view_neg.class == "ClassConstDef" and editor_view_neg.value ~= ClassConstDef.value or
373
+ description_neg and description_neg.class == "ClassConstDef" and description_neg.value ~= ClassConstDef.value)) then
374
+ return {[[--== Negate & EditorViewNeg ==--
375
+ If negating the makes sense for this Condition, define EditorViewNeg, otherwise delete EditorViewNeg and Negate.
376
+
377
+ Sample: "Building is not <BuildingClass>".]], hintColor, table.find(self, negate), table.find(self, editor_view_neg) }
378
+ end
379
+ end
380
+
381
+ local doc_warning = self:DocumentationWarning("Condition", "check")
382
+ if not doc_warning then
383
+ local __eval = self:FindSubitem("__eval")
384
+ if not (__eval and __eval.class == "ClassMethodDef" and __eval.code ~= ClassMethodDef.code) and
385
+ not (__eval and __eval.class == "PropertyDefFunc")
386
+ then
387
+ return {[[--== __eval & GetError ==--
388
+ Implement __eval, thinking about potential circumstances in which it might not work.
389
+
390
+ Perform edit-time property validity checks in GetError. Thanks!]], hintColor, table.find(self, __eval) }
391
+ end
392
+ end
393
+ end
394
+
395
+ function ConditionDef:GetWarning()
396
+ return self:DocumentationWarning("Condition", "check")
397
+ end
398
+
399
+ function Condition:CompareOp(value, context, amount)
400
+ local op = self.Condition
401
+ local amount = amount or self.Amount
402
+ if op == ">=" then
403
+ return value >= amount
404
+ elseif op == "<=" then
405
+ return value <= amount
406
+ elseif op == ">" then
407
+ return value > amount
408
+ elseif op == "<" then
409
+ return value < amount
410
+ elseif op == "==" then
411
+ return value == amount
412
+ else -- "~="
413
+ return value ~= amount
414
+ end
415
+ end
416
+
417
+ DefineClass.ConditionComparisonDef = {
418
+ __parents = { "ConditionDef" },
419
+ }
420
+
421
+ function ConditionComparisonDef:OnEditorNew(parent, ged, is_paste)
422
+ if is_paste then return end
423
+ self[1] = self[1] or PropertyDefChoice:new{ id = "Condition", help = "The comparison to perform", items = function (self) return { ">=", "<=", ">", "<", "==", "~=" } end, default = false, }
424
+ self[2] = self[2] or PropertyDefNumber:new{ id = "Amount", help = "The value to compare against", default = false, }
425
+ self[3] = self[3] or ClassConstDef:new{ name = "RequiredObjClasses", type = "string_list", }
426
+ self[4] = self[4] or ClassConstDef:new{ name = "EditorView", type = "translate", untranslated = true, }
427
+ self[5] = self[5] or ClassConstDef:new{ name = "Documentation", type = "text" }
428
+ self[6] = self[6] or ClassMethodDef:new{ name = "__eval", params = "obj, context", code = function(self, obj, context)
429
+ -- Calculate the value to compare in 'count' here
430
+ return self:CompareOp(count, context) end, }
431
+ self[7] = self[7] or ClassMethodDef:new{ name = "GetError", params = "", code = function()
432
+ if not self.Condition then
433
+ return "Missing Condition"
434
+ elseif not self.Amount then
435
+ return "Missing Amount"
436
+ end
437
+ end }
438
+ end
439
+
440
+
441
+ ----- Effect (an action that has an effect on the game, e.g. providing resources)
442
+
443
+ DefineClass.Effect = {
444
+ __parents = { "FunctionObject" },
445
+ NoIngameDescription = false,
446
+ EditorExcludeAsNested = true,
447
+ __exec = function(self, obj, context) end,
448
+ }
449
+
450
+ function Effect:Execute(...)
451
+ return procall(self.__exec, self, ...)
452
+ end
453
+
454
+
455
+ DefineClass.EffectsWithParams = {
456
+ __parents = { "Effect" },
457
+ properties = {
458
+ { id = "__params", name = "Parameters", editor = "expression", params = "self, obj, context, ...", default = function (self, obj, context, ...) return obj, context, ... end, },
459
+ { id = "Effects", name = "Effects", editor = "nested_list", default = false, base_class = "Effect", all_descendants = true, },
460
+ },
461
+ EditorView = Untranslated("Effects with parameters"),
462
+ }
463
+
464
+ function EffectsWithParams:__exec(...)
465
+ _ExecuteEffectList(self.Effects, self:__params(...))
466
+ end
467
+
468
+
469
+ DefineClass.EffectDef = {
470
+ __parents = { "FunctionObjectDef" },
471
+ group = "Effects",
472
+ DefParentClassList = { "Effect" },
473
+ GedEditor = "ClassDefEditor",
474
+ }
475
+
476
+ function EffectDef:OnEditorNew(parent, ged, is_paste)
477
+ if is_paste then return end
478
+ self[1] = self[1] or ClassConstDef:new{ name = "RequiredObjClasses", type = "string_list", }
479
+ self[2] = self[2] or ClassConstDef:new{ name = "ForbiddenObjClasses", type = "string_list", }
480
+ self[3] = self[3] or ClassConstDef:new{ name = "ReturnClass", type = "text", }
481
+ self[4] = self[4] or ClassConstDef:new{ name = "EditorView", type = "translate", untranslated = true, }
482
+ self[5] = self[5] or ClassConstDef:new{ name = "Documentation", type = "text", }
483
+ self[6] = self[6] or ClassMethodDef:new{ name = "__exec", params = "obj, context", }
484
+ self[7] = self[7] or ClassConstDef:new{ name = "EditorNestedObjCategory", type = "text" }
485
+ end
486
+
487
+ function EffectDef:GetError()
488
+ local required = self:FindSubitem("RequiredObjClasses")
489
+ local forbidden = self:FindSubitem("ForbiddenObjClasses")
490
+ if required and #(required.value or "") == 0 or forbidden and #(forbidden.value or "") == 0 then
491
+ return {[[--== RequiredObjClasses & ForbiddenObjClasses ==--
492
+ Please define the expected classes, or delete if unused.]], hintColor, table.find(self, required), table.find(self, forbidden) }
493
+ end
494
+
495
+ --[=[ local return_class = self:FindSubitem("ReturnClass")
496
+ local return_class_fn = self:FindSubitem("GetReturnClass")
497
+ if return_class and (return_class.class ~= "ClassConstDef" or return_class.value == ClassConstDef.value) or
498
+ return_class_fn and (return_class_fn.class ~= "ClassMethodDef" or return_class_fn.code == ClassMethodDef.code) then
499
+ return {[[--== ReturnClass / GetReturnClass ==--
500
+ Please specify your Effect's return value class, or delete if no return value.
501
+
502
+ Effects that associate a new object to a StoryBit must return the object.]], hintColor, table.find(self, return_class), table.find(self, return_class_fn) }
503
+ end]=]
504
+
505
+ local description = self:FindSubitem("Description") -- deprecated
506
+ local description_fn = self:FindSubitem("GetDescription") -- deprecated
507
+ local editor_view = self:FindSubitem("EditorView")
508
+ local editor_view_fn = self:FindSubitem("GetEditorView")
509
+ if not (description and description.class == "ClassConstDef" and description.value ~= ClassConstDef.value) and
510
+ not (description and description.class == "PropertyDefText") and
511
+ not (description_fn and description_fn.class == "ClassMethodDef" and description_fn.code ~= ClassMethodDef.code) and
512
+ not (editor_view and editor_view.class == "ClassConstDef" and editor_view.value ~= ClassConstDef.value) and
513
+ not (editor_view_fn and editor_view_fn.class == "ClassMethodDef" and editor_view_fn.code ~= ClassMethodDef.code) then
514
+ return {[[--== Add Properties & EditorView ==--
515
+ Add the Effect's properties and EditorView/GetEditorView() to format it in Ged.
516
+
517
+ Sample: "Increase trade price of <Resource> by <Percent>%".]], hintColor, table.find(self, editor_view), table.find(self, editor_view_fn) }
518
+ end
519
+
520
+ local doc_warning = self:DocumentationWarning("Effect", "do")
521
+ if doc_warning then
522
+ return
523
+ end
524
+ return self:CheckExecMethod()
525
+ end
526
+
527
+ function EffectDef:CheckExecMethod()
528
+ local execute = self:FindSubitem("__exec")
529
+ if not (execute and execute.class == "ClassMethodDef" and execute.code ~= ClassMethodDef.code) then
530
+ return {[[--== Execute ==--
531
+ Implement __exec, thinking about potential circumstances in which it might not work.
532
+
533
+ Perform edit-time property validity checks in GetError. Thanks!
534
+ ]], hintColor, table.find(self, execute) }
535
+ end
536
+ end
537
+
538
+ function EffectDef:GetWarning()
539
+ return self:DocumentationWarning("Effect", "do")
540
+ end
541
+
542
+ function GetEditorConditionsAndEffectsText(texts, obj)
543
+ local trigger = rawget(obj,"Trigger") or ""
544
+ for _, condition in ipairs(obj.Conditions or empty_table) do
545
+ if trigger == "once" then
546
+ texts[#texts+1] = "\t\t" .. Untranslated( "once ") .. Untranslated(_InternalTranslate(condition:GetEditorView(), condition, false))
547
+ elseif trigger == "always" then
548
+ texts[#texts+1] = "\t\t" .. Untranslated( "always ") .. Untranslated(_InternalTranslate(condition:GetEditorView(), condition, false))
549
+ elseif trigger == "activation" then
550
+ texts[#texts+1] = "\t\t" .. Untranslated(_InternalTranslate(condition:GetEditorView(), condition, false)) .. Untranslated( " starts")
551
+ elseif trigger == "deactivation" then
552
+ texts[#texts+1] = "\t\t" .. Untranslated(_InternalTranslate(condition:GetEditorView(), condition, false)) .. Untranslated( " ends")
553
+ else
554
+ texts[#texts+1] = "\t\t" .. Untranslated(_InternalTranslate(condition:GetEditorView(), condition, false))
555
+ end
556
+ end
557
+ for _, effect in ipairs(obj.Effects or empty_table) do
558
+ texts[#texts+1] = "\t\t\t" .. Untranslated(_InternalTranslate(effect:GetEditorView(), effect, false))
559
+ end
560
+ end
561
+
562
+ function GetEditorStringListPropText(texts, obj, Prop)
563
+ if not obj[Prop] or not next(obj[Prop]) then
564
+ return
565
+ end
566
+ local string_list = {}
567
+ for _, str in ipairs(obj[Prop]) do
568
+ string_list[#string_list+1]= Untranslated(str)
569
+ end
570
+ string_list = table.concat(string_list, ", ")
571
+ texts[#texts+1] = "\t\t\t" .. Untranslated(Prop)..": "..string_list
572
+ end
573
+
574
+ function EvalConditionList(list, ...)
575
+ if list and #list > 0 then
576
+ local ok, result = procall(_EvalConditionList, list, ...)
577
+ if not ok then
578
+ return false
579
+ end
580
+ if not result then
581
+ return false
582
+ end
583
+ end
584
+ return true
585
+ end
586
+
587
+ -- unprotected call - used in already protected calls
588
+ function _EvalConditionList(list, ...)
589
+ for _, cond in ipairs(list) do
590
+ if cond:__eval(...) then
591
+ if cond.Negate then
592
+ return false
593
+ end
594
+ else
595
+ if not cond.Negate then
596
+ return false
597
+ end
598
+ end
599
+ end
600
+ return true
601
+ end
602
+
603
+ function ExecuteEffectList(list, ...)
604
+ if list and #list > 0 then
605
+ procall(_ExecuteEffectList, list, ...)
606
+ end
607
+ end
608
+
609
+ -- unprotected call - used in already protected calls
610
+ function _ExecuteEffectList(list, ...)
611
+ for _, effect in ipairs(list) do
612
+ effect:__exec(...)
613
+ end
614
+ end
615
+
616
+ function ComposeSubobjectName(parents)
617
+ local ids = {}
618
+ for i = 1, #parents do
619
+ local parent = parents[i]
620
+ local parent_id
621
+ if IsKindOfClasses(parent, "Condition", "Effect") then
622
+ parent_id = parent.class
623
+ else
624
+ parent_id = parent:HasMember("id") and parent.id or (parent:HasMember("ParamId") and parent.ParamId) or parent.class or "?"
625
+ end
626
+ ids[#ids + 1] = parent_id or "?"
627
+ end
628
+ return table.concat(ids, ".")
629
+ end
630
+
631
+
632
+ ----- New scripting
633
+
634
+ DefineClass("ScriptTestHarnessProgram", "ScriptProgram") -- this class displays a Test button in the place of the Save button in the Script Editor
635
+
636
+ function ScriptTestHarnessProgram:GetEditedScriptStatusText()
637
+ return "<center><color 0 128 0>This is a test script, press Ctrl-T to run it."
638
+ end
639
+
640
+ function ScriptDomainsCombo()
641
+ local items = { { text = "", value = false } }
642
+ for name, class in pairs(ClassDescendants("ScriptBlock")) do
643
+ if class.ScriptDomain then
644
+ if not table.find(items, "value", class.ScriptDomain) then
645
+ table.insert(items, { text = class.ScriptDomain, value = class.ScriptDomain })
646
+ end
647
+ end
648
+ end
649
+ return items
650
+ end
651
+
652
+ DefineClass.ScriptComponentDef = {
653
+ __parents = { "ClassDef" },
654
+ properties = {
655
+ { id = "DefPropertyTranslation", no_edit = true, },
656
+ { id = "DefStoreAsTable", no_edit = true, },
657
+ { id = "DefPropertyTabs", no_edit = true, },
658
+ { id = "DefUndefineClass", no_edit = true, },
659
+ { category = "Script Component", id = "DefParentClassList", name = "Parent classes", editor = "string_list", items = function(obj, prop_meta, validate_fn)
660
+ if validate_fn == "validate_fn" then
661
+ -- function for preset validation, checks whether the property value is from "items"
662
+ return "validate_fn", function(value, obj, prop_meta)
663
+ return value == "" or g_Classes[value]
664
+ end
665
+ end
666
+ return table.keys2(g_Classes, true, "")
667
+ end
668
+ },
669
+ { category = "Script Component", id = "EditorName", name = "Menu name", editor = "text", default = "", },
670
+ { category = "Script Component", id = "EditorSubmenu", name = "Menu category", editor = "combo", default = "", items = PresetsPropCombo("ScriptComponentDef", "EditorSubmenu", "") },
671
+ { category = "Script Component", id = "Documentation", editor = "text", lines = 1, default = "", },
672
+ { category = "Script Component", id = "ScriptDomain", name = "Script domain", editor = "combo", default = false, items = function() return ScriptDomainsCombo() end },
673
+
674
+ { category = "Code", id = "Params", name = "Parameters", editor = "text", default = "", },
675
+ { category = "Code", id = "Param1Help", name = "Param1 help", editor = "text", default = "",
676
+ no_edit = function(self) local _, num = string.gsub(self.Params .. ",", "([%w_]+)%s*,%s*", "") return num < 1 end,
677
+ },
678
+ { category = "Code", id = "Param2Help", name = "Param2 help", editor = "text", default = "",
679
+ no_edit = function(self) local _, num = string.gsub(self.Params .. ",", "([%w_]+)%s*,%s*", "") return num < 2 end,
680
+ },
681
+ { category = "Code", id = "Param3Help", name = "Param3 help", editor = "text", default = "",
682
+ no_edit = function(self) local _, num = string.gsub(self.Params .. ",", "([%w_]+)%s*,%s*", "") return num < 3 end,
683
+ },
684
+ { category = "Code", id = "HasGenerateCode", editor = "bool", default = false, },
685
+ { category = "Code", id = "CodeTemplate", name = "Code template", editor = "text", lines = 1, default = "",
686
+ help = "Here, self.Prop gets replaced with Prop's Lua value.\n$self.Prop omits the quotes, e.g. for variable names.",
687
+ no_edit = function(self) return self.HasGenerateCode end, dont_save = function(self) return self.HasGenerateCode end, },
688
+ { category = "Code", id = "DefGenerateCode", name = "GenerateCode", editor = "func", params = "self, pstr, indent", default = empty_func,
689
+ no_edit = function(self) return not self.HasGenerateCode end, dont_save = function(self) return not self.HasGenerateCode end,},
690
+
691
+ { category = "Test Harness", sort_order = 10000, id = "GetTestParams", editor = "func", default = function(self) return SelectedObj end, dont_save = true, },
692
+ { category = "Test Harness", sort_order = 10000, id = "TestHarness", name = "Test harness", editor = "script", default = false, dont_save = true,
693
+ params = function(self) return self.Params end,
694
+ },
695
+ { category = "Test Harness", sort_order = 10000, id = "_", editor = "buttons", buttons = {
696
+ { name = "Create", is_hidden = function(self) return self.TestHarness end, func = "CreateTestHarness" },
697
+ { name = "Recreate", is_hidden = function(self) return not self.TestHarness end, func = "CreateTestHarness" },
698
+ { name = "Test", is_hidden = function(self) return not self.TestHarness end, func = "Test" },
699
+ }},
700
+ },
701
+ GedEditor = false,
702
+ EditorViewPresetPrefix = "",
703
+ }
704
+
705
+ -- Will replace instances of the parameter names - whole words only, as listed in the Params property.
706
+ -- (for example, making Object become $self.Param1 in CodeTemplate, if Object is the 1st parameter)
707
+ function ScriptComponentDef:SubstituteParamNames(str, prefix, in_tag)
708
+ local from_to, n = {}, 1
709
+ for param in string.gmatch(self.Params .. ",", "([%w_]+)%s*,%s*") do
710
+ from_to[param] = (prefix or "") .. "Param" .. n
711
+ n = n + 1
712
+ end
713
+
714
+ local t = {}
715
+ for word, other in str:gmatch("([%a%d_]*)([^%a%d_]*)") do
716
+ if not in_tag or other:starts_with(">") then
717
+ word = from_to[word] or word
718
+ end
719
+ t[#t + 1] = word
720
+ t[#t + 1] = other
721
+ end
722
+ return table.concat(t)
723
+ end
724
+
725
+ function ScriptComponentDef:GenerateConsts(code)
726
+ code:append("\tEditorName = \"", self.EditorName, "\",\n")
727
+ code:append("\tEditorSubmenu = \"", self.EditorSubmenu, "\",\n")
728
+ code:append("\tDocumentation = \"", self.Documentation, "\",\n")
729
+ if self.ScriptDomain then
730
+ code:append("\tScriptDomain = \"", self.ScriptDomain, "\",\n")
731
+ end
732
+
733
+ local code_template = self:SubstituteParamNames(self.CodeTemplate, "$self.") -- allows using parameter names from Params instead of $self.Param1, etc.
734
+ code:append("\tCodeTemplate = ")
735
+ code:append(ValueToLuaCode(code_template))
736
+ code:append(",\n")
737
+
738
+ local n = 1
739
+ for param in string.gmatch(self.Params .. ",", "([%w_]+)%s*,%s*") do
740
+ code:appendf("\tParam%dName = \"%s\",\n", n, param)
741
+ n = n + 1
742
+ end
743
+ if self.Param1Help ~= "" then
744
+ code:append("\tParam1Help = \"", self.Param1Help, "\",\n")
745
+ end
746
+ if self.Param2Help ~= "" then
747
+ code:append("\tParam2Help = \"", self.Param2Help, "\",\n")
748
+ end
749
+ if self.Param3Help ~= "" then
750
+ code:append("\tParam3Help = \"", self.Param3Help, "\",\n")
751
+ end
752
+ ClassDef.GenerateConsts(self, code)
753
+ end
754
+
755
+ function ScriptComponentDef:GenerateMethods(code)
756
+ if self.HasGenerateCode then
757
+ local method_def = ClassMethodDef:new{ name = "GenerateCode", params = "pstr, indent", code = self.DefGenerateCode }
758
+ method_def:GenerateCode(code, self.id)
759
+ end
760
+ ClassDef.GenerateMethods(self, code)
761
+ end
762
+
763
+ function ScriptComponentDef:CreateTestHarness(root, prop_id, ged)
764
+ CreateRealTimeThread(function()
765
+ if self:IsDirty() then
766
+ GedSetUiStatus("lua_reload", "Saving...")
767
+ self:Save()
768
+ WaitMsg("Autorun")
769
+ end
770
+
771
+ self.TestHarness = self:CreateHarnessScriptProgram()
772
+ GedCreateOrEditScript(ged, self, "TestHarness", self.TestHarness)
773
+ PopulateParentTableCache(self)
774
+ ObjModified(self)
775
+ end)
776
+ end
777
+
778
+ function ScriptComponentDef:Test(root, prop_id, ged)
779
+ CreateRealTimeThread(function()
780
+ if self:IsDirty() then
781
+ GedSetUiStatus("lua_reload", "Saving...")
782
+ self:Save()
783
+ WaitMsg("Autorun")
784
+ end
785
+
786
+ local eval, msg = self.TestHarness:Compile()
787
+ if not msg then -- compilation successful
788
+ local ok, result = procall(eval, self.GetTestParams())
789
+ if not ok then
790
+ msg = string.format("%s returned an error %s.", self.id, tostring(result))
791
+ elseif type(result) == "table" then
792
+ msg = string.format("%s returned a %s.\n\nCheck the newly opened Inspector window in-game.", self.id, result.class or "table")
793
+ Inspect(result)
794
+ else
795
+ msg = string.format("%s returned '%s'.", self.id, tostring(result))
796
+ end
797
+ end
798
+ ged:ShowMessage("Test Result", msg)
799
+ ObjModified(self.TestHarness)
800
+ end)
801
+ end
802
+
803
+ function ScriptComponentDef:GetError()
804
+ if self.EditorName == "" then
805
+ return { "Please set Menu name.", hintColor }
806
+ elseif self.EditorSubmenu == "" then
807
+ return { "Please set Menu category.", hintColor }
808
+ elseif self.CodeTemplate == "" and self.DefGenerateCode == empty_func then
809
+ return { "Please set either a CodeTemplate string, or a GenerateCode function.", hintColor }
810
+ end
811
+ end
812
+
813
+
814
+ DefineClass.ScriptConditionDef = {
815
+ __parents = { "ScriptComponentDef" },
816
+ properties = {
817
+ { category = "Condition", id = "DefHasNegate", name = "Has Negate", editor = "bool", default = false, },
818
+ { category = "Condition", id = "DefHasGetEditorView", name = "Has GetEditorView", editor = "bool", default = false, },
819
+ { category = "Condition", id = "DefAutoPrependParam1", name = "Auto-prepend '<Param1>:'", editor = "bool", default = true,
820
+ no_edit = function(self) return self.DefHasGetEditorView or self.Params == "" end },
821
+ { category = "Condition", id = "DefEditorView", name = "EditorView", editor = "text", translate = false, default = "",
822
+ no_edit = function(self) return self.DefHasGetEditorView end, dont_save = function(self) return self.DefHasGetEditorView end, },
823
+ { category = "Condition", id = "DefEditorViewNeg", name = "EditorViewNeg", editor = "text", translate = false, default = "",
824
+ no_edit = function(self) return self.DefHasGetEditorView or not self.DefHasNegate end, dont_save = function(self) return self.DefHasGetEditorView or not self.DefHasNegate end, },
825
+ { category = "Condition", id = "DefGetEditorView", name = "GetEditorView", editor = "func", params = "self", default = empty_func,
826
+ no_edit = function(self) return not self.DefHasGetEditorView end, dont_save = function(self) return not self.DefHasGetEditorView end },
827
+ },
828
+ group = "Conditions",
829
+ DefParentClassList = { "ScriptCondition" },
830
+ GedEditor = "ClassDefEditor",
831
+ }
832
+
833
+ function ScriptConditionDef:GenerateConsts(code)
834
+ if self.DefHasNegate then
835
+ code:append("\tHasNegate = true,\n")
836
+ end
837
+ if not self.DefHasGetEditorView then
838
+ local ev, evneg = self.DefEditorView, self.DefEditorViewNeg
839
+ if self.DefAutoPrependParam1 and self.Params ~= "" then
840
+ ev = "<Param1>: " .. ev
841
+ evneg = "<Param1>: " .. evneg
842
+ end
843
+ code:append("\tEditorView = Untranslated(\"", self:SubstituteParamNames(ev, "", "in_tag"), "\"),\n")
844
+ if self.DefHasNegate then
845
+ code:append("\tEditorViewNeg = Untranslated(\"", self:SubstituteParamNames(evneg, "", "in_tag"), "\"),\n")
846
+ end
847
+ end
848
+ ScriptComponentDef.GenerateConsts(self, code)
849
+ end
850
+
851
+ function ScriptConditionDef:GenerateMethods(code)
852
+ if self.DefHasGetEditorView then
853
+ local method_def = ClassMethodDef:new{ name = "GetEditorView", code = self.DefGetEditorView }
854
+ method_def:GenerateCode(code, self.id)
855
+ end
856
+ ScriptComponentDef.GenerateMethods(self, code)
857
+ end
858
+
859
+ function ScriptConditionDef:CreateHarnessScriptProgram()
860
+ local test_obj = g_Classes[self.id]:new()
861
+ local program = ScriptTestHarnessProgram:new{
862
+ Params = self.Params,
863
+ ScriptReturn:new{ test_obj }
864
+ }
865
+ PopulateParentTableCache(program)
866
+ test_obj:OnAfterEditorNew()
867
+ return program
868
+ end
869
+
870
+ function ScriptConditionDef:GetError()
871
+ if self.DefHasNegate then
872
+ if (self.DefEditorView == "" or self.DefEditorViewNeg == "") and self.DefGetEditorView == empty_func then
873
+ return { "Please either set EditorView and EditorViewNeg, or define a GetEditorView method.", hintColor }
874
+ end
875
+ else
876
+ if self.DefEditorView == "" and self.DefGetEditorView == empty_func then
877
+ return { "Please either set EditorView, or define a GetEditorView method.", hintColor }
878
+ end
879
+ end
880
+ end
881
+
882
+
883
+ DefineClass.ScriptEffectDef = {
884
+ __parents = { "ScriptComponentDef" },
885
+ properties = {
886
+ { category = "Condition", id = "DefHasGetEditorView", name = "Has GetEditorView", editor = "bool", default = false, },
887
+ { category = "Condition", id = "DefAutoPrependParam1", name = "Auto-prepend '<Param1>:'", editor = "bool", default = true,
888
+ no_edit = function(self) return self.DefHasGetEditorView or self.Params == "" end },
889
+ { category = "Condition", id = "DefEditorView", name = "EditorView", editor = "text", translate = false, default = "",
890
+ no_edit = function(self) return self.DefHasGetEditorView end, dont_save = function(self) return self.DefHasGetEditorView end, },
891
+ { category = "Condition", id = "DefGetEditorView", name = "GetEditorView", editor = "func", params = "self", default = empty_func,
892
+ no_edit = function(self) return not self.DefHasGetEditorView end, dont_save = function(self) return not self.DefHasGetEditorView end, },
893
+ },
894
+ group = "Effects",
895
+ DefParentClassList = { "ScriptSimpleStatement" },
896
+ GedEditor = "ClassDefEditor",
897
+ }
898
+
899
+ function ScriptEffectDef:GenerateConsts(code)
900
+ if not self.DefHasGetEditorView then
901
+ local ev = self.DefEditorView
902
+ if self.DefAutoPrependParam1 and self.Params ~= "" then
903
+ ev = "<Param1>: " .. ev
904
+ end
905
+ code:append("\tEditorView = Untranslated(\"", self:SubstituteParamNames(ev, "", "in_tag"), "\"),\n")
906
+ end
907
+ ScriptComponentDef.GenerateConsts(self, code)
908
+ end
909
+
910
+ function ScriptEffectDef:GenerateMethods(code)
911
+ if self.DefHasGetEditorView then
912
+ local method_def = ClassMethodDef:new{ name = "GetEditorView", code = self.DefGetEditorView }
913
+ method_def:GenerateCode(code, self.id)
914
+ end
915
+ ScriptComponentDef.GenerateMethods(self, code)
916
+ end
917
+
918
+ function ScriptEffectDef:CreateHarnessScriptProgram()
919
+ local test_obj = g_Classes[self.id]:new()
920
+ local program = ScriptTestHarnessProgram:new{ [1] = test_obj, Params = self.Params }
921
+ PopulateParentTableCache(program)
922
+ test_obj:OnAfterEditorNew()
923
+ return program
924
+ end
925
+
926
+ function ScriptEffectDef:GetError()
927
+ if self.DefEditorView == "" and self.DefGetEditorView == empty_func then
928
+ return { "Please either set EditorView, or define a GetEditorView method.", hintColor }
929
+ end
930
+ end
CommonLua/Classes/ClassDefSubItem.lua ADDED
@@ -0,0 +1,1476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DefineClass.ClassDefSubItem = {
2
+ __parents = { "PropertyObject" },
3
+ }
4
+
5
+ function ClassDefSubItem:ToStringWithColor(value, t)
6
+ local text = t and value or ValueToLuaCode(value)
7
+ t = t or type(value)
8
+ local color
9
+ if t == "string" or IsT(value) then
10
+ color = RGB(60, 140, 40)
11
+ elseif t == "boolean" or t == "nil" then
12
+ color = RGB(75, 105, 198)
13
+ elseif t == "number" then
14
+ color = RGB(150, 50, 20)
15
+ elseif t == "function" then
16
+ text = string.gsub(text, "^function ", "<color 75 105 198>function</color><color 92 92 92>")
17
+ text = string.gsub(text, "end$", "<color 75 105 198>end</color>")
18
+ end
19
+ if not color then
20
+ return text
21
+ end
22
+ local r, g, b = GetRGB(color)
23
+ return string.format("<color %s %s %s><tags off>%s<tags on></color>", r, g, b, text)
24
+ end
25
+
26
+
27
+ ----- PropertyDef
28
+
29
+ local function GetCategoryItems(self)
30
+ local categories = PresetGroupCombo("PropertyCategory", "Default")()
31
+ local parent
32
+ ForEachPreset("ClassDef", function(preset)
33
+ parent = parent or table.find(preset, self) and preset
34
+ end)
35
+ if parent then
36
+ local tmp = table.invert(categories)
37
+ for _, prop in ipairs(parent) do
38
+ if IsKindOf(prop, "PropertyDef") then
39
+ tmp[prop.category or ""] = true
40
+ end
41
+ end
42
+ categories = table.keys(tmp)
43
+ end
44
+ table.sort(categories, function(a, b)
45
+ if a and b then
46
+ return a < b
47
+ else
48
+ return b
49
+ end
50
+ end)
51
+ return categories
52
+ end
53
+
54
+ local reusable_expressions = {
55
+ dont_save = "Don't save",
56
+ read_only = "Read only",
57
+ no_edit = "Hidden",
58
+ no_validate = "No validation",
59
+ }
60
+
61
+ local function reusable_expressions_combo(self)
62
+ local ret = {
63
+ { text = "true", value = true },
64
+ { text = "false", value = false },
65
+ { text = "expression", value = "expression"},
66
+ }
67
+ local preset = GetParentTableOfKind(self, "ClassDef")
68
+ for _, property_def in ipairs(preset) do
69
+ if IsKindOf(property_def, "PropertyDef") then
70
+ for id, name in pairs(reusable_expressions) do
71
+ if property_def[id] == "expression" then
72
+ table.insert(ret, {
73
+ text = "Reuse " .. name .. " from " .. property_def.id,
74
+ value = property_def.id .. "." .. id
75
+ })
76
+ end
77
+ end
78
+ end
79
+ end
80
+ return ret
81
+ end
82
+
83
+ function ValidateIdentifier(self, value)
84
+ return (type(value) ~= "string" or not value:match("^[%a_][%w_]*$")) and "Please enter a valid identifier"
85
+ end
86
+
87
+ DefineClass.PropertyDef = {
88
+ __parents = { "ClassDefSubItem" },
89
+ properties = {
90
+ { category = "Property", id = "category", name = "Category", editor = "combo", items = GetCategoryItems, default = false, },
91
+ { category = "Property", id = "id", name = "Id", editor = "text", default = "", validate = ValidateIdentifier },
92
+ { category = "Property", id = "name", name = "Name", editor = "text", translate = function(self) return self.translate_in_ged end, default = false },
93
+ { category = "Property", id = "help", name = "Help", editor = "text", translate = function(self) return self.translate_in_ged end, lines = 1, max_lines = 3, default = false },
94
+
95
+ { category = "Property", id = "dont_save", name = "Don't save", editor = "choice", default = false, items = reusable_expressions_combo },
96
+ { category = "Property", id = "dont_save_expression", name = "Don't save", editor = "expression", default = return_true, params = "self, prop_meta",
97
+ no_edit = function(self) return type(self.dont_save) == "boolean" end,
98
+ read_only = function(self) return self.dont_save ~= "expression" end,
99
+ dont_save = function(self) return self.dont_save ~= "expression" end, },
100
+ { category = "Property", id = "read_only", name = "Read only", editor = "choice", default = false, items = reusable_expressions_combo },
101
+ { category = "Property", id = "read_only_expression", name = "Read only", editor = "expression", default = return_true, params = "self, prop_meta",
102
+ no_edit = function(self) return type(self.read_only) == "boolean" end,
103
+ read_only = function(self) return self.read_only ~= "expression" end,
104
+ dont_save = function(self) return self.read_only ~= "expression" end, },
105
+ { category = "Property", id = "no_edit", name = "Hidden", editor = "choice", default = false, items = reusable_expressions_combo },
106
+ { category = "Property", id = "no_edit_expression", name = "Hidden", editor = "expression", default = return_true, params = "self, prop_meta",
107
+ no_edit = function(self) return type(self.no_edit) == "boolean" end,
108
+ read_only = function(self) return self.no_edit ~= "expression" end,
109
+ dont_save = function(self) return self.no_edit ~= "expression" end, },
110
+ { category = "Property", id = "no_validate", name = "No validation", editor = "choice", default = false, items = reusable_expressions_combo },
111
+ { category = "Property", id = "no_validate_expression", name = "No validation", editor = "expression", default = return_true, params = "self, prop_meta",
112
+ no_edit = function(self) return type(self.no_validate) == "boolean" end,
113
+ read_only = function(self) return self.no_validate ~= "expression" end,
114
+ dont_save = function(self) return self.no_validate ~= "expression" end, },
115
+
116
+ { category = "Property", id = "buttons", name = "Buttons", editor = "nested_list", base_class = "PropertyDefPropButton", default = false, inclusive = true,
117
+ help = "Button function is searched by name in the object, the root parent (Preset?), and then globally.\n\nParameters are (self, root, prop_id, ged) for the object method, (root, obj, prop_id, ged) otherwise." },
118
+ { category = "Property", id = "template", name = "Template", editor = "bool", default = false, help = "Marks template properties for classes which inherit 'ClassTemplate'"},
119
+ { category = "Property", id = "validate", name = "Validate", editor = "expression", params = "self, value", help = "A function called by Ged when changing the value. Returns error, updated_value."},
120
+ { category = "Property", id = "extra_code", name = "Extra Code", editor = "text", lines = 1, max_lines = 5, default = false, help = "Additional code to insert in the property metadata" },
121
+ },
122
+ editor = false,
123
+ validate = false,
124
+ context = false,
125
+ gender = false,
126
+ os_path = false,
127
+ translate_in_ged = false,
128
+ }
129
+
130
+ function PropertyDef:GetEditorView()
131
+ local category = ""
132
+ if self.category then
133
+ category = string.format("<color 45 138 138>[%s]</color> ", self.category)
134
+ end
135
+ return string.format("%s<color 75 105 198>%s</color> %s <color 158 158 158>=</color> <color 150 90 40>%s",
136
+ category, self.editor, self.id, self:ToStringWithColor(self.default))
137
+ end
138
+
139
+ local function getTranslatableValue(text, translate)
140
+ if not text or text == "" then return end
141
+ if text then
142
+ assert(IsT(text) == translate)
143
+ end
144
+ return text
145
+ end
146
+
147
+ local reuse_error_fn = function() return "Unable to locate expression to reuse." end
148
+ local reuse_prop_ids = { dont_save_expression = "dont_save", read_only_expression = "read_only", no_edit_expression = "no_edit", no_validate_expression = "no_validate" }
149
+
150
+ function PropertyDef:GetProperty(prop)
151
+ local main_prop_id = reuse_prop_ids[prop]
152
+ if main_prop_id then
153
+ local value = self:GetProperty(main_prop_id)
154
+ if type(value) == "string" and value ~= "expression" then
155
+ local reuse_prop_id, reuse = value:match("([%w_]+)%.([%w_]+)")
156
+ if reuse then
157
+ local preset = GetParentTableOfKind(self, "ClassDef")
158
+ local property_def = table.find_value(preset, "id", reuse_prop_id)
159
+ return property_def and property_def[reuse .. "_expression"] or reuse_error_fn
160
+ end
161
+ end
162
+ end
163
+ return ClassDefSubItem.GetProperty(self, prop)
164
+ end
165
+
166
+ function PropertyDef:GenerateExpressionSettingCode(code, id)
167
+ local value = self[id]
168
+ if type(value) ~= "boolean" then
169
+ local expr = self:GetProperty(id .. "_expression")
170
+ if expr ~= reuse_error_fn then
171
+ code:appendf("%s = function(self) %s end, ", id, GetFuncBody(expr))
172
+ end
173
+ elseif value then
174
+ code:appendf("%s = true, ", id)
175
+ end
176
+ end
177
+
178
+ function PropertyDef:GenerateCode(code, translate, extra_code_fn)
179
+ if self.id == "" then return end
180
+ code:append("\t\t{ ")
181
+ if self.category and self.category ~= "" then
182
+ code:appendf("category = \"%s\", ", self.category)
183
+ end
184
+ code:append("id = \"", self.id, "\", ")
185
+ local name, help = getTranslatableValue(self.name, translate), getTranslatableValue(self.help, translate)
186
+ if name then
187
+ code:append("name = ", ValueToLuaCode(name), ", ")
188
+ end
189
+ if help then
190
+ code:append("help = ", ValueToLuaCode(help), ", ")
191
+ end
192
+ code:append("\n\t\t\t")
193
+ code:appendf("editor = \"%s\", default = %s, ", self.editor, self:GenerateDefaultValueCode())
194
+
195
+ self:GenerateExpressionSettingCode(code, "dont_save")
196
+ self:GenerateExpressionSettingCode(code, "read_only")
197
+ self:GenerateExpressionSettingCode(code, "no_edit")
198
+ self:GenerateExpressionSettingCode(code, "no_validate")
199
+ if self.validate then
200
+ code:appendf("validate = function(self, value) %s end, ", GetFuncBody(self.validate))
201
+ end
202
+
203
+ if self.buttons and #self.buttons > 0 then
204
+ code:append("buttons = {")
205
+ for _, data in ipairs(self.buttons) do
206
+ if data.Name ~= "" then
207
+ if data.IsHidden ~= empty_func then
208
+ code:appendf([[ {name = "%s", func = "%s", is_hidden = function(self) %s end }, ]], data.Name, data.FuncName, GetFuncBody(data.IsHidden))
209
+ else
210
+ code:appendf([[ {name = "%s", func = "%s"}, ]], data.Name, data.FuncName)
211
+ end
212
+ end
213
+ end
214
+ code:append("}, ")
215
+ end
216
+ if self.template then code:append("template = true, ") end
217
+ if self.extra_code and self.extra_code ~= "" or extra_code_fn then
218
+ local ext_code = self.extra_code and self.extra_code:gsub(",$", "")
219
+ if extra_code_fn then
220
+ ext_code = ext_code and (ext_code .. ", " .. extra_code_fn(self)) or extra_code_fn(self)
221
+ ext_code = ext_code:gsub(",$", "")
222
+ end
223
+ if ext_code and ext_code ~= "" then
224
+ code:append("\n\t\t\t", ext_code)
225
+ code:append(", ")
226
+ end
227
+ end
228
+ self:GenerateAdditionalPropCode(code, translate)
229
+ code:append("},\n")
230
+ end
231
+
232
+ function PropertyDef:GenerateDefaultValueCode()
233
+ return ValueToLuaCode(self.default, ' ', nil, {} --[[ enable property injection ]])
234
+ end
235
+
236
+ function PropertyDef:ValidateProperty(prop_meta)
237
+ if not self.no_validate then
238
+ return PropertyObject.ValidateProperty(self, prop_meta)
239
+ end
240
+ end
241
+
242
+ function PropertyDef:GenerateAdditionalPropCode(code, translate)
243
+ end
244
+
245
+ function PropertyDef:AppendFunctionCode(code, prop_name)
246
+ if not self[prop_name] then return end
247
+ local name, params, body = GetFuncSource(self[prop_name])
248
+ code:appendf("%s = function (%s)\n", prop_name, params)
249
+ if type(body) == "string" then
250
+ body = string.split(body, "\n")
251
+ end
252
+ code:append("\t", body and table.concat(body, "\n\t") or "", "\n")
253
+ code:append("end, \n")
254
+ end
255
+
256
+ function PropertyDef:GetError()
257
+ if not self.no_validate and self.extra_code and (self.extra_code:find("[^%w_]items%s*=") or self.extra_code:find("^items%s*=")) then
258
+ return "Please don't define 'items' as extra code. Use the dedicated Items property instead.\nThis is to make items appear in the default value property."
259
+ end
260
+ end
261
+
262
+ function PropertyDef:CleanupForSave()
263
+ end
264
+
265
+ function PropertyDef:EmulatePropEval(metadata_id, default, prop_meta, validate_fn)
266
+ local prop_meta = self
267
+ local classdef_preset = GetParentTableOfKind(self, "ClassDef")
268
+ if not classdef_preset then return default end
269
+ local obj_class = g_Classes[classdef_preset.id]
270
+ local instance = obj_class and not obj_class:IsKindOf("CObject") and obj_class:new() or {}
271
+ if validate_fn then
272
+ return eval_items(prop_meta[metadata_id], instance, prop_meta)
273
+ end
274
+ return prop_eval(prop_meta[metadata_id], instance, prop_meta, default)
275
+ end
276
+
277
+ local function EmulatePropEval(metadata_id, default)
278
+ return function(self, prop_meta, validate_fn)
279
+ return self:EmulatePropEval(metadata_id, default, prop_meta, validate_fn)
280
+ end
281
+ end
282
+
283
+ DefineClass.PropertyDefPropButton = {
284
+ __parents = { "PropertyObject" },
285
+ properties = {
286
+ { id = "Name", editor = "text", default = ""},
287
+ { id = "FuncName", editor = "text", default = ""},
288
+ { id = "IsHidden", editor = "expression", default = empty_func},
289
+ },
290
+ EditorView = Untranslated("[<Name>] = <FuncName>"),
291
+ }
292
+
293
+ DefineClass.PropertyDefButtons = {
294
+ __parents = { "PropertyDef" },
295
+ properties = {
296
+ { id = "default", name = "Default value", editor = false, default = false, },
297
+ },
298
+ editor = "buttons",
299
+ EditorName = "Buttons property",
300
+ EditorSubmenu = "Extras",
301
+ }
302
+
303
+ DefineClass.PropertyDefBool = {
304
+ __parents = { "PropertyDef" },
305
+ properties = {
306
+ { category = "Bool", id = "default", name = "Default value", editor = "bool", default = false, },
307
+ },
308
+ editor = "bool",
309
+ EditorName = "Bool property",
310
+ EditorSubmenu = "Basic property",
311
+ }
312
+
313
+ DefineClass.PropertyDefTable = {
314
+ __parents = { "PropertyDef" },
315
+ properties = {
316
+ { category = "Table", id = "default", name = "Default value", editor = "prop_table", default = false, },
317
+ { category = "Table", id = "lines", name = "Lines", editor = "number", default = 1, },
318
+ },
319
+ editor = "prop_table",
320
+ EditorName = "Table property",
321
+ EditorSubmenu = "Objects",
322
+ }
323
+
324
+ function PropertyDefTable:GenerateAdditionalPropCode(code, translate)
325
+ if self.lines > 1 then
326
+ code:append("indent = \"\", lines = 1, max_lines = ", self.lines, ", ")
327
+ end
328
+ end
329
+
330
+ DefineClass.PropertyDefPoint = {
331
+ __parents = { "PropertyDef" },
332
+ properties = {
333
+ { category = "Point", id = "default", name = "Default value", editor = "point", default = false, },
334
+ },
335
+ editor = "point",
336
+ EditorName = "Point property",
337
+ EditorSubmenu = "Basic property",
338
+ }
339
+
340
+ DefineClass.PropertyDefPoint2D = {
341
+ __parents = { "PropertyDef" },
342
+ properties = {
343
+ { category = "Point2D", id = "default", name = "Default value", editor = "point2d", default = false, },
344
+ },
345
+ editor = "point2d",
346
+ EditorName = "Point2D property",
347
+ EditorSubmenu = "Basic property",
348
+ }
349
+
350
+ DefineClass.PropertyDefRect = {
351
+ __parents = { "PropertyDef" },
352
+ properties = {
353
+ { category = "Rect", id = "default", name = "Default value", editor = "rect", default = false, },
354
+ },
355
+ editor = "rect",
356
+ EditorName = "Rect property",
357
+ EditorSubmenu = "Basic property",
358
+ }
359
+
360
+ DefineClass.PropertyDefMargins = {
361
+ __parents = { "PropertyDef" },
362
+ properties = {
363
+ { category = "Margins", id = "default", name = "Default value", editor = "margins", default = false, },
364
+ },
365
+ editor = "margins",
366
+ EditorName = "Margins property",
367
+ EditorSubmenu = "Extras",
368
+ }
369
+
370
+ DefineClass.PropertyDefPadding = {
371
+ __parents = { "PropertyDef" },
372
+ properties = {
373
+ { category = "Padding", id = "default", name = "Default value", editor = "padding", default = false, },
374
+ },
375
+ editor = "padding",
376
+ EditorName = "Padding property",
377
+ EditorSubmenu = "Extras",
378
+ }
379
+
380
+ DefineClass.PropertyDefBox = {
381
+ __parents = { "PropertyDef" },
382
+ properties = {
383
+ { category = "Box", id = "default", name = "Default value", editor = "box", default = false, },
384
+ },
385
+ editor = "box",
386
+ EditorName = "Box property",
387
+ EditorSubmenu = "Basic property",
388
+ }
389
+
390
+ DefineClass.PropertyDefNumber = {
391
+ __parents = { "PropertyDef" },
392
+ properties = {
393
+ { category = "Number", id = "default", name = "Default value", editor = "number", default = false, scale = function(obj) return obj.scale end, min = function(obj) return obj.min end, max = function(obj) return obj.max end, },
394
+ { category = "Number", id = "scale", name = "Scale", editor = "choice", default = 1, items = function() return table.keys2(const.Scale, true, 1, 10, 100, 1000, 1000000) end, },
395
+ { category = "Number", id = "step", name = "Step", editor = "number", default = 1, scale = function(obj) return obj.scale end, },
396
+ { category = "Number", id = "float", name = "Float", editor = "bool", default = false, },
397
+ { category = "Number", id = "slider", name = "Slider", editor = "bool", default = false, },
398
+ { category = "Number", id = "min", name = "Min", editor = "number", default = min_int64, scale = function(obj) return obj.scale end, no_edit = PropChecker("custom_lims", true)},
399
+ { category = "Number", id = "max", name = "Max", editor = "number", default = max_int64, scale = function(obj) return obj.scale end, no_edit = PropChecker("custom_lims", true) },
400
+ { category = "Number", id = "custom_min", name = "Min", editor = "expression", default = false, no_edit = PropChecker("custom_lims", false) },
401
+ { category = "Number", id = "custom_max", name = "Max", editor = "expression", default = false, no_edit = PropChecker("custom_lims", false) },
402
+ { category = "Number", id = "custom_lims", name = "Custom Lims", editor = "bool", default = false, help = "Use custom limits"},
403
+ { category = "Number", id = "modifiable", name = "Modifiable", editor = "bool", default = false, help = "Marks modifiable properties for classes which inherit 'Modifiable' class"},
404
+ },
405
+ editor = "number",
406
+ EditorName = "Number property",
407
+ EditorSubmenu = "Basic property",
408
+ }
409
+
410
+ function PropertyDefNumber:GenerateAdditionalPropCode(code, translate)
411
+ local scale = self.scale
412
+ if scale ~= 1 then
413
+ code:appendf(type(scale) == "number" and "scale = %d, " or "scale = \"%s\", ", scale)
414
+ end
415
+ if self.step ~= 1 then code:appendf("step = %d, ", self.step) end
416
+ if self.slider then code:append("slider = true, ") end
417
+ if self.custom_lims then
418
+ if self.custom_min ~= PropertyDefNumber.custom_min then
419
+ local name, params, body = GetFuncSource(self.custom_min)
420
+ body = type(body) == "table" and table.concat(body, "\n") or body
421
+ code:appendf("min = function(self) %s end, ", body)
422
+ end
423
+ if self.custom_max ~= PropertyDefNumber.custom_max then
424
+ local name, params, body = GetFuncSource(self.custom_max)
425
+ body = type(body) == "table" and table.concat(body, "\n") or body
426
+ code:appendf("max = function(self) %s end, ", body)
427
+ end
428
+ else
429
+ if self.min ~= PropertyDefNumber.min then code:appendf("min = %d, ", self.min) end
430
+ if self.max ~= PropertyDefNumber.max then code:appendf("max = %d, ", self.max) end
431
+ end
432
+ if self.modifiable then code:append("modifiable = true, ") end
433
+ end
434
+
435
+ function PropertyDefNumber:OnEditorSetProperty(prop_id, old_value, ged)
436
+ if prop_id == "slider" and self.slider then
437
+ if self.min == PropertyDefNumber.min then self.min = 0 end
438
+ if self.max == PropertyDefNumber.max then self.max = 100 * (const.Scale[self.scale] or self.scale) end
439
+ end
440
+ end
441
+
442
+ DefineClass.PropertyDefRange = {
443
+ __parents = { "PropertyDef" },
444
+ properties = {
445
+ { category = "Range", id = "default", name = "Default value", editor = "range", scale = function(obj) return obj.scale end, min = function(obj) return obj.min end, max = function(obj) return obj.max end, default = false, },
446
+ { category = "Range", id = "scale", name = "Scale", editor = "choice", default = 1, items = function() return table.keys2(const.Scale, true, 1, 10, 100, 1000, 1000000) end, },
447
+ { category = "Range", id = "step", name = "Step", editor = "number", default = 1, scale = function(obj) return obj.scale end, },
448
+ { category = "Range", id = "slider", name = "Slider", editor = "bool", default = false, },
449
+ { category = "Range", id = "min", name = "Min", editor = "number", default = min_int64 },
450
+ { category = "Range", id = "max", name = "Max", editor = "number", default = max_int64 },
451
+ },
452
+ editor = "range",
453
+ EditorName = "Range property",
454
+ EditorSubmenu = "Basic property",
455
+ }
456
+
457
+ function PropertyDefRange:GenerateAdditionalPropCode(code, translate)
458
+ if self.scale ~= 1 then
459
+ code:appendf(type(self.scale) == "number" and "scale = %d, " or "scale = \"%s\", ", self.scale)
460
+ end
461
+ if self.step ~= 1 then code:appendf("step = %d, ", self.step) end
462
+ if self.slider then code:append("slider = true, ") end
463
+ if self.min ~= PropertyDefRange.min then code:appendf("min = %d, ", self.min) end
464
+ if self.max ~= PropertyDefRange.max then code:appendf("max = %d, ", self.max) end
465
+ end
466
+
467
+ function PropertyDefRange:OnEditorSetProperty(prop_id, old_value, ged)
468
+ if prop_id == "slider" and self.slider then
469
+ if self.min == PropertyDefRange.min then self.min = 0 end
470
+ if self.max == PropertyDefRange.max then self.max = 100 * (const.Scale[self.scale] or self.scale) end
471
+ end
472
+ end
473
+
474
+ local function translate_only(obj) return not obj.translate end
475
+ TextGenderOptions = {
476
+ {value = false, text = "None"},
477
+ {value = "ask", text = "Request translation gender for each language (for nouns)"},
478
+ {value = "variants", text = "Generate separate translation texts for each gender"},
479
+ }
480
+
481
+ DefineClass.PropertyDefText = {
482
+ __parents = { "PropertyDef" },
483
+ properties = {
484
+ { category = "Text", id = "default", name = "Default value", editor = "text", default = false, translate = function (obj) return obj.translate end, },
485
+ { category = "Text", id = "translate", name = "Translate", editor = "bool", default = true, },
486
+ { category = "Text", id = "wordwrap", name = "Wordwrap", editor = "bool", default = false, },
487
+ { category = "Text", id = "gender", name = "Gramatical gender", editor = "choice", default = false, items = TextGenderOptions,
488
+ no_edit = translate_only, dont_save = translate_only },
489
+ { category = "Text", id = "lines", name = "Lines", editor = "number", default = false, },
490
+ { category = "Text", id = "max_lines", name = "Max lines", editor = "number", default = false, },
491
+ { category = "Text", id = "trim_spaces", name = "Trim Spaces", editor = "bool", default = true, },
492
+ { category = "Text", id = "context", name = "Context", editor = "text", default = "",
493
+ no_edit = translate_only, dont_save = translate_only }
494
+ },
495
+ editor = "text",
496
+ EditorName = "Text property",
497
+ EditorSubmenu = "Basic property",
498
+ }
499
+
500
+ function PropertyDefText:OnEditorSetProperty(prop_id, old_value, ged)
501
+ if prop_id == "translate" then
502
+ self:UpdateLocalizedProperty("default", self.translate)
503
+ end
504
+ end
505
+
506
+ function PropertyDefText:GenerateAdditionalPropCode(code, translate)
507
+ if self.translate then code:append("translate = true, ") end
508
+ if self.wordwrap then code:append("wordwrap = true, ") end
509
+ if self.lines then code:appendf("lines = %d, ", self.lines) end
510
+ if self.max_lines then code:appendf("max_lines = %d, ", self.max_lines) end
511
+ if self.trim_spaces==false then code:append("trim_spaces = false, ") end
512
+ local context = self.translate and self.context or ""
513
+ if context ~= "" then
514
+ assert(not self.gender, "Custom context code is not compatible with gender specification") -- you should include |gender-ask or |gender-variants in the function result
515
+ code:append("context = ", context, ", ")
516
+ elseif self.gender then
517
+ code:append('context = "|gender-', self.gender, '", ')
518
+ end
519
+ end
520
+
521
+ DefineClass.PropertyDefChoice = {
522
+ __parents = { "PropertyDef" },
523
+ properties = {
524
+ { category = "Choice", id = "default", name = "Default value", editor = "choice", default = false, items = EmulatePropEval("items", {""}) },
525
+ { category = "Choice", id = "items", name = "Items", editor = "expression", default = false, },
526
+ { category = "Choice", id = "show_recent_items", name = "Show recent items", editor = "number", default = 0, },
527
+ },
528
+ translate = false,
529
+ editor = "choice",
530
+ EditorName = "Choice property",
531
+ EditorSubmenu = "Basic property",
532
+ }
533
+
534
+ function PropertyDefChoice:GenerateAdditionalPropCode(code, translate)
535
+ if self.items then
536
+ code:append("items = ")
537
+ ValueToLuaCode(self.items, nil, code)
538
+ code:append(", ")
539
+ end
540
+ if self.show_recent_items and self.show_recent_items ~= 0 then
541
+ code:appendf("show_recent_items = %d,", self.show_recent_items)
542
+ end
543
+ end
544
+
545
+ function PropertyDefChoice:GetConvertToPresetIdClass()
546
+ local preset_class
547
+ if self.items then
548
+ local src = GetFuncSourceString(self.items)
549
+ local _, _, capture = string.find(src, 'PresetsCombo%("([%w_+-]*)"%)')
550
+ preset_class = capture
551
+ end
552
+ return preset_class
553
+ end
554
+
555
+ DefineClass.PropertyDefCombo = {
556
+ __parents = { "PropertyDef" },
557
+ properties = {
558
+ { category = "Combo", id = "default", name = "Default value", editor = "combo", default = false, items = EmulatePropEval("items", {""}) },
559
+ { category = "Combo", id = "items", name = "Items", editor = "expression", default = false, },
560
+ { category = "Combo", id = "translate", name = "Translate", editor = "bool", default = false, },
561
+ { category = "Combo", id = "show_recent_items", name = "Show recent items", editor = "number", default = 0, },
562
+ },
563
+ editor = "combo",
564
+ EditorName = "Combo property",
565
+ EditorSubmenu = "Basic property",
566
+ }
567
+
568
+ PropertyDefCombo.GenerateAdditionalPropCode = PropertyDefChoice.GenerateAdditionalPropCode
569
+
570
+ DefineClass.PropertyDefPickerBase = {
571
+ __parents = { "PropertyDef" },
572
+ properties = {
573
+ { category = "Picker", id = "default", name = "Default value", editor = "combo", default = false, items = EmulatePropEval("items", {""}) },
574
+ { category = "Picker", id = "items", name = "Items", editor = "expression", default = false, },
575
+ { category = "Picker", id = "max_rows", name = "Max rows", editor = "number", default = false, help = "Maximum number of rows displayed." },
576
+ { category = "Picker", id = "multiple", name = "Multiple selection", editor = "bool", default = false, },
577
+ { category = "Picker", id = "small_font", name = "Small font", editor = "bool", default = false, },
578
+ { category = "Picker", id = "filter_by_prop", name = "Filter by prop", editor = "text", default = false, help = "Links this property to a text property with the specified id that serves as a filter." },
579
+ },
580
+ }
581
+
582
+ function PropertyDefPickerBase:GenerateAdditionalPropCode(code, translate)
583
+ PropertyDefChoice.GenerateAdditionalPropCode(self, code, translate)
584
+ if self.max_rows then code:appendf("max_rows = %d, ", self.max_rows) end
585
+ if self.multiple then code:append ("multiple = true, ") end
586
+ if self.small_font then code:append ("small_font = true, ") end
587
+ if self.filter_by_prop then code:appendf("filter_by_prop = \"%s\", ", self.filter_by_prop) end
588
+ end
589
+
590
+ DefineClass.PropertyDefTextPicker = {
591
+ __parents = { "PropertyDefPickerBase" },
592
+ properties = {
593
+ { category = "Picker", id = "horizontal", name = "Horizontal", editor = "bool", default = false, help = "Display items horizontally." },
594
+ },
595
+ editor = "text_picker",
596
+ EditorName = "Text picker property",
597
+ EditorSubmenu = "Extras",
598
+ }
599
+
600
+ function PropertyDefTextPicker:GenerateAdditionalPropCode(code, translate)
601
+ PropertyDefPickerBase.GenerateAdditionalPropCode(self, code, translate)
602
+ if self.horizontal then code:append("horizontal = true, ") end
603
+ end
604
+
605
+ DefineClass.PropertyDefTexturePicker = {
606
+ __parents = { "PropertyDefPickerBase" },
607
+ properties = {
608
+ { category = "Picker", id = "thumb_width", name = "Width", editor = "number", default = false, },
609
+ { category = "Picker", id = "thumb_height", name = "Height", editor = "number", default = false, },
610
+ { category = "Picker", id = "thumb_zoom", name = "Zoom", editor = "number", min = 100, max = 200, slider = true, default = false, help = "Scale the texture up, displaying only the middle part with Width x Height dimensions.", },
611
+ { category = "Picker", id = "alt_prop", name = "Alt prop", editor = "text", default = false, help = "Id of another property that gets set by Alt-click on this property.", },
612
+ { category = "Picker", id = "base_color_map", name = "Base color map", editor = "bool", default = false, help = "Use to display a base color map texture in the correct colors.", },
613
+ },
614
+ editor = "texture_picker",
615
+ EditorName = "Texture picker property",
616
+ EditorSubmenu = "Extras",
617
+ }
618
+
619
+ function PropertyDefTexturePicker:GenerateAdditionalPropCode(code, translate)
620
+ PropertyDefPickerBase.GenerateAdditionalPropCode(self, code, translate)
621
+ if self.thumb_width then code:appendf("thumb_width = %d, ", self.thumb_width) end
622
+ if self.thumb_height then code:appendf("thumb_height = %d, ", self.thumb_height) end
623
+ if self.thumb_zoom then code:appendf("thumb_zoom = %d, ", self.thumb_zoom) end
624
+ if self.alt_prop then code:appendf("alt_prop = \"%s\", ", self.alt_prop) end
625
+ if self.base_color_map then code:append ("base_color_map = true, ") end
626
+ end
627
+
628
+ DefineClass.PropertyDefSet = {
629
+ __parents = { "PropertyDef" },
630
+ properties = {
631
+ { category = "Set", id = "default", name = "Default value", editor = "set", default = false, items = EmulatePropEval("items", {""}), three_state = function(obj) return obj.three_state end, max_items_in_set = function(obj) return obj.max_items_in_set end, },
632
+ { category = "Set", id = "items", name = "Items", editor = "expression", default = false, },
633
+ { category = "Set", id = "arbitrary_value", name = "Allow arbitrary value", editor = "bool", default = false, },
634
+ { category = "Set", id = "three_state", name = "Three-state", editor = "bool", default = false, help = "Each set item can have one of the three value 'nil', 'true', or 'false'." },
635
+ { category = "Set", id = "max_items_in_set", name = "Max items", editor = "number", default = 0, help = "Max number of items in the set (0 = no limit)." },
636
+ },
637
+ translate = false,
638
+ editor = "set",
639
+ EditorName = "Set property",
640
+ EditorSubmenu = "Basic property",
641
+ }
642
+
643
+ function PropertyDefSet:GenerateAdditionalPropCode(code, translate)
644
+ if self.three_state then code:append("three_state = true, ") end
645
+ if self.arbitrary_value then code:append("arbitrary_value = true, ") end
646
+ if self.max_items_in_set ~= 0 then code:appendf("max_items_in_set = %d, ", self.max_items_in_set) end
647
+ if self.items then
648
+ code:append("items = ")
649
+ ValueToLuaCode(self.items, nil, code)
650
+ code:append(", ")
651
+ end
652
+ end
653
+
654
+ DefineClass.PropertyDefGameStatefSet = {
655
+ __parents = { "PropertyDef" },
656
+ properties = {
657
+ { category = "Set", id = "default", name = "Default value", editor = "set", default = false, items = function() return GetGameStateFilter() end, three_state = true, },
658
+ },
659
+ translate = false,
660
+ editor = "set",
661
+ EditorName = "GameState Set property",
662
+ EditorSubmenu = "Basic property",
663
+ }
664
+
665
+ function PropertyDefGameStatefSet:GenerateAdditionalPropCode(code, translate)
666
+ code:append("three_state = true, items = function (self) return GetGameStateFilter() end, ")
667
+ code:append('buttons = { {name = "Check Game States", func = "PropertyDefGameStatefSetCheck"}, },')
668
+ end
669
+
670
+ function PropertyDefGameStatefSetCheck(_, obj, prop_id, ged)
671
+ ged:ShowMessage("Test Result", GetMismatchGameStates(obj[prop_id]))
672
+ end
673
+
674
+ DefineClass.PropertyDefColor = {
675
+ __parents = { "PropertyDef" },
676
+ properties = {
677
+ { category = "Color", id = "default", name = "Default value", editor = "color", default = RGB(0, 0, 0), },
678
+ },
679
+ editor = "color",
680
+ EditorName = "Color property",
681
+ EditorSubmenu = "Basic property",
682
+ }
683
+
684
+ DefineClass.PropertyDefImage = {
685
+ __parents = { "PropertyDef" },
686
+ properties = {
687
+ { category = "Image", id = "default", name = "Default value", editor = "image", default = false, },
688
+ { category = "Image", id = "img_size", name = "Image box", editor = "number", default = 200, },
689
+ { category = "Image", id = "img_box", name = "Image border", editor = "number", default = 1, },
690
+ { category = "Image", id = "base_color_map", name = "Base color", editor = "bool", default = false, },
691
+ },
692
+ editor = "image",
693
+ EditorName = "Image preview property",
694
+ EditorSubmenu = "Extras",
695
+ }
696
+
697
+ function PropertyDefImage:GenerateAdditionalPropCode(code, translate)
698
+ code:appendf("img_size = %d, img_box = %d, base_color_map = %s, ", self.img_size, self.img_box, tostring(self.base_color_map))
699
+ end
700
+
701
+ DefineClass.PropertyDefGrid = {
702
+ __parents = { "PropertyDef" },
703
+ properties = {
704
+ { category = "Grid", id = "default", name = "Default value", editor = "grid", default = false, },
705
+ { category = "Grid", id = "frame", name = "Frame", editor = "number", default = 0, },
706
+ { category = "Grid", id = "color", name = "Color", editor = "bool", default = false },
707
+ { category = "Grid", id = "min", name = "Min Size", editor = "number", default = 0, },
708
+ { category = "Grid", id = "max", name = "Max Size", editor = "number", default = 0 },
709
+ },
710
+ editor = "grid",
711
+ EditorName = "Grid property",
712
+ EditorSubmenu = "Extras",
713
+ }
714
+
715
+ function PropertyDefGrid:GenerateAdditionalPropCode(code, translate)
716
+ if self.frame > 0 then code:appendf("frame = %d, ", self.frame) end
717
+ if self.min > 0 then code:appendf("min = %d, ", self.min) end
718
+ if self.max > 0 then code:appendf("max = %d, ", self.max) end
719
+ if self.color then code:append("color = true, ") end
720
+ end
721
+
722
+ DefineClass.PropertyDefMaterial = {
723
+ __parents = { "PropertyDef" },
724
+ properties = {
725
+ { category = "Color", id = "default", name = "Default value", editor = "rgbrm", default = RGBRM(200, 200, 200, 0, 0) },
726
+ },
727
+ editor = "rgbrm",
728
+ EditorName = "Material property",
729
+ EditorSubmenu = "Extras",
730
+ }
731
+
732
+ DefineClass.PropertyDefBrowse = {
733
+ __parents = { "PropertyDef" },
734
+ properties = {
735
+ { category = "Browse", id = "default", name = "Default value", editor = "browse", default = false, },
736
+ { category = "Browse", id = "folder", name = "Folder", editor = "text", default = "UI", },
737
+ { category = "Browse", id = "filter", name = "Filter", editor = "text", default = "Image files|*.tga", },
738
+ { category = "Browse", id = "extension", name = "Force extension", editor = "text", default = false,
739
+ buttons = { { name = "No extension", func = function(self) self.extension = "" ObjModified(self) end, } },
740
+ },
741
+ { category = "Browse", id = "image_preview_size", name = "Image preview size", editor = "number", default = 0, },
742
+ },
743
+ editor = "browse",
744
+ EditorName = "Browse property",
745
+ EditorSubmenu = "Basic property",
746
+ }
747
+
748
+ function PropertyDefBrowse:GenerateAdditionalPropCode(code, translate)
749
+ local folder, filter = self.folder or "", self.filter or ""
750
+ if folder ~= "" then
751
+ -- detect if we have a table or a single string for self.folder
752
+ if folder:match("^%s*[{\"]") then
753
+ code:appendf("folder = %s, ", self.folder)
754
+ else
755
+ code:appendf("folder = \"%s\", ", self.folder)
756
+ end
757
+ end
758
+ if self.filter ~= "" then code:appendf("filter = \"%s\", ", self.filter) end
759
+ if self.extension ~= PropertyDefBrowse.extension then code:appendf("force_extension = \"%s\", ", self.extension) end
760
+ if self.image_preview_size > 0 then code:appendf("image_preview_size = %i, ", self.image_preview_size) end
761
+ end
762
+
763
+
764
+ DefineClass.PropertyDefUIImage = {
765
+ __parents = { "PropertyDef" },
766
+ properties = {
767
+ { category = "Browse", id = "default", name = "Default value", editor = "ui_image", default = false, },
768
+ { category = "Browse", id = "filter", name = "Filter", editor = "text", default = "All files|*.*", },
769
+ { category = "Browse", id = "extension", name = "Force extension", editor = "text", default = false,
770
+ buttons = { { name = "No extension", func = function(self) self.extension = "" ObjModified(self) end, } },
771
+ },
772
+ { category = "Browse", id = "image_preview_size", name = "Image preview size", editor = "number", default = 0, },
773
+ },
774
+ editor = "ui_image",
775
+ EditorName = "UI image property",
776
+ EditorSubmenu = "Basic property",
777
+ }
778
+
779
+ function PropertyDefUIImage:GenerateAdditionalPropCode(code, translate)
780
+ if self.filter ~= PropertyDefUIImage.filter then code:appendf("filter = \"%s\", ", self.filter) end
781
+ if self.extension ~= PropertyDefUIImage.extension then code:appendf("force_extension = \"%s\", ", self.extension) end
782
+ if self.image_preview_size > 0 then code:appendf("image_preview_size = %i, ", self.image_preview_size) end
783
+ end
784
+
785
+ DefineClass.PropertyDefFunc = {
786
+ __parents = { "PropertyDef" },
787
+ properties = {
788
+ { category = "Func", id = "params", name = "Params", editor = "text", default = "self", },
789
+ { category = "Func", id = "default", name = "Default value", editor = "func", default = empty_func, lines = 1, max_lines = 20, params = function (self) return self.params end, },
790
+ },
791
+ editor = "func",
792
+ EditorName = "Func property",
793
+ EditorSubmenu = "Code",
794
+ }
795
+
796
+ function PropertyDefFunc:ToStringWithColor(value)
797
+ if value == empty_func then
798
+ return PropertyDef.ToStringWithColor(self, string.format("function (%s) end", self.params), "function")
799
+ end
800
+ return PropertyDef.ToStringWithColor(self, value)
801
+ end
802
+
803
+
804
+ function PropertyDefFunc:GenerateDefaultValueCode()
805
+ if not self.default then return "false" end
806
+ return GetFuncSourceString(self.default, "", self.params or "self")
807
+ end
808
+
809
+ function PropertyDefFunc:GenerateAdditionalPropCode(code, translate)
810
+ if self.params and self.params ~= "self" then
811
+ code:appendf("params = \"%s\", ", self.params)
812
+ end
813
+ end
814
+
815
+ DefineClass.PropertyDefExpression = {
816
+ __parents = { "PropertyDef" },
817
+ properties = {
818
+ { category = "Expression", id = "params", name = "Params", editor = "text", default = "self", },
819
+ { category = "Expression", id = "default", name = "Default value", editor = "expression", default = false, params = function(self) return self.params end, },
820
+ },
821
+ editor = "expression",
822
+ EditorName = "Expression property",
823
+ EditorSubmenu = "Code",
824
+ }
825
+
826
+ function PropertyDefExpression:GenerateDefaultValueCode()
827
+ if not self.default then return "false" end
828
+ return GetFuncSourceString(self.default, "", self.params or "self")
829
+ end
830
+
831
+ function PropertyDefExpression:GenerateAdditionalPropCode(code, translate)
832
+ if self.params and self.params ~= "self" then
833
+ code:appendf("params = \"%s\", ", self.params)
834
+ end
835
+ end
836
+
837
+ DefineClass.PropertyDefShortcut = {
838
+ __parents = { "PropertyDef" },
839
+ properties = {
840
+ { category = "Expression", id = "shortcut_type", name = "Shortcut type", editor = "choice", default = "keyboard&mouse", items = {
841
+ "keyboard&mouse",
842
+ "keyboard",
843
+ "gamepad"
844
+ }, },
845
+ { category = "Expression", id = "default", name = "Default value", editor = "text", default = "", no_edit = true, },
846
+ },
847
+ editor = "shortcut",
848
+ EditorName = "Shortcut property",
849
+ EditorSubmenu = "Extras",
850
+ }
851
+
852
+ function PropertyDefShortcut:GenerateAdditionalPropCode(code, translate)
853
+ code:appendf("shortcut_type = \"%s\", ", self.shortcut_type)
854
+ end
855
+
856
+ ----- Presets - preset_id & preset_id_list
857
+
858
+ function IsPresetWithConstantGroup(classdef)
859
+ if not classdef then return end
860
+ local prop = classdef:GetPropertyMetadata("Group")
861
+ return not prop or prop_eval(prop.no_edit, classdef, prop, true) or prop_eval(prop.read_only, classdef, prop, true)
862
+ end
863
+
864
+ DefineClass.PropertyDefPresetIdBase = {
865
+ __parents = { "PropertyDef" },
866
+ properties = {
867
+ { category = "PresetId", id = "preset_class", name = "Preset class", editor = "choice", default = "",
868
+ items = ClassDescendantsCombo("Preset", false, function(name, class)
869
+ return not IsKindOfClasses(class, "ClassDef", "ClassDefSubItem")
870
+ end),
871
+ },
872
+ { category = "PresetId", id = "preset_group", name = "Preset group", editor = "choice", default = "",
873
+ help = "Restricts the choice to the specified group of the preset class.",
874
+ items = function(obj)
875
+ local class = g_Classes[obj.preset_class]
876
+ local preset_class = class.PresetClass or obj.preset_class
877
+ return class and PresetGroupsCombo(preset_class) or empty_table
878
+ end,
879
+ no_edit = function(obj) -- disable group restriction for classes with uneditable "Group" property
880
+ local class = g_Classes[obj.preset_class]
881
+ return not class or IsPresetWithConstantGroup(class)
882
+ end, },
883
+ { category = "PresetId", id = "preset_filter", name = "PresetFilter", editor = "func", params = "preset, obj, prop_meta", lines = 1, max_lines = 20, default = false },
884
+ { category = "PresetId", id = "extra_item", name = "Extra item", editor = "text", default = false },
885
+ { category = "PresetId", id = "default", name = "Default value", editor = "choice",
886
+ items = function(obj)
887
+ local class = g_Classes[obj.preset_class]
888
+ if class and (class.GlobalMap or IsPresetWithConstantGroup(class) or obj.preset_group ~= "") then
889
+ return PresetsCombo(obj.preset_class, obj.preset_group ~= "" and obj.preset_group, {"", obj.extra_item or nil})
890
+ end
891
+ return { false }
892
+ end, default = false },
893
+ },
894
+ editor = "preset_id",
895
+ EditorName = "PresetId property",
896
+ EditorSubmenu = "Basic property",
897
+ }
898
+
899
+ function PropertyDefPresetIdBase:GenerateAdditionalPropCode(code, translate)
900
+ if self.preset_class ~= "" then code:appendf("preset_class = \"%s\", ", self.preset_class) end
901
+ if self.preset_group ~= "" then code:appendf("preset_group = \"%s\", ", self.preset_group) end
902
+ if self.extra_item then code:appendf("extra_item = \"%s\", ", self.extra_item) end
903
+ self:AppendFunctionCode(code, "preset_filter")
904
+ end
905
+
906
+ function PropertyDefPresetIdBase:OnEditorSetProperty(prop_id, old_value, ...)
907
+ if prop_id == "preset_class" then
908
+ self.preset_group = nil
909
+ self.default = nil
910
+ elseif prop_id == "preset_group" then
911
+ self.default = nil
912
+ end
913
+ ObjModified(self)
914
+ end
915
+
916
+ function PropertyDefPresetIdBase:GetError()
917
+ local class = g_Classes[self.preset_class]
918
+ if class and not (class.GlobalMap or IsPresetWithConstantGroup(class) or self.preset_group ~= "") then
919
+ return string.format("%s doesn't have GlobalMap - all presets can't be listed. Please specify a Preset group.\n\nIf you want all presets to be selectable, either add GlobalMap, or create two properties - one for selecting a preset group and another for selecting a preset from that group.", self.preset_class)
920
+ end
921
+ end
922
+
923
+ DefineClass.PropertyDefPresetId = {
924
+ __parents = { "PropertyDefPresetIdBase" },
925
+ }
926
+
927
+ DefineClass.PropertyDefPresetIdList = {
928
+ __parents = { "PropertyDefPresetIdBase", "WeightedListProps" },
929
+ properties = {
930
+ { category = "PresetId", id = "default", name = "Default value", editor = "preset_id_list",
931
+ preset_class = function(obj) return obj.preset_class end,
932
+ preset_group = function(obj) return obj.preset_group end,
933
+ extra_item = function(obj) return obj.extra_item end,
934
+ default = {}, },
935
+ },
936
+ editor = "preset_id_list",
937
+ EditorName = "PresetId list property",
938
+ EditorSubmenu = "Lists",
939
+ }
940
+
941
+ function PropertyDefPresetIdList:GenerateAdditionalPropCode(code, translate)
942
+ PropertyDefPresetIdBase.GenerateAdditionalPropCode(self, code, translate)
943
+ code:append("item_default = \"\"")
944
+ self:GenerateWeightPropCode(code)
945
+ code:append(", ")
946
+ end
947
+
948
+
949
+ ----- Nested object & nested list
950
+
951
+ local function BaseClassCombo(obj, prop_meta, validate_fn)
952
+ if validate_fn == "validate_fn" then
953
+ -- function for preset validation, checks whether the property value is from "items"
954
+ return "validate_fn", function(value, obj, prop_meta)
955
+ local class = g_Classes[value]
956
+ return value == "" or IsKindOf(class, "PropertyObject") and not IsKindOf(class, "CObject")
957
+ end
958
+ end
959
+ return ClassDescendantsList("PropertyObject", function(name, def)
960
+ return not def.__ancestors.CObject and name ~= "CObject"
961
+ end, "")
962
+ end
963
+
964
+ DefineClass.PropertyDefObject = {
965
+ __parents = { "PropertyDef" },
966
+ default = false,
967
+ editor = "object",
968
+ properties = {
969
+ { category = "Property", id = "base_class", name = "Base class", editor = "choice", items = function() return ClassDescendantsListInclusive("Object") end, default = "Object" },
970
+ { category = "Property", id = "format_func", name = "Format", editor = "func", default = GetObjectPropEditorFormatFuncDefault, lines = 1, max_lines = 10, params = "gameobj"},
971
+ },
972
+ EditorName = "Object property",
973
+ EditorSubmenu = "Objects",
974
+ }
975
+
976
+ function PropertyDefObject:GenerateAdditionalPropCode(code, translate)
977
+ code:appendf("base_class = \"%s\", ", self.base_class)
978
+ self:AppendFunctionCode(code, "format_func")
979
+ end
980
+
981
+ DefineClass.PropertyDefNestedObj = {
982
+ __parents = { "PropertyDef" },
983
+ properties = {
984
+ { category = "Nested Object", id = "base_class", name = "Base class", editor = "choice", items = BaseClassCombo, default = "PropertyObject" },
985
+ { category = "Nested Object", id = "inclusive", name = "Allow base class", editor = "bool", default = false, },
986
+ { category = "Nested Object", id = "no_descendants", name = "No descendants", editor = "bool", default = false, },
987
+ { category = "Nested Object", id = "all_descendants", name = "Allow all descendants", editor = "bool", default = false, no_edit = function (self) return self.no_descendants end, },
988
+ { category = "Nested Object", id = "class_filter", name = "ClassFilter", editor = "func", default = false, lines = 1, max_lines = 20, params = "name, class, obj"},
989
+ { category = "Nested Object", id = "format", name = "Format in Ged", editor = "text", default = "<EditorView>", },
990
+ { category = "Nested Object", id = "auto_expand", name = "Auto Expand", editor = "bool", default = false, },
991
+ { category = "Nested Object", id = "default", name = "Default value", editor = "bool", default = false, no_edit = true, },
992
+ },
993
+ editor = "nested_obj",
994
+ EditorName = "Nested object property",
995
+ EditorSubmenu = "Objects",
996
+ }
997
+
998
+ function PropertyDefNestedObj:GenerateAdditionalPropCode(code, translate)
999
+ code:appendf("base_class = \"%s\", ", self.base_class)
1000
+ if self.inclusive then code:append("inclusive = true, ") end
1001
+ if self.no_descendants then code:append("no_descendants = true, ")
1002
+ elseif self.all_descendants then code:append("all_descendants = true, ") end
1003
+ if self.auto_expand then code:appendf("auto_expand = true, ") end
1004
+ if self.format ~= PropertyDefNestedObj.format then code:appendf("format = \"%s\"", self.format) end
1005
+ self:AppendFunctionCode(code, "class_filter")
1006
+ end
1007
+
1008
+ function PropertyDefNestedObj:GetError()
1009
+ if self.base_class == "PropertyObject" then
1010
+ return "Please specify base class for the nested object(s)."
1011
+ end
1012
+ end
1013
+
1014
+ DefineClass.PropertyDefNestedList = {
1015
+ __parents = { "PropertyDef" },
1016
+ properties = {
1017
+ { category = "Nested List", id = "base_class", name = "Base class", editor = "choice", items = BaseClassCombo, default = "PropertyObject" },
1018
+ { category = "Nested List", id = "inclusive", name = "Allow base class", editor = "bool", default = false, },
1019
+ { category = "Nested List", id = "no_descendants", name = "No descendants", editor = "bool", default = false, },
1020
+ { category = "Nested List", id = "all_descendants", name = "Allow all descendants", editor = "bool", default = false, no_edit = function (self) return self.no_descendants end, },
1021
+ { category = "Nested List", id = "class_filter", name = "ClassFilter", editor = "func", default = false, lines = 1, max_lines = 20, params = "name, class, obj"},
1022
+ { category = "Nested List", id = "format", name = "Format in Ged", editor = "text", default = "<EditorView>", },
1023
+ { category = "Nested List", id = "auto_expand", name = "Auto Expand", editor = "bool", default = false, },
1024
+ { category = "Nested List", id = "default", name = "Default value", editor = "bool", default = false, no_edit = true, },
1025
+ },
1026
+ editor = "nested_list",
1027
+ EditorName = "Nested list property",
1028
+ EditorSubmenu = "Lists",
1029
+ }
1030
+
1031
+ PropertyDefNestedList.GenerateAdditionalPropCode = PropertyDefNestedObj.GenerateAdditionalPropCode
1032
+ PropertyDefNestedList.GetError = PropertyDefNestedObj.GetError
1033
+
1034
+ DefineClass.PropertyDefPropertyArray = {
1035
+ __parents = { "PropertyDef" },
1036
+ properties = {
1037
+ { category = "Dynamic Props", id = "from", name = "Generate id from", editor = "choice", default = false,
1038
+ items = { "Table keys", "Table values", "Table field values", "Preset ids" },
1039
+ },
1040
+ { category = "Dynamic Props", id = "field", name = "Table field", editor = "text", translate = false, default = "",
1041
+ no_edit = function(self) return self.from ~= "Table field values" end,
1042
+ },
1043
+ { category = "Dynamic Props", id = "items", name = "Items", editor = "expression", default = false,
1044
+ no_edit = function(self) return self.from == "Preset ids" end,
1045
+ },
1046
+ { category = "Dynamic Props", id = "preset", name = "Preset class", editor = "choice", default = "",
1047
+ items = ClassDescendantsCombo("Preset"),
1048
+ no_edit = function(self) return self.from ~= "Preset ids" end,
1049
+ },
1050
+ { category = "Dynamic Props", id = "prop_meta_update", name = "Update prop_meta", editor = "func", params = "self, prop_meta", default = false,
1051
+ help = "Update the prop_meta of each generated property from prop_meta.id, prop_meta.index (consecutive number), and prop_meta.prest/value.",
1052
+ },
1053
+ { category = "Dynamic Props", id = "prop", name = "Property template", editor = "nested_obj", base_class = "PropertyDef", auto_expand = true, default = false,
1054
+ suppress_props = { id = true, name = true, category = true, dont_save = true, no_edit = true, template = true, },
1055
+ },
1056
+ },
1057
+ editor = "property_array",
1058
+ EditorName = "Property array",
1059
+ EditorSubmenu = "Objects",
1060
+ default = false,
1061
+ }
1062
+
1063
+ function PropertyDefPropertyArray:GenerateAdditionalPropCode(code, translate)
1064
+ local from_preset = self.from == "Preset ids" and self.preset ~= "" and self.preset
1065
+ if self.prop and (from_preset or not from_preset and self.items ~= false) then
1066
+ if from_preset then
1067
+ code:appendf("from = '%s', ", self.preset)
1068
+ else
1069
+ code:append("items = ")
1070
+ ValueToLuaCode(self.items, nil, code)
1071
+ code:appendf(", from = '%s', ", self.from)
1072
+ if self.from == "Table field values" then
1073
+ code:appendf("field = '%s', ", self.field)
1074
+ end
1075
+ if self.prop_meta_update then
1076
+ code:appendf("prop_meta_update = ")
1077
+ ValueToLuaCode(self.prop_meta_update, nil, code)
1078
+ code:appendf(", ")
1079
+ end
1080
+ end
1081
+ code:append("\nprop_meta =\n")
1082
+ self.prop.id = " "
1083
+ self.prop:GenerateCode(code, translate)
1084
+ end
1085
+ end
1086
+
1087
+ DefineClass.PropertyDefScript = {
1088
+ __parents = { "PropertyDef" },
1089
+ properties = {
1090
+ { category = "Script", id = "condition", name = "Is condition list", editor = "bool", default = false, },
1091
+ { category = "Script", id = "params_exp", name = "Params is an expression", editor = "bool", default = false, },
1092
+ { category = "Script", id = "params", name = "Params", editor = "text", default = "self", },
1093
+ { category = "Script", id = "script_domain", name = "Script domain", editor = "choice", default = false, items = function() return ScriptDomainsCombo() end },
1094
+ },
1095
+ default = false,
1096
+ editor = "script",
1097
+ EditorName = "Script",
1098
+ EditorSubmenu = "Code",
1099
+ }
1100
+
1101
+ function PropertyDefScript:GenerateAdditionalPropCode(code, translate)
1102
+ if self.condition then
1103
+ code:append('class = "ScriptConditionList", ')
1104
+ end
1105
+ if self.params_exp then
1106
+ code:appendf('params = function(self) return %s end, ', self.params)
1107
+ else
1108
+ code:appendf('params = "%s", ', self.params)
1109
+ end
1110
+ if self.script_domain then
1111
+ code:appendf('script_domain = "%s", ', self.script_domain)
1112
+ end
1113
+ end
1114
+
1115
+
1116
+ ----- Primitive lists
1117
+
1118
+ DefineClass.WeightedListProps = {
1119
+ __parents = { "PropertyObject" },
1120
+ properties = {
1121
+ { category = "Weights", id = "weights", name = "Weights", editor = "bool", default = false,
1122
+ no_edit = function(obj) return obj:DisableWeights() end, help = "Associates weights to the list items"},
1123
+ { category = "Weights", id = "weight_default", name = "Default Weight", editor = "number", default = 100,
1124
+ no_edit = function(obj) return not obj.weights end, help = "Default weight for each list item" },
1125
+ { category = "Weights", id = "value_key", name = "Value Key", editor = "text", default = "value",
1126
+ no_edit = function(obj) return not obj.weights end, help = "Name of the 'value' key in each list item. Can be a number too." },
1127
+ { category = "Weights", id = "weight_key", name = "Weight Key", editor = "text", default = "weight",
1128
+ no_edit = function(obj) return not obj.weights end, help = "Name of the 'weight' key in each list item. Can be a number too." },
1129
+ },
1130
+ DisableWeights = empty_func,
1131
+ }
1132
+
1133
+ function WeightedListProps:GetItemKeys()
1134
+ local value_key = self.value_key
1135
+ if value_key == "" then
1136
+ value_key = "value"
1137
+ end
1138
+ value_key = tonumber(value_key) or value_key
1139
+
1140
+ local weight_key = self.weight_key
1141
+ if weight_key == "" then
1142
+ weight_key = "weight"
1143
+ end
1144
+ weight_key = tonumber(weight_key) or weight_key
1145
+
1146
+ return value_key, weight_key
1147
+ end
1148
+
1149
+ function WeightedListProps:GenerateWeightPropCode(code)
1150
+ if not self.weights then
1151
+ return
1152
+ end
1153
+ code:append(", weights = true")
1154
+ local value_key, weight_key = self:GetItemKeys()
1155
+ if value_key ~= "value" then
1156
+ code:append(", value_key = ")
1157
+ ValueToLuaCode(value_key, nil, code)
1158
+ end
1159
+ if weight_key ~= "weight" then
1160
+ code:append(", weight_key = ")
1161
+ ValueToLuaCode(weight_key, nil, code)
1162
+ end
1163
+ if self.weight_default ~= 100 then
1164
+ code:append(", weight_default = ")
1165
+ ValueToLuaCode(self.weight_default, nil, code)
1166
+ end
1167
+ end
1168
+
1169
+ DefineClass.PropertyDefPrimitiveList = {
1170
+ __parents = { "PropertyDef", "WeightedListProps" },
1171
+ properties = {
1172
+ { category = "List", id = "item_default", name = "Item Default", editor = "text", default = false, },
1173
+ { category = "List", id = "items", name = "Items", editor = "expression", default = false, },
1174
+ { category = "List", id = "max_items", name = "Max number of items", editor = "number", default = -1, },
1175
+ },
1176
+ editor = "",
1177
+ EditorName = "",
1178
+ }
1179
+
1180
+ function PropertyDefPrimitiveList:DisableWeights()
1181
+ return self.editor ~= "number_list" and self.editor ~= "string_list"
1182
+ end
1183
+
1184
+ function PropertyDefPrimitiveList:GenerateAdditionalPropCode(code, translate)
1185
+ code:append("item_default = ")
1186
+ ValueToLuaCode(self.item_default, nil, code)
1187
+ code:append(", items = ")
1188
+ ValueToLuaCode(self.items, nil, code)
1189
+ if self.arbitrary_value then
1190
+ code:append(", arbitrary_value = true")
1191
+ end
1192
+ if self.max_items >= 0 then
1193
+ code:append(", max_items = ")
1194
+ ValueToLuaCode(self.max_items, nil, code)
1195
+ end
1196
+ self:GenerateWeightPropCode(code)
1197
+ code:append(", ")
1198
+ end
1199
+
1200
+ DefineClass.PropertyDefNumberList = {
1201
+ __parents = { "PropertyDefPrimitiveList" },
1202
+
1203
+ properties = {
1204
+ { category = "List", id = "default", name = "Default value", editor = "number_list",
1205
+ default = {}, item_default = function(self) return self.item_default end, items = EmulatePropEval("items", {0}) },
1206
+ { category = "List", id = "item_default", name = "Item Default", editor = "number", default = 0, },
1207
+ },
1208
+
1209
+ editor = "number_list",
1210
+ EditorName = "Number list property",
1211
+ EditorSubmenu = "Lists",
1212
+ }
1213
+
1214
+ DefineClass.PropertyDefStringList = {
1215
+ __parents = { "PropertyDefPrimitiveList" },
1216
+
1217
+ properties = {
1218
+ { category = "List", id = "default", name = "Default value", editor = "string_list",
1219
+ default = {}, items = EmulatePropEval("items", {""}),
1220
+ item_default = function(self) return self.item_default end,
1221
+ arbitrary_value = function(self) return self.arbitrary_value end, },
1222
+ { category = "List", id = "item_default", name = "Item default", editor = "combo",
1223
+ default = "", items = EmulatePropEval("items", {""}) },
1224
+ { category = "List", id = "arbitrary_value", name = "Allow arbitrary value", editor = "bool", default = false, },
1225
+ },
1226
+
1227
+ editor = "string_list",
1228
+ EditorName = "String list property",
1229
+ EditorSubmenu = "Lists",
1230
+ }
1231
+
1232
+ DefineClass.PropertyDefTList = {
1233
+ __parents = { "PropertyDefPrimitiveList" },
1234
+
1235
+ properties = {
1236
+ { category = "List", id = "default", name = "Default value", editor = "T_list",
1237
+ default = {}, item_default = function(self) return self.item_default end, items = EmulatePropEval("items", {""}) },
1238
+ { category = "List", id = "item_default", name = "Item default", editor = "text", default = "", translate = true},
1239
+ { category = "Text", id = "context", name = "Context", editor = "text", default = "", }
1240
+ },
1241
+
1242
+ editor = "T_list",
1243
+ EditorName = "Translated list property",
1244
+ EditorSubmenu = "Lists",
1245
+ }
1246
+
1247
+ function PropertyDefTList:GenerateAdditionalPropCode(code, translate)
1248
+ PropertyDefPrimitiveList.GenerateAdditionalPropCode(self, code, translate)
1249
+ if self.context and self.context ~= "" then
1250
+ code:append("context = " .. self.context .. ", ")
1251
+ end
1252
+ end
1253
+
1254
+ DefineClass.PropertyDefHelp = {
1255
+ __parents = { "PropertyDef" },
1256
+ default = false,
1257
+ editor = "help",
1258
+ EditorName = "Help text",
1259
+ EditorSubmenu = "Extras",
1260
+ }
1261
+
1262
+
1263
+ ----- ClassConstDef
1264
+
1265
+ local const_items = {
1266
+ { text = "Bool", value = "bool" },
1267
+ { text = "Number", value = "number" },
1268
+ { text = "Text", value = "text" },
1269
+ { text = "Translated Text", value = "translate" },
1270
+ { text = "Point", value = "point" },
1271
+ { text = "Box", value = "rect" },
1272
+ { text = "Color", value = "color" },
1273
+ { text = "Range", value = "range" },
1274
+ { text = "Image", value = "browse" },
1275
+ { text = "Table", value = "prop_table" },
1276
+ { text = "String List", value = "string_list" },
1277
+ { text = "Number List", value = "number_list" },
1278
+ }
1279
+
1280
+ DefineClass.ClassConstDef = {
1281
+ __parents = { "ClassDefSubItem" },
1282
+ properties = {
1283
+ { category = "Const", id = "name", name = "Name", editor = "text", default = "", validate = ValidateIdentifier },
1284
+ { category = "Const", id = "type", name = "Type", editor = "choice", default = "bool", items = const_items, },
1285
+ { category = "Const", id = "value", name = "Value", editor = function(self) return self.type == "translate" and "text" or self.type end,
1286
+ translate = function(self) return self.type == "translate" end, default = false,
1287
+ lines = function(self) return self.type == "prop_table" and 3 or self.type == "text" and 1 end,
1288
+ max_lines = function(self) return self.type == "text" and 256 end, },
1289
+ { category = "Const", id = "untranslated", name = "Untranslated", editor = "bool",
1290
+ no_edit = function(self) return self.type ~= "translate" and self.type ~= "text" end, default = false, }
1291
+ },
1292
+ EditorName = "Class member",
1293
+ EditorSubmenu = "Code",
1294
+ }
1295
+
1296
+ function ClassConstDef:OnEditorSetProperty(prop_id, old_value, ged)
1297
+ if prop_id == "type" then
1298
+ local value = self.type
1299
+ if value == "text" and old_value == "translate" then
1300
+ self:UpdateLocalizedProperty("value", false)
1301
+ elseif value == "translate" and old_value == "text" then
1302
+ self:UpdateLocalizedProperty("value", true)
1303
+ else
1304
+ self.value = nil
1305
+ end
1306
+ end
1307
+ end
1308
+
1309
+ function ClassConstDef:GetValue()
1310
+ if not self.value and (self.type == "text" or self.type == "translate") then
1311
+ return ""
1312
+ end
1313
+ return self.value
1314
+ end
1315
+
1316
+ function ClassConstDef:GetEditorView()
1317
+ local result = "<color 45 138 138>Class.</color>"
1318
+ if self.type == "translate" then
1319
+ result = result .. string.format(self.untranslated and '%s = Untranslated(%s)' or '%s = T(%s)', self.name, self:ToStringWithColor(TDevModeGetEnglishText(self:GetValue())))
1320
+ else
1321
+ result = result .. string.format("%s = %s", self.name, self:ToStringWithColor(self:GetValue()))
1322
+ end
1323
+ return result
1324
+ end
1325
+
1326
+ function ClassConstDef:GenerateCode(code)
1327
+ if not self.name:match("^[%w_]+$") then return end
1328
+ if self.untranslated then
1329
+ code:append("\t", self.name, " = Untranslated(" )
1330
+ if self.type == "text" then
1331
+ ValueToLuaCode(self:GetValue(), nil, code)
1332
+ elseif self.type == "translate" then
1333
+ ValueToLuaCode(TDevModeGetEnglishText(self:GetValue()), nil, code)
1334
+ end
1335
+ code:append("),\n")
1336
+ return
1337
+ end
1338
+ code:append("\t", self.name, " = ")
1339
+ ValueToLuaCode(self:GetValue(), nil, code)
1340
+ code:append(",\n")
1341
+ end
1342
+
1343
+
1344
+ ----- ClassMethodDef
1345
+
1346
+ local default_methods = {
1347
+ "",
1348
+ "GetEditorView()",
1349
+ "GetError()",
1350
+ "GetWarning()",
1351
+ "OnEditorSetProperty(prop_id, old_value, ged)",
1352
+ "OnEditorNew(parent, ged, is_paste)",
1353
+ "OnEditorDelete(parent, ged)",
1354
+ "OnEditorSelect(selected, ged)",
1355
+ }
1356
+
1357
+ function ClassMethodDefKnownMethodsCombo(method_def)
1358
+ local defaults = { delete = true }
1359
+ for _, method in ipairs(default_methods) do
1360
+ local name = method:match("(.+)%(")
1361
+ if name then defaults[name] = true end
1362
+ end
1363
+
1364
+ local methods = {}
1365
+ local class_def = GetParentTableOfKind(method_def, "ClassDef")
1366
+ for _, parent in ipairs(class_def.DefParentClassList) do
1367
+ local class = g_Classes[parent]
1368
+ while class and class.class ~= "PropertyObject" and class.class ~= "Preset" do
1369
+ for k, v in pairs(class) do
1370
+ if type(v) == "function" then
1371
+ local name, params, body = GetFuncSource(v)
1372
+ local sep_idx = name and name:find(":", 1, true)
1373
+ if sep_idx then
1374
+ name = name:sub(sep_idx + 1)
1375
+ if not defaults[name] then
1376
+ methods[#methods + 1] = string.format("%s(%s)", name, params:trim_spaces())
1377
+ end
1378
+ end
1379
+ end
1380
+ end
1381
+ class = getmetatable(class)
1382
+ end
1383
+ end
1384
+ table.sort(methods)
1385
+ return #methods == 0 and default_methods or table.iappend(table.iappend(table.copy(default_methods), {"---"}), methods)
1386
+ end
1387
+
1388
+ DefineClass.ClassMethodDef = {
1389
+ __parents = { "ClassDefSubItem" },
1390
+ properties = {
1391
+ { category = "Method", id = "name", name = "Name", editor = "combo", default = "",
1392
+ items = ClassMethodDefKnownMethodsCombo,
1393
+ validate = function (self, value)
1394
+ local sep_idx = type(value) == "string" and value:find("(", 1, true)
1395
+ local name = sep_idx and value:sub(1, sep_idx - 1) or value
1396
+ if type(value) ~= "string" or not name:match("^[%w_]*$") then
1397
+ return "Value must be a valid identifier or a function prototype."
1398
+ end
1399
+ end, },
1400
+ { category = "Method", id = "params", name = "Params", editor = "text", default = "", },
1401
+ { category = "Method", id = "comment", name = "Comment", editor = "text", default = "", lines = 1, max_lines = 5, },
1402
+ { category = "Method", id = "code", name = "Code", editor = "func", default = false, lines = 1, max_lines = 100,
1403
+ params = function (self) return self.params == "" and "self" or "self, " .. self.params end, },
1404
+ },
1405
+ EditorName = "Method",
1406
+ EditorSubmenu = "Code",
1407
+ }
1408
+
1409
+ function ClassMethodDef:GetEditorView()
1410
+ local ret = string.format("<color 75 105 198>function</color> <color 45 138 138>Class:</color>%s(%s)", self.name, self.params)
1411
+ if self.comment ~= "" then
1412
+ ret = string.format("%s <color 0 128 0> -- %s", ret, self.comment)
1413
+ end
1414
+ return ret
1415
+ end
1416
+
1417
+ function ClassMethodDef:GenerateCode(code, class_name)
1418
+ if not self.name:match("^[%w_]+$") then return end
1419
+ code:appendf("function %s:%s(%s)\n", class_name, self.name, self.params)
1420
+ local name, params, body = GetFuncSource(self.code)
1421
+ if type(body) == "string" then
1422
+ body = string.split(body, "\n")
1423
+ end
1424
+ code:append("\t", body and table.concat(body, "\n\t") or "", "\n")
1425
+ code:append("end\n\n")
1426
+ end
1427
+
1428
+ function ClassMethodDef:ContainsCode(snippet)
1429
+ local name, params, body = GetFuncSource(self.code)
1430
+ if type(body) == "table" then
1431
+ body = table.concat(body, "\n")
1432
+ end
1433
+ return body and body:find(snippet, 1, true)
1434
+ end
1435
+
1436
+ function ClassMethodDef:OnEditorSetProperty(prop_id, old_value, ged)
1437
+ local method = self.name
1438
+ if prop_id == "name" and method:find("(", 1, true) then
1439
+ self.name = method:match("(.+)%(")
1440
+ self.params = method:sub(#self.name + 2, -2)
1441
+ end
1442
+ end
1443
+
1444
+
1445
+ ----- ClassGlobalCodeDef
1446
+
1447
+ DefineClass.ClassGlobalCodeDef = {
1448
+ __parents = { "ClassDefSubItem" },
1449
+ properties = {
1450
+ { id = "comment", name = "Comment", editor = "text", default = "", },
1451
+ { id = "code", name = "Code", editor = "func", default = false, lines = 1, max_lines = 100, params = "", },
1452
+ },
1453
+ EditorName = "Code",
1454
+ EditorSubmenu = "Code",
1455
+ }
1456
+
1457
+ function ClassGlobalCodeDef:GetEditorView()
1458
+ if self.comment == "" then
1459
+ return "code"
1460
+ end
1461
+ return string.format("code <color 0 128 0>-- %s</color>", self.comment)
1462
+ end
1463
+
1464
+ function ClassGlobalCodeDef:GenerateCode(code, class_name)
1465
+ local name, params, body = GetFuncSource(self.code)
1466
+ if not body then return end
1467
+ code:append("----- ", class_name, " ", self.comment, "\n\n")
1468
+ if type(body) == "table" then
1469
+ for _, line in ipairs(body) do
1470
+ code:append(line, "\n")
1471
+ end
1472
+ else
1473
+ code:append(body)
1474
+ end
1475
+ code:append("\n")
1476
+ end
CommonLua/Classes/ClassDefs/ClassDef-Common.generated.lua ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- ========== GENERATED BY ClassDef Editor (Ctrl-Alt-F3) DO NOT EDIT MANUALLY! ==========
2
+
3
+ --- Defines a class `StoryBitWithWeight` that inherits from `PropertyObject`.
4
+ ---
5
+ --- This class represents a story bit with a weight value that determines its probability of being selected.
6
+ ---
7
+ --- @class StoryBitWithWeight
8
+ --- @field StoryBitId string The ID of the story bit.
9
+ --- @field NoCooldown boolean Whether to skip cooldowns for subsequent story bit activations.
10
+ --- @field ForcePopup boolean Whether to directly display the popup without a notification phase.
11
+ --- @field Weight number The weight of the story bit, used to determine its probability of being selected.
12
+ --- @field StorybitSets string A comma-separated list of the story bit sets this story bit belongs to.
13
+ --- @field OneTime boolean Whether this story bit can only be activated once.
14
+ DefineClass.StoryBitWithWeight = {__parents={"PropertyObject"}, __generated_by_class="ClassDef",
15
+
16
+ properties={{id="StoryBitId", name="Id", editor="preset_id", default=false, preset_class="StoryBit"},
17
+ {id="NoCooldown", help="Don't activate any cooldowns for subsequent StoryBit activations", editor="bool",
18
+ default=false}, {id="ForcePopup", name="Force Popup",
19
+ help="Specifying true skips the notification phase, and directly displays the popup", editor="bool",
20
+ default=true}, {id="Weight", name="Weight", editor="number", default=100, min=0},
21
+ {id="StorybitSets", name="Storybit sets", editor="text", default="<StorybitSets>", dont_save=true,
22
+ read_only=true}, {id="OneTime", editor="bool", default=false, dont_save=true, read_only=true}},
23
+ EditorView=Untranslated('"Activate StoryBit <StoryBitId> (weight: <Weight>)"')}
24
+ --- Returns a comma-separated string of the story bit sets that the current story bit belongs to.
25
+ ---
26
+ --- If the story bit preset does not exist or has no sets defined, this function returns "None".
27
+ ---
28
+ --- @return string A comma-separated list of the story bit sets, or "None" if there are no sets.
29
+ function StoryBitWithWeight:GetStorybitSets()
30
+ local preset = StoryBits[self.StoryBitId]
31
+ if not preset or not next(preset.Sets) then
32
+ return "None"
33
+ end
34
+ local items = {}
35
+ for set in sorted_pairs(preset.Sets) do
36
+ items[#items + 1] = set
37
+ end
38
+ return table.concat(items, ", ")
39
+ end
40
+
41
+ function StoryBitWithWeight:GetStorybitSets()
42
+ local preset = StoryBits[self.StoryBitId]
43
+ if not preset or not next(preset.Sets) then
44
+ return "None"
45
+ end
46
+ local items = {}
47
+ for set in sorted_pairs(preset.Sets) do
48
+ items[#items + 1] = set
49
+ end
50
+ return table.concat(items, ", ")
51
+ end
52
+ --- Returns whether the current story bit can only be activated once.
53
+ ---
54
+ --- @return boolean Whether the story bit can only be activated once.
55
+ function StoryBitWithWeight:GetOneTime()
56
+ local preset = StoryBits[self.StoryBitId]
57
+ return preset and preset.OneTime
58
+ end
59
+ --- Returns an error message if the StoryBit preset for the current StoryBitWithWeight instance is invalid.
60
+ ---
61
+ --- @return string An error message if the StoryBit preset is invalid, or nil if it is valid.
62
+ function StoryBitWithWeight:GetError()
63
+ local story_bit = StoryBits[self.StoryBitId]
64
+ if not story_bit then
65
+ return "Invalid StoryBit preset"
66
+ end
67
+ end
68
+
CommonLua/Classes/ClassDefs/ClassDef-Conditions.generated.lua ADDED
@@ -0,0 +1,521 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- ========== GENERATED BY ClassDef Editor (Ctrl-Alt-F3) DO NOT EDIT MANUALLY! ==========
2
+
3
+ DefineClass.CheckAND = {
4
+ __parents = { "Condition", },
5
+ __generated_by_class = "ConditionDef",
6
+
7
+ properties = {
8
+ { id = "Negate", name = "Negate",
9
+ editor = "bool", default = false, },
10
+ { id = "Conditions",
11
+ editor = "nested_list", default = false, base_class = "Condition", },
12
+ },
13
+ EditorView = Untranslated("AND"),
14
+ EditorViewNeg = Untranslated("NOT AND"),
15
+ Documentation = "Checks if all of the nested conditions are true.",
16
+ }
17
+
18
+ function CheckAND:__eval(obj, ...)
19
+ for _, cond in ipairs(self.Conditions) do
20
+ if cond:__eval(obj, ...) then
21
+ if cond.Negate then
22
+ return false
23
+ end
24
+ else
25
+ if not cond.Negate then
26
+ return false
27
+ end
28
+ end
29
+ end
30
+ return true
31
+ end
32
+
33
+ function CheckAND:GetWarning()
34
+ if #(self.Conditions or empty_table) < 2 then
35
+ return "CheckAND should have at least 2 parameters"
36
+ end
37
+ end
38
+
39
+ DefineClass.CheckCooldown = {
40
+ __parents = { "Condition", },
41
+ __generated_by_class = "ConditionDef",
42
+
43
+ properties = {
44
+ { id = "Negate", name = "Negate Condition", help = "If true, checks for the opposite condition",
45
+ editor = "bool", default = false, },
46
+ { id = "CooldownObj", name = "Cooldown object",
47
+ editor = "combo", default = "Game", items = function (self) return {"obj", "context", "Player", "Game"} end, },
48
+ { id = "Cooldown",
49
+ editor = "preset_id", default = "Disabled", preset_class = "CooldownDef", },
50
+ },
51
+ EditorView = Untranslated("<CooldownObj> cooldown <Cooldown> is not active"),
52
+ EditorViewNeg = Untranslated("<CooldownObj> cooldown <Cooldown> is active"),
53
+ Documentation = "Checks if a given cooldown is active",
54
+ EditorNestedObjCategory = "",
55
+ }
56
+
57
+ function CheckCooldown:__eval(obj, context)
58
+ local cooldown_obj = self.CooldownObj
59
+ if cooldown_obj == "Player" then
60
+ obj = ResolveEventPlayer(obj, context)
61
+ elseif cooldown_obj == "Game" then
62
+ obj = Game
63
+ elseif cooldown_obj == "context" then
64
+ obj = context
65
+ end
66
+ assert(not obj or IsKindOf(obj, "CooldownObj"))
67
+ return not IsKindOf(obj, "CooldownObj") or not obj:GetCooldown(self.Cooldown)
68
+ end
69
+
70
+ function CheckCooldown:GetError()
71
+ if not CooldownDefs[self.Cooldown] then
72
+ return "No such cooldown"
73
+ end
74
+ end
75
+
76
+ DefineClass.CheckDifficulty = {
77
+ __parents = { "Condition", },
78
+ __generated_by_class = "ConditionDef",
79
+
80
+ properties = {
81
+ { id = "Negate", name = "Negate",
82
+ editor = "bool", default = false, },
83
+ { id = "Difficulty", name = "Difficulty",
84
+ editor = "preset_id", default = "", preset_class = "GameDifficultyDef", },
85
+ },
86
+ EditorView = Untranslated("Difficulty <Difficulty>"),
87
+ EditorViewNeg = Untranslated("Difficulty not <Difficulty>"),
88
+ Documentation = "Checks game difficulty.",
89
+ }
90
+
91
+ function CheckDifficulty:__eval(obj, context)
92
+ return GetGameDifficulty() == self.Difficulty
93
+ end
94
+
95
+ DefineClass.CheckExpression = {
96
+ __parents = { "Condition", },
97
+ __generated_by_class = "ConditionDef",
98
+
99
+ properties = {
100
+ { id = "EditorViewComment", help = "Text that explains the expression and is shown in the editor view field.",
101
+ editor = "text", default = "Check expression", },
102
+ { id = "Params",
103
+ editor = "text", default = "self, obj", },
104
+ { id = "Expression",
105
+ editor = "expression", default = function (self) return true end,
106
+ params = function(self) return self.Params end, },
107
+ },
108
+ Documentation = "Checks expression (function) result.",
109
+ }
110
+
111
+ function CheckExpression:GetEditorView()
112
+ return self.EditorViewComment and Untranslated(self.EditorViewComment) or Untranslated("Check expression")
113
+ end
114
+
115
+ function CheckExpression:__eval(...)
116
+ return self:Expression(...)
117
+ end
118
+
119
+ DefineClass.CheckGameRule = {
120
+ __parents = { "Condition", },
121
+ __generated_by_class = "ConditionDef",
122
+
123
+ properties = {
124
+ { id = "Negate", name = "Negate",
125
+ editor = "bool", default = false, },
126
+ { id = "Rule", name = "Rule",
127
+ editor = "preset_id", default = false, preset_class = "GameRuleDef", },
128
+ },
129
+ EditorView = Untranslated("Game rule <Rule> is active"),
130
+ EditorViewNeg = Untranslated("Game rule <Rule> is not active"),
131
+ Documentation = "Checks if a game rule is active.",
132
+ }
133
+
134
+ function CheckGameRule:__eval(obj, context)
135
+ return IsGameRuleActive(self.Rule)
136
+ end
137
+
138
+ DefineClass.CheckGameState = {
139
+ __parents = { "Condition", },
140
+ __generated_by_class = "ConditionDef",
141
+
142
+ properties = {
143
+ { id = "Negate", name = "Negate",
144
+ editor = "bool", default = false, },
145
+ { id = "GameState", name = "Game state",
146
+ editor = "preset_id", default = false, preset_class = "GameStateDef", },
147
+ },
148
+ EditorView = Untranslated("Game state <u(GameState)> is active"),
149
+ EditorViewNeg = Untranslated("Game state <u(GameState)> is not active"),
150
+ Documentation = "Checks if a game state is active.",
151
+ }
152
+
153
+ function CheckGameState:__eval(obj, context)
154
+ return GameState[self.GameState]
155
+ end
156
+
157
+ function CheckGameState:GetError()
158
+ if not GameStateDefs[self.GameState] then
159
+ return "No such GameState"
160
+ end
161
+ end
162
+
163
+ DefineClass.CheckMapRandom = {
164
+ __parents = { "Condition", },
165
+ __generated_by_class = "ConditionDef",
166
+
167
+ properties = {
168
+ { id = "Chance",
169
+ editor = "number", default = 10, scale = "%", min = 0, max = 100, },
170
+ { id = "Seed", help = "Seed should be different on each instance.",
171
+ editor = "number", default = false, buttons = { {name = "Rand", func = "Rand"}, }, },
172
+ },
173
+ EditorView = Untranslated("Map chance <percent(Chance)>"),
174
+ Documentation = "Checks a random chance which stays the same until the map changes.",
175
+ }
176
+
177
+ function CheckMapRandom:__eval(obj, context)
178
+ return abs(MapLoadRandom + self.Seed) % 100 < self.Chance
179
+ end
180
+
181
+ function CheckMapRandom:OnEditorNew(parent, ged, is_paste)
182
+ self.Seed = AsyncRand()
183
+ end
184
+
185
+ function CheckMapRandom:Rand()
186
+ self.Seed = AsyncRand()
187
+ ObjModified(self)
188
+ end
189
+
190
+ DefineClass.CheckOR = {
191
+ __parents = { "Condition", },
192
+ __generated_by_class = "ConditionDef",
193
+
194
+ properties = {
195
+ { id = "Negate", name = "Negate",
196
+ editor = "bool", default = false, },
197
+ { id = "Conditions",
198
+ editor = "nested_list", default = false, base_class = "Condition", },
199
+ },
200
+ EditorView = Untranslated("OR"),
201
+ EditorViewNeg = Untranslated("NOT OR"),
202
+ Documentation = "Checks if one of the nested conditions is true.",
203
+ }
204
+
205
+ function CheckOR:__eval(obj, ...)
206
+ for _, cond in ipairs(self.Conditions) do
207
+ if cond:__eval(obj, ...) then
208
+ if not cond.Negate then
209
+ return true
210
+ end
211
+ else
212
+ if cond.Negate then
213
+ return true
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ function CheckOR:GetWarning()
220
+ if #(self.Conditions or empty_table) < 2 then
221
+ return "CheckOR should have at least 2 parameters"
222
+ end
223
+ end
224
+
225
+ DefineClass.CheckPropValue = {
226
+ __parents = { "Condition", },
227
+ __generated_by_class = "ConditionDef",
228
+
229
+ properties = {
230
+ { id = "BaseClass", name = "Class",
231
+ editor = "combo", default = false, items = function (self) return ClassDescendantsList("PropertyObject") end, },
232
+ { id = "NonMatching", name = "Non-matching objects", help = "When the object does not match the provided class. \nnot IsKindOf(obj, Class)",
233
+ editor = "choice", default = "fail", items = function (self) return {"fail", "succeed"} end, },
234
+ { id = "PropId", name = "Prop",
235
+ editor = "combo", default = false, items = function (self) return self:GetNumericProperties() end, },
236
+ { id = "Condition", name = "Condition",
237
+ editor = "choice", default = "==", items = function (self) return { ">=", "<=", ">", "<", "==", "~=" } end, },
238
+ { id = "Amount", name = "Amount",
239
+ editor = "number", default = 0,
240
+ scale = function(self) return self:GetAmountMeta("scale") end, },
241
+ },
242
+ EditorView = Untranslated("<BaseClass>.<PropId> <Condition> <Amount>"),
243
+ Documentation = "Checks the value of a property.",
244
+ }
245
+
246
+ function CheckPropValue:__eval(obj, context)
247
+ if not obj or not IsKindOf(obj, self.BaseClass) then
248
+ return self.NonMatching ~= "fail"
249
+ end
250
+ local value = obj:GetProperty(self.PropId) or 0
251
+ return self:CompareOp(value, context)
252
+ end
253
+
254
+ function CheckPropValue:GetError()
255
+ local class = g_Classes[self.BaseClass]
256
+ if not class then
257
+ return "No such class"
258
+ end
259
+ local prop_meta = class:GetPropertyMetadata(self.PropId)
260
+ if not prop_meta then
261
+ return "No such property"
262
+ end
263
+ end
264
+
265
+ function CheckPropValue:GetNumericProperties()
266
+ local class = g_Classes[self.BaseClass]
267
+ local properties = class and class:GetProperties() or empty_table
268
+ local props = {}
269
+ for i = #properties, 1, -1 do
270
+ if properties[i].editor == "number" then
271
+ props[#props + 1] = properties[i].id
272
+ end
273
+ end
274
+ return props
275
+ end
276
+
277
+ function CheckPropValue:GetAmountMeta(meta, default)
278
+ local class = g_Classes[self.BaseClass]
279
+ local prop_meta = class and class:GetPropertyMetadata(self.PropId)
280
+ if prop_meta then return prop_meta[meta] end
281
+ return default
282
+ end
283
+
284
+ DefineClass.CheckRandom = {
285
+ __parents = { "Condition", },
286
+ __generated_by_class = "ConditionDef",
287
+
288
+ properties = {
289
+ { id = "Chance",
290
+ editor = "number", default = 10, scale = "%", min = 0, max = 100, },
291
+ },
292
+ EditorView = Untranslated("Chance <percent(Chance)>"),
293
+ Documentation = "Checks a random chance.",
294
+ }
295
+
296
+ function CheckRandom:__eval(obj, context)
297
+ return InteractionRand(100, "CheckRandom") < self.Chance
298
+ end
299
+
300
+ DefineClass.CheckTime = {
301
+ __parents = { "Condition", },
302
+ __generated_by_class = "ConditionDef",
303
+
304
+ properties = {
305
+ { id = "TimeScale", name = "Time Scale",
306
+ editor = "choice", default = "h", items = function (self) return GetTimeScalesCombo() end, },
307
+ { id = "TimeMin", name = "Min Time",
308
+ editor = "number", default = false, },
309
+ { id = "TimeMax", name = "Max Time",
310
+ editor = "number", default = false, },
311
+ },
312
+ EditorView = Untranslated("Time<opt(TimeMin,' after ',TimeScale)><opt(TimeMax,' before ',TimeScale)>"),
313
+ Documentation = "Checks if the game time matches an interval.",
314
+ }
315
+
316
+ function CheckTime:__eval(obj, context)
317
+ local scale = const.Scale[self.TimeScale] or 1
318
+ local min, max = self.TimeMin, self.TimeMax
319
+ local time = GameTime()
320
+ return (not min or time >= min * scale) and (not max or time <= max * scale)
321
+ end
322
+
323
+ function CheckTime:GetError()
324
+ if not self.TimeMin and not self.TimeMax then
325
+ return "No time restriction specified"
326
+ end
327
+ end
328
+
329
+ DefineClass.ScriptAND = {
330
+ __parents = { "ScriptCondition", },
331
+ __generated_by_class = "ScriptConditionDef",
332
+
333
+ HasNegate = true,
334
+ EditorView = Untranslated("AND"),
335
+ EditorViewNeg = Untranslated("NOT AND"),
336
+ EditorName = "AND",
337
+ EditorSubmenu = "Conditions",
338
+ Documentation = "Checks if all of the nested conditions are true.",
339
+ CodeTemplate = "(self[and])",
340
+ ContainerClass = "ScriptValue",
341
+ }
342
+
343
+ DefineClass.ScriptCheckCooldown = {
344
+ __parents = { "ScriptCondition", },
345
+ __generated_by_class = "ScriptConditionDef",
346
+
347
+ properties = {
348
+ { id = "CooldownObj", name = "Cooldown object",
349
+ editor = "combo", default = "Game", items = function (self) return {"parameter", "Player", "Game"} end, },
350
+ { id = "Cooldown",
351
+ editor = "preset_id", default = "Disabled", preset_class = "CooldownDef", },
352
+ },
353
+ HasNegate = true,
354
+ EditorName = "Check cooldown",
355
+ EditorSubmenu = "Conditions",
356
+ Documentation = "Checks if a given cooldown is active.",
357
+ CodeTemplate = "",
358
+ Param1Name = "Object",
359
+ }
360
+
361
+ function ScriptCheckCooldown:GetEditorView()
362
+ return string.format("%s%s cooldown %s is %sactive",
363
+ self.CooldownObj == "Game" and 'Game' or self.Param1,
364
+ self.CooldownObj == "Player" and "'s player" or "",
365
+ self.Cooldown or "false",
366
+ self.Negate and "not " or "")
367
+ end
368
+
369
+ function ScriptCheckCooldown:GenerateCode(pstr, indent)
370
+ if self.Negate then pstr:append("not ") end
371
+ if self.CooldownObj == "Game" then
372
+ pstr:appendf('Game:GetCooldown("%s")', self.Cooldown)
373
+ elseif self.CooldownObj == "Player" then
374
+ pstr:appendf('ResolveEventPlayer(%s):GetCooldown("%s")', self.Param1, self.Cooldown)
375
+ else
376
+ pstr:appendf('(IsKindOf(%s, "CooldownObj") and %s:GetCooldown("%s"))', self.Param1, self.Param1, self.Cooldown)
377
+ end
378
+ end
379
+
380
+ function ScriptCheckCooldown:GetError()
381
+ if not CooldownDefs[self.Cooldown] then
382
+ return "No such cooldown"
383
+ end
384
+ end
385
+
386
+ DefineClass.ScriptCheckGameState = {
387
+ __parents = { "ScriptCondition", },
388
+ __generated_by_class = "ScriptConditionDef",
389
+
390
+ properties = {
391
+ { id = "GameState", name = "Game state",
392
+ editor = "preset_id", default = false, preset_class = "GameStateDef", },
393
+ },
394
+ HasNegate = true,
395
+ EditorView = Untranslated("Game state <u(GameState)> is active"),
396
+ EditorViewNeg = Untranslated("Game state <u(GameState)> not active"),
397
+ EditorName = "Check game state",
398
+ EditorSubmenu = "Conditions",
399
+ Documentation = "Checks if a game state is active.",
400
+ CodeTemplate = "GameState[self.GameState]",
401
+ }
402
+
403
+ function ScriptCheckGameState:GetError()
404
+ if not GameStateDefs[self.GameState] then
405
+ return "No such GameState"
406
+ end
407
+ end
408
+
409
+ DefineClass.ScriptCheckPropValue = {
410
+ __parents = { "ScriptCondition", },
411
+ __generated_by_class = "ScriptConditionDef",
412
+
413
+ properties = {
414
+ { id = "BaseClass", name = "Class",
415
+ editor = "combo", default = false, items = function (self) return ClassDescendantsList("PropertyObject") end, },
416
+ { id = "PropId", name = "Prop",
417
+ editor = "combo", default = false, items = function (self) return self:GetNumericProperties() end, },
418
+ { id = "NonMatchingValue", name = "Value for non-matching objects", help = "Value used when the object does not match the provided class: not IsKindOf(obj, Class)",
419
+ editor = "number", default = 0,
420
+ scale = function(self) return self:GetAmountMeta("scale") end, },
421
+ { id = "Condition", name = "Condition",
422
+ editor = "choice", default = "==", items = function (self) return { ">=", "<=", ">", "<", "==", "~=" } end, },
423
+ { id = "Amount", name = "Amount",
424
+ editor = "number", default = 0,
425
+ scale = function(self) return self:GetAmountMeta("scale") end, },
426
+ },
427
+ EditorView = Untranslated("<Param1>: <BaseClass>.<PropId> <Condition> <Amount>"),
428
+ EditorName = "Check a property value",
429
+ EditorSubmenu = "Conditions",
430
+ Documentation = "Checks the value of a numeric property.",
431
+ CodeTemplate = "(IsKindOf($self.Param1, self.BaseClass) and $self.Param1:GetProperty(self.PropId) or self.NonMatchingValue) $self.Condition self.Amount",
432
+ Param1Name = "Object",
433
+ }
434
+
435
+ function ScriptCheckPropValue:GetError()
436
+ local class = g_Classes[self.BaseClass]
437
+ if not class then
438
+ return "No such class"
439
+ end
440
+ local prop_meta = class:GetPropertyMetadata(self.PropId)
441
+ if not prop_meta then
442
+ return "No such property"
443
+ end
444
+ end
445
+
446
+ function ScriptCheckPropValue:GetNumericProperties()
447
+ local class = g_Classes[self.BaseClass]
448
+ local properties = class and class:GetProperties() or empty_table
449
+ local props = {}
450
+ for i = #properties, 1, -1 do
451
+ if properties[i].editor == "number" then
452
+ props[#props + 1] = properties[i].id
453
+ end
454
+ end
455
+ return props
456
+ end
457
+
458
+ function ScriptCheckPropValue:GetAmountMeta(meta, default)
459
+ local class = g_Classes[self.BaseClass]
460
+ local prop_meta = class and class:GetPropertyMetadata(self.PropId)
461
+ if prop_meta then return prop_meta[meta] end
462
+ return default
463
+ end
464
+
465
+ DefineClass.ScriptCheckTime = {
466
+ __parents = { "ScriptCondition", },
467
+ __generated_by_class = "ScriptConditionDef",
468
+
469
+ properties = {
470
+ { id = "TimeScale", name = "Time Scale",
471
+ editor = "choice", default = "h", items = function (self) return GetTimeScalesCombo() end, },
472
+ { id = "TimeMin", name = "Min Time",
473
+ editor = "number", default = false, },
474
+ { id = "TimeMax", name = "Max Time",
475
+ editor = "number", default = false, },
476
+ },
477
+ EditorView = Untranslated("Time<opt(TimeMin,' after ',TimeScale)><opt(TimeMax,' before ',TimeScale)>"),
478
+ EditorName = "Check time",
479
+ EditorSubmenu = "Conditions",
480
+ Documentation = "Checks if the game time matches an interval.",
481
+ CodeTemplate = "",
482
+ }
483
+
484
+ function ScriptCheckTime:GenerateCode(pstr, indent)
485
+ local scale = self.TimeScale
486
+ if scale ~= "" then
487
+ scale = scale == "sec" and "000" or string.format('*const.Scale["%s"]', self.TimeScale)
488
+ end
489
+ local min, max = self.TimeMin, self.TimeMax
490
+ if min and max then
491
+ pstr:appendf("GameTime() >= %d%s and GameTime() <= %d%s", min, scale, max, scale)
492
+ elseif min then
493
+ pstr:appendf("GameTime() >= %d%s", min, scale)
494
+ elseif max then
495
+ pstr:appendf("GameTime() <= %d%s", max, scale)
496
+ end
497
+ end
498
+
499
+ function ScriptCheckTime:GetError()
500
+ if not self.TimeMin and not self.TimeMax then
501
+ return "No time restriction specified."
502
+ end
503
+ if self.TimeMin and self.TimeMax and self.TimeMin > self.TimeMax then
504
+ return "TimeMin is greater than TimeMax."
505
+ end
506
+ end
507
+
508
+ DefineClass.ScriptOR = {
509
+ __parents = { "ScriptCondition", },
510
+ __generated_by_class = "ScriptConditionDef",
511
+
512
+ HasNegate = true,
513
+ EditorView = Untranslated("OR"),
514
+ EditorViewNeg = Untranslated("NOT OR"),
515
+ EditorName = "OR",
516
+ EditorSubmenu = "Conditions",
517
+ Documentation = "Checks if at least one of the nested conditions is true.",
518
+ CodeTemplate = "(self[or])",
519
+ ContainerClass = "ScriptValue",
520
+ }
521
+
CommonLua/Classes/ClassDefs/ClassDef-Config.generated.lua ADDED
@@ -0,0 +1,1354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- ========== GENERATED BY ClassDef Editor (Ctrl-Alt-F3) DO NOT EDIT MANUALLY! ==========
2
+
3
+ DefineClass.Achievement = {
4
+ __parents = { "MsgReactionsPreset", },
5
+ __generated_by_class = "PresetDef",
6
+
7
+ properties = {
8
+ { id = "display_name", name = "Display Name",
9
+ editor = "text", default = false, translate = true, },
10
+ { id = "description", name = "Description",
11
+ editor = "text", default = false, translate = true, context = "(limited to 100 characters on XBOX)", },
12
+ { id = "how_to", name = "How To",
13
+ editor = "text", default = false, translate = true, context = "(limited to 100 characters on XBOX)", },
14
+ { id = "image", name = "Image",
15
+ editor = "ui_image", default = false, },
16
+ { id = "secret", name = "Secret",
17
+ editor = "bool", default = false, },
18
+ { id = "target", name = "Target",
19
+ editor = "number", default = 0, },
20
+ { id = "time", name = "Time",
21
+ editor = "number", default = 0, },
22
+ { id = "save_interval", name = "Save Interval",
23
+ editor = "number", default = false, },
24
+ { category = "PS4", id = "ps4_trophy_group", name = "Trophy Group",
25
+ editor = "preset_id", default = "Auto", preset_class = "TrophyGroup", extra_item = "Auto", },
26
+ { category = "PS4", id = "ps4_used_trophy_group", name = "Used Trophy Group",
27
+ editor = "preset_id", default = false, dont_save = true, read_only = true, no_edit = function(self) return self:GetTrophyGroup("ps4") == "" end, no_validate = true, preset_class = "TrophyGroup", },
28
+ { category = "PS4", id = "ps4_duplicate", name = "Duplicate",
29
+ editor = "buttons", default = false, dont_save = true, },
30
+ { category = "PS4", id = "ps4_id", name = "Trophy Id",
31
+ editor = "number", default = -1, no_edit = function(self) return self:GetTrophyGroup("ps4") == "" end, buttons = { {name = "Generate", func = "GenerateTrophyIDs"}, }, min = -1, max = 128, },
32
+ { category = "PS4", id = "ps4_grade", name = "Grade",
33
+ editor = "choice", default = "bronze", no_edit = function(self) return self:GetTrophyGroup("ps4") == "" end, items = function (self) return PlayStationTrophyGrades end, },
34
+ { category = "PS4", id = "ps4_points", name = "Points",
35
+ editor = "number", default = false, dont_save = true, read_only = true, no_edit = function(self) return self:GetTrophyGroup("ps4") == "" end, },
36
+ { category = "PS4", id = "ps4_grouppoints", name = "Group Points", help = "Total sum for the base game should be 950 - 1050. For each expansion <= 200.",
37
+ editor = "text", default = false, dont_save = true, read_only = true, no_edit = function(self) return self:GetTrophyGroup("ps4") == "" end, },
38
+ { category = "PS4", id = "ps4_icon", name = "Icon",
39
+ editor = "ui_image", default = "", dont_save = true, read_only = true, no_edit = function(self) return self:GetTrophyGroup("ps4") == "" end,
40
+ no_validate = true, filter = "All files|*.png", },
41
+ { category = "PS4", id = "ps4_platinum_linked", name = "Platinum Linked",
42
+ editor = "bool", default = true, no_edit = function(self) local trophy_group_0 = GetTrophyGroupById("ps4", 0)
43
+ return self:GetTrophyGroup("ps4") ~= trophy_group_0 or self.ps4_grade == "platinum" end, },
44
+ { category = "PS5", id = "ps5_trophy_group", name = "Trophy Group",
45
+ editor = "preset_id", default = "Auto", preset_class = "TrophyGroup", extra_item = "Auto", },
46
+ { category = "PS5", id = "ps5_used_trophy_group", name = "Used Trophy Group",
47
+ editor = "preset_id", default = false, dont_save = true, read_only = true, no_edit = function(self) return self:GetTrophyGroup("ps5") == "" end, no_validate = true, preset_class = "TrophyGroup", },
48
+ { category = "PS5", id = "ps5_id", name = "Trophy Id",
49
+ editor = "number", default = -1, no_edit = function(self) return self:GetTrophyGroup("ps5") == "" end, buttons = { {name = "Generate", func = "GenerateTrophyIDs"}, }, min = -1, max = 128, },
50
+ { category = "PS5", id = "ps5_grade", name = "Grade",
51
+ editor = "choice", default = "bronze", no_edit = function(self) return self:GetTrophyGroup("ps4") == "" end, items = function (self) return PlayStationTrophyGrades end, },
52
+ { category = "PS5", id = "ps5_points", name = "Points",
53
+ editor = "number", default = false, dont_save = true, read_only = true, no_edit = function(self) return self:GetTrophyGroup("ps4") == "" end, },
54
+ { category = "PS5", id = "ps5_grouppoints", name = "Group Points", help = "Total sum for the base game should be 950 - 1050. For each expansion <= 200.",
55
+ editor = "number", default = false, dont_save = true, read_only = true, no_edit = function(self) return self:GetTrophyGroup("ps4") == "" end, },
56
+ { category = "PS5", id = "ps5_icon", name = "Icon",
57
+ editor = "ui_image", default = "", dont_save = true, read_only = true, no_edit = function(self) return self:GetTrophyGroup("ps4") == "" end,
58
+ no_validate = true, filter = "All files|*.png", },
59
+ { category = "Xbox", id = "xbox_id", name = "Achievement Id",
60
+ editor = "number", default = -1, },
61
+ { category = "Steam", id = "steam_id", name = "Steam Id", help = "If not specified, the id of the preset will be used.",
62
+ editor = "text", default = false, },
63
+ { category = "Epic", id = "epic_id", name = "Epic Id", help = "If not specified, the id of the preset will be used.",
64
+ editor = "text", default = false, },
65
+ { category = "Epic", id = "flavor_text", name = "Flavor text",
66
+ editor = "text", default = false, translate = true, },
67
+ },
68
+ HasSortKey = true,
69
+ GlobalMap = "AchievementPresets",
70
+ EditorMenubarName = "Achievements",
71
+ EditorIcon = "CommonAssets/UI/Icons/top trophy winner.png",
72
+ EditorMenubar = "Editors.Lists",
73
+ }
74
+
75
+ function Achievement:GetCompleteText()
76
+ local unlocked = GetAchievementFlags(self.id)
77
+ return unlocked and self.description or self.how_to
78
+ end
79
+
80
+ function Achievement:GetTrophyGroup(platform)
81
+ -- Use stored group if not Auto
82
+ local group_name_field = platform .. "_trophy_group"
83
+ if self[group_name_field] ~= "Auto" then
84
+ return self[group_name_field]
85
+ end
86
+
87
+ -- Use last explicit trophy group in trophy's DLC
88
+ local trophies = PresetArray(Achievement, function(achievement)
89
+ return achievement.SaveIn == self.save_in
90
+ end)
91
+ if #trophies ~= 0 then
92
+ for i=#trophies,1,-1 do
93
+ local group = trophies[i][group_name_field]
94
+ if group ~= "" and group ~= "Auto" then
95
+ return group
96
+ end
97
+ end
98
+ end
99
+
100
+ -- Fallback to a trophy group with id which matches the DLC's id
101
+ local group = FindPreset("TrophyGroup", self.save_in)
102
+ if group then return group.id end
103
+
104
+ -- Fallback to inferring from DLC's handling
105
+ local dlc = FindPreset("DLCConfig", self.save_in)
106
+ local handling_field = platform .. "_handling"
107
+ if dlc and dlc[handling_field] ~= "Embed" then
108
+ -- We can not auto pick a group for "real" DLC trophy if the is non matching its id.
109
+ -- Do not include excluded DLCs' trophies.
110
+ return ""
111
+ end
112
+
113
+ return GetTrophyGroupById(platform, 0)
114
+ end
115
+
116
+ function Achievement:IsBaseGameTrophy(platform)
117
+ local group = self:GetTrophyGroup(platform)
118
+ return group ~= "" and TrophyGroupPresets[group]:IsBaseGameGroup(platform)
119
+ end
120
+
121
+ function Achievement:IsPlatinumLinked(platform)
122
+ local trophy_group_0 = GetTrophyGroupById(platform, 0)
123
+ local trophy_group = self:GetTrophyGroup(platform)
124
+ local platinum_linked = trophy_group == trophy_group_0 and self[platform .. "_grade"] ~= "platinum"
125
+ if platform == "ps4" then
126
+ platinum_linked = platinum_linked and self.ps4_platinum_linked
127
+ end
128
+ return platinum_linked
129
+ end
130
+
131
+ function Achievement:IsCurrentlyUsed()
132
+ return
133
+ (Platform.steam and self.steam_id) or
134
+ (Platform.epic and self.epic_id) or
135
+ (Platform.ps4 and self.ps4_id >= 0) or
136
+ (Platform.ps5 and self.ps5_id >= 0) or
137
+ (Platform.xbox and self.xbox_id > 0) or
138
+ (Platform.pc and (self.image or self.msg_reactions))
139
+ end
140
+
141
+ function Achievement:GenerateTrophyIDs(root, prop_id)
142
+ local platform = string.match(prop_id, "(.*)_id")
143
+ local trophy_id_field = prop_id
144
+ local group_id_field = platform .. "_gid"
145
+
146
+ local trophies = PresetArray(Achievement, function(achievement)
147
+ return achievement:GetTrophyGroup(platform) ~= ""
148
+ end)
149
+
150
+ local trophies_by_group = {}
151
+ for _, trophy in ipairs(trophies) do
152
+ local group = TrophyGroupPresets[trophy:GetTrophyGroup(platform)]
153
+ local group_id = group[group_id_field]
154
+ trophies_by_group[group_id] = trophies_by_group[group_id] or {}
155
+ local group_trophies = trophies_by_group[group_id]
156
+ group_trophies[#group_trophies + 1] = trophy
157
+ end
158
+
159
+ local trophy_id = 0
160
+ for _, group_trophies in sorted_pairs(trophies_by_group) do
161
+ for _, trophy in ipairs(group_trophies) do
162
+ if trophy[trophy_id_field] ~= trophy_id then
163
+ trophy[trophy_id_field] = trophy_id
164
+ trophy:MarkDirty()
165
+ end
166
+ trophy_id = trophy_id + 1
167
+ end
168
+ end
169
+ end
170
+
171
+ function Achievement:Getps4_grouppoints()
172
+ local group = self:GetTrophyGroup("ps4")
173
+ local group_points = CalcTrophyGroupPoints(group, "ps4")
174
+ local trophy_group_0 = GetTrophyGroupById("ps4", 0)
175
+ if group == trophy_group_0 then
176
+ local platinum_linked_points = CalcTrophyPlatinumLinkedPoints("ps4")
177
+ if platinum_linked_points ~= group_points then
178
+ return string.format("%d + %d", platinum_linked_points, group_points - platinum_linked_points)
179
+ end
180
+ end
181
+
182
+ return tostring(group_points)
183
+ end
184
+
185
+ function Achievement:Getps4_used_trophy_group()
186
+ return self:GetTrophyGroup("ps4")
187
+ end
188
+
189
+ function Achievement:Getps4_points()
190
+ return TrophyGradesPlayStationPoints[self.ps4_grade] or 0
191
+ end
192
+
193
+ function Achievement:Getps4_icon()
194
+ local _, icon_path = GetPlayStationTrophyIcon(self, "ps4")
195
+ return icon_path
196
+ end
197
+
198
+ function Achievement:Getps5_used_trophy_group()
199
+ return self:GetTrophyGroup("ps4")
200
+ end
201
+
202
+ function Achievement:Getps5_points()
203
+ return TrophyGradesPlayStationPoints[self.ps5_grade] or 0
204
+ end
205
+
206
+ function Achievement:Getps5_grouppoints()
207
+ return CalcTrophyGroupPoints(self:GetTrophyGroup("ps5"), "ps5")
208
+ end
209
+
210
+ function Achievement:Getps5_icon()
211
+ local _, icon_path = GetPlayStationTrophyIcon(self, "ps5")
212
+ return icon_path
213
+ end
214
+
215
+ function Achievement:GetError(platform)
216
+ local errors = {}
217
+
218
+ local ShouldTestPlatform = function(test_platform)
219
+ return (not platform or platform == test_platform)
220
+ end
221
+
222
+ local trophies = PresetArray(Achievement)
223
+ local GetPlayStationErrors = function(platform)
224
+ local trophy_id_field = platform .. "_id"
225
+ local self_trophy_id = self[trophy_id_field]
226
+ if self.id == "PlatinumTrophy" and self_trophy_id ~= 0 then
227
+ errors[#errors + 1] = string.format("%s platinum trophy's id must be 0!", string.upper(platform))
228
+ elseif self:GetTrophyGroup(platform) ~= "" and self_trophy_id < 0 then
229
+ errors[#errors + 1] = string.format("Missing %s trophy id!", platform)
230
+ elseif self_trophy_id >= 0 then
231
+ table.sortby_field(trophies, trophy_id_field)
232
+ local next_trophy_id = 0
233
+ local trophy_id_holes = {}
234
+ for _, trophy in ipairs(trophies) do
235
+ local curr_trophy_id = trophy[trophy_id_field]
236
+ if next_trophy_id < self_trophy_id and curr_trophy_id >= 0 then
237
+ if curr_trophy_id > next_trophy_id then
238
+ if curr_trophy_id - next_trophy_id > 1 then
239
+ trophy_id_holes[#trophy_id_holes + 1] = string.format("%d-%d", next_trophy_id, curr_trophy_id)
240
+ else
241
+ trophy_id_holes[#trophy_id_holes + 1] = next_trophy_id
242
+ end
243
+ end
244
+ next_trophy_id = curr_trophy_id + 1
245
+ end
246
+ if self ~= trophy and self_trophy_id == curr_trophy_id then
247
+ errors[#errors + 1] = string.format("Duplicated %s trophy id (%s)!", platform, trophy.id)
248
+ end
249
+ end
250
+
251
+ if #trophy_id_holes ~= 0 then
252
+ errors[#errors + 1] = string.format("%s trophy ids are not consecutive, missing %s!", string.upper(platform), table.concat(trophy_id_holes, ", "))
253
+ end
254
+ end
255
+ end
256
+
257
+ if ShouldTestPlatform("ps4") then GetPlayStationErrors("ps4") end
258
+ if ShouldTestPlatform("ps5") then GetPlayStationErrors("ps5") end
259
+
260
+ if ShouldTestPlatform("xbox_one") or ShouldTestPlatform("xbox_series") then
261
+ if self.description and #TDevModeGetEnglishText(self.description) > 100 then
262
+ errors[#errors + 1] = string.format("XBOX achievement description must be limited to 100 characters!")
263
+ end
264
+ if self.how_to and #TDevModeGetEnglishText(self.how_to) > 100 then
265
+ errors[#errors + 1] = string.format("XBOX achievement how to must be limited to 100 characters!")
266
+ end
267
+ end
268
+
269
+ return #errors ~= 0 and table.concat(errors, "\n")
270
+ end
271
+
272
+ function Achievement:GetWarning(platform)
273
+ local warnings = {}
274
+
275
+ local ShouldTestPlatform = function(test_platform)
276
+ return (not platform or platform == test_platform) and self:GetTrophyGroup(test_platform) ~= ""
277
+ end
278
+
279
+ local GetPlayStationWarnings = function(platform)
280
+ local is_placeholder, icon_path = GetPlayStationTrophyIcon(self, platform)
281
+ if is_placeholder then
282
+ warnings[#warnings + 1] = string.format("Missing %s trophy icon (placeholder used): %s", platform, icon_path)
283
+ end
284
+
285
+ local group = self:GetTrophyGroup(platform)
286
+ local group_points = CalcTrophyGroupPoints(group, platform)
287
+ local trophy_group_0 = GetTrophyGroupById(platform, 0)
288
+ if trophy_group_0 == group then
289
+ local platinum_linked_points = CalcTrophyPlatinumLinkedPoints(platform)
290
+ local min, max = GetTrophyBaseGameNonPlatinumLinkedPointsRange(platform)
291
+ local non_platinum_linked_points = group_points - platinum_linked_points
292
+ if non_platinum_linked_points < min or max < non_platinum_linked_points then
293
+ warnings[#warnings + 1] = string.format(
294
+ "%s non platinum linked trophy points sum in base group is not between %d and %d.",
295
+ string.upper(platform), min, max
296
+ )
297
+ end
298
+ group_points = platinum_linked_points
299
+ end
300
+
301
+ local min, max = GetTrophyGroupPointsRange(group, platform)
302
+ if group_points < min or max < group_points then
303
+ warnings[#warnings + 1] = string.format(
304
+ "%s trophy group points sum is not between %d and %d.",
305
+ string.upper(platform), min, max
306
+ )
307
+ end
308
+ end
309
+
310
+ if ShouldTestPlatform("ps4") then GetPlayStationWarnings("ps4") end
311
+ if ShouldTestPlatform("ps5") then GetPlayStationWarnings("ps5") end
312
+
313
+ if ShouldTestPlatform("ps5") then
314
+ if #self.id > 32 then
315
+ warnings[#warnings + 1] = string.format("Trophy Id maximum length is 32 (< %d)!", #self.id)
316
+ end
317
+ if string.find(self.id, "[^%w]") then
318
+ warnings[#warnings + 1] = "Trophy Id contains non-alphanumeric characters!"
319
+ end
320
+ end
321
+
322
+ return #warnings ~= 0 and table.concat(warnings, "\n")
323
+ end
324
+
325
+ function Achievement:SaveAll(...)
326
+ ForEachPreset(Achievement, function(trophy)
327
+ if trophy:GetTrophyGroup("ps4") == "" then
328
+ trophy:MarkDirty()
329
+ trophy.ps4_id = -1
330
+ end
331
+ if trophy:GetTrophyGroup("ps5") == "" then
332
+ trophy:MarkDirty()
333
+ trophy.ps5_id = -1
334
+ end
335
+ end)
336
+ Preset.SaveAll(self, ...)
337
+ end
338
+
339
+ DefineClass.DLCConfig = {
340
+ __parents = { "Preset", },
341
+ __generated_by_class = "PresetDef",
342
+
343
+ properties = {
344
+ { category = "General", id = "display_name", name = "Display Name",
345
+ editor = "text", default = false, translate = true, },
346
+ { category = "General", id = "description", name = "Description",
347
+ editor = "text", default = false, translate = true, },
348
+ { category = "General", id = "required_lua_revision",
349
+ editor = "number", default = 237259, },
350
+ { category = "General", id = "pre_load",
351
+ editor = "func", default = function (self)
352
+ if not IsDlcOwned(self) then
353
+ return "remove"
354
+ end
355
+ end, },
356
+ { category = "General", id = "load_anyway", name = "Enable Loading Saves When Missing", help = "Set to true if you want to be able to load a save with this dlc missing. If the dlc was deleted, but still present in the save's metadata - it is consider as load_anyway = true (Developer only)",
357
+ editor = "bool", default = true, },
358
+ { category = "General", id = "show_in_reports", name = "Show in Reports",
359
+ editor = "bool", default = false, },
360
+ { category = "General", id = "post_load",
361
+ editor = "func", default = function (self)
362
+ g_AvailableDlc[self.name] = true
363
+ end, },
364
+ { category = "Build Steam", id = "steam_dlc_id", name = "Steam DLC Id",
365
+ editor = "number", default = false, },
366
+ { category = "Build Pops", id = "pops_dlc_id", name = "Pops DLC Id",
367
+ editor = "text", default = false, },
368
+ { category = "Build Epic", id = "epic_dlc_id", name = "Artifact Id (Dev)", help = "Where the DLC pack will be pushed to. Can be looked up in the EpicGames dev portal.",
369
+ editor = "text", default = false, },
370
+ { category = "Build Epic", id = "epic_catalog_dlc_id", name = "Catalog Id (Live)", help = "Which catalog item (seen in the Epic Launcher) the game will check ownership for. Can be in looked up in the .mancpn files after downloading from the Epic Launcher (AppName corresponds to ArtifactId, we need CatalogItemId of the live item).",
371
+ editor = "text", default = false, },
372
+ { category = "Build", id = "generate_build_rule", name = "Generate Build Rule", help = "With name Dlc%Id%",
373
+ editor = "bool", default = false, },
374
+ { category = "Build", id = "deprecated", name = "Deprecated", help = "Used only for compatibility",
375
+ editor = "bool", default = false, },
376
+ { category = "Build", id = "dont_localize", name = "Dont localize contents", help = "Skip the DLC contents when running localization",
377
+ editor = "bool", default = false, },
378
+ { category = "Build", id = "localization", name = "Has localization packs", help = "If the Dlc should include latest localization packfiles",
379
+ editor = "bool", default = false, },
380
+ { category = "Build", id = "generate_art_folders",
381
+ editor = "buttons", default = false, buttons = { {name = "Locate PS4 Art", func = "LocatePS4Art"}, {name = "LocateXboxArt", func = "LocateXboxArt"}, }, },
382
+ { category = "Build", id = "ext_name", help = "(optional) name of executables",
383
+ editor = "text", default = false, },
384
+ { category = "Build", id = "lua", help = "If the Dlc should include Lua.hpk",
385
+ editor = "bool", default = false, },
386
+ { category = "Build", id = "data", help = "If the Dlc should include Data.hpk",
387
+ editor = "bool", default = false, },
388
+ { category = "Build", id = "nonentitytextures", help = "If the Dlc should include all post release non-entity textures, texture lists and bin assets",
389
+ editor = "bool", default = false, },
390
+ { category = "Build", id = "entitytextures", help = "If the Dlc should include all post release entity textures and texture lists",
391
+ editor = "bool", default = false, },
392
+ { category = "Build", id = "ui", help = "If the Dlc should include UI.hpk",
393
+ editor = "bool", default = false, },
394
+ { category = "Build", id = "shaders", help = "If the Dlc should include lastest shader packs.",
395
+ editor = "bool", default = false, },
396
+ { category = "Build", id = "sounds", help = "If the Dlc should include the latest Sounds.hpk",
397
+ editor = "bool", default = false, },
398
+ { category = "Build", id = "resource_metadata", help = "Generate ressource metadata",
399
+ editor = "bool", default = true, },
400
+ { category = "Build", id = "content_dep", help = "List of rules to be build before the content.hpk is packed (e.g. BinAssets)",
401
+ editor = "prop_table", default = {}, },
402
+ { category = "Build", id = "content_files", help = "Files added to the dlc content.hpk (e.g. PatchTextures.hpk, etc.)",
403
+ editor = "nested_list", default = false, base_class = "DLCConfigContentFile", inclusive = true, },
404
+ { category = "BuildPS4", id = "ps4_handling", name = "Handling", help = "Enable - Creates a full dlc package\nEmbed - Creates only a .hpk to be embedded in the main game package\nEmbedPatch - Creates a .hpk that gets embedded in the main game package and replaces an old dlc\nExclude - Not shipped on this platform",
405
+ editor = "combo", default = "Exclude", items = function (self) return { "Enable", "Embed", "EmbedPatch", "Exclude" } end, },
406
+ { category = "BuildPS4", id = "ps4_label", name = "Label",
407
+ editor = "text", default = false, no_edit = function(self) return self.ps4_handling ~= "Enable" and self.ps4_handling ~= "EmbedPatch" end, },
408
+ { category = "BuildPS4", id = "ps4_version", name = "Version",
409
+ editor = "text", default = "01.00", no_edit = function(self) return self.ps4_handling ~= "Enable" and self.ps4_handling ~= "EmbedPatch" end, },
410
+ { category = "BuildPS4", id = "ps4_entitlement_key", name = "Entitlement Key", help = "Unique 16 byte key. Will be automatically generated. Must be kept secret and not regenerated after certification.",
411
+ editor = "text", default = false, read_only = true, no_edit = function(self) return self.ps4_handling ~= "Enable" and self.ps4_handling ~= "EmbedPatch" end, },
412
+ { category = "BuildPS5", id = "ps5_handling", name = "Handling", help = "Enable - Creates a full dlc package\nEmbed - Creates only a .hpk to be embedded in the main game package\nEmbedPatch - Creates a .hpk that gets embedded in the main game package and replaces an old dlc\nExclude - Not shipped on this platform",
413
+ editor = "combo", default = "Exclude", items = function (self) return { "Enable", "Embed", "EmbedPatch", "Exclude" } end, },
414
+ { category = "BuildPS5", id = "ps5_label", name = "Label",
415
+ editor = "text", default = false, no_edit = function(self) return self.ps5_handling ~= "Enable" and self.ps5_handling ~= "EmbedPatch" end, },
416
+ { category = "BuildPS5", id = "ps5_master_version", name = "Master Version",
417
+ editor = "text", default = "01.00", no_edit = function(self) return self.ps5_handling ~= "Enable" and self.ps5_handling ~= "EmbedPatch" end, },
418
+ { category = "BuildPS5", id = "ps5_content_version", name = "Content Version",
419
+ editor = "text", default = "01.000.000", no_edit = function(self) return self.ps5_handling ~= "Enable" and self.ps5_handling ~= "EmbedPatch" end, },
420
+ { category = "BuildPS5", id = "ps5_entitlement_key", name = "Entitlement Key", help = "Unique 16 byte key. Will be automatically generated. Must be kept secret and not regenerated after certification.",
421
+ editor = "text", default = false, read_only = true, no_edit = function(self) return self.ps5_handling ~= "Enable" and self.ps5_handling ~= "EmbedPatch" end, },
422
+ { category = "BuildXbox", id = "xbox_handling", name = "Handling", help = "Enable - Creates a full dlc package\nEmbed - Creates only a .hpk to be embedded in the main game package\nEmbedPatch - Creates a .hpk that gets embedded in the main game package and replaces an old dlc\nExclude - Not shipped on this platform",
423
+ editor = "combo", default = "Exclude", items = function (self) return { "Enable", "Embed", "EmbedPatch", "Exclude" } end, },
424
+ { category = "BuildXbox", id = "xbox_name",
425
+ editor = "text", default = false, no_edit = function(self) return self.xbox_handling ~= "Enable" and self.xbox_handling ~= "EmbedPatch" end, },
426
+ { category = "BuildXbox", id = "xbox_store_id",
427
+ editor = "text", default = false, no_edit = function(self) return self.xbox_handling ~= "Enable" and self.xbox_handling ~= "EmbedPatch" end, },
428
+ { category = "BuildXbox", id = "xbox_display_name",
429
+ editor = "text", default = false, no_edit = function(self) return self.xbox_handling ~= "Enable" and self.xbox_handling ~= "EmbedPatch" end, },
430
+ { category = "BuildXbox", id = "xbox_identity",
431
+ editor = "text", default = false, no_edit = function(self) return self.xbox_handling ~= "Enable" and self.xbox_handling ~= "EmbedPatch" end, },
432
+ { category = "BuildXbox", id = "xbox_version",
433
+ editor = "text", default = "1.0.0.0", no_edit = function(self) return self.xbox_handling ~= "Enable" and self.xbox_handling ~= "EmbedPatch" end, },
434
+ { category = "BuildWindowsStore", id = "ws_identity_name",
435
+ editor = "text", default = false, },
436
+ { category = "BuildWindowsStore", id = "ws_version",
437
+ editor = "text", default = "1.0.0.0", },
438
+ { category = "BuildWindowsStore", id = "ws_store_id",
439
+ editor = "text", default = false, },
440
+ { id = "SaveIn",
441
+ editor = "text", default = false, read_only = true, no_edit = true, },
442
+ { category = "Build", id = "public", help = "information from/about this DLC can be made public",
443
+ editor = "bool", default = false, },
444
+ { category = "Build", id = "split_files", help = "These files will be split and added to the DLC.",
445
+ editor = "string_list", default = {}, item_default = "", items = false, arbitrary_value = true, },
446
+ },
447
+ HasCompanionFile = true,
448
+ SingleFile = false,
449
+ EditorMenubarName = "DLC config",
450
+ EditorIcon = "CommonAssets/UI/Icons/add buy cart plus.png",
451
+ EditorMenubar = "DLC",
452
+ save_in = "future",
453
+ }
454
+
455
+ function DLCConfig:GetEditorView()
456
+ local str = self.id
457
+ if self.generate_build_rule then
458
+ str = str .. " <color 0 128 128>build</color>"
459
+ end
460
+ if self.deprecated then
461
+ str = str .. " <color 128 128 0>deprecated</color>"
462
+ end
463
+ if self.Comment ~= "" then
464
+ str = str .. " <color 0 128 0>" .. self.Comment .. "</color>"
465
+ end
466
+ return str
467
+ end
468
+
469
+ function DLCConfig:LocatePS4Art(root)
470
+ local folder = "svnAssets/Source/ps4/" .. root.id .. "/"
471
+ local files = { "icon0.png" }
472
+ if not io.exists(folder) then
473
+ io.createpath(folder)
474
+ end
475
+ for _, file in ipairs(files) do
476
+ local path = folder .. file
477
+ if not io.exists(path) then
478
+ CopyFile("CommonAssets/Images/Achievements/PS4/ICON0.PNG", path)
479
+ end
480
+ end
481
+ OS_LocateFile(folder)
482
+ end
483
+
484
+ function DLCConfig:LocateXboxArt(root)
485
+ local folder = "svnAssets/Source/xbox/" .. root.id .. "/"
486
+ local files = { "Logo.png", "SmallLogo.png", "WideLogo.png" }
487
+ if not io.exists(folder) then
488
+ io.createpath(folder)
489
+ end
490
+ for _, file in ipairs(files) do
491
+ local path = folder .. file
492
+ if not io.exists(path) then
493
+ CopyFile("CommonAssets/Images/Achievements/PS4/ICON0.PNG", path)
494
+ end
495
+ end
496
+ OS_LocateFile(folder)
497
+ end
498
+
499
+ function DLCConfig:GetCompanionFileSavePath(save_path)
500
+ local dlc_id = string.match(save_path, "(%w+)%.lua")
501
+ assert(dlc_id)
502
+ if not dlc_id then dlc_id = "unknown" end
503
+ return "svnProject/Dlc/" .. self.id .. "/autorun.lua"
504
+ end
505
+
506
+ function DLCConfig:GenerateCompanionFileCode(code)
507
+ -- generate autorun
508
+ local autorun_template = {
509
+ name = self.id,
510
+ deprecated = self.deprecated or nil,
511
+ display_name = self.display_name,
512
+ required_lua_revision = self.required_lua_revision,
513
+ ps4_trophy_group_description = self.ps4_trophy_group_description,
514
+ ps5_trophy_group_description = self.ps5_trophy_group_description,
515
+ steam_dlc_id = self.steam_dlc_id,
516
+ pops_dlc_id = self.pops_dlc_id,
517
+ epic_dlc_id = self.epic_dlc_id,
518
+ epic_catalog_dlc_id = self.epic_catalog_dlc_id,
519
+ ps4_label = self.ps4_label,
520
+ ps5_label = self.ps5_label,
521
+ xbox_store_id = self.xbox_store_id,
522
+ ps4_gid = self.ps4_gid,
523
+ ps5_gid = self.ps5_gid,
524
+ pre_load = self.pre_load,
525
+ post_load = self.post_load,
526
+ }
527
+ code:append("return ")
528
+ code:append(TableToLuaCode(autorun_template))
529
+ end
530
+
531
+ function DLCConfig:SaveAll(...)
532
+ local class = self.PresetClass or self.class
533
+ local dlcs = PresetArray(class)
534
+
535
+ local PlayStationGenerateEntitlementKeys = function(additional_contents, platform)
536
+ local handling = platform .. "_handling"
537
+ local entitlement_key = platform .. "_entitlement_key"
538
+
539
+ local used_entitlement_keys = {}
540
+ for _, additional_content in ipairs(additional_contents) do
541
+ if additional_content[entitlement_key] then
542
+ used_entitlement_keys[additional_content[entitlement_key]] = true
543
+ end
544
+ end
545
+
546
+ for _, additional_content in ipairs(additional_contents) do
547
+ if additional_content[handling] == "Enable" and not additional_content[entitlement_key] then
548
+ repeat
549
+ additional_content[entitlement_key] = random_hex(128)
550
+ until not used_entitlement_keys[additional_content[entitlement_key]]
551
+ used_entitlement_keys[additional_content[entitlement_key]] = true
552
+ additional_content:MarkDirty()
553
+ end
554
+ end
555
+ end
556
+
557
+ PlayStationGenerateEntitlementKeys(dlcs, "ps4")
558
+ PlayStationGenerateEntitlementKeys(dlcs, "ps5")
559
+
560
+ Preset.SaveAll(self, ...)
561
+
562
+ local epic_ids = {}
563
+ ForEachPreset(class, function(preset, group)
564
+ local epic_catalog_dlc_id = preset.epic_catalog_dlc_id
565
+ if (epic_catalog_dlc_id or "") ~= "" then
566
+ epic_ids[#epic_ids + 1] = epic_catalog_dlc_id
567
+ end
568
+ end)
569
+ local text = string.format("%sg_EpicDlcIds = %s", exported_files_header_warning, TableToLuaCode(epic_ids))
570
+ local path = "svnProject/Lua/EpicDlcIds.lua"
571
+ local err = SaveSVNFile(path, text)
572
+ if err then
573
+ printf("Failed to save %s: %s", path, err);
574
+ end
575
+ end
576
+
577
+ ----- DLCConfig Class DLCConfigContentFile
578
+
579
+ DefineClass.DLCConfigContentFile = {
580
+ __parents = { "PropertyObject" },
581
+ properties = {
582
+ { id = "Source", editor = "text", default = ""},
583
+ { id = "Destination", editor = "text", default = ""},
584
+ },
585
+ }
586
+
587
+ DefineClass.GradingLUTSource = {
588
+ __parents = { "Preset", },
589
+ __generated_by_class = "PresetDef",
590
+
591
+ properties = {
592
+ { category = "Input", id = "src_path", name = "Path",
593
+ editor = "browse", default = false, folder = "svnAssets/Source/Textures/LUTs", filter = "LUT (*.cube)|*.cube", force_extension = ".cube", },
594
+ { category = "Input", id = "display_name", name = "Display name",
595
+ editor = "text", default = false, translate = true, },
596
+ { category = "Output", id = "size", name = "Size",
597
+ editor = "number", default = false, dont_save = true, read_only = true, },
598
+ { category = "Output", id = "color_space", name = "Color Space",
599
+ editor = "text", default = false, dont_save = true, read_only = true, },
600
+ { category = "Output", id = "color_gamma", name = "Color Gamma",
601
+ editor = "text", default = false, dont_save = true, read_only = true, },
602
+ { category = "Output", id = "dst_path", name = "Path",
603
+ editor = "text", default = false, dont_save = true, read_only = true, buttons = { {name = "Locate", func = "LUT_LocateFile"}, }, },
604
+ },
605
+ HasSortKey = true,
606
+ GlobalMap = "GradingLUTs",
607
+ EditorMenubarName = "Grading LUTs",
608
+ EditorMenubar = "Editors.Art",
609
+ dst_dir = "Textures/LUTs/",
610
+ }
611
+
612
+ DefineModItemPreset("GradingLUTSource", { EditorName = "Photo Mode - Grading LUT", EditorSubmenu = "Other" })
613
+
614
+ function GradingLUTSource:OnPreSave()
615
+ if self:IsDirty() or self:IsDataDirty() then
616
+ self:OnSrcChange()
617
+ end
618
+ end
619
+
620
+ function GradingLUTSource:Getsize()
621
+ return hr.ColorGradingLUTSize
622
+ end
623
+
624
+ function GradingLUTSource:GetDstDir()
625
+ if self:IsModItem() then
626
+ return self.mod.content_path .. self.dst_dir
627
+ end
628
+ return self.dst_dir
629
+ end
630
+
631
+ function GradingLUTSource:Getcolor_space()
632
+ return GetColorSpaceName(hr.ColorGradingLUTColorSpace)
633
+ end
634
+
635
+ function GradingLUTSource:Getcolor_gamma()
636
+ return GetColorGammaName(hr.ColorGradingLUTColorGamma)
637
+ end
638
+
639
+ function GradingLUTSource:OnSrcChange()
640
+ CreateRealTimeThread(function(self)
641
+ local dst_dir = self:GetDstDir()
642
+ if not io.exists(dst_dir) then
643
+ local err = AsyncCreatePath(dst_dir)
644
+ if err then
645
+ print(string.format("Could not create path %s: err", dst_dir, err))
646
+ end
647
+ if not self:IsModItem() then
648
+ SVNAddFile(dst_dir)
649
+ end
650
+ end
651
+
652
+ local dst_path = self:Getdst_path()
653
+ ImportColorGradingLUT(self:Getsize(), dst_path, self.src_path)
654
+
655
+ Sleep(3000)
656
+ if not self:IsModItem() then
657
+ SVNAddFile(dst_path)
658
+ SVNAddFile(self.src_path)
659
+ end
660
+ end, self)
661
+ end
662
+
663
+ function GradingLUTSource:Getdst_path()
664
+ return self:GetResourcePath()
665
+ end
666
+
667
+ function GradingLUTSource:GetResourcePath()
668
+ return string.format("%s%s.dds", self:GetDstDir(), self.id)
669
+ end
670
+
671
+ function GradingLUTSource:GetError()
672
+ local errors = {}
673
+ if not self.src_path then
674
+ errors[#errors + 1] = "Missing input path."
675
+ elseif not io.exists(self.src_path) then
676
+ errors[#errors + 1] = "Invalid input path."
677
+ end
678
+ return #errors ~= 0 and table.concat(errors, "\n")
679
+ end
680
+
681
+ function GradingLUTSource:GetDisplayName()
682
+ return self.display_name
683
+ end
684
+
685
+ function GradingLUTSource:GetEditorView()
686
+ if self:GetDisplayName() then
687
+ return self.id .. ' <color 128 90 30>"' .. _InternalTranslate(self:GetDisplayName()) .. '"'
688
+ else
689
+ return self.id
690
+ end
691
+ end
692
+
693
+ function GradingLUTSource:IsModItem()
694
+ return config.Mods and self:IsKindOf("ModItem")
695
+ end
696
+
697
+ function GradingLUTSource:IsDataDirty()
698
+ if not IsFSUnpacked() and not self:IsModItem() then
699
+ return false
700
+ end
701
+
702
+ local src_timestamp, src_err = io.getmetadata(self.src_path, "modification_time")
703
+ if src_err then
704
+ print(string.format("[GradingLUTs] Failed checking %s for modification: %s", self.src_path, src_err))
705
+ return false
706
+ end
707
+
708
+ local dst_timestamp, dst_err = io.getmetadata(self:Getdst_path(), "modification_time")
709
+ return dst_err or src_timestamp > dst_timestamp
710
+ end
711
+
712
+ ----- GradingLUTSource
713
+
714
+ if Platform.pc and Platform.developer then
715
+
716
+ function CleanGradingLUTsDir(luts_dir)
717
+ local err, processed_luts = AsyncListFiles(luts_dir, "*.dds", "relative")
718
+ if err then
719
+ print(string.format("[GradingLUTs] Failed listing processed LUTs: %s", err))
720
+ end
721
+ for _,lut in pairs(GradingLUTs) do
722
+ if lut:GetDstDir() == luts_dir then
723
+ table.remove_entry(processed_luts, lut.id .. ".dds")
724
+ end
725
+ end
726
+ for _,lut in ipairs(processed_luts) do
727
+ local lut_path = luts_dir .. lut
728
+ local err = AsyncFileDelete(lut_path)
729
+ if err then
730
+ print(string.format("[GradingLUTs] Failed deleting %s: %s", lut_path, err))
731
+ elseif luts_dir == GradingLUTSource.dst_dir then
732
+ SVNDeleteFile(lut_path)
733
+ end
734
+ end
735
+ end
736
+
737
+ function CleanGradingLUTsDirs()
738
+ if not IsFSUnpacked() then
739
+ CleanGradingLUTsDir(GradingLUTSource.dst_dir)
740
+ end
741
+ for _, mod in ipairs(ModsList) do
742
+ CleanGradingLUTsDir(mod.content_path .. GradingLUTSource.dst_dir)
743
+ end
744
+ end
745
+
746
+ function OnMsg.PresetSave(class)
747
+ if IsKindOf(class, "GradingLUTSource") then
748
+ CleanGradingLUTsDirs()
749
+ end
750
+ end
751
+
752
+ function OnMsg.DataLoaded()
753
+ for _,lut in pairs(GradingLUTs) do
754
+ if lut:IsDataDirty() then
755
+ lut:OnSrcChange()
756
+ end
757
+ end
758
+ CleanGradingLUTsDirs()
759
+ end
760
+
761
+ function LUT_LocateFile(preset)
762
+ OS_LocateFile(preset:Getdst_path())
763
+ end
764
+
765
+ end
766
+
767
+ DefineClass.PlayStationActivities = {
768
+ __parents = { "MsgReactionsPreset", },
769
+ __generated_by_class = "PresetDef",
770
+
771
+ properties = {
772
+ { id = "title", name = "Title", help = "The name of the challenge. This field can be localized.",
773
+ editor = "text", default = false, translate = true, },
774
+ { id = "description", name = "Description", help = "The description of the challenge. This field can be localized.",
775
+ editor = "text", default = false, translate = true, wordwrap = true, lines = 3, max_lines = 10, },
776
+ { id = "_openEndedHelp", help = "An open-ended activity has no specific completion objective. It ends when the player chooses to end it. For example, batting practice in MLB® The Show™, build mode in Dreams, or realms in God of War.\n\nOpen-ended activities can contain tasks and subtasks that can be used to track optional objectives within the activity.\n\nThe system handles open-ended activities like progress activities, but when an open-ended activity ends, any result sent is ignored.\n\nJust like progress activities, results for single-player activities can be passed using UDS events. You must use the matches API to pass results for open-ended activities that are being played in multiplayer scenarios.",
777
+ editor = "help", default = false, dont_save = true, read_only = true, no_edit = function(self) return self.category ~= "openEnded" end, },
778
+ { id = "_progressHelp", help = "A progress activity is defined as any activity that requires the player to complete an objective or series of objectives in order to complete the activity. For example, chapters in Uncharted, or quests in Horizon Zero Dawn.\n\nProgress activities can optionally contain tasks and subtasks that players can use to understand what they should do next and track how close they are to completing an activity. See Tasks and Subtasks for more information on how these can be used.\n\nProgress activities must have a result when ended. For single-player progress activities, the game can set the result to COMPLETED, FAILED, or ABANDONED and pass this result back to the platform by means of the UDS activityEnd event. If you end a progress activity as COMPLETED or FAILED, it is written into the player's history and progress is reset for the next instance.\n\nNote:\nThese outcomes are automatically tagged on any publicly available UGC that is created. In the case of successful completion, this UGC can be surfaced to other players who are at the same point in the game as a form of help or walkthrough.\n\nFor the multiplayer match case, SUCCESS or FAILED are the only supported results. You must use the matches API to pass these results. If you want to make an activity no longer active while retaining its progress, you must move the match to ONHOLD through its status property.",
779
+ editor = "help", default = false, dont_save = true, read_only = true, no_edit = function(self) return self.category ~= "progress" end, },
780
+ { id = "category", name = "Category",
781
+ editor = "choice", default = "openEnded", items = function (self) return { "openEnded", "progress" } end, },
782
+ { id = "default_playtime_estimate", name = "Default Playtime Estimate (minutes)", help = 'The default playtime estimate is displayed in System UI when the system has not determined the estimated playtime. Once the system determines the estimated playtime, the value may be switched over from the default playtime that is specified. You can specify the time in minute at the activity, task and subtask level.\n• When the category is not "challenge", allow the value at 5-minute intervals. (e.g. 5, 10, 15);\n• When the category is "challenge", allow the value at 1-minute intervals (e.g. 1, 2, 3);\n• When the type is "task" or "subTask", allow the value at 1-minute intervals (e.g. 1, 2, 3).',
783
+ editor = "number", default = false, step = 5, min = 0, },
784
+ { id = "available_by_default", name = "Available By Default", help = 'When set to true, this automatically sets the availability of an activity to available. Use this for any activity that the player can play from the very first time they launch the game. For players who have the Spoiler Warning set to warn on "Everything You Haven\'t Seen Yet", this setting instructs the Spoiler service to ignore this activity as containing any spoilers, even when it hasn\'t yet been seen by the user.',
785
+ editor = "bool", default = true, },
786
+ { id = "hidden_by_default", name = "Hidden By Default", help = "When set to true, this activity, task, or subtask is considered a spoiler throughout the UX of the platform, until it becomes available, started, or ended for the player. This means that players see a spoiler flag on any user-generated content containing this activity, task, or subtask if they have not encountered it in the game yet. Additionally, if a friend is playing a hidden activity that the player hasn't encountered yet, the card is obscured for the player when viewed on the friend's profile.",
787
+ editor = "bool", default = false, },
788
+ { id = "is_required_for_completion", name = "Required For Completion", help = "This is used to determine if the player must complete the activity to complete the main story and to pass the activities TRC if your game has a main story. Primarily, this is used to determine the sorting of activities, as activities with isRequiredforCompletion set to true that the player has never completed are more likely to be suggested to the player. In addition, this can be set on tasks. When completed, those tasks are treated as part of the progress of the activity, ultimately controlling the completion percentage progress bars. If set to false, then tasks are ignored in the completion percentage progress bar giving you more granular control of those bars. You cannot set this value on subtasks. All subtasks are considered required for completion.",
789
+ editor = "bool", default = false, no_edit = function(self) return self.category ~= "progress" end, },
790
+ { id = "abandon_on_done_map", name = "Abandon on DoneMap",
791
+ editor = "bool", default = false, },
792
+ { id = "state", name = "Is Active",
793
+ editor = "bool", default = false, dont_save = true, read_only = true, buttons = { {name = "Start", func = "Start"}, {name = "Abandon", func = "Abandon"}, {name = "Complete", func = "Complete"}, {name = "Fail", func = "Fail"}, }, },
794
+ { id = "_test_buttons",
795
+ editor = "buttons", default = false, buttons = { {name = "Test Launch Now", func = "Launch"}, {name = "Test Launch On Next Boot", func = "DbgLaunchOnBoot"}, }, },
796
+ { id = "Launch", name = "Launch",
797
+ editor = "func", default = function (self) end, },
798
+ { id = "fullscreen_image", name = "Fullscreen Image", help = "• Dimension : 3840x2160 px\n• Image Format : PNG\n• 24-bit non-Interlaced\n• Full screen image used",
799
+ editor = "ui_image", default = false, dont_save = true, read_only = true, no_validate = true, },
800
+ { id = "card_image", name = "Card Image", help = "Image used on action cards representing the game or challenge and in notifications triggered for a challenge.\n• Dimension : 864x1040 px\n• Image Format : PNG\n• 24 bit non-Interlaced",
801
+ editor = "ui_image", default = false, dont_save = true, read_only = true, no_validate = true, },
802
+ },
803
+ GlobalMap = "ActivitiesPresets",
804
+ EditorMenubarName = "PlayStation Activities",
805
+ EditorMenubar = "Editors.Other",
806
+ }
807
+
808
+ function PlayStationActivities:Getfullscreen_image()
809
+ return string.format("svnAssets/Source/Images/Activities/%s_fullscreen.png", self.id)
810
+ end
811
+
812
+ function PlayStationActivities:Getcard_image()
813
+ return string.format("svnAssets/Source/Images/Activities/%s_card.png", self.id)
814
+ end
815
+
816
+ function PlayStationActivities:Start()
817
+ if Platform.ps5 then
818
+ AsyncPlayStationActivityStart(self.id)
819
+ end
820
+ AccountStorage.PlayStationStartedActivities[self.id] = true
821
+ SaveAccountStorage(5000)
822
+ end
823
+
824
+ function PlayStationActivities:DbgLaunchOnBoot()
825
+ if not Platform.ps5 then
826
+ AccountStorage.PlayStationActivityDbgLaunchOnBoot = self.id
827
+ SaveAccountStorage(1000)
828
+ end
829
+ end
830
+
831
+ function PlayStationActivities:IsActive()
832
+ return AccountStorage.PlayStationStartedActivities[self.id]
833
+ end
834
+
835
+ function PlayStationActivities:Getstate()
836
+ return AccountStorage.PlayStationStartedActivities[self.id]
837
+ end
838
+
839
+ function PlayStationActivities:Complete()
840
+ if Platform.ps5 then
841
+ AsyncPlayStationActivityEnd(self.id, const.PlayStationActivityOutcomeCompleted)
842
+ end
843
+ AccountStorage.PlayStationStartedActivities[self.id] = nil
844
+ SaveAccountStorage(5000)
845
+ end
846
+
847
+ function PlayStationActivities:Fail()
848
+ if Platform.ps5 then
849
+ AsyncPlayStationActivityEnd(self.id, const.PlayStationActivityOutcomeFailed)
850
+ end
851
+ AccountStorage.PlayStationStartedActivities[self.id] = nil
852
+ SaveAccountStorage(5000)
853
+ end
854
+
855
+ function PlayStationActivities:Abandon()
856
+ if Platform.ps5 then
857
+ AsyncPlayStationActivityEnd(self.id, const.PlayStationActivityOutcomeAbandoned)
858
+ end
859
+ AccountStorage.PlayStationStartedActivities[self.id] = nil
860
+ SaveAccountStorage(5000)
861
+ end
862
+
863
+ function PlayStationActivities:GetWarning()
864
+ local warnings = {}
865
+
866
+ if not rawget(self, "Launch") then
867
+ warnings[#warnings + 1] = "Missing launch procedure!"
868
+ end
869
+
870
+ return #warnings ~= 0 and table.concat(warnings, "\n")
871
+ end
872
+
873
+ ----- PlayStationActivities
874
+
875
+ if Platform.developer or Platform.ps5 then
876
+
877
+ if FirstLoad then
878
+ g_DelayedLaunchActivity = false
879
+ g_PauseLaunchActivityReasons = {
880
+ ["EngineStarted"] = true,
881
+ ["AccountStorage"] = true
882
+ }
883
+ end
884
+
885
+ function PlayStationLaunchActivity(activity_id)
886
+ if g_PauseLaunchActivityReasons ~= empty_table then
887
+ g_DelayedLaunchActivity = activity_id
888
+ return
889
+ end
890
+
891
+ local activity = ActivitiesPresets[activity_id]
892
+ if activity then
893
+ assert(rawget(activity, "Launch"), "Activity not launchable!")
894
+ activity:Launch()
895
+ return
896
+ end
897
+ assert(false, string.format("Missing activity '%s'!", activity_id))
898
+ end
899
+
900
+ function PauseLaunchActivity(reason)
901
+ g_PauseLaunchActivityReasons[reason] = true
902
+ end
903
+
904
+ function ResumeLaunchActivity(reason)
905
+ g_PauseLaunchActivityReasons[reason] = nil
906
+ if g_DelayedLaunchActivity and g_PauseLaunchActivityReasons == empty_table then
907
+ PlayStationLaunchActivity(g_DelayedLaunchActivity)
908
+ g_DelayedLaunchActivity = false
909
+ end
910
+ end
911
+
912
+ function PlayStationGetActiveActivities(account_ids)
913
+ if not account_ids then
914
+ local err, account_id = PlayStationGetUserAccountId()
915
+ if err then return err end
916
+ account_ids = { account_id }
917
+ end
918
+
919
+ account_ids = table.map(account_ids, tostring)
920
+ local uri = string.format("/v1/users/activities?accountIds=%s&limit=10", table.concat(account_ids, ","))
921
+ local err, http_code, request_result_json = AsyncOpWait(PSNAsyncOpTimeout, nil, "AsyncPlayStationWebApiRequest", "activities", uri, "", "GET", "", {})
922
+ if err or http_code ~= 200 then
923
+ return err or "Failed", http_code
924
+ end
925
+
926
+ local err, request_result = JSONToLua(request_result_json)
927
+ if err then return err end
928
+
929
+ local result = {}
930
+ for _,account_id in ipairs(account_ids) do
931
+ local user = table.find_value(request_result.users, "accountId", account_id)
932
+ result[#result + 1] = user and user.activities or empty_table
933
+ end
934
+
935
+ return nil, result
936
+ end
937
+
938
+ function OnMsg.DoneMap()
939
+ for activity_id,_ in pairs(AccountStorage.PlayStationStartedActivities) do
940
+ if g_DelayedLaunchActivity == activity_id then
941
+ goto continue
942
+ end
943
+
944
+ local activity = ActivitiesPresets[activity_id]
945
+ if activity.abandon_on_done_map then
946
+ activity:Abandon()
947
+ end
948
+
949
+ ::continue::
950
+ end
951
+ end
952
+
953
+ function OnMsg.ChangeMap()
954
+ PauseLaunchActivity("ChangeMap")
955
+ end
956
+
957
+ function OnMsg.ChangeMapDone()
958
+ ResumeLaunchActivity("ChangeMap")
959
+ end
960
+
961
+ function OnMsg.EngineStarted()
962
+ ResumeLaunchActivity("EngineStarted")
963
+ CreateRealTimeThread(function()
964
+ -- The SDK does not provide any info on activities from last run.
965
+ -- Wait for AccountStorage because we store it there
966
+ while not AccountStorage do
967
+ WaitMsg("AccountStorageChanged")
968
+ end
969
+
970
+ -- First run? Create started activities table.
971
+ AccountStorage.PlayStationStartedActivities = AccountStorage.PlayStationStartedActivities or {}
972
+
973
+ -- If PSN is available use activity state from there.
974
+ if Platform.ps5 then
975
+ local err, psn_activities = PlayStationGetActiveActivities()
976
+ if not err and psn_activities[1] then
977
+ table.clear(AccountStorage.PlayStationStartedActivities)
978
+ for _,activity in ipairs(psn_activities[1]) do
979
+ AccountStorage.PlayStationStartedActivities[activity.activityId] = true
980
+ end
981
+ end
982
+ end
983
+
984
+ if not Platform.ps5 and AccountStorage.PlayStationActivityDbgLaunchOnBoot then
985
+ PlayStationLaunchActivity(AccountStorage.PlayStationActivityDbgLaunchOnBoot)
986
+ AccountStorage.PlayStationActivityDbgLaunchOnBoot = false
987
+ SaveAccountStorage(1000)
988
+ end
989
+
990
+ -- Abandon all activities that cannot persist between game runs.
991
+ for activity_id,_ in pairs(AccountStorage.PlayStationStartedActivities) do
992
+ local activity = ActivitiesPresets[activity_id]
993
+ if not activity.abandon_on_done_map and g_DelayedLaunchActivity == activity_id then
994
+ goto continue
995
+ end
996
+
997
+ if activity.abandon_on_done_map then
998
+ activity:Abandon()
999
+ end
1000
+
1001
+ ::continue::
1002
+ end
1003
+
1004
+ ResumeLaunchActivity("AccountStorage")
1005
+ end)
1006
+ end
1007
+
1008
+ end
1009
+
1010
+ DefineClass.RichPresence = {
1011
+ __parents = { "Preset", },
1012
+ __generated_by_class = "PresetDef",
1013
+
1014
+ properties = {
1015
+ { id = "name", name = "Name",
1016
+ editor = "text", default = false, translate = true, },
1017
+ { id = "desc", name = "Description",
1018
+ editor = "text", default = false, translate = true, },
1019
+ { id = "xbox_id", name = "Xbox ID",
1020
+ editor = "text", default = false, },
1021
+ },
1022
+ GlobalMap = "RichPresencePresets",
1023
+ EditorMenubarName = "Rich Presence",
1024
+ EditorMenubar = "Editors.Lists",
1025
+ }
1026
+
1027
+ DefineClass.TrophyGroup = {
1028
+ __parents = { "Preset", },
1029
+ __generated_by_class = "PresetDef",
1030
+
1031
+ properties = {
1032
+ { category = "BuildPS4", id = "ps4_gid", name = "Group ID", help = "Those must be consecutive and unique.",
1033
+ editor = "number", default = -1, buttons = { {name = "Generate", func = "GenerateGroupIDs"}, }, min = -1, max = 128, },
1034
+ { category = "BuildPS4", id = "ps4_name", name = "Name",
1035
+ editor = "text", default = false, translate = true, },
1036
+ { category = "BuildPS4", id = "ps4_description", name = "Trophy Group Description",
1037
+ editor = "text", default = false, translate = true, },
1038
+ { category = "BuildPS4", id = "ps4_icon", name = "Icon",
1039
+ editor = "ui_image", default = "", dont_save = true, read_only = true,
1040
+ no_validate = true, filter = "All files|*.png", },
1041
+ { category = "BuildPS4", id = "ps4_trophies", name = "Trophies",
1042
+ editor = "preset_id_list", default = {}, dont_save = true, read_only = true, no_validate = true, preset_class = "Achievement", item_default = "", },
1043
+ { category = "BuildPS5", id = "ps5_gid", name = "Group ID", help = "Those must be consecutive and unique.",
1044
+ editor = "number", default = -1, buttons = { {name = "Generate", func = "GenerateGroupIDs"}, }, min = -1, max = 128, },
1045
+ { category = "BuildPS5", id = "ps5_name", name = "Name",
1046
+ editor = "text", default = false, translate = true, },
1047
+ { category = "BuildPS5", id = "ps5_description", name = "Description",
1048
+ editor = "text", default = false, translate = true, },
1049
+ { category = "BuildPS5", id = "ps5_icon", name = "Icon",
1050
+ editor = "ui_image", default = "", dont_save = true, read_only = true,
1051
+ no_validate = true, filter = "All files|*.png", },
1052
+ { category = "BuildPS5", id = "ps5_trophies", name = "Trophies",
1053
+ editor = "preset_id_list", default = {}, dont_save = true, read_only = true, no_validate = true, preset_class = "Achievement", item_default = "", },
1054
+ },
1055
+ GlobalMap = "TrophyGroupPresets",
1056
+ EditorMenubarName = "Trophy Groups",
1057
+ EditorIcon = "CommonAssets/UI/Icons/top trophy winner.png",
1058
+ EditorMenubar = "Editors.Lists",
1059
+ }
1060
+
1061
+ function TrophyGroup:Getps4_icon()
1062
+ local _, icon_path = GetPlayStationTrophyGroupIcon(self.id, "ps4")
1063
+ return icon_path
1064
+ end
1065
+
1066
+ function TrophyGroup:Getps4_trophies()
1067
+ return self:GetTrophies("ps4")
1068
+ end
1069
+
1070
+ function TrophyGroup:Getps5_icon()
1071
+ local _, icon_path = GetPlayStationTrophyGroupIcon(self.id, "ps5")
1072
+ return icon_path
1073
+ end
1074
+
1075
+ function TrophyGroup:Getps5_trophies()
1076
+ return self:GetTrophies("ps5")
1077
+ end
1078
+
1079
+ function TrophyGroup:GenerateGroupIDs(root, prop_id)
1080
+ local platform = string.match(prop_id, "(.*)_gid")
1081
+ local group_id_field = prop_id
1082
+ local groups_counter = 0
1083
+ ForEachPreset(TrophyGroup, function(group)
1084
+ local is_group_used = CalcTrophyGroupPoints(group.id, platform) ~= 0
1085
+
1086
+ local group_id = -1
1087
+ if is_group_used then
1088
+ group_id = groups_counter
1089
+ groups_counter = groups_counter + 1
1090
+ end
1091
+
1092
+ if group[group_id_field] ~= group_id then
1093
+ group[group_id_field] = group_id
1094
+ group:MarkDirty()
1095
+ end
1096
+ end)
1097
+ end
1098
+
1099
+ function TrophyGroup:GetTrophies(platform)
1100
+ local trophies = PresetArray(Achievement, function(achievement)
1101
+ return achievement:GetTrophyGroup(platform) == self.id
1102
+ end)
1103
+ return table.imap(trophies, function(trophy)
1104
+ return trophy.id
1105
+ end)
1106
+ end
1107
+
1108
+ function TrophyGroup:IsBaseGameGroup(platform)
1109
+ if self[platform .. "_gid"] < 0 then return false end
1110
+ local dlc = FindPreset("DLCConfig", self.save_in)
1111
+ return not dlc or dlc[platform .. "_handling"] == "Embed"
1112
+ end
1113
+
1114
+ function TrophyGroup:GetError(platform)
1115
+ local errors = {}
1116
+
1117
+ local ShouldTestPlatform = function(test_platform)
1118
+ return (not platform or platform == test_platform)
1119
+ end
1120
+
1121
+ local groups = PresetArray(TrophyGroup)
1122
+ local GetPlayStationErrors = function(platform)
1123
+ local group_id_field = platform .. "_gid"
1124
+ local self_group_id = self[group_id_field]
1125
+ if CalcTrophyGroupPoints(self.id, platform) > 0 and self_group_id < 0 then
1126
+ errors[#errors + 1] = string.format("Missing %s trophy group id!", platform)
1127
+ elseif self_group_id >= 0 then
1128
+ table.sortby_field(groups, group_id_field)
1129
+ local next_group_id = 0
1130
+ local group_id_holes = {}
1131
+ for _, group in ipairs(groups) do
1132
+ local curr_group_id = group[group_id_field]
1133
+ if next_group_id < self_group_id and curr_group_id >= 0 then
1134
+ if curr_group_id > next_group_id then
1135
+ if curr_group_id - next_group_id > 1 then
1136
+ group_id_holes[#group_id_holes + 1] = string.format("%d-%d", next_group_id, curr_group_id)
1137
+ else
1138
+ group_id_holes[#group_id_holes + 1] = next_group_id
1139
+ end
1140
+ end
1141
+ next_group_id = curr_group_id + 1
1142
+ end
1143
+ if self ~= group and self_group_id == curr_group_id then
1144
+ errors[#errors + 1] = string.format("Duplicated %s trophy group id (%s)!", platform, group.id)
1145
+ end
1146
+ end
1147
+
1148
+ if #group_id_holes ~= 0 then
1149
+ errors[#errors + 1] = string.format("%s group ids are not consecutive, missing %s!", string.upper(platform), table.concat(group_id_holes, ", "))
1150
+ end
1151
+ end
1152
+ end
1153
+
1154
+ if ShouldTestPlatform("ps4") then GetPlayStationErrors("ps4") end
1155
+ if ShouldTestPlatform("ps5") then GetPlayStationErrors("ps5") end
1156
+
1157
+ return #errors ~= 0 and table.concat(errors, "\n")
1158
+ end
1159
+
1160
+ function TrophyGroup:GetWarning(platform)
1161
+ local warnings = {}
1162
+
1163
+ local ShouldTestPlatform = function(test_platform)
1164
+ return (not platform or platform == test_platform)
1165
+ end
1166
+
1167
+ local GetPlayStationWarnings = function(platform)
1168
+ local trophies = self:GetTrophies(platform)
1169
+ if self[platform .. "_gid"] >= 0 then
1170
+ if #trophies == 0 then
1171
+ warnings[#warnings + 1] = string.format("Has %s group id but no trophies assigned!", platform)
1172
+ end
1173
+ local is_placeholder, icon_path = GetPlayStationTrophyGroupIcon(self.id, platform)
1174
+ if is_placeholder then
1175
+ warnings[#warnings + 1] = string.format("Missing %s trophy group icon (placeholder used): %s", platform, icon_path)
1176
+ end
1177
+ end
1178
+
1179
+ local is_base_game_group = self:IsBaseGameGroup(platform)
1180
+ for _, trophy_name in ipairs(trophies) do
1181
+ local trophy = FindPreset("Achievement", trophy_name)
1182
+ local is_base_game_trophy = trophy:IsBaseGameTrophy(platform)
1183
+ if trophy.save_in ~= self.save_in and not (is_base_game_group and is_base_game_trophy) then
1184
+ warnings[#warnings + 1] = string.format(
1185
+ "%s trophy %s saved in %s while the group is saved in %s.",
1186
+ string.upper(platform), trophy_name, trophy.save_in, self.save_in)
1187
+ end
1188
+ end
1189
+ end
1190
+
1191
+ if ShouldTestPlatform("ps4") then GetPlayStationWarnings("ps4") end
1192
+ if ShouldTestPlatform("ps5") then GetPlayStationWarnings("ps5") end
1193
+
1194
+ return #warnings ~= 0 and table.concat(warnings, "\n")
1195
+ end
1196
+
1197
+ DefineClass.VideoDef = {
1198
+ __parents = { "Preset", },
1199
+ __generated_by_class = "PresetDef",
1200
+
1201
+ properties = {
1202
+ { category = "Common", id = "source",
1203
+ editor = "browse", default = false, folder = "svnAssets/Source/Movies", filter = "Video Files|*.avi|All Files|*.*", },
1204
+ { category = "Common", id = "ffmpeg_input_pattern",
1205
+ editor = "text", default = '-i "$(source)"', },
1206
+ { category = "Common", id = "sound",
1207
+ editor = "browse", default = false, folder = "svnAssets/Source/Movies", filter = "Audio Files|*.wav", },
1208
+ { category = "Desktop", id = "present_desktop",
1209
+ editor = "bool", default = true, },
1210
+ { category = "Desktop", id = "extension_desktop",
1211
+ editor = "text", default = "ivf",
1212
+ no_edit = function(self) return not self.present_desktop end, },
1213
+ { category = "Desktop", id = "ffmpeg_commandline_desktop",
1214
+ editor = "text", default = "-c:v vp8 -preset veryslow",
1215
+ no_edit = function(self) return not self.present_desktop end, },
1216
+ { category = "Desktop", id = "bitrate_desktop",
1217
+ editor = "number", default = 8000,
1218
+ no_edit = function(self) return not self.present_desktop end, },
1219
+ { category = "Desktop", id = "framerate_desktop",
1220
+ editor = "number", default = 30,
1221
+ no_edit = function(self) return not self.present_desktop end, },
1222
+ { category = "Desktop", id = "resolution_desktop",
1223
+ editor = "point", default = point(1920, 1080),
1224
+ no_edit = function(self) return not self.present_desktop end, },
1225
+ { category = "PS4", id = "present_ps4",
1226
+ editor = "bool", default = true, },
1227
+ { category = "PS4", id = "extension_ps4",
1228
+ editor = "text", default = "bsf",
1229
+ no_edit = function(self) return not self.present_ps4 end, },
1230
+ { category = "PS4", id = "ffmpeg_commandline_ps4",
1231
+ editor = "text", default = "-c:v h264 -profile:v high422 -pix_fmt yuv420p -x264opts force-cfr -bsf h264_mp4toannexb -f h264 -r 30000/1001",
1232
+ no_edit = function(self) return not self.present_ps4 end, },
1233
+ { category = "PS4", id = "bitrate_ps4",
1234
+ editor = "number", default = 6000,
1235
+ no_edit = function(self) return not self.present_ps4 end, },
1236
+ { category = "PS4", id = "framerate_ps4",
1237
+ editor = "number", default = 30,
1238
+ no_edit = function(self) return not self.present_ps4 end, },
1239
+ { category = "PS4", id = "resolution_ps4",
1240
+ editor = "point", default = point(1920, 1080),
1241
+ no_edit = function(self) return not self.present_ps4 end, },
1242
+ { category = "PS5", id = "present_ps5",
1243
+ editor = "bool", default = true, },
1244
+ { category = "PS5", id = "extension_ps5",
1245
+ editor = "text", default = "bsf",
1246
+ no_edit = function(self) return not self.present_ps5 end, },
1247
+ { category = "PS5", id = "ffmpeg_commandline_ps5",
1248
+ editor = "text", default = "-c:v h264 -profile:v high422 -pix_fmt yuv420p -x264opts force-cfr -bsf h264_mp4toannexb -f h264 -r 30000/1001",
1249
+ no_edit = function(self) return not self.present_ps5 end, },
1250
+ { category = "PS5", id = "bitrate_ps5",
1251
+ editor = "number", default = 6000,
1252
+ no_edit = function(self) return not self.present_ps5 end, },
1253
+ { category = "PS5", id = "framerate_ps5",
1254
+ editor = "number", default = 30,
1255
+ no_edit = function(self) return not self.present_ps5 end, },
1256
+ { category = "PS5", id = "resolution_ps5",
1257
+ editor = "point", default = point(1920, 1080),
1258
+ no_edit = function(self) return not self.present_ps5 end, },
1259
+ { category = "Xbox One", id = "present_xbox_one",
1260
+ editor = "bool", default = true, },
1261
+ { category = "Xbox One", id = "extension_xbox_one",
1262
+ editor = "text", default = "mp4",
1263
+ no_edit = function(self) return not self.present_xbox_one end, },
1264
+ { category = "Xbox One", id = "ffmpeg_commandline_xbox_one",
1265
+ editor = "text", default = "-c:v h264 -preset veryslow -pix_fmt yuv420p",
1266
+ no_edit = function(self) return not self.present_xbox_one end, },
1267
+ { category = "Xbox One", id = "bitrate_xbox_one",
1268
+ editor = "number", default = 8000,
1269
+ no_edit = function(self) return not self.present_xbox_one end, },
1270
+ { category = "Xbox One", id = "framerate_xbox_one",
1271
+ editor = "number", default = 30,
1272
+ no_edit = function(self) return not self.present_xbox_one end, },
1273
+ { category = "Xbox One", id = "resolution_xbox_one",
1274
+ editor = "point", default = point(1920, 1080),
1275
+ no_edit = function(self) return not self.present_xbox_one end, },
1276
+ { category = "Xbox Series", id = "present_xbox_series",
1277
+ editor = "bool", default = true, },
1278
+ { category = "Xbox Series", id = "extension_xbox_series",
1279
+ editor = "text", default = "mp4",
1280
+ no_edit = function(self) return not self.present_xbox_series end, },
1281
+ { category = "Xbox Series", id = "ffmpeg_commandline_xbox_series",
1282
+ editor = "text", default = "-c:v libx265 -tag:v hvc1 -preset veryslow -pix_fmt yuv420p",
1283
+ no_edit = function(self) return not self.present_xbox_series end, },
1284
+ { category = "Xbox Series", id = "bitrate_xbox_series",
1285
+ editor = "number", default = 8000,
1286
+ no_edit = function(self) return not self.present_xbox_series end, },
1287
+ { category = "Xbox Series", id = "framerate_xbox_series",
1288
+ editor = "number", default = 30,
1289
+ no_edit = function(self) return not self.present_xbox_series end, },
1290
+ { category = "Xbox Series", id = "resolution_xbox_series",
1291
+ editor = "point", default = point(1920, 1080),
1292
+ no_edit = function(self) return not self.present_xbox_series end, },
1293
+ { category = "Switch", id = "present_switch",
1294
+ editor = "bool", default = true, },
1295
+ { category = "Switch", id = "extension_switch",
1296
+ editor = "text", default = "mp4",
1297
+ no_edit = function(self) return not self.present_switch end, },
1298
+ { category = "Switch", id = "ffmpeg_commandline_switch",
1299
+ editor = "text", default = "-c:v h264 -preset veryslow -pix_fmt yuv420p",
1300
+ no_edit = function(self) return not self.present_switch end, },
1301
+ { category = "Switch", id = "bitrate_switch",
1302
+ editor = "number", default = 700,
1303
+ no_edit = function(self) return not self.present_switch end, },
1304
+ { category = "Switch", id = "framerate_switch",
1305
+ editor = "number", default = 30,
1306
+ no_edit = function(self) return not self.present_switch end, },
1307
+ { category = "Switch", id = "resolution_switch",
1308
+ editor = "point", default = point(1280, 720),
1309
+ no_edit = function(self) return not self.present_switch end, },
1310
+ },
1311
+ HasCompanionFile = true,
1312
+ GlobalMap = "VideoDefs",
1313
+ EditorMenubarName = "Video defs",
1314
+ EditorIcon = "CommonAssets/UI/Icons/outline video.png",
1315
+ EditorMenubar = "Editors.Engine",
1316
+ }
1317
+
1318
+ function VideoDef:GetPropsForPlatform(platform)
1319
+ assert(table.find({ "desktop", "ps4", "ps5", "xbox_one", "xbox_series", "switch" }, platform))
1320
+ local result = {}
1321
+ local props = { "extension", "ffmpeg_commandline", "bitrate", "framerate", "resolution", "present" }
1322
+ for key, value in ipairs(props) do
1323
+ result[value] = self[value .. "_" .. platform]
1324
+ end
1325
+ local video_path = string.match(self.source or "", "svnAssets/Source/(.+)")
1326
+ if video_path then
1327
+ local dir, name, ext = SplitPath(video_path)
1328
+ result.video_game_path = dir .. name .. "." .. result.extension
1329
+ end
1330
+
1331
+ local sound_path = string.match(self.sound or "", "svnAssets/Source/(.+)")
1332
+ if sound_path then
1333
+ local dir, name, ext = SplitPath(sound_path)
1334
+ result.sound_game_path = dir .. name
1335
+ end
1336
+
1337
+ return result
1338
+ end
1339
+
1340
+ DefineClass.VoiceActorDef = {
1341
+ __parents = { "Preset", },
1342
+ __generated_by_class = "PresetDef",
1343
+
1344
+ properties = {
1345
+ { id = "VoiceId", name = "VoiceID",
1346
+ editor = "text", default = false, },
1347
+ },
1348
+ GlobalMap = "VoiceActors",
1349
+ EditorMenubarName = "Voice Actors",
1350
+ EditorIcon = "CommonAssets/UI/Icons/human male man people person.png",
1351
+ EditorMenubar = "Editors.Audio",
1352
+ EditorView = Untranslated("<u(Id)> <color 0 128 0><u(VoiceId)>"),
1353
+ }
1354
+
CommonLua/Classes/ClassDefs/ClassDef-Default.generated.lua ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- ========== GENERATED BY ClassDef Editor (Ctrl-Alt-F3) DO NOT EDIT MANUALLY! ==========
2
+
3
+ DefineClass.AnimComponentWeight = {
4
+ __parents = { "PropertyObject", },
5
+ __generated_by_class = "ClassDef",
6
+
7
+ properties = {
8
+ { id = "AnimComponent",
9
+ editor = "preset_id", default = false, preset_class = "AnimComponent", },
10
+ { id = "BlendAfterChannel", help = "If false, the component will execute on the channel animation, if true, it will execute after the channel has beel blended with all before it.",
11
+ editor = "bool", default = false, },
12
+ },
13
+ }
14
+
15
+ DefineClass.AnimLimbData = {
16
+ __parents = { "PropertyObject", },
17
+ __generated_by_class = "ClassDef",
18
+
19
+ properties = {
20
+ { id = "fit_bone", help = "Bone name to be fit to target",
21
+ editor = "text", default = false, },
22
+ { id = "joint_bone",
23
+ editor = "text", default = false, },
24
+ { id = "joint_companion_bone",
25
+ editor = "text", default = false, },
26
+ { id = "top_bone",
27
+ editor = "text", default = false, },
28
+ { id = "top_companion_bone",
29
+ editor = "text", default = false, },
30
+ { id = "fit_normal", help = "Local bone space normal direction to be fit to target",
31
+ editor = "point", default = point(0, 1000, 0), },
32
+ { id = "fit_offset", help = "Local bone space position offset to be fit to target",
33
+ editor = "point", default = point(0, 0, 0), },
34
+ { id = "joint_axis", help = "Local bone space joint axis direction",
35
+ editor = "point", default = point(0, 0, 1000), },
36
+ },
37
+ }
38
+
39
+ DefineClass.CommonGameSettings = {
40
+ __parents = { "InitDone", },
41
+ __generated_by_class = "ClassDef",
42
+
43
+ properties = {
44
+ { category = "Modifiers", id = "game_difficulty", name = T(607013881337, --[[ClassDef Default CommonGameSettings name]] "Game difficulty"),
45
+ editor = "preset_id", default = false, preset_class = "GameDifficultyDef", },
46
+ { category = "Modifiers", id = "seed_text", name = T(968127954818, --[[ClassDef Default CommonGameSettings name]] "Seed"), help = T(657915341595, --[[ClassDef Default CommonGameSettings help]] "Text used to generate seed. If empty a random (async) seed will be used."),
47
+ editor = "text", default = "", },
48
+ { category = "Modifiers", id = "game_rules", name = T(567633276594, --[[ClassDef Default CommonGameSettings name]] "Game Rules"),
49
+ editor = "prop_table", default = false, },
50
+ { category = "Modifiers", id = "forced_game_rules", name = T(957452987296, --[[ClassDef Default CommonGameSettings name]] "Forced game rules"), help = T(745042998514, --[[ClassDef Default CommonGameSettings help]] "If a rule is set to true, it is force enabled, false means force disabled."),
51
+ editor = "prop_table", default = false, dont_save = true, no_edit = true, },
52
+ },
53
+ StoreAsTable = true,
54
+ id = false,
55
+ save_id = false,
56
+ loaded_from_id = false,
57
+ }
58
+
59
+ function CommonGameSettings:Init()
60
+ self.id = self.id or random_encode64(96)
61
+
62
+ local difficulty = table.get(Presets, "GameDifficultyDef", 1) or empty_table
63
+ difficulty = difficulty[(#difficulty + 1) / 2]
64
+ self.game_difficulty = self.game_difficulty or difficulty and difficulty.id or nil
65
+
66
+ self.game_rules = self.game_rules or {}
67
+ self.forced_game_rules = self.forced_game_rules or {}
68
+ ForEachPreset("GameRule", function(rule)
69
+ if rule.init_as_active then
70
+ self:AddGameRule(rule.id)
71
+ end
72
+ end)
73
+ end
74
+
75
+ function CommonGameSettings:ToggleListValue(prop_id, item_id)
76
+ if prop_id == "game_rules" then
77
+ self:ToggleGameRule(item_id)
78
+ return
79
+ end
80
+ local value = self[prop_id]
81
+ if value[item_id] then
82
+ value[item_id] = nil
83
+ else
84
+ value[item_id] = true
85
+ end
86
+ end
87
+
88
+ function CommonGameSettings:ToggleGameRule(rule_id)
89
+ local value = self.game_rules[rule_id]
90
+ if value then
91
+ self:RemoveGameRule(rule_id)
92
+ else
93
+ self:AddGameRule(rule_id)
94
+ end
95
+ end
96
+
97
+ function CommonGameSettings:AddGameRule(rule_id)
98
+ if self:CanAddGameRule(rule_id) then
99
+ self.game_rules[rule_id] = true
100
+ end
101
+ end
102
+
103
+ function CommonGameSettings:RemoveGameRule(rule_id)
104
+ if self:CanRemoveGameRule(rule_id) then
105
+ self.game_rules[rule_id] = nil
106
+ end
107
+ end
108
+
109
+ function CommonGameSettings:SetForceEnabledGameRule(rule_id, set)
110
+ if set then
111
+ self:AddGameRule(rule_id)
112
+ else
113
+ self:RemoveGameRule(rule_id)
114
+ end
115
+ self.forced_game_rules[rule_id] = set and true or nil
116
+ end
117
+
118
+ function CommonGameSettings:SetForceDisabledGameRule(rule_id, set)
119
+ if set then
120
+ self:RemoveGameRule(rule_id)
121
+ end
122
+ if set then
123
+ self.forced_game_rules[rule_id] = false
124
+ else
125
+ self.forced_game_rules[rule_id] = nil
126
+ end
127
+ end
128
+
129
+ function CommonGameSettings:CanAddGameRule(rule_id)
130
+ if self.forced_game_rules[rule_id] == false then
131
+ return
132
+ end
133
+ local rule = GameRuleDefs[rule_id]
134
+ return not self:IsGameRuleActive(rule_id) and rule and rule:IsCompatible(self.game_rules)
135
+ end
136
+
137
+ function CommonGameSettings:CanRemoveGameRule(rule_id)
138
+ return self.forced_game_rules[rule_id] == nil
139
+ end
140
+
141
+ function CommonGameSettings:IsGameRuleActive(rule_id)
142
+ return self.game_rules[rule_id]
143
+ end
144
+
145
+ function CommonGameSettings:CopyCategoryTo(other, category)
146
+ for _, prop in ipairs(self:GetProperties()) do
147
+ if prop.category == category then
148
+ local value = self:GetProperty(prop.id)
149
+ value = type(value) == "table" and table.copy(value) or value
150
+ other:SetProperty(prop.id, value)
151
+ end
152
+ end
153
+ end
154
+
155
+ function CommonGameSettings:Clone()
156
+ local obj = CooldownObj.Clone(self)
157
+ obj.id = self.id or nil
158
+ obj.save_id = self.save_id or nil
159
+ obj.loaded_from_id = self.loaded_from_id or nil
160
+ return obj
161
+ end
162
+
163
+ DefineClass.Explanation = {
164
+ __parents = { "PropertyObject", },
165
+ __generated_by_class = "ClassDef",
166
+
167
+ properties = {
168
+ { id = "Text",
169
+ editor = "text", default = false, translate = true, },
170
+ { id = "Id",
171
+ editor = "text", default = false, },
172
+ { id = "ObjIsKindOf", name = "Object is of type", help = "The explanation is provided only if the parameter object inherits the specified class.",
173
+ editor = "combo", default = "", items = function (self) return ClassDescendantsList("PropertyObject") end, },
174
+ { id = "Conditions",
175
+ editor = "nested_list", default = false, base_class = "Condition", },
176
+ },
177
+ EditorView = Untranslated("Explanation: <Text>"),
178
+ }
179
+
180
+ ----- Explanation
181
+
182
+ function GetFirstExplanation(list, obj, ...)
183
+ local IsKindOf = IsKindOf
184
+ local EvalConditionList = EvalConditionList
185
+ for _, explanation in ipairs(list) do
186
+ local kind_of = explanation.ObjIsKindOf or ""
187
+ if (kind_of == "" or IsKindOf(obj, kind_of)) and EvalConditionList(explanation.Conditions, obj, ...) then
188
+ return explanation.Text, explanation.Id
189
+ end
190
+ end
191
+ return ""
192
+ end
193
+
CommonLua/Classes/ClassDefs/ClassDef-Effects.generated.lua ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- ========== GENERATED BY ClassDef Editor (Ctrl-Alt-F3) DO NOT EDIT MANUALLY! ==========
2
+
3
+ DefineClass.ChangeGameStateEffect = {
4
+ __parents = { "Effect", },
5
+ __generated_by_class = "EffectDef",
6
+
7
+ properties = {
8
+ { id = "GameState", name = "Game state",
9
+ editor = "preset_id", default = false, preset_class = "GameStateDef", },
10
+ { id = "Value", name = "Value",
11
+ editor = "bool", default = false, },
12
+ },
13
+ EditorView = Untranslated("<select(Value,'Clear','Set')> game state <GameState>"),
14
+ Documentation = "Changes a game state",
15
+ }
16
+
17
+ function ChangeGameStateEffect:__exec(obj, context)
18
+ ChangeGameState(self.GameState, self.Value)
19
+ end
20
+
21
+ function ChangeGameStateEffect:GetError()
22
+ if not GameStateDefs[self.GameState] then
23
+ return "No such GameState"
24
+ end
25
+ end
26
+
27
+ DefineClass.ChangeLightmodel = {
28
+ __parents = { "Effect", },
29
+ __generated_by_class = "EffectDef",
30
+
31
+ properties = {
32
+ { id = "Lightmodel", name = "Light model", help = "Specify a light model, or leave as 'false' to restore the previous one.",
33
+ editor = "preset_id", default = false, preset_class = "LightmodelPreset", },
34
+ },
35
+ EditorView = Untranslated("<if(Lightmodel)>Change light model to <Lightmodel>.</if><if(not(Lightmodel))>Restore last light model.</if>"),
36
+ Documentation = "Changes the current light model, or restores the last one if Light model is 'false'.",
37
+ }
38
+
39
+ function ChangeLightmodel:__exec(obj, context)
40
+ SetLightmodelOverride(false, self.Lightmodel)
41
+ end
42
+
43
+ DefineClass.EffectsWithCondition = {
44
+ __parents = { "Effect", },
45
+ __generated_by_class = "EffectDef",
46
+
47
+ properties = {
48
+ { id = "Conditions",
49
+ editor = "nested_list", default = false, base_class = "Condition", },
50
+ { id = "Effects",
51
+ editor = "nested_list", default = false, base_class = "Effect", all_descendants = true, },
52
+ { id = "EffectsElse", name = "Else",
53
+ editor = "nested_list", default = false, base_class = "Effect", all_descendants = true, },
54
+ },
55
+ EditorView = Untranslated("Effects with condition"),
56
+ Documentation = "Executes different effects when a list of conditions is true or not.",
57
+ }
58
+
59
+ function EffectsWithCondition:__exec(obj, ...)
60
+ if _EvalConditionList(self.Conditions, obj, ...) then
61
+ for _, effect in ipairs(self.Effects) do
62
+ effect:__exec(obj, ...)
63
+ end
64
+ return true
65
+ else
66
+ for _, effect in ipairs(self.EffectsElse) do
67
+ effect:__exec(obj, ...)
68
+ end
69
+ end
70
+ end
71
+
72
+ DefineClass.ExecuteCode = {
73
+ __parents = { "Effect", },
74
+ __generated_by_class = "EffectDef",
75
+
76
+ properties = {
77
+ { id = "Params",
78
+ editor = "text", default = "self, obj", },
79
+ { id = "SaveAsText",
80
+ editor = "bool", default = false, },
81
+ { id = "Code",
82
+ editor = "func", default = function (self) end,
83
+ params = function(self) return self.Params end, no_edit = function(self) return self.SaveAsText end, },
84
+ { id = "FuncCode",
85
+ editor = "text", default = false,
86
+ params = function(self) return self.Params end, no_edit = function(self) return not self.SaveAsText end, lines = 1, },
87
+ },
88
+ EditorView = Untranslated("Execute Code"),
89
+ Documentation = "Execute arbitrary code.",
90
+ }
91
+
92
+ function ExecuteCode:__exec(...)
93
+ if self.SaveAsText and self.FuncCode then
94
+ local prop_meta = self:GetPropertyMetadata("FuncCode")
95
+ local params = prop_meta.params(self, prop_meta) or "self"
96
+ local func, err = CompileFunc("FuncCode", params, self.FuncCode)
97
+ if not func then
98
+ assert(false, err)
99
+ return false
100
+ end
101
+ return func(self, ...)
102
+ end
103
+ return self.Code(self, ...)
104
+ end
105
+
106
+ function ExecuteCode:__toluacode(...)
107
+ if not self.SaveAsText then
108
+ assert(not g_PresetForbidSerialize, "Attempt to save ExecuteCode not from Ged!")
109
+ end
110
+
111
+ return Effect.__toluacode(self, ...)
112
+ end
113
+
114
+ function ExecuteCode:GetError()
115
+ local code
116
+ if self.SaveAsText then
117
+ if self.FuncCode then
118
+ code = string.split(self.FuncCode, "\n")
119
+ end
120
+ else
121
+ if self.Code then
122
+ local _,_, body = GetFuncSource(self.Code)
123
+ code = body
124
+ end
125
+ end
126
+ if not code then return end
127
+
128
+ code = (type(code) == "string") and {code} or code
129
+ for _, line in ipairs(code) do
130
+ if string.match(line, "T{") then
131
+ return "FuncCode can't use T{}"
132
+ end
133
+ if string.match(line, "Translated%(") then
134
+ return "FuncCode can't use Translated()"
135
+ end
136
+ if string.match(line, "Untranslated%(") then
137
+ return "FuncCode can't use Untranslated()"
138
+ end
139
+ end
140
+ end
141
+
142
+ DefineClass.ModifyCooldownEffect = {
143
+ __parents = { "Effect", },
144
+ __generated_by_class = "EffectDef",
145
+
146
+ properties = {
147
+ { id = "CooldownObj", name = "Cooldown object",
148
+ editor = "combo", default = "Game", items = function (self) return {"obj", "context", "Player", "Game"} end, },
149
+ { id = "Cooldown",
150
+ editor = "preset_id", default = "Disabled", preset_class = "CooldownDef", },
151
+ { id = "TimeScale", name = "Time Scale",
152
+ editor = "choice", default = "h", items = function (self) return GetTimeScalesCombo() end, },
153
+ { id = "Time", help = "If time is not provided the default time from the cooldown definition is used.",
154
+ editor = "number", default = 0,
155
+ scale = function(self) return self.TimeScale end, },
156
+ { id = "RandomTime",
157
+ editor = "number", default = 0,
158
+ scale = function(self) return self.TimeScale end, },
159
+ },
160
+ EditorView = Untranslated("Add to <CooldownObj> cooldown <Cooldown>"),
161
+ Documentation = "Adds time to an existing cooldown",
162
+ EditorNestedObjCategory = "",
163
+ }
164
+
165
+ function ModifyCooldownEffect:__exec(obj, context)
166
+ local cooldown_obj = self.CooldownObj
167
+ if cooldown_obj == "Player" then
168
+ obj = ResolveEventPlayer(obj, context)
169
+ elseif cooldown_obj == "Game" then
170
+ obj = Game
171
+ elseif cooldown_obj == "context" then
172
+ obj = context
173
+ end
174
+ assert(not obj or IsKindOf(obj, "CooldownObj"))
175
+ if IsKindOf(obj, "CooldownObj") then
176
+ local rand = self.RandomTime
177
+ local time = self.Time + (rand > 0 and InteractionRand(rand, self.Cooldown, obj) or 0)
178
+ obj:ModifyCooldown(self.Cooldown, time)
179
+ end
180
+ end
181
+
182
+ function ModifyCooldownEffect:GetError()
183
+ if not CooldownDefs[self.Cooldown] then
184
+ return "No such cooldown"
185
+ end
186
+ end
187
+
188
+ DefineClass.PlayActionFX = {
189
+ __parents = { "Effect", },
190
+ __generated_by_class = "EffectDef",
191
+
192
+ properties = {
193
+ { id = "ActionFX", name = "ActionFX",
194
+ editor = "combo", default = "", items = function (self) return PresetsPropCombo("FXPreset", "Action", "") end, },
195
+ { id = "ActionMoment", name = "ActionMoment",
196
+ editor = "combo", default = "start", items = function (self) return PresetsPropCombo("FXPreset", "Moment") end, },
197
+ },
198
+ EditorView = Untranslated("PlayFX <FX>"),
199
+ Documentation = "PlayFX",
200
+ }
201
+
202
+ function PlayActionFX:__exec(obj, context)
203
+ if self.ActionFX ~= "" then
204
+ PlayFX(self.ActionFX, self.ActionMoment, obj, context)
205
+ end
206
+ end
207
+
208
+ DefineClass.RemoveGameNotificationEffect = {
209
+ __parents = { "Effect", },
210
+ __generated_by_class = "EffectDef",
211
+
212
+ properties = {
213
+ { id = "NotificationId",
214
+ editor = "text", default = false, },
215
+ },
216
+ EditorView = Untranslated("Remove notification <NotificationId>"),
217
+ Documentation = "Removes the specified notification if it is present",
218
+ }
219
+
220
+ function RemoveGameNotificationEffect:__exec(obj, context)
221
+ RemoveGameNotification(self.NotificationId)
222
+ end
223
+
224
+ function RemoveGameNotificationEffect:GetError()
225
+ if not self.NotificationId then
226
+ return "No notification id set"
227
+ end
228
+ end
229
+
230
+ DefineClass.ScriptStoryBitActivate = {
231
+ __parents = { "ScriptSimpleStatement", },
232
+ __generated_by_class = "ScriptEffectDef",
233
+
234
+ properties = {
235
+ { id = "StoryBitId", name = "Id",
236
+ editor = "preset_id", default = false, preset_class = "StoryBit", },
237
+ { id = "NoCooldown", help = "Don't activate any cooldowns for subsequent StoryBit activations",
238
+ editor = "bool", default = false, },
239
+ { id = "ForcePopup", name = "Force Popup", help = "Specifying true skips the notification phase, and directly displays the popup",
240
+ editor = "bool", default = true, },
241
+ },
242
+ EditorView = Untranslated("Activate story bit <StoryBitId>"),
243
+ EditorName = "Activate story bit",
244
+ EditorSubmenu = "Effects",
245
+ Documentation = "",
246
+ CodeTemplate = 'ForceActivateStoryBit(self.StoryBitId, $self.Param1, self.ForcePopup and "immediate", $self.Param2, self.NoCooldown)',
247
+ Param1Name = "obj",
248
+ Param2Name = "context",
249
+ }
250
+
251
+ function ScriptStoryBitActivate:GetError()
252
+ local story_bit = StoryBits[self.StoryBitId]
253
+ if not story_bit then
254
+ return "Invalid StoryBit preset"
255
+ end
256
+ end
257
+
258
+ DefineClass.SelectObjectEffect = {
259
+ __parents = { "Effect", },
260
+ __generated_by_class = "EffectDef",
261
+
262
+ properties = {
263
+ { id = "SelectionIsEmpty", name = "Only if selection is empty",
264
+ editor = "bool", default = false, },
265
+ { id = "ObjNonEmpty", name = "Only if obj is not empty",
266
+ editor = "bool", default = false, },
267
+ },
268
+ EditorView = Untranslated("Select object"),
269
+ Documentation = "Select the object",
270
+ }
271
+
272
+ function SelectObjectEffect:__exec(obj, context)
273
+ if self.SelectionIsEmpty and SelectedObj then return end
274
+ if self.ObjNonEmpty and not obj then return end
275
+ SelectObj(obj)
276
+ end
277
+
278
+ DefineClass.SetCooldownEffect = {
279
+ __parents = { "Effect", },
280
+ __generated_by_class = "EffectDef",
281
+
282
+ properties = {
283
+ { id = "CooldownObj", name = "Cooldown object",
284
+ editor = "combo", default = "Game", items = function (self) return {"obj", "context", "Player", "Game"} end, },
285
+ { id = "Cooldown",
286
+ editor = "preset_id", default = "Disabled", preset_class = "CooldownDef", },
287
+ { id = "TimeScale", name = "Time Scale",
288
+ editor = "choice", default = "h", items = function (self) return GetTimeScalesCombo() end, },
289
+ { id = "TimeMin", name = "Time min", help = "If time is not provided the default time from the cooldown definition is used.",
290
+ editor = "number", default = false,
291
+ scale = function(self) return self.TimeScale end, },
292
+ { id = "TimeMax", name = "Time max", help = "If time is not provided the default time from the cooldown definition is used.",
293
+ editor = "number", default = false,
294
+ scale = function(self) return self.TimeScale end, no_edit = function(self) return not self.TimeMin end, },
295
+ },
296
+ EditorView = Untranslated("Set <CooldownObj> cooldown <Cooldown>"),
297
+ Documentation = "Sets a cooldown",
298
+ EditorNestedObjCategory = "",
299
+ }
300
+
301
+ function SetCooldownEffect:__exec(obj, context)
302
+ local cooldown_obj = self.CooldownObj
303
+ if cooldown_obj == "Player" then
304
+ obj = ResolveEventPlayer(obj, context)
305
+ elseif cooldown_obj == "Game" then
306
+ obj = Game
307
+ elseif cooldown_obj == "context" then
308
+ obj = context
309
+ end
310
+ assert(not obj or IsKindOf(obj, "CooldownObj"))
311
+ if IsKindOf(obj, "CooldownObj") then
312
+ local min, max = self.TimeMin, self.TimeMax
313
+ local time
314
+ if min then
315
+ time = min
316
+ if max then
317
+ time = InteractionRandRange(min, max, self.Cooldown, obj)
318
+ end
319
+ end
320
+ obj:SetCooldown(self.Cooldown, time)
321
+ end
322
+ end
323
+
324
+ function SetCooldownEffect:GetError()
325
+ if not CooldownDefs[self.Cooldown] then
326
+ return "No such cooldown"
327
+ end
328
+ end
329
+
330
+ DefineClass.StoryBitActivate = {
331
+ __parents = { "Effect", },
332
+ __generated_by_class = "EffectDef",
333
+
334
+ properties = {
335
+ { id = "Id", name = "Id",
336
+ editor = "preset_id", default = false, preset_class = "StoryBit", },
337
+ { id = "NoCooldown", help = "Don't activate any cooldowns for subsequent StoryBit activations",
338
+ editor = "bool", default = false, },
339
+ { id = "ForcePopup", name = "Force Popup", help = "Specifying true skips the notification phase, and directly displays the popup",
340
+ editor = "bool", default = true, },
341
+ { id = "StorybitSets", name = "Storybit sets",
342
+ editor = "text", default = "<StorybitSets>", dont_save = true, read_only = true, },
343
+ { id = "OneTime",
344
+ editor = "bool", default = false, dont_save = true, read_only = true, },
345
+ },
346
+ EditorView = Untranslated('"Activate StoryBit <Id>"'),
347
+ Documentation = "Activates a StoryBit with the specified Id",
348
+ NoIngameDescription = true,
349
+ EditorNestedObjCategory = "Story Bits",
350
+ }
351
+
352
+ function StoryBitActivate:GetStorybitSets()
353
+ local preset = StoryBits[self.Id]
354
+ if not preset or not next(preset.Sets) then return "None" end
355
+ local items = {}
356
+ for set in sorted_pairs(preset.Sets) do
357
+ items[#items + 1] = set
358
+ end
359
+ return table.concat(items, ", ")
360
+ end
361
+
362
+ function StoryBitActivate:GetOneTime()
363
+ local preset = StoryBits[self.Id]
364
+ return preset and preset.OneTime
365
+ end
366
+
367
+ function StoryBitActivate:__exec(obj, context)
368
+ ForceActivateStoryBit(self.Id, obj, self.ForcePopup and "immediate", context, self.NoCooldown)
369
+ end
370
+
371
+ DefineClass.StoryBitActivateRandom = {
372
+ __parents = { "Effect", },
373
+ __generated_by_class = "EffectDef",
374
+
375
+ properties = {
376
+ { id = "StoryBits", help = "A list of storybits with weight. One will be chosen and activated based on weight and met prerequisites.",
377
+ editor = "nested_list", default = false, base_class = "StoryBitWithWeight", all_descendants = true, },
378
+ },
379
+ Documentation = "Performs a weighted random on a list of story bits and activates the one that is picked (if any)",
380
+ EditorNestedObjCategory = "Story Bits",
381
+ }
382
+
383
+ function StoryBitActivateRandom:GetEditorView(...)
384
+ local items = {}
385
+ for i, item in ipairs(self.StoryBits) do
386
+ local id = item.StoryBitId or ""
387
+ if item.Weight then
388
+ id = id .. " (" .. item.Weight .. ")"
389
+ end
390
+ items[i] = id
391
+ end
392
+ local names_text = next(items) and table.concat(items, ", ") or "None"
393
+ return Untranslated(string.format("Activate random event: %s", names_text))
394
+ end
395
+
396
+ function StoryBitActivateRandom:__exec(obj, context)
397
+ TryActivateRandomStoryBit(self.StoryBits, obj, context)
398
+ end
399
+
400
+ function StoryBitActivateRandom:GetError()
401
+ if not next(StoryBits) then return end
402
+ if not next(self.StoryBits) then
403
+ return "No StoryBits to pick from "
404
+ else
405
+ for i, item in ipairs(self.StoryBits) do
406
+ local id = item.StoryBitId
407
+ if id ~= "" and not StoryBits[id] then
408
+ return string.format("No such storybit: %s", id)
409
+ end
410
+ end
411
+ end
412
+ end
413
+
414
+ DefineClass.StoryBitEnableRandom = {
415
+ __parents = { "Effect", },
416
+ __generated_by_class = "EffectDef",
417
+
418
+ properties = {
419
+ { id = "StoryBits", name = "Story Bits", help = "List of StoryBit ids to pick from",
420
+ editor = "preset_id_list", default = {}, preset_class = "StoryBit", item_default = "", },
421
+ { id = "Weights", name = "Weights", help = "Weights for the entries in StoryBits (default 100)",
422
+ editor = "number_list", default = {}, item_default = 100, items = false, },
423
+ },
424
+ Documentation = "Performs a weighted random on a list of story bits and enables the one that is picked (if any)",
425
+ EditorNestedObjCategory = "Story Bits",
426
+ }
427
+
428
+ function StoryBitEnableRandom:GetEditorView(...)
429
+ local items = {}
430
+ local weights = self.Weights
431
+ for i, id in ipairs(self.StoryBits) do
432
+ local w = weights[i]
433
+ if w then
434
+ id = id .. " (" .. w .. ")"
435
+ end
436
+ items[i] = id
437
+ end
438
+ local names_text = next(items) and table.concat(items, ", ") or "None"
439
+ return Untranslated(string.format("Enable random event: %s", names_text))
440
+ end
441
+
442
+ function StoryBitEnableRandom:__exec(obj, context)
443
+ local items = {}
444
+ local weights = self.Weights
445
+ local states = g_StoryBitStates
446
+ for i, id in ipairs(self.StoryBits) do
447
+ local state = states[id]
448
+ if not state then
449
+ local def = StoryBits[id]
450
+ if def and not def.Enabled then
451
+ local weight = weights[i] or 100
452
+ items[#items + 1] = {id, weight}
453
+ end
454
+ end
455
+ end
456
+ local item = table.weighted_rand(items, 2)
457
+ if not item then
458
+ return
459
+ end
460
+ local id = item[1]
461
+ local storybit = StoryBits[id]
462
+ StoryBitState:new{
463
+ id = id,
464
+ object = storybit.InheritsObject and context and context.object or nil,
465
+ player = ResolveEventPlayer(obj, context),
466
+ inherited_title = context and context:GetTitle() or nil,
467
+ inherited_image = context and context:GetImage() or nil,
468
+ }
469
+ end
470
+
471
+ function StoryBitEnableRandom:GetError()
472
+ if not next(StoryBits) then return end
473
+ if not next(self.StoryBits) then
474
+ return "No StoryBits to pick from "
475
+ else
476
+ for i, id in ipairs(self.StoryBits) do
477
+ if id ~= "" and not StoryBits[id] then
478
+ return string.format("No such storybit: %s", id)
479
+ end
480
+ end
481
+ end
482
+ end
483
+
484
+ DefineClass.ViewObjectEffect = {
485
+ __parents = { "Effect", },
486
+ __generated_by_class = "EffectDef",
487
+
488
+ EditorView = Untranslated("View object"),
489
+ Documentation = "Move the camera to view the object",
490
+ }
491
+
492
+ function ViewObjectEffect:__exec(obj, context)
493
+ ViewObject(obj)
494
+ end
495
+
CommonLua/Classes/ClassDefs/ClassDef-PresetDefs.generated.lua ADDED
@@ -0,0 +1,2072 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- ========== GENERATED BY ClassDef Editor (Ctrl-Alt-F3) DO NOT EDIT MANUALLY! ==========
2
+
3
+ DefineClass.ActorFXClassDef = {
4
+ __parents = { "Preset", },
5
+ __generated_by_class = "PresetDef",
6
+
7
+ properties = {
8
+ { category = "Preset", id = "help", help = T(757555070861, --[[PresetDef ActorFXClassDef help]] "Use the Group property to define the ActorFXClass parent - all FX from the parent are inherited.\n\nEntries in the Default group are considered top level and have no parents."),
9
+ editor = "help", default = false, },
10
+ },
11
+ PropertyTranslation = true,
12
+ GlobalMap = "ActorFXClassDefs",
13
+ EditorMenubarName = "FX Classes",
14
+ EditorMenubar = "Editors.Art",
15
+ StoreAsTable = true,
16
+ }
17
+
18
+ ----- ActorFXClassDef
19
+
20
+ function OnMsg.GatherFXActors(list)
21
+ ForEachPreset("ActorFXClassDef", function(preset, group, list)
22
+ list[#list + 1] = preset.id
23
+ end, list)
24
+ end
25
+
26
+ function OnMsg.GetCustomFXInheritActorRules(custom_inherit)
27
+ ForEachPreset("ActorFXClassDef", function(preset, group, custom_inherit)
28
+ if preset.group ~= "Default" then
29
+ assert(ActorFXClassDefs[preset.group]) -- any preset group other than 'Default' should have its own ActorFXClass definition; actor classes in 'Default' are top-level
30
+ custom_inherit[#custom_inherit + 1] = preset.id
31
+ custom_inherit[#custom_inherit + 1] = preset.group
32
+ end
33
+ end, custom_inherit)
34
+ end
35
+
36
+ DefineClass.AnimComponent = {
37
+ __parents = { "Preset", },
38
+ __generated_by_class = "PresetDef",
39
+
40
+ properties = {
41
+ { id = "label", name = "Label", help = "Label that identifies the kind of IK",
42
+ editor = "text", default = false, },
43
+ },
44
+ GlobalMap = "AnimComponents",
45
+ EditorMenubar = "Editors.Art",
46
+ }
47
+
48
+ DefineClass.AnimIKLimbAdjust = {
49
+ __parents = { "AnimComponent", },
50
+ __generated_by_class = "PresetDef",
51
+
52
+ properties = {
53
+ { id = "Limbs",
54
+ editor = "nested_list", default = false, base_class = "AnimLimbData", inclusive = true, auto_expand = true, },
55
+ { id = "adjust_root_to_reach_targets", help = "Adjust root position along an axis in case limb targets are too far from the current position",
56
+ editor = "bool", default = false, },
57
+ { id = "adjust_root_axis", help = "Axis to adjust root position to reach far out limb targets",
58
+ editor = "point", default = point(0, 0, 1000), },
59
+ { id = "max_target_speed", help = "Maximum units per second the adjusted limb fit positions are allowed to move, 0 means no limit",
60
+ editor = "number", default = 5000, min = 0, },
61
+ },
62
+ PresetClass = "AnimComponent",
63
+ }
64
+
65
+ DefineClass.AnimIKLookAt = {
66
+ __parents = { "AnimComponent", },
67
+ __generated_by_class = "PresetDef",
68
+
69
+ properties = {
70
+ { id = "pivot_bone",
71
+ editor = "text", default = false, },
72
+ { id = "pivot_parents", help = "Number of bones to distribute the rotation between",
73
+ editor = "number", default = 1, min = 1, },
74
+ { id = "aim_bone",
75
+ editor = "text", default = false, },
76
+ { id = "aim_forward", help = "Forward direction in local bone space",
77
+ editor = "point", default = point(-1000, 0, 0), },
78
+ { id = "aim_up", help = "Up direction in local bone space",
79
+ editor = "point", default = point(0, -1000, 0), },
80
+ { id = "max_vertical_angle",
81
+ editor = "number", default = 2700, scale = "deg", min = 0, max = 10800, },
82
+ { id = "max_horizontal_angle",
83
+ editor = "number", default = 5400, scale = "deg", min = 0, max = 10800, },
84
+ { id = "out_of_bound_vertical_snap", help = "Snap vertical angle to 0 if target is outside horizontal limits",
85
+ editor = "bool", default = false, },
86
+ { id = "max_angular_speed", help = "Maximum local angle per second",
87
+ editor = "number", default = 5400, scale = "deg", min = 0, max = 43200, },
88
+ },
89
+ PresetClass = "AnimComponent",
90
+ }
91
+
92
+ DefineClass.AnimMetadata = {
93
+ __parents = { "Preset", },
94
+ __generated_by_class = "PresetDef",
95
+
96
+ properties = {
97
+ { category = "FX", id = "Action", name = "Test action",
98
+ editor = "combo", default = false, dont_save = true, items = function (self)
99
+ return table.ifilter(PresetsPropCombo("FXPreset", "Action")(), function(idx, item)
100
+ return FXActionToAnim(item) == item
101
+ end)
102
+ end, show_recent_items = 7,},
103
+ { category = "FX", id = "Actor", name = "Test actor",
104
+ editor = "combo", default = false, dont_save = true, items = function (self) return ActionFXClassCombo() end, show_recent_items = 7,},
105
+ { category = "FX", id = "Target", name = "Test target",
106
+ editor = "combo", default = false, dont_save = true, items = function (self) return TargetFXClassCombo end, show_recent_items = 7,},
107
+ { category = "FX", id = "FXInherits",
108
+ editor = "string_list", default = {}, item_default = "idle", items = function (self) return IsValidEntity(self.group) and GetStates(self.group) or { "idle" } end, },
109
+ { category = "Moments", id = "ReconfirmAll",
110
+ editor = "buttons", default = false, buttons = { {name = "Reconfirm", func = "ReconfirmMoments"}, }, },
111
+ { category = "Moments", id = "Moments",
112
+ editor = "nested_list", default = false, base_class = "AnimMoment", inclusive = true, },
113
+ { category = "Animation", id = "SpeedModifier",
114
+ editor = "number", default = 100, min = 10, max = 1000, },
115
+ { category = "Animation", id = "StepModifier",
116
+ editor = "number", default = 100, min = 10, max = 1000, },
117
+ { category = "Anim Components", id = "VariationWeight",
118
+ editor = "number", default = 100, slider = true, min = 0, max = 10000, },
119
+ { category = "Anim Components", id = "RandomizePhase",
120
+ editor = "number", default = -1, min = -1, },
121
+ { category = "Anim Components", id = "AnimComponents",
122
+ editor = "nested_list", default = false, base_class = "AnimComponentWeight", inclusive = true, auto_expand = true, },
123
+ },
124
+ GedEditor = "",
125
+ }
126
+
127
+ function AnimMetadata:GetAction()
128
+ if not self.Action then
129
+ local obj = GetAnimationMomentsEditorObject()
130
+ if obj then
131
+ local anim = AnimationMomentsEditorMode == "selection" and obj:Getanim() or GetStateName(obj:GetState())
132
+ return FXAnimToAction(anim)
133
+ end
134
+ end
135
+ return self.Action
136
+ end
137
+
138
+ function AnimMetadata:GetActor()
139
+ if not self.Actor then
140
+ local obj = GetAnimationMomentsEditorObject()
141
+ if obj and g_Classes.Unit then
142
+ obj = rawget(obj, "obj") or obj
143
+ local obj_class = obj.class
144
+ if obj:IsKindOfClasses("Unit", "BaseObjectAME") or obj_class == "DummyUnit" then
145
+ obj_class = "Unit"
146
+ end
147
+ return g_Classes[obj_class].fx_actor_class or obj_class
148
+ end
149
+ end
150
+ return self.Actor
151
+ end
152
+
153
+ function AnimMetadata:PostLoad()
154
+ local ent_speed_mod = const.AnimSpeedScale * self.SpeedModifier / 100
155
+ local entity = self.group
156
+ local state = GetStateIdx(self.id)
157
+ SetStateSpeedModifier(entity, state, ent_speed_mod)
158
+ SetStateStepModifier(entity, state, self.StepModifier)
159
+ end
160
+
161
+ function AnimMetadata:OnPreSave()
162
+ -- fixup revisions of files that were modified locally when animation moments were added
163
+ for _, moment in ipairs(self.Moments) do
164
+ if moment.AnimRevision == 999999999 then
165
+ moment.AnimRevision = EntitySpec:GetAnimRevision(self.group, self.id)
166
+ end
167
+ end
168
+ end
169
+
170
+ function AnimMetadata:ReconfirmMoments(root, prop_id, ged)
171
+ local revision = GetAnimationMomentsEditorObject().AnimRevision
172
+ for _, moment in ipairs(self.Moments or empty_table) do
173
+ if moment.AnimRevision ~= revision then
174
+ moment.AnimRevision = revision
175
+ ObjModified(moment)
176
+ end
177
+ end
178
+ ObjModified(self)
179
+ ObjModified(ged:ResolveObj("Animations"))
180
+ end
181
+
182
+ function AnimMetadata:GetError()
183
+ local entity = self.group
184
+ if not IsValidEntity(entity) then
185
+ return "No such entity " .. (entity or "")
186
+ end
187
+ local state = self.id
188
+ if not HasState(entity, state) then
189
+ return "No such anim " .. entity .. "." .. (state or "")
190
+ end
191
+ end
192
+
193
+ ----- AnimMetadata remove group and id properties
194
+
195
+ table.insert(AnimMetadata.properties, { id = "Id", editor = "text", no_edit = true })
196
+ table.insert(AnimMetadata.properties, { id = "Group", editor = "text", no_edit = true })
197
+
198
+ DefineClass.Appearance = {
199
+ __parents = { "PropertyObject", },
200
+ __generated_by_class = "ClassDef",
201
+
202
+ properties = {
203
+ { category = "Body", id = "Body", name = "Body",
204
+ editor = "combo", default = false, items = function (self) return GetCharacterBodyComboItems() end, },
205
+ { category = "Body", id = "BodyColor", name = "Body Color",
206
+ editor = "nested_obj", default = false, base_class = "ColorizationPropSet", },
207
+ { category = "Head", id = "Head", name = "Head",
208
+ editor = "combo", default = false, items = function (self) return GetCharacterHeadComboItems(self) end, },
209
+ { category = "Head", id = "HeadColor", name = "Head Color",
210
+ editor = "nested_obj", default = false, base_class = "ColorizationPropSet", },
211
+ { category = "Shirt", id = "Shirt", name = "Shirt",
212
+ editor = "combo", default = false, items = function (self) return GetCharacterShirtComboItems(self) end, },
213
+ { category = "Shirt", id = "ShirtColor", name = "Shirt Color",
214
+ editor = "nested_obj", default = false, base_class = "ColorizationPropSet", },
215
+ { category = "Pants", id = "Pants", name = "Pants",
216
+ editor = "combo", default = false, items = function (self) return GetCharacterPantsComboItems(self) end, },
217
+ { category = "Pants", id = "PantsColor", name = "Pants Color",
218
+ editor = "nested_obj", default = false, base_class = "ColorizationPropSet", },
219
+ { category = "Armor", id = "Armor", name = "Armor",
220
+ editor = "combo", default = false, items = function (self) return GetCharacterArmorComboItems(self) end, },
221
+ { category = "Armor", id = "ArmorColor", name = "Armor Color",
222
+ editor = "nested_obj", default = false, base_class = "ColorizationPropSet", },
223
+ { category = "Chest", id = "Chest", name = "Chest",
224
+ editor = "combo", default = false, items = function (self) return GetCharacterChestComboItems(self) end, },
225
+ { category = "Chest", id = "ChestSpot", name = "Chest Spot", help = "Where to attach the hat",
226
+ editor = "combo", default = "Torso", items = function (self) return {"Torso", "Origin"} end, },
227
+ { category = "Chest", id = "ChestColor", name = "Chest Color",
228
+ editor = "nested_obj", default = false, base_class = "ColorizationPropSet", },
229
+ { category = "Hip", id = "Hip", name = "Hip",
230
+ editor = "combo", default = false, items = function (self) return GetCharacterHipComboItems(self) end, },
231
+ { category = "Hip", id = "HipSpot", name = "Hip Spot", help = "Where to attach the hat",
232
+ editor = "combo", default = "Groin", items = function (self) return {"Groin", "Origin"} end, },
233
+ { category = "Hip", id = "HipColor", name = "Hip Color",
234
+ editor = "nested_obj", default = false, base_class = "ColorizationPropSet", },
235
+ { category = "Hat", id = "Hat", name = "Hat",
236
+ editor = "combo", default = false, items = function (self) return GetCharacterHatComboItems() end, },
237
+ { category = "Hat", id = "HatSpot", name = "Hat Spot", help = "Where to attach the hat",
238
+ editor = "combo", default = "Head", items = function (self) return {"Head", "Origin"} end, },
239
+ { category = "Hat", id = "HatAttachOffsetX", name = "Hat Attach Offset X",
240
+ editor = "number", default = false, scale = "cm", slider = true, min = -50, max = 50, },
241
+ { category = "Hat", id = "HatAttachOffsetY", name = "Hat Attach Offset Y",
242
+ editor = "number", default = false, scale = "cm", slider = true, min = -50, max = 50, },
243
+ { category = "Hat", id = "HatAttachOffsetZ", name = "Hat Attach Offset Z",
244
+ editor = "number", default = false, scale = "cm", slider = true, min = -50, max = 50, },
245
+ { category = "Hat", id = "HatAttachOffsetAngle", name = "Hat Attach Offset Angle",
246
+ editor = "number", default = false, scale = "deg", slider = true, min = -18000, max = 10800, },
247
+ { category = "Hat", id = "HatColor", name = "Hat Color",
248
+ editor = "nested_obj", default = false, base_class = "ColorizationPropSet", },
249
+ { category = "Hat", id = "Hat2", name = "Hat2",
250
+ editor = "combo", default = false, items = function (self) return GetCharacterHatComboItems() end, },
251
+ { category = "Hat", id = "Hat2Spot", name = "Hat 2 Spot", help = "Where to attach the hat",
252
+ editor = "combo", default = "Head", items = function (self) return {"Head", "Origin"} end, },
253
+ { category = "Hat", id = "Hat2AttachOffsetX", name = "Hat 2 Attach Offset X",
254
+ editor = "number", default = false, scale = "cm", slider = true, min = -50, max = 50, },
255
+ { category = "Hat", id = "Hat2AttachOffsetY", name = "Hat 2 Attach Offset Y",
256
+ editor = "number", default = false, scale = "cm", slider = true, min = -50, max = 50, },
257
+ { category = "Hat", id = "Hat2AttachOffsetZ", name = "Hat 2 Attach Offset Z",
258
+ editor = "number", default = false, scale = "cm", slider = true, min = -50, max = 50, },
259
+ { category = "Hat", id = "Hat2AttachOffsetAngle", name = "Hat 2 Attach Offset Angle",
260
+ editor = "number", default = false, scale = "deg", slider = true, min = -18000, max = 10800, },
261
+ { category = "Hat", id = "Hat2Color", name = "Hat 2 Color",
262
+ editor = "nested_obj", default = false, base_class = "ColorizationPropSet", },
263
+ { category = "Hair", id = "Hair", name = "Hair",
264
+ editor = "combo", default = false, items = function (self) return GetCharacterHairComboItems(self) end, },
265
+ { category = "Hair", id = "HairSpot", name = "Hair Spot", help = "Where to attach the hat",
266
+ editor = "combo", default = "Head", items = function (self) return {"Head"} end, },
267
+ { category = "Hair", id = "HairColor", name = "Hair Color",
268
+ editor = "nested_obj", default = false, base_class = "ColorizationPropSet", },
269
+ { category = "Hair", id = "HairParam1", name = "Hair Spec Strength",
270
+ editor = "number", default = 51, slider = true, min = 0, max = 255, },
271
+ { category = "Hair", id = "HairParam2", name = "Hair Env Strength",
272
+ editor = "number", default = 51, slider = true, min = 0, max = 255, },
273
+ { category = "Hair", id = "HairParam3", name = "Hair Light Softness",
274
+ editor = "number", default = 255, slider = true, min = 0, max = 255, },
275
+ { category = "Hair", id = "HairParam4", name = "Hair Specular Colorization",
276
+ editor = "number", default = 0, no_edit = true, slider = true, min = 0, max = 255, },
277
+ },
278
+ }
279
+
280
+ DefineClass.AppearancePreset = {
281
+ __parents = { "Preset", "Appearance", },
282
+ __generated_by_class = "PresetDef",
283
+
284
+ properties = {
285
+ { id = "ViewInChararacterEditorButton",
286
+ editor = "buttons", default = false, buttons = { {name = "View in Anim Metadata Editor", func = "ViewInAnimMetadataEditor"}, }, },
287
+ { id = "ViewInAnimMetadataEditor",
288
+ editor = "func", default = function (self)
289
+ CloseAnimationMomentsEditor() -- close if opened
290
+ OpenAnimationMomentsEditor(self.id)
291
+ end, no_edit = true, },
292
+ },
293
+ GlobalMap = "AppearancePresets",
294
+ EditorMenubarName = "Appearance Editor",
295
+ EditorIcon = "CommonAssets/UI/Icons/business compare decision direction marketing.png",
296
+ EditorMenubar = "Characters",
297
+ EditorCustomActions = {
298
+ {
299
+ FuncName = "RefreshApperanceToAllUnits",
300
+ Icon = "CommonAssets/UI/Ged/play",
301
+ Menubar = "Actions",
302
+ Name = "Apply to All",
303
+ Rollover = "Refreshes all units on map with this appearance",
304
+ Toolbar = "main",
305
+ },
306
+ },
307
+ }
308
+
309
+ function AppearancePreset:GetError()
310
+ local parts = table.copy(AppearanceObject.attached_parts)
311
+ table.insert(parts, "Body")
312
+ local results = {}
313
+ for _, part in ipairs(parts) do
314
+ if self[part] and self[part] ~= "" and not IsValidEntity(self[part]) then
315
+ results[#results+1] = string.format("%s: invalid entity %s", part, self[part])
316
+ end
317
+ end
318
+ if next(results) then
319
+ return table.concat(results, "\n")
320
+ end
321
+ end
322
+
323
+ DefineClass.BadgePresetDef = {
324
+ __parents = { "Preset", },
325
+ __generated_by_class = "PresetDef",
326
+
327
+ properties = {
328
+ { id = "HasArrow", name = "HasArrow",
329
+ editor = "bool", default = false, },
330
+ { id = "ArrowTemplate", name = "ArrowTemplate",
331
+ editor = "combo", default = false, items = function (self) return XTemplateCombo("XBadgeArrow", false) end, },
332
+ { id = "UITemplate", name = "UITemplate", help = "The UI template which defines how the badge will look above the object.",
333
+ editor = "combo", default = false, items = function (self) return XTemplateCombo() end, },
334
+ { id = "ZoomUI", name = "Zoom UI",
335
+ editor = "bool", default = false, },
336
+ { id = "EntityName", name = "EntityName", help = "The entity to spawn as a badge on the unit, if any.",
337
+ editor = "combo", default = false, items = function (self)
338
+ return table.keys2(table.filter(GetAllEntities(), function(e) return e:sub(1, 2) == "Iw" end, "none"))
339
+ end, },
340
+ { id = "AttachSpotName", name = "Attach Spot Name", help = "If set the badge will attach to the spot with that name.",
341
+ editor = "text", default = false, },
342
+ { id = "attachOffset", name = "Entity Attach Offset", help = "An offset from the specified point to attach the badge to. Will overwrite any such offsets from the entity class.",
343
+ editor = "point", default = false, },
344
+ { id = "noRotate", name = "Don't Rotate Arrow", help = "If set the arrow template will not be rotated according to the direction of the target but just stick on the edge of the screen.",
345
+ editor = "bool", default = false, },
346
+ { id = "noHide", name = "Don't Hide", help = "Don't hide this badge if there are other badges on the target. Badges marked as \"noHide\" also do not count towards \"other badges on the target\".",
347
+ editor = "bool", default = false, },
348
+ { id = "handleMouse", name = "Handle Mouse", help = "If enabled the badge will have a thread running to make sure it can handle mouse events like a normal UI window.",
349
+ editor = "bool", default = false, },
350
+ { id = "BadgePriority", name = "BadgePriority",
351
+ editor = "number", default = false, },
352
+ },
353
+ GlobalMap = "BadgePresetDefs",
354
+ }
355
+
356
+ DefineClass.BindingsMenuCategory = {
357
+ __parents = { "ListPreset", },
358
+ __generated_by_class = "PresetDef",
359
+
360
+ properties = {
361
+ { id = "Name",
362
+ editor = "text", default = false, translate = true, },
363
+ },
364
+ }
365
+
366
+ DefineClass.BugReportTag = {
367
+ __parents = { "ListPreset", },
368
+ __generated_by_class = "PresetDef",
369
+
370
+ properties = {
371
+ { id = "Platform", help = "Whether this is a Platform tag.",
372
+ editor = "bool", default = false, },
373
+ { id = "Automatic",
374
+ editor = "bool", default = false, },
375
+ { id = "ShowInExternal", name = "Show in External Bug Report",
376
+ editor = "bool", default = false, },
377
+ },
378
+ }
379
+
380
+ DefineClass.Camera = {
381
+ __parents = { "Preset", },
382
+ __generated_by_class = "PresetDef",
383
+
384
+ properties = {
385
+ { category = "Preset", id = "comment", name = "comment",
386
+ editor = "text", default = "Camera", },
387
+ { category = "Preset", id = "display_name", name = "Display Name",
388
+ editor = "text", default = T(145449857928, --[[PresetDef Camera default]] "Camera"), translate = true, },
389
+ { category = "Preset", id = "description", name = "Description",
390
+ editor = "text", default = false, translate = true, lines = 3, max_lines = 10, },
391
+ { category = "Camera", id = "map", name = "Map",
392
+ editor = "combo", default = false,
393
+ cam_prop = true, items = function (self) return ListMaps() end, },
394
+ { category = "Camera", id = "SavedGame", name = "Saved Game",
395
+ editor = "text", default = false, },
396
+ { category = "Camera", id = "order", name = "Order",
397
+ editor = "number", default = 0, },
398
+ { category = "Camera", id = "locked", name = "Locked",
399
+ editor = "bool", default = false,
400
+ cam_prop = true, },
401
+ { category = "Camera", id = "flip_to_adjacent", name = "Flip to Adjacent",
402
+ editor = "bool", default = false, },
403
+ { category = "Camera", id = "fade_in", name = "Fade In",
404
+ editor = "number", default = 200, },
405
+ { category = "Camera", id = "fade_out", name = "Fade Out",
406
+ editor = "number", default = 200, },
407
+ { category = "Camera", id = "movement", name = "Movement",
408
+ editor = "combo", default = "", items = function (self) return table.keys2(CameraMovementTypes, nil, "") end, },
409
+ { category = "Camera", id = "interpolation", name = "Interpolation",
410
+ editor = "combo", default = "linear", items = function (self) return table.keys2(CameraInterpolationTypes) end, },
411
+ { category = "Camera", id = "duration", name = "Duration",
412
+ editor = "number", default = 1000, },
413
+ { category = "Camera", id = "buttonsSrc",
414
+ editor = "buttons", default = false, buttons = { {name = "View Start", func = "ViewStart"}, {name = "Set Start", func = "SetStart"}, }, },
415
+ { category = "Camera", id = "cam_lookat",
416
+ editor = "point", default = false,
417
+ cam_prop = true, },
418
+ { category = "Camera", id = "cam_pos",
419
+ editor = "point", default = false,
420
+ cam_prop = true, },
421
+ { category = "Camera", id = "buttonsDest",
422
+ editor = "buttons", default = false, buttons = { {name = "View Dest", func = "ViewDest"}, {name = "Set Dest", func = "SetDest"}, }, },
423
+ { category = "Camera", id = "cam_dest_lookat",
424
+ editor = "point", default = false,
425
+ cam_prop = true, },
426
+ { category = "Camera", id = "cam_dest_pos",
427
+ editor = "point", default = false,
428
+ cam_prop = true, },
429
+ { category = "Camera", id = "cam_type",
430
+ editor = "choice", default = "Max", items = function (self) return GetCameraTypesItems end, },
431
+ { category = "Camera", id = "fovx",
432
+ editor = "number", default = 4200,
433
+ cam_prop = true, },
434
+ { category = "Camera", id = "zoom",
435
+ editor = "number", default = 2000,
436
+ cam_prop = true, },
437
+ { category = "Camera", id = "lightmodel", name = "Light Model", help = "Specify a light model, or leave as 'false' to restore the previous one.",
438
+ editor = "preset_id", default = false, preset_class = "LightmodelPreset", },
439
+ { category = "Camera", id = "interface", name = "Interface in Screenshots", help = "Check this to include game interface in the screenshots",
440
+ editor = "bool", default = false, },
441
+ { category = "Camera", id = "cam_props",
442
+ editor = "prop_table", default = false,
443
+ cam_prop = true, indent = "", lines = 1, max_lines = 20, },
444
+ { category = "Camera", id = "camera_properties", name = "Camera Properties",
445
+ editor = "prop_table", default = false, no_edit = true, indent = "", lines = 1, max_lines = 20, },
446
+ { category = "Functions", id = "beginFunc", name = "Begin() Function",
447
+ editor = "func", default = function (self) end, },
448
+ { category = "Functions", id = "endFunc", name = "End() Function",
449
+ editor = "func", default = function (self) end, },
450
+ },
451
+ GlobalMap = "PredefinedCameras",
452
+ EditorMenubarName = "Camera Editor",
453
+ EditorIcon = "CommonAssets/UI/Icons/outline video.png",
454
+ EditorMenubar = "Map",
455
+ EditorCustomActions = {
456
+ {
457
+ FuncName = "OpenShowcase",
458
+ Icon = "CommonAssets/UI/Ged/play",
459
+ Menubar = "Actions",
460
+ Name = "ShowcaseUI",
461
+ Rollover = 'Showcase UI <newline>Toggles "Show Case" interface showing all cameras from a group, sorted by order',
462
+ Toolbar = "main",
463
+ },
464
+ {
465
+ FuncName = "GedOpCreateCameraDest",
466
+ Icon = "CommonAssets/UI/Ged/create_camera_destination",
467
+ Menubar = "Actions",
468
+ Name = "CameraDest",
469
+ Rollover = "Create Camera Destination",
470
+ Toolbar = "main",
471
+ },
472
+ {
473
+ FuncName = "GedOpUpdateCamera",
474
+ Icon = "CommonAssets/UI/Ged/update_current_camera",
475
+ Menubar = "Actions",
476
+ Name = "UpdateCamera",
477
+ Rollover = "Update Camera",
478
+ Toolbar = "main",
479
+ },
480
+ {
481
+ FuncName = "GedOpViewMovement",
482
+ Icon = "CommonAssets/UI/Ged/preview",
483
+ IsToggledFuncName = "GedOpIsViewMovementToggled",
484
+ Menubar = "Actions",
485
+ Name = "ViewMovement",
486
+ Rollover = "View Movement",
487
+ Toolbar = "main",
488
+ },
489
+ {
490
+ FuncName = "GedOpUnlockCamera",
491
+ Icon = "CommonAssets/UI/Ged/unlock_camera",
492
+ Menubar = "Actions",
493
+ Name = "UnlockCamera",
494
+ Rollover = "Unlock Camera",
495
+ Toolbar = "main",
496
+ },
497
+ {
498
+ FuncName = "GedOpMaxCamera",
499
+ Icon = "CommonAssets/UI/Ged/max_camera",
500
+ Menubar = "Camera",
501
+ Name = "MaxCamera",
502
+ Rollover = "Max Camera",
503
+ Toolbar = "main",
504
+ },
505
+ {
506
+ FuncName = "GedOpRTSCamera",
507
+ Icon = "CommonAssets/UI/Ged/rts_camera",
508
+ Menubar = "Camera",
509
+ Name = "RTSCamera",
510
+ Rollover = "RTS Camera",
511
+ Toolbar = "main",
512
+ },
513
+ {
514
+ FuncName = "GedOpTacCamera",
515
+ Icon = "CommonAssets/UI/Ged/tac_camera",
516
+ Menubar = "Camera",
517
+ Name = "TacCamera",
518
+ Rollover = "Tac Camera",
519
+ Toolbar = "main",
520
+ },
521
+ {
522
+ FuncName = "GedOpCreateReferenceImages",
523
+ Icon = "CommonAssets/UI/Ged/create_reference_images",
524
+ Menubar = "Actions",
525
+ Name = "CreateReferenceImages",
526
+ Rollover = "Create Reference Images(used during Night Build Game Tests)",
527
+ Toolbar = "main",
528
+ },
529
+ {
530
+ FuncName = "GedOpTakeScreenshots",
531
+ Icon = "CommonAssets/UI/Ged/camera",
532
+ Menubar = "Actions",
533
+ Name = "TakeScreenshot",
534
+ Rollover = "Takes screenshot of the selected camera(s)",
535
+ Toolbar = "main",
536
+ },
537
+ {
538
+ FuncName = "GedPrgPresetToggleStrips",
539
+ Icon = "CommonAssets/UI/Ged/explorer",
540
+ IsToggledFuncName = "GedPrgPresetBlackStripsVisible",
541
+ Menubar = "Actions",
542
+ Name = "ToggleBlackStrips",
543
+ Rollover = "Toggle black strips (Alt-T)",
544
+ Shortcut = "Alt-T",
545
+ SortKey = "strips",
546
+ Toolbar = "main",
547
+ },
548
+ },
549
+ }
550
+
551
+ function Camera:ApplyProperties(dont_lock, should_fade, ged)
552
+ if (self.SavedGame or "") ~= "" then
553
+ if (SavegameMeta or empty_table).savename ~= self.SavedGame then
554
+ if not ged or ged:WaitQuestion("Load Game", "Load camera save?", "Yes", "No") == "ok" then
555
+ LoadGame(self.SavedGame)
556
+ end
557
+ end
558
+ elseif (self.map or "") ~= "" then
559
+ if GetMapName() ~= self.map then
560
+ if ged then
561
+ if ged:WaitQuestion("Change Map", "Change camera map?", "Yes", "No") == "ok" then
562
+ ChangeMap(self.map)
563
+ else
564
+ return
565
+ end
566
+ else
567
+ ChangeMap(self.map)
568
+ end
569
+ end
570
+ end
571
+ SetCamera(self.cam_pos, self.cam_lookat, self.cam_type, self.zoom, self.cam_props, self.fovx)
572
+ if self.locked and not dont_lock then
573
+ LockCamera("CameraPreset")
574
+ else
575
+ UnlockCamera("CameraPreset")
576
+ end
577
+ if should_fade and self.fade_in > 0 then
578
+ local fade_in = should_fade and self.fade_in or 0
579
+ local fade = OpenDialog("Fade")
580
+ fade.idFade:SetVisible(true, "instant")
581
+ if should_fade then
582
+ WaitResourceManagerRequests(1000,1)
583
+ end
584
+ fade.idFade.FadeOutTime = should_fade and self.fade_in or 0
585
+ fade.idFade:SetVisible(false)
586
+ end
587
+ if self.movement ~= "" then
588
+ local camera1 = { pos = self.cam_pos, lookat = self.cam_lookat }
589
+ local camera2 = { pos = self.cam_dest_pos, lookat = self.cam_dest_lookat }
590
+ InterpolateCameraMaxWakeup(camera1, camera2, self.duration, nil, self.interpolation, self.movement)
591
+ end
592
+ if self.lightmodel then
593
+ SetLightmodel(1, self.lightmodel)
594
+ end
595
+ self:beginFunc()
596
+ if should_fade and self.fade_in > 0 then
597
+ local fade_in = should_fade and self.fade_in or 0
598
+ if self.movement == "" then
599
+ Sleep(fade_in)
600
+ elseif fade_in > self.duration then
601
+ Sleep(fade_in - self.duration)
602
+ end
603
+ end
604
+ end
605
+
606
+ function Camera:RevertProperties(should_fade)
607
+ if should_fade and self.fade_out > 0 then
608
+ local fade_out = should_fade and self.fade_out or 0
609
+ local fade = GetDialog("Fade")
610
+ if fade then
611
+ fade.idFade:SetVisible(false, "instant")
612
+ fade.idFade.FadeInTime = fade_out
613
+ fade.idFade:SetVisible(true)
614
+ Sleep(fade_out)
615
+ end
616
+ end
617
+ CloseDialog("Fade")
618
+ self:endFunc()
619
+ end
620
+
621
+ function Camera:QueryProperties()
622
+ local cam_pos, cam_lookat, cam_type, zoom, cam_props, fovx = GetCamera()
623
+ if cam_type ~= "Max" then
624
+ self.movement = ""
625
+ end
626
+ self.cam_pos = cam_pos
627
+ self.cam_lookat = cam_lookat
628
+ self.cam_type = cam_type
629
+ self.zoom = zoom
630
+ self.cam_props = cam_props
631
+ self.locked = camera.IsLocked()
632
+ self.fovx = fovx
633
+ self.map = GetMapName()
634
+ GedObjectModified(self)
635
+ end
636
+
637
+ function Camera:PostLoad()
638
+ Preset.PostLoad(self)
639
+ -- old format compatibility code:
640
+ local cam_props = self.camera_properties or empty_table
641
+ for _, prop in ipairs(self:GetProperties()) do
642
+ if prop.cam_prop then
643
+ local value = cam_props[prop.id]
644
+ if value ~= nil then
645
+ self[prop.id] = value
646
+ end
647
+ end
648
+ end
649
+ self.camera_properties = nil
650
+ end
651
+
652
+ function Camera:SetStart()
653
+ local cam_pos, cam_lookat = GetCamera()
654
+ self:SetProperty("cam_pos", cam_pos)
655
+ self:SetProperty("cam_lookat", cam_lookat)
656
+ ObjModified(self)
657
+ end
658
+
659
+ function Camera:ViewStart()
660
+ SetCamera(self.cam_pos, self.cam_lookat, self.cam_type, self.zoom, self.cam_props, self.fovx)
661
+ end
662
+
663
+ function Camera:SetDest()
664
+ local cam_pos, cam_lookat = GetCamera()
665
+ self:SetProperty("cam_dest_pos", cam_pos)
666
+ self:SetProperty("cam_dest_lookat", cam_lookat)
667
+ self:SetProperty("cam_type", "Max") -- movement forces Max camera
668
+ ObjModified(self)
669
+ end
670
+
671
+ function Camera:ViewDest(camera)
672
+ local pos = self.cam_dest_pos or self.cam_pos
673
+ local lookat = self.cam_dest_lookat or self.cam_lookat
674
+ SetCamera(pos, lookat, self.cam_type, self.zoom, self.cam_props, self.fovx)
675
+ end
676
+
677
+ function Camera:GetEditorView()
678
+ if tonumber(self.order) then
679
+ return Untranslated("<u(id)> <color 0 200 0><u(Comment)>(Showcase: #<u(order)>)</color>")
680
+ else
681
+ return Untranslated("<u(id)> <color 0 200 0><u(Comment)></color>")
682
+ end
683
+ end
684
+
685
+ function Camera:OnEditorNew(parent, ged, is_paste)
686
+ self.order = (#Presets.Camera[self.group] or 0) + 1
687
+ self:SetId(self:GenerateUniquePresetId("Camera_" .. self.order))
688
+ self:QueryProperties()
689
+ end
690
+
691
+ DefineClass.CheatDef = {
692
+ __parents = { "DisplayPreset", "TODOPreset", },
693
+ __generated_by_class = "PresetDef",
694
+
695
+ properties = {
696
+ { id = "in_menu", name = "Show in menu",
697
+ editor = "combo", default = "", items = function (self) return {""} end, show_recent_items = 10,},
698
+ { id = "modes", name = "Menu modes",
699
+ editor = "string_list", default = {}, dont_save = function(self) return self.in_menu == "" end, no_edit = function(self) return self.in_menu == "" end, item_default = "", items = false, arbitrary_value = true, },
700
+ { category = "Execution", id = "params", name = "Parameters", help = 'Cheats without parameters or with parameters "Selection" appear in the game cheats menu',
701
+ editor = "combo", default = "", items = function (self) return {"", "SelectedObj", "Selection"} end, },
702
+ { category = "Execution", id = "param_values", name = "Parameter values",
703
+ editor = "expression", default = false, dont_save = function(self) local params = self.params:trim_spaces() return params == "" or params == "Selection" or params == "SelectedObj" end, no_edit = function(self) local params = self.params:trim_spaces() return params == "" or params == "Selection" or params == "SelectedObj" end, },
704
+ { category = "Execution", id = "sync", name = "Sync",
705
+ editor = "bool", default = true, },
706
+ { category = "Execution", id = "run", name = "Run",
707
+ editor = "func", default = function (self) end,
708
+ params = function(obj) return obj:GetParamNames() end, },
709
+ { category = "Execution", id = "state", name = "State", help = 'Can return "hidden", "disabled" or nil',
710
+ editor = "func", default = function (self) end,
711
+ params = function(obj) return obj:GetParamNames() end, },
712
+ },
713
+ GlobalMap = "CheatDefs",
714
+ EditorMenubarName = "Cheats",
715
+ EditorMenubar = "Editors.Engine",
716
+ }
717
+
718
+ function CheatDef:GetParamNames()
719
+ local params = self.params:trim_spaces()
720
+ if params == "" then return "self" end
721
+ if params == "SelectedObj" then return "self, obj" end
722
+ if params == "Selection" then return "self, objs" end
723
+ return "self, " .. self.params
724
+ end
725
+
726
+ function CheatDef:GetParams()
727
+ local params = self.params:trim_spaces()
728
+ if params == "" then return end
729
+ if params == "SelectedObj" then return SelectedObj end
730
+ if params == "Selection" then return Selection end
731
+ if self.param_values then
732
+ return self:param_values()
733
+ end
734
+ end
735
+
736
+ function CheatDef:Exec()
737
+ if self.sync then
738
+ NetSyncEvent("CheatDef", self.id, self:GetParams())
739
+ else
740
+ self:run(self:GetParams())
741
+ end
742
+ end
743
+
744
+ function CheatDef:SpawnAction(host)
745
+ XAction:new({
746
+ ActionMode = table.concat(self.modes,","),
747
+ ActionMenubar = self.in_menu ~= "" and self.in_menu or nil,
748
+ ActionName = self:GetDisplayName(),
749
+ ActionDescription = self:GetDescription(),
750
+ ActionSortKey = string.format("%09d", self.SortKey or 0),
751
+ ActionTranslate = true,
752
+ ActionState = function(self, host)
753
+ return self.cheat:state(self.cheat:GetParams())
754
+ end,
755
+ OnAction = function (self, host, source, ...)
756
+ self.cheat:Exec()
757
+ end,
758
+ cheat = self,
759
+ }, host)
760
+ end
761
+
762
+ ----- CheatDef
763
+
764
+ function NetSyncEvents.CheatDef(id, ...)
765
+ local def = CheatDefs[id]
766
+ if not AreCheatsEnabled() or not def then return end
767
+ print("Cheat", def.id)
768
+ LogCheatUsed(def.id)
769
+ def:run(...)
770
+ end
771
+
772
+ function OnMsg.Shortcuts(host)
773
+ ForEachPreset("CheatDef", function(preset, group, host)
774
+ if preset.in_menu ~= "" then
775
+ preset:SpawnAction(host)
776
+ end
777
+ end, host)
778
+ end
779
+
780
+ function OnMsg.PresetSave(class)
781
+ if IsKindOf(g_Classes[class], "CheatDef") then
782
+ ReloadShortcuts()
783
+ end
784
+ end
785
+
786
+ DefineClass.CommonTags = {
787
+ __parents = { "ListPreset", },
788
+ __generated_by_class = "PresetDef",
789
+
790
+ }
791
+
792
+ DefineClass.DisplayPreset = {
793
+ __parents = { "Preset", },
794
+ __generated_by_class = "PresetDef",
795
+
796
+ properties = {
797
+ { category = "General", id = "display_name", name = "Display Name",
798
+ editor = "text", default = "", translate = true, },
799
+ { category = "General", id = "display_name_caps", name = "Display Name Caps",
800
+ editor = "text", default = "", translate = true, },
801
+ { category = "General", id = "description", name = "Description",
802
+ editor = "text", default = "", translate = true, lines = 3, max_lines = 20, },
803
+ { category = "General", id = "description_gamepad", name = "Gamepad description",
804
+ editor = "text", default = "", translate = true, wordwrap = true, lines = 3, max_lines = 20, },
805
+ { category = "General", id = "flavor_text", name = "Flavor text",
806
+ editor = "text", default = "", translate = true, lines = 3, max_lines = 20, },
807
+ { category = "General", id = "new_in", name = "New in update", help = 'Update showing this preset with a "New!" tag',
808
+ editor = "text", default = "", no_edit = function(self) return not self.NewFeatureTag end, },
809
+ },
810
+ AltFormat = "<EditorViewPresetPrefix><display_name><EditorViewPresetPostfix><color 0 128 0><opt(u(Comment),' ','')><color 128 128 128><opt(u(save_in),' - ','')>",
811
+ EditorMenubarName = false,
812
+ __hierarchy_cache = true,
813
+ DescriptionFlavor = T(334322641039, --[[PresetDef DisplayPreset value]] "<description><newline><newline><flavor><flavor_text></flavor>"),
814
+ NewFeatureTag = T(820790154429, --[[PresetDef DisplayPreset value]] "<em>NEW!</em> "),
815
+ }
816
+
817
+ function DisplayPreset:GetDisplayName()
818
+ if self.new_in == config.NewFeaturesUpdate and self.NewFeatureTag then
819
+ return self.NewFeatureTag .. self.display_name
820
+ else
821
+ return self.display_name
822
+ end
823
+ end
824
+
825
+ function DisplayPreset:GetDisplayNameCaps()
826
+ if self.new_in == config.NewFeaturesUpdate and self.NewFeatureTag then
827
+ return self.NewFeatureTag .. self.display_name_caps
828
+ else
829
+ return self.display_name_caps
830
+ end
831
+ end
832
+
833
+ function DisplayPreset:GetDescription()
834
+ local description = self.description
835
+ if GetUIStyleGamepad() and self.description_gamepad ~= "" then
836
+ description = self.description_gamepad
837
+ end
838
+
839
+ if self.flavor_text == "" or not self.DescriptionFlavor then
840
+ return description
841
+ else
842
+ return T{self.DescriptionFlavor, self, description = description}
843
+ end
844
+ end
845
+
846
+ function DisplayPreset:OnEditorNew()
847
+ self.new_in = config.NewFeaturesUpdate or nil
848
+ end
849
+
850
+ ----- DisplayPreset DisplayPresetCombo
851
+
852
+ function DisplayPresetCombo(class, filter, ...)
853
+ local params = pack_params(...)
854
+ return function()
855
+ return ForEachPreset(class, function(preset, group, items, filter, params)
856
+ if not filter or filter(preset, unpack_params(params)) then
857
+ items[#items + 1] = { value = preset.id, text = preset:GetDisplayName() }
858
+ end
859
+ end, {}, filter, params)
860
+ end
861
+ end
862
+
863
+ DefineClass.GameDifficultyDef = {
864
+ __parents = { "MsgReactionsPreset", "DisplayPreset", },
865
+ __generated_by_class = "PresetDef",
866
+
867
+ properties = {
868
+ { id = "effects", name = "Effects on NewMapLoaded",
869
+ editor = "nested_list", default = false, base_class = "Effect", all_descendants = true, auto_expand = true, },
870
+ },
871
+ HasGroups = false,
872
+ HasSortKey = true,
873
+ HasParameters = true,
874
+ GlobalMap = "GameDifficulties",
875
+ EditorMenubarName = "Game Difficulty",
876
+ EditorIcon = "CommonAssets/UI/Icons/bullet list.png",
877
+ EditorMenubar = "Editors.Lists",
878
+ Documentation = "Creates a custom game difficulty and sets custom effects on the loaded map.",
879
+ }
880
+
881
+ DefineModItemPreset("GameDifficultyDef", { EditorName = "Game difficulty", EditorSubmenu = "Gameplay" })
882
+
883
+ ----- GameDifficultyDef
884
+
885
+ function GetGameDifficulty()
886
+ local game = Game
887
+ return game and game.game_difficulty
888
+ end
889
+
890
+ function OnMsg.NewMapLoaded()
891
+ if not mapdata.GameLogic or not Game then return end
892
+ local difficulty = GameDifficulties[GetGameDifficulty()]
893
+ ExecuteEffectList(difficulty and difficulty.effects, Game)
894
+ end
895
+
896
+ function AddDifficultyLootConditions()
897
+ ForEachPreset("GameDifficultyDef", function(preset, group)
898
+ local difficulty = preset.id
899
+ LootCondition["Difficulty " .. difficulty] = function() return (Game and Game.game_difficulty) == difficulty end
900
+ end)
901
+ end
902
+
903
+ OnMsg.PresetSave = AddDifficultyLootConditions
904
+ OnMsg.DataLoaded = AddDifficultyLootConditions
905
+
906
+ DefineClass.GameRuleDef = {
907
+ __parents = { "MsgReactionsPreset", "DisplayPreset", },
908
+ __generated_by_class = "PresetDef",
909
+
910
+ properties = {
911
+ { category = "General", id = "init_as_active", name = "Active by default",
912
+ editor = "bool", default = false, },
913
+ { category = "General", id = "option", name = "Add as option",
914
+ editor = "bool", default = false, },
915
+ { id = "option_id", name = "Option Id",
916
+ editor = "text", default = false,
917
+ no_edit = function(self) return not self.option end, },
918
+ { category = "General", id = "exclusionlist", name = "Exclusion List", help = "List of other game rules that are not compatible with this one. If this rule is active the player won't be able to enable the rules in the exclusion list.",
919
+ editor = "preset_id_list", default = {}, preset_class = "GameRuleDef", item_default = "", },
920
+ { id = "effects", name = "Effects on NewMapLoaded",
921
+ editor = "nested_list", default = false, base_class = "Effect", all_descendants = true, },
922
+ },
923
+ HasGroups = false,
924
+ HasSortKey = true,
925
+ HasParameters = true,
926
+ GlobalMap = "GameRuleDefs",
927
+ EditorMenubarName = "Game Rules",
928
+ EditorIcon = "CommonAssets/UI/Icons/bullet list.png",
929
+ EditorMenubar = "Editors.Lists",
930
+ Documentation = "Defines a new game rule that can be activated by players when starting a new game.",
931
+ }
932
+
933
+ DefineModItemPreset("GameRuleDef", { EditorName = "Game rule", EditorSubmenu = "Gameplay" })
934
+
935
+ function GameRuleDef:IsCompatible(active_rules)
936
+ local exclusions = table.invert(self.exclusionlist or empty_table)
937
+ local id = self.id
938
+ for rule_id in pairs(active_rules) do
939
+ if exclusions[rule_id] or table.find(GameRuleDefs[rule_id].exclusionlist or empty_table, id) then
940
+ return false
941
+ end
942
+ end
943
+ return true
944
+ end
945
+
946
+ ----- GameRuleDef
947
+
948
+ function IsGameRuleActive(rule_id)
949
+ local game = Game
950
+ return (game and game.game_rules or empty_table)[rule_id]
951
+ end
952
+
953
+ function OnMsg.NewMapLoaded()
954
+ if not mapdata.GameLogic or not Game then return end
955
+ ForEachPreset("GameRuleDef", function(rule)
956
+ if IsGameRuleActive(rule.id) then
957
+ ExecuteEffectList(rule.effects, Game)
958
+ end
959
+ end)
960
+ end
961
+
962
+ DefineClass.GameStateDef = {
963
+ __parents = { "DisplayPreset", },
964
+ __generated_by_class = "PresetDef",
965
+
966
+ properties = {
967
+ { id = "ShowInFilter", name = "Show in filters",
968
+ editor = "bool", default = true, },
969
+ { id = "PlayFX", name = "Play FX",
970
+ editor = "bool", default = true, },
971
+ { id = "MapState", name = "Is map state", help = "Map states are removed when exiting a map",
972
+ editor = "bool", default = true, },
973
+ { id = "PersistInSaveGame", name = "Persist in save game",
974
+ editor = "bool", default = false, },
975
+ { id = "Gossip", name = "Gossip", help = "Send gossip on state change",
976
+ editor = "bool", default = false, },
977
+ { id = "GroupExclusive", name = "Exclusive for the group", help = "When set, removes any other game states from the same group",
978
+ editor = "bool", default = false, },
979
+ { id = "Color", name = "Color", help = "Used for easier visual identification of the game state in editors",
980
+ editor = "color", default = 4278190080, },
981
+ { id = "Icon", name = "Icon",
982
+ editor = "ui_image", default = false, },
983
+ { id = "AutoSet", name = "Auto set", help = "State is recalculated on every GameStateChange",
984
+ editor = "nested_list", default = false, base_class = "Condition", },
985
+ { category = "Debug", id = "CurrentState", name = "Current state",
986
+ editor = "bool", default = false, dont_save = true, },
987
+ },
988
+ HasSortKey = true,
989
+ GlobalMap = "GameStateDefs",
990
+ EditorMenubarName = "Game States",
991
+ EditorMenubar = "Editors.Lists",
992
+ EditorViewPresetPostfix = Untranslated("<select(AutoSet,'',' [autoset]')><select(CurrentState,'',' [on]')>"),
993
+ }
994
+
995
+ function GameStateDef:GetCurrentState()
996
+ return GameState[self.id]
997
+ end
998
+
999
+ function GameStateDef:SetCurrentState(state)
1000
+ return ChangeGameState(self.id, state)
1001
+ end
1002
+
1003
+ function GameStateDef:OnDataUpdated()
1004
+ RebuildAutoSetGameStates()
1005
+ end
1006
+
1007
+ ----- GameStateDef PlayFX
1008
+
1009
+ function OnMsg.GameStateChanged(changed)
1010
+ local fx
1011
+ for state, active in pairs(changed) do
1012
+ local def = GameStateDefs[state]
1013
+ if def and def.PlayFX then
1014
+ fx = fx or {}
1015
+ fx[#fx + 1] = state
1016
+ fx[state] = active
1017
+ end
1018
+ end
1019
+ if not fx then return end
1020
+ table.sort(fx)
1021
+ -- as certain FX depend on game states, delay the actual FX trigger to allow all of the game state changes to be invoked
1022
+ CreateGameTimeThread(function(fx)
1023
+ for _, state in ipairs(fx) do
1024
+ if fx[state] then
1025
+ PlayFX(state, "start")
1026
+ else
1027
+ PlayFX(state, "end")
1028
+ end
1029
+ end
1030
+ end, fx)
1031
+ end
1032
+
1033
+ function OnMsg.GatherFXActions(list)
1034
+ ForEachPreset("GameStateDef", function(state, group, list)
1035
+ if state.PlayFX then
1036
+ list[#list + 1] = state.id
1037
+ end
1038
+ end, list)
1039
+ end
1040
+
1041
+ ----- GameStateDef GetGameStateFilter
1042
+
1043
+ function GetGameStateFilter()
1044
+ return ForEachPreset("GameStateDef", function(state, group, items)
1045
+ if state.ShowInFilter then
1046
+ items[#items + 1] = state.id
1047
+ end
1048
+ end, {})
1049
+ end
1050
+
1051
+ ----- GameStateDef MapState
1052
+
1053
+ function OnMsg.DoneMap()
1054
+ local map_states = ForEachPreset("GameStateDef", function(state, group, map_states, GameState)
1055
+ if state.MapState and GameState[state.id] then
1056
+ map_states[state.id] = false
1057
+ end
1058
+ end, {}, GameState)
1059
+ ChangeGameState(map_states)
1060
+ end
1061
+
1062
+ ----- GameStateDef Persist
1063
+
1064
+ function OnMsg.PersistSave(data)
1065
+ local persisted_gamestate = {}
1066
+ ForEachPreset("GameStateDef", function(state, group, persisted_gamestate, GameState)
1067
+ if state.PersistInSaveGame and GameState[state.id] then
1068
+ persisted_gamestate[state.id] = true
1069
+ end
1070
+ end, persisted_gamestate, GameState)
1071
+ if next(persisted_gamestate) then
1072
+ data.GameState = persisted_gamestate
1073
+ end
1074
+ end
1075
+
1076
+ function OnMsg.PersistLoad(data)
1077
+ local persisted_gamestate = data.GameState or empty_table
1078
+ ForEachPreset("GameStateDef", function(state, group, persisted_gamestate, GameState)
1079
+ if state.PersistInSaveGame then
1080
+ GameState[state.id] = persisted_gamestate[state.id] or false
1081
+ end
1082
+ end, persisted_gamestate, GameState)
1083
+ end
1084
+
1085
+ DefineClass.LootDef = {
1086
+ __parents = { "Preset", },
1087
+ __generated_by_class = "PresetDef",
1088
+
1089
+ properties = {
1090
+ { category = "Loot", id = "loot", name = "Loot",
1091
+ editor = "choice", default = "random", items = function (self) return {"random", "all", "first", "cycle", "each then last"} end, },
1092
+ { category = "Test", id = "TestDlcs", name = "Test dlcs",
1093
+ editor = "set", default = set(), dont_save = true, items = function (self) return DlcComboItems() end, },
1094
+ { category = "Test", id = "TestConditions", name = "Loot conditions",
1095
+ editor = "set", default = set(), dont_save = true, items = function (self) return table.keys2(LootCondition, true) end, },
1096
+ { category = "Test", id = "TestGameConditions", name = "Test Additional Conditions", help = "If not set the additional conditions are ignored during testing. \nIf set, the additional conditions are evaluated against the current state of the game. Therefore test results can change when the current game state changes.",
1097
+ editor = "bool", default = false, },
1098
+ { category = "Test", id = "TestFile", name = "Output CSV",
1099
+ editor = "text", default = "svnProject/items.csv", dont_save = true, buttons = { {name = "Write", func = "WriteChancesCSV"}, }, },
1100
+ { category = "Test", id = "TestResults", name = "Test results",
1101
+ editor = "text", default = false, dont_save = true, read_only = true, lines = 1, max_lines = 30, },
1102
+ },
1103
+ GlobalMap = "LootDefs",
1104
+ ContainerClass = "LootDefEntry",
1105
+ EditorMenubarName = "Loot Tables",
1106
+ EditorShortcut = "Ctrl-L",
1107
+ EditorIcon = "CommonAssets/UI/Icons/currency dollar finance money payment.png",
1108
+ EditorView = Untranslated("<if(eq(loot,'all'))><color 255 128 64></if><id><color 0 128 0><opt(u(Comment),' - ','')>"),
1109
+ EditorPreview = Untranslated("<Preview>"),
1110
+ Documentation = "Creates a new loot definition that contains possible items to drop.",
1111
+ }
1112
+
1113
+ DefineModItemPreset("LootDef", { EditorName = "Loot definition", EditorSubmenu = "Gameplay" })
1114
+
1115
+ function LootDef:GenerateLootSeed(init_seed, looter, looted)
1116
+ local loot, seed = self.loot
1117
+ if loot == "cycle" or loot == "each then last" then
1118
+ seed = (init_seed == -1) and InteractionRand(nil, "Loot", looter, looted) or (init_seed + 1)
1119
+ seed = (loot == "each then last") and Min(seed, #self) or seed
1120
+ else
1121
+ seed = InteractionRand(nil, "Loot", looter, looted)
1122
+ end
1123
+
1124
+ return seed
1125
+ end
1126
+
1127
+ function LootDef:GenerateLoot(looter, looted, seed, items, modifiers, amount_modifier)
1128
+ local rand, loot
1129
+ seed = seed or self:GenerateLootSeed(nil, looter, looted)
1130
+ NetUpdateHash("LootDef:GenerateLoot", seed)
1131
+
1132
+ if self.loot == "random" then
1133
+ local weight, none_weight = 0, 0
1134
+ for _, entry in ipairs(self) do
1135
+ if entry:TestConditions(looter, looted) then
1136
+ if entry.class == "LootEntryNoLoot" then
1137
+ none_weight = none_weight + entry.weight
1138
+ else
1139
+ weight = weight + entry.weight
1140
+ end
1141
+ end
1142
+ end
1143
+ rand, seed = BraidRandom(seed, weight + none_weight)
1144
+ if rand >= weight then return end
1145
+ for _, entry in ipairs(self) do
1146
+ if entry.class ~= "LootEntryNoLoot" and entry:TestConditions(looter, looted) then
1147
+ rand = rand - entry.weight
1148
+ if rand < 0 then
1149
+ loot = entry:GenerateLoot(looter, looted, seed, items, modifiers, amount_modifier)
1150
+ break
1151
+ end
1152
+ end
1153
+ end
1154
+ elseif self.loot == "all" then
1155
+ for _, entry in ipairs(self) do
1156
+ rand, seed = BraidRandom(seed)
1157
+ if entry:TestConditions(looter, looted) then
1158
+ local entry_loot = entry:GenerateLoot(looter, looted, seed, items, modifiers, amount_modifier)
1159
+ loot = loot or entry_loot
1160
+ end
1161
+ end
1162
+ elseif self.loot == "first" then
1163
+ for _, entry in ipairs(self) do
1164
+ if entry:TestConditions(looter, looted) then
1165
+ loot = entry:GenerateLoot(looter, looted, seed, items, modifiers, amount_modifier)
1166
+ break
1167
+ end
1168
+ end
1169
+ elseif self.loot == "cycle" then
1170
+ local start_idx = 1 + seed % #self
1171
+ local idx = start_idx
1172
+ local entry = self[idx]
1173
+ local entry_ok = entry:TestConditions(looter, looted)
1174
+ while not entry_ok do
1175
+ idx = (idx < #self) and (idx + 1) or 1
1176
+ entry = self[idx]
1177
+ entry_ok = (idx ~= start_idx) and entry:TestConditions(looter, looted)
1178
+ end
1179
+ if entry_ok then
1180
+ loot = entry:GenerateLoot(looter, looted, seed, items, modifiers, amount_modifier)
1181
+ end
1182
+ else -- if self.loot == "each then last" then
1183
+ if seed < #self then
1184
+ for idx = seed, #self do
1185
+ local entry = self[idx]
1186
+ if entry:TestConditions(looter, looted) then
1187
+ loot = entry:GenerateLoot(looter, looted, seed, items, modifiers, amount_modifier)
1188
+ break
1189
+ end
1190
+ end
1191
+ else
1192
+ for idx = #self, 1, -1 do
1193
+ local entry = self[idx]
1194
+ if entry:TestConditions(looter, looted) then
1195
+ loot = entry:GenerateLoot(looter, looted, seed, items, modifiers, amount_modifier)
1196
+ break
1197
+ end
1198
+ end
1199
+ end
1200
+ end
1201
+ Msg("GenerateLoot", self, items, loot)
1202
+ return loot
1203
+ end
1204
+
1205
+ function LootDef:ListChances(items, env, chance, amount_modifier)
1206
+ if self.loot == "random" then
1207
+ local weight = 0
1208
+ for _, entry in ipairs(self) do
1209
+ if entry:ListChancesTest(env) then
1210
+ weight = weight + entry.weight
1211
+ end
1212
+ end
1213
+ if weight <= 0 then return end
1214
+ for _, entry in ipairs(self) do
1215
+ if entry.class ~= "LootEntryNoLoot" and entry:ListChancesTest(env) then
1216
+ entry:ListChances(items, env, chance * entry.weight / weight, amount_modifier)
1217
+ end
1218
+ end
1219
+ elseif self.loot == "all" then
1220
+ for _, entry in ipairs(self) do
1221
+ if entry:ListChancesTest(env) then
1222
+ entry:ListChances(items, env, chance, amount_modifier)
1223
+ end
1224
+ end
1225
+ else -- self.loot == "first"
1226
+ for _, entry in ipairs(self) do
1227
+ if entry:ListChancesTest(env) then
1228
+ return entry:ListChances(items, env, chance, amount_modifier)
1229
+ end
1230
+ end
1231
+ end
1232
+ end
1233
+
1234
+ function LootDef:SetTestDlcs(v)
1235
+ LootTestDlcs=v
1236
+ end
1237
+
1238
+ function LootDef:GetTestDlcs()
1239
+ return LootTestDlcs
1240
+ end
1241
+
1242
+ function LootDef:SetTestConditions(v)
1243
+ LootTestConditions=v
1244
+ end
1245
+
1246
+ function LootDef:GetTestConditions()
1247
+ return LootTestConditions
1248
+ end
1249
+
1250
+ function LootDef:SetTestFile(v)
1251
+ LootTestFile=v
1252
+ end
1253
+
1254
+ function LootDef:GetTestFile()
1255
+ return LootTestFile
1256
+ end
1257
+
1258
+ function LootDef:WriteChancesCSV(root)
1259
+ local item_list = root:GetTestItems()
1260
+ SaveCSV(LootTestFile, item_list, nil, {"Chance (%)", "Item"})
1261
+ end
1262
+
1263
+ function LootDef:GetTestItems()
1264
+ local env = {
1265
+ dlcs = {[""] = true},
1266
+ conditions = {[""] = true},
1267
+ }
1268
+ for v in pairs(LootTestDlcs) do
1269
+ env.dlcs[v] = true
1270
+ end
1271
+ for v in pairs(LootTestConditions) do
1272
+ env.conditions[v] = true
1273
+ end
1274
+ env.game_conditions = self.TestGameConditions
1275
+
1276
+ local items = {}
1277
+ self:ListChances(items, env, 1.0)
1278
+ local item_list = {}
1279
+ for item, chance in pairs(items) do
1280
+ if chance > 0.000000001 then
1281
+ item_list[#item_list + 1] = { chance, item }
1282
+ end
1283
+ end
1284
+ table.sort(item_list,function(a,b)
1285
+ if a[1] == b[1] then
1286
+ return a[2] < b[2]
1287
+ end
1288
+ return a[1]>b[1]
1289
+ end)
1290
+ return item_list
1291
+ end
1292
+
1293
+ function LootDef:GetTestResults()
1294
+ local item_list = self:GetTestItems()
1295
+ local nothing = 1.0
1296
+ for i, pair in ipairs(item_list) do
1297
+ item_list[i] = string.format("%6.02f%% %s", pair[1] * 100, pair[2])
1298
+ nothing = nothing - pair[1]
1299
+ end
1300
+ if nothing > 0.0001 then
1301
+ item_list[#item_list + 1] = string.format("%6.02f%% Nothing", nothing * 100)
1302
+ end
1303
+ return table.concat(item_list, "\n")
1304
+ end
1305
+
1306
+ function LootDef:GetPreview()
1307
+ local texts = {}
1308
+ for _, entry in ipairs(self) do
1309
+ texts[#texts+1] = entry:GetEditorPreview()
1310
+ end
1311
+ return table.concat(texts, "; ")
1312
+ end
1313
+
1314
+ ----- LootDef
1315
+
1316
+ LootTestDlcs = {}
1317
+ LootTestConditions = {}
1318
+ LootTestFile = "svnProject/items.csv"
1319
+
1320
+ ----- LootDef mod item
1321
+
1322
+ if config.Mods then
1323
+ AppendClass.ModItemLootDef = {
1324
+ properties = {
1325
+ { id = "TestDlcs", },
1326
+ { id = "TestConditions", },
1327
+ { id = "TestGameConditions", },
1328
+ { id = "TestFile", },
1329
+ { id = "TestResults", },
1330
+ },
1331
+ }
1332
+
1333
+ DefineClass.ModItemLootDefEdit = {
1334
+ __parents = { "ModItemChangePropBase" },
1335
+ properties = {
1336
+ { id = "TargetClass" },
1337
+ { id = "TargetProp" },
1338
+ { id = "EditType" },
1339
+ { id = "TargetFunc" },
1340
+ { category = "Mod", id = "TargetId", name = "Loot table", default = "", editor = "choice", items = function(self, prop_meta) return PresetsCombo(self.TargetClass)(self, prop_meta) end, no_edit = function(self) return not self:ResolveTargetMap() end, reapply = true },
1341
+ { category = "Change Property", id = "TargetValue", name = "Entries to append", default = false, editor = "bool", },
1342
+ },
1343
+ TargetClass = "LootDef",
1344
+ TargetProp = "__children",
1345
+ EditType = "Append To Table",
1346
+
1347
+ EditorName = "Loot definition change",
1348
+ EditorSubmenu = "Gameplay",
1349
+ Documentation = "Appends entries to a specific Loot definition. The Test button will apply the change.",
1350
+ }
1351
+
1352
+ function ModItemLootDefEdit:GetModItemDescription()
1353
+ if self.name == "" and self.TargetId ~= "" then
1354
+ return Untranslated("<u(TargetId)>[...]")
1355
+ end
1356
+ return self.ModItemDescription
1357
+ end
1358
+
1359
+ function ModItemLootDefEdit:ResolvePropTarget()
1360
+ return {
1361
+ id = "__children",
1362
+ editor = "nested_list",
1363
+ base_class = LootDef.ContainerClass,
1364
+ default = false,
1365
+ }
1366
+ end
1367
+
1368
+ function ModItemLootDefEdit:GetPropValue()
1369
+ local preset = self:ResolveTargetPreset()
1370
+ local result = {}
1371
+ for i,child in ipairs(preset) do
1372
+ result[#result + 1] = child:Clone()
1373
+ end
1374
+ return result
1375
+ end
1376
+
1377
+ function ModItemLootDefEdit:AssignValue(preset, value)
1378
+ table.iclear(preset)
1379
+ table.iappend(preset, value)
1380
+ ModLogF("%s %s: %s[...] = %s", self.class, self.mod.title, self.TargetId, ValueToStr(value))
1381
+ end
1382
+ end
1383
+
1384
+ DefineClass.LootDefEntry = {
1385
+ __parents = { "PropertyObject", },
1386
+ __generated_by_class = "ClassDef",
1387
+
1388
+ properties = {
1389
+ { category = "Conditions", id = "disable", name = "Disable",
1390
+ editor = "bool", default = false, },
1391
+ { category = "Conditions", id = "comment", name = "Comment",
1392
+ editor = "text", default = false, },
1393
+ { category = "Conditions", id = "weight", name = "Weight",
1394
+ editor = "number", default = 1000, scale = 1000, min = 0, max = 1000000000, },
1395
+ { category = "Conditions", id = "dlc", name = "Require dlc",
1396
+ editor = "choice", default = "", items = function (self) return DlcComboItems() end, },
1397
+ { category = "Conditions", id = "negate", name = "Negate loot condition",
1398
+ editor = "bool", default = false, },
1399
+ { category = "Conditions", id = "condition", name = "Loot condition", help = "Loot specific conditions (defined in LootConditions) such as game difficulty. These can be manipulated in Test section to simulate expected loot results.",
1400
+ editor = "choice", default = "", items = function (self) return table.keys2(LootCondition, true) end, },
1401
+ { category = "Conditions", id = "game_conditions", name = "Additional conditions",
1402
+ editor = "nested_list", default = false, base_class = "Condition", },
1403
+ },
1404
+ StoreAsTable = true,
1405
+ EditorView = Untranslated("<if(disable)>*** </if><FormatAsFloat(weight,1000)><tab 50><dlc><tab 170><if(negate)>!</if><condition><tab 300><EntryView><color 0 128 0><opt(u(comment),'<tab 600>','')>"),
1406
+ EntryView = Untranslated("<class>"),
1407
+ EditorName = "Loot Entry",
1408
+ }
1409
+
1410
+ function LootDefEntry:GetEditorPreview()
1411
+ local text =(T{self.EntryView, self})
1412
+ local txt
1413
+ if self.weight~=1000 then
1414
+ txt =(txt or "(")..Untranslated("w:"..self.weight)
1415
+ end
1416
+ if self.dlc~="" then
1417
+ txt =(txt or "(")..Untranslated(" d:"..self.dlc)
1418
+ end
1419
+
1420
+ local condition_texts = {}
1421
+ if self.condition~="" then
1422
+ condition_texts[#condition_texts+1] = Untranslated((self.negate and " !" or " ") .. self.condition)
1423
+ end
1424
+ for _, condition in ipairs(self.game_conditions) do
1425
+ condition_texts[#condition_texts+1] = Untranslated(_InternalTranslate(condition:GetEditorView(), condition, false))
1426
+ end
1427
+ if next(condition_texts) then
1428
+ txt = (txt or "(") ..table.concat(condition_texts, ";")
1429
+ end
1430
+ if txt then
1431
+ text = text..txt..Untranslated(")")
1432
+ end
1433
+ return text
1434
+ end
1435
+
1436
+ function LootDefEntry:TestConditions(looter, looted)
1437
+ if self.disable then return end
1438
+ if not IsDlcAvailable(self.dlc) then
1439
+ return false
1440
+ end
1441
+ local res = LootCondition[self.condition](looter, looted)
1442
+ if self.negate then
1443
+ res = not res
1444
+ end
1445
+ res = res and EvalConditionList(self.game_conditions, looter, looted)
1446
+ return res
1447
+ end
1448
+
1449
+ function LootDefEntry:GenerateLoot(looter, looted, seed, items, modifiers, amount_modifier)
1450
+ assert(false, self.class .. ":GenerateLoot() not implemented")
1451
+ end
1452
+
1453
+ function LootDefEntry:ListChancesTest(env)
1454
+ if self.disable or not env.dlcs[self.dlc] then
1455
+ return
1456
+ end
1457
+ local res = env.conditions[self.condition]
1458
+ if self.negate then
1459
+ res = not res
1460
+ end
1461
+ if env.game_conditions then
1462
+ res = res and EvalConditionList(self.game_conditions)
1463
+ end
1464
+ return res
1465
+ end
1466
+
1467
+ function LootDefEntry:ListChances(items, env, chance, amount_modifier)
1468
+ assert(false, self.class .. ":ListChances() not implemented")
1469
+ end
1470
+
1471
+ ----- LootDefEntry LootCondition
1472
+
1473
+ LootCondition = rawget(_G, "LootCondition") or {
1474
+ [""] = function(looter, looted) return true end,
1475
+ }
1476
+ setmetatable(LootCondition, {
1477
+ __index = function() return empty_func end,
1478
+ })
1479
+
1480
+ DefineClass.LootEntryLootDef = {
1481
+ __parents = { "LootDefEntry", },
1482
+ __generated_by_class = "ClassDef",
1483
+
1484
+ properties = {
1485
+ { category = "Loot", id = "loot_def", name = "Loot def",
1486
+ editor = "preset_id", default = false, preset_class = "LootDef", },
1487
+ { category = "Loot", id = "amount_modifier", name = "Amount modifier", help = "Modifies the amount of resources generated from the loot",
1488
+ editor = "number", default = 1000000, scale = 1000000, step = 100000, min = 1000, max = 1000000000, },
1489
+ },
1490
+ EntryView = Untranslated("<loot_def> <if(not_eq(amount_modifier,1000000))>x<FormatAsFloat(amount_modifier,1000000,3,true)></if>"),
1491
+ EditorName = "Invoke Loot Definition",
1492
+ }
1493
+
1494
+ function LootEntryLootDef:GenerateLoot(looter, looted, seed, items, modifiers, amount_modifier)
1495
+ local loot_def = LootDefs[self.loot_def]
1496
+ if not loot_def then return end
1497
+ return loot_def:GenerateLoot(looter, looted, seed, items, modifiers, MulDivRound(amount_modifier or 1000000, self.amount_modifier, 1000000))
1498
+ end
1499
+
1500
+ function LootEntryLootDef:ListChances(items, env, chance, amount_modifier)
1501
+ local loot_def = LootDefs[self.loot_def]
1502
+ if not loot_def then return end
1503
+ local nesting = env.nesting or 0
1504
+ if nesting > 100 then
1505
+ local item = "LootDef: " .. self.loot_def
1506
+ items[item] = (items[item] or 0.0) + chance
1507
+ return
1508
+ end
1509
+ env.nesting = nesting + 1
1510
+ loot_def:ListChances(items, env, chance, MulDivRound(amount_modifier or 1000000, self.amount_modifier, 1000000))
1511
+ assert(env.nesting == nesting + 1)
1512
+ env.nesting = nesting
1513
+ end
1514
+
1515
+ function LootEntryLootDef:GetError()
1516
+ local loot_def = GetParentTableOfKindNoCheck(self, "LootDef")
1517
+ if loot_def and self.loot_def == loot_def.id then
1518
+ return "Recursive LootDef!"
1519
+ end
1520
+ end
1521
+
1522
+ DefineClass.LootEntryNoLoot = {
1523
+ __parents = { "LootDefEntry", },
1524
+ __generated_by_class = "ClassDef",
1525
+
1526
+ EntryView = Untranslated("<color 192 0 0 >No loot"),
1527
+ EditorName = "No Loot",
1528
+ }
1529
+
1530
+ function LootEntryNoLoot:ListChances(items, env, chance, amount_modifier)
1531
+
1532
+ end
1533
+
1534
+ function LootEntryNoLoot:GenerateLoot(looter, looted, seed, items, modifiers, amount_modifier)
1535
+
1536
+ end
1537
+
1538
+ DefineClass.NoisePreset = {
1539
+ __parents = { "Preset", "PerlinNoise", },
1540
+ __generated_by_class = "PresetDef",
1541
+
1542
+ GlobalMap = "NoisePresets",
1543
+ PresetClass = "NoisePreset",
1544
+ EditorMenubarName = "Noise Editor",
1545
+ EditorIcon = "CommonAssets/UI/Icons/bell message new notification sign.png",
1546
+ EditorMenubar = "Map",
1547
+ }
1548
+
1549
+ DefineClass.ObjMaterial = {
1550
+ __parents = { "ListPreset", },
1551
+ __generated_by_class = "PresetDef",
1552
+
1553
+ properties = {
1554
+ { id = "invulnerable", name = "Invulnerable",
1555
+ editor = "bool", default = false, },
1556
+ { id = "impenetrable", name = "Impenetrable",
1557
+ editor = "bool", default = false, },
1558
+ { id = "is_prop", name = "Prop Material",
1559
+ editor = "bool", default = false, },
1560
+ { id = "max_hp", name = "Max HP",
1561
+ editor = "number", default = 100, },
1562
+ { id = "breakdown_defense", name = "Breakdown Defense", help = "If the material is attached to a door, this defense is added to the break difficulty.",
1563
+ editor = "number", default = 30, },
1564
+ { id = "destruction_propagation_strength", name = "Destruction Propagation Strength", help = "If the material is attached to a door, this defense is added to the break difficulty.",
1565
+ editor = "number", default = 0, },
1566
+ { id = "FXTarget", name = "FX Target",
1567
+ editor = "text", default = false, },
1568
+ { id = "noise_on_hit", name = "Noise On Hit",
1569
+ editor = "number", default = 0, min = 0, },
1570
+ { id = "noise_on_break", name = "Noise On Break",
1571
+ editor = "number", default = 0, min = 0, },
1572
+ },
1573
+ GlobalMap = "ObjMaterials",
1574
+ EditorMenubarName = "ObjMaterial Editor",
1575
+ FilterClass = "ObjMaterialFilter",
1576
+ }
1577
+
1578
+ DefineClass.ObjMaterialFilter = {
1579
+ __parents = { "GedFilter", },
1580
+ __generated_by_class = "ClassDef",
1581
+
1582
+ properties = {
1583
+ { id = "invulnerable", name = "Invulnerable",
1584
+ editor = "set", default = false, max_items_in_set = 1, items = function (self) return { "true", "false" } end, },
1585
+ { id = "impenetrable", name = "Impenetrable",
1586
+ editor = "set", default = false, max_items_in_set = 1, items = function (self) return { "true", "false" } end, },
1587
+ { id = "is_prop", name = "Prop Material",
1588
+ editor = "set", default = false, max_items_in_set = 1, items = function (self) return { "true", "false" } end, },
1589
+ },
1590
+ }
1591
+
1592
+ function ObjMaterialFilter:FilterObject(obj)
1593
+ local function filter(prop)
1594
+ local filter, value = self[prop], obj[prop]
1595
+ return filter and (filter["true"] and not value or filter["false"] and value)
1596
+ end
1597
+ return not (filter("invulnerable") or filter("impenetrable") or filter("is_prop"))
1598
+ end
1599
+
1600
+ DefineClass.PhotoFilterPreset = {
1601
+ __parents = { "Preset", },
1602
+ __generated_by_class = "PresetDef",
1603
+
1604
+ properties = {
1605
+ { category = "General", id = "display_name", name = "Display Name",
1606
+ editor = "text", default = false, translate = true, },
1607
+ { category = "General", id = "description", name = "Description",
1608
+ editor = "text", default = false, translate = true, },
1609
+ { category = "General", id = "shader_file", name = "Shader Filename",
1610
+ editor = "browse", default = "", folder = "Shaders/", filter = "FX files|*.fx", },
1611
+ { category = "General", id = "shader_pass", name = "Shader Pass",
1612
+ editor = "text", default = "Generic", },
1613
+ { category = "General", id = "texture1", name = "Texture 1",
1614
+ editor = "browse", default = "", filter = "Image files|*.tga", },
1615
+ { category = "General", id = "texture2", name = "Texture 2",
1616
+ editor = "browse", default = "", filter = "Image files|*.tga", },
1617
+ { category = "General", id = "activate", name = "Run on activation",
1618
+ editor = "func", default = function (self) end, },
1619
+ { category = "General", id = "deactivate", name = "Run on deactivation",
1620
+ editor = "func", default = function (self) end, },
1621
+ },
1622
+ HasSortKey = true,
1623
+ GlobalMap = "PhotoFilterPresetMap",
1624
+ EditorMenubarName = "Photo Filters",
1625
+ EditorIcon = "CommonAssets/UI/Icons/camera digital image media photo photography picture.png",
1626
+ EditorMenubar = "Editors.Other",
1627
+ }
1628
+
1629
+ function PhotoFilterPreset:GetShaderDescriptor()
1630
+ return {
1631
+ shader = self.shader_file,
1632
+ pass = self.shader_pass,
1633
+ tex1 = self.texture1,
1634
+ tex2 = self.texture2,
1635
+ activate = self.activate,
1636
+ deactivate = self.deactivate,
1637
+ }
1638
+ end
1639
+
1640
+ DefineClass.PhotoFramePreset = {
1641
+ __parents = { "Preset", },
1642
+ __generated_by_class = "PresetDef",
1643
+
1644
+ properties = {
1645
+ { category = "General", id = "display_name", name = "Display Name",
1646
+ editor = "text", default = false, translate = true, },
1647
+ { id = "frame_file", name = "Frame Filename",
1648
+ editor = "ui_image", default = false, filter = "Image files|*.png;*.dds", force_extension = "", },
1649
+ { category = "General", id = "translate", name = "Translate",
1650
+ editor = "bool", default = true, },
1651
+ },
1652
+ HasSortKey = true,
1653
+ GlobalMap = "PhotoFramePresetMap",
1654
+ EditorMenubarName = "Photo Frames",
1655
+ EditorIcon = "CommonAssets/UI/Icons/camera digital image media photo photography picture.png",
1656
+ EditorMenubar = "Editors.Other",
1657
+ Documentation = "Allows adding new frames for the photo mode.",
1658
+ }
1659
+
1660
+ DefineModItemPreset("PhotoFramePreset", { EditorName = "Photo Mode - frame", EditorSubmenu = "Other" })
1661
+
1662
+ function PhotoFramePreset:GetName()
1663
+ if self.translate then
1664
+ return self.display_name
1665
+ else
1666
+ return Untranslated(self.display_name)
1667
+ end
1668
+ end
1669
+
1670
+ DefineClass.RadioStationPreset = {
1671
+ __parents = { "DisplayPreset", },
1672
+ __generated_by_class = "PresetDef",
1673
+
1674
+ properties = {
1675
+ { category = "General", id = "Folder", name = "Folder",
1676
+ editor = "browse", default = "Music", folder = "Music", filter = "folder", },
1677
+ { category = "General", id = "SilenceDuration", name = "Silence between tracks",
1678
+ editor = "number", default = 1000, scale = "sec", },
1679
+ { category = "General", id = "Volume", name = "Volume", help = "Volume to play at each new track",
1680
+ editor = "number", default = false, },
1681
+ { category = "General", id = "FadeOutTime", name = "Fade Out Time", help = "Time to fade out to a new volume",
1682
+ editor = "number", default = false,
1683
+ no_edit = function(self) return not self.Volume end, scale = "sec", },
1684
+ { category = "General", id = "FadeOutVolume", name = "FadeOutVolume", help = "Volume to fade out to after the fade out time",
1685
+ editor = "number", default = false,
1686
+ no_edit = function(self) return not self.FadeOutTime end, },
1687
+ { category = "General", id = "Mode", name = "Mode", help = 'Tracks play randomly by default but if "list" mode is set they play one after another',
1688
+ editor = "choice", default = false, items = function (self) return {"list"} end, },
1689
+ },
1690
+ HasSortKey = true,
1691
+ GlobalMap = "RadioStationPresets",
1692
+ EditorMenubarName = "Radio Stations",
1693
+ EditorIcon = "CommonAssets/UI/Icons/notes.png",
1694
+ EditorMenubar = "Editors.Audio",
1695
+ EditorCustomActions = {
1696
+ {
1697
+ FuncName = "TestPlay",
1698
+ Icon = "CommonAssets/UI/Ged/play",
1699
+ Name = "Play",
1700
+ Toolbar = "main",
1701
+ },
1702
+ },
1703
+ Documentation = "Adds a custom radio station, allowing to select folders with tracks to be implemented in the game instead of the soundtrack.",
1704
+ }
1705
+
1706
+ DefineModItemPreset("RadioStationPreset", { EditorName = "Radio station", EditorSubmenu = "Other" })
1707
+
1708
+ function RadioStationPreset:TestPlay()
1709
+ StartRadioStation(self.id)
1710
+ end
1711
+
1712
+ function RadioStationPreset:Play()
1713
+ local playlist = self:GetPlaylist()
1714
+ Playlists.Radio = playlist
1715
+ if not Music or Music.Playlist == "Radio" then
1716
+ SetMusicPlaylist("Radio", true, "force")
1717
+ end
1718
+ end
1719
+
1720
+ function RadioStationPreset:GetPlaylist()
1721
+ local playlist = PlaylistCreate(self.Folder)
1722
+ playlist.SilenceDuration = self.SilenceDuration
1723
+ playlist.Volume = self.Volume
1724
+ playlist.FadeOutTime = self.FadeOutTime
1725
+ playlist.FadeOutVolume = self.FadeOutVolume
1726
+
1727
+ return playlist
1728
+ end
1729
+
1730
+ ----- RadioStationPreset
1731
+
1732
+ if FirstLoad then
1733
+ ActiveRadioStation = false
1734
+ ActiveRadioStationThread = false
1735
+ ActiveRadioStationStart = RealTime()
1736
+ end
1737
+
1738
+ function StartRadioStation(station_id, delay, force)
1739
+ local station = RadioStationPresets[station_id or false] or RadioStationPresets[GetDefaultRadioStation() or false]
1740
+ station_id = station and station.id or false
1741
+ if force or (ActiveRadioStation ~= station_id and mapdata and mapdata.GameLogic) then
1742
+ DbgMusicPrint(string.format("Start radio '%s' with %d delay%s", station_id, delay or 0, force and "[forced]" or ""))
1743
+ if ActiveRadioStation and config.Radio then
1744
+ local session_duration = (RealTime() - ActiveRadioStationStart) / 1000
1745
+ Msg("RadioStationSession", ActiveRadioStation, session_duration)
1746
+ NetGossip("RadioStationSession", ActiveRadioStation, session_duration)
1747
+ end
1748
+ ActiveRadioStation = station_id
1749
+ ActiveRadioStationStart = RealTime()
1750
+ DeleteThread(ActiveRadioStationThread)
1751
+ ActiveRadioStationThread = CreateRealTimeThread(function(station)
1752
+ Sleep(delay or 0)
1753
+ if station then station:Play() end
1754
+ ActiveRadioStationThread = false
1755
+ end, station)
1756
+ Msg("RadioStationPlay", station_id, station)
1757
+ end
1758
+ end
1759
+
1760
+ function OnMsg.QuitGame()
1761
+ if config.Radio then
1762
+ StartRadioStation(false)
1763
+ end
1764
+ end
1765
+
1766
+ function OnMsg.LoadGame()
1767
+ if config.Radio then
1768
+ StartRadioStation(GetAccountStorageOptionValue("RadioStation"))
1769
+ end
1770
+ end
1771
+
1772
+ function OnMsg.NewMapLoaded()
1773
+ if config.Radio then
1774
+ StartRadioStation(GetAccountStorageOptionValue("RadioStation"))
1775
+ end
1776
+ end
1777
+
1778
+ function GetDefaultRadioStation()
1779
+ return const.MusicDefaultRadioStation or ""
1780
+ end
1781
+
1782
+ ----- RadioStationPreset mod item
1783
+
1784
+ if rawget(_G, "ModItemRadioStationPreset") then
1785
+ local properties = ModItemRadioStationPreset.properties
1786
+ if not properties then
1787
+ local properties = {}
1788
+ ModItemRadioStationPreset.properties = properties
1789
+ end
1790
+
1791
+ local org_prop = table.find_value(RadioStationPreset.properties, "id", "Folder")
1792
+ local prop = table.copy(org_prop, "deep")
1793
+ prop.default = "RadioStations"
1794
+ table.insert(properties, prop)
1795
+
1796
+ local oldOnEditorNew = ModItemRadioStationPreset.OnEditorNew or empty_func
1797
+ function ModItemRadioStationPreset.OnEditorNew(self, ...)
1798
+ local radio_stations_path = self.mod.content_path .. "RadioStations/"
1799
+ local radio_stations_os_path, err = ConvertToOSPath(radio_stations_path)
1800
+ assert(not err)
1801
+ AsyncCreatePath(radio_stations_os_path)
1802
+ self:SetProperty("Folder", radio_stations_os_path)
1803
+ return oldOnEditorNew(self, ...)
1804
+ end
1805
+ end
1806
+
1807
+ DefineClass.RoofTypes = {
1808
+ __parents = { "ListPreset", },
1809
+ __generated_by_class = "PresetDef",
1810
+
1811
+ properties = {
1812
+ { id = "display_name", name = "Display Name",
1813
+ editor = "text", default = false, translate = true, },
1814
+ { id = "default_inclination", name = "Default Inclination",
1815
+ editor = "number", default = 1200, scale = "deg", slider = true, min = 0, max = 2700, },
1816
+ },
1817
+ }
1818
+
1819
+ DefineClass.RoomDecalData = {
1820
+ __parents = { "ListPreset", },
1821
+ __generated_by_class = "PresetDef",
1822
+
1823
+ }
1824
+
1825
+ DefineClass.TODOPreset = {
1826
+ __parents = { "Preset", },
1827
+ __generated_by_class = "PresetDef",
1828
+
1829
+ TODOItems = {
1830
+ "Implement",
1831
+ "Review",
1832
+ "Test",
1833
+ },
1834
+ EditorMenubarName = false,
1835
+ }
1836
+
1837
+ DefineClass.TagsProperty = {
1838
+ __parents = { "PropertyObject", },
1839
+ __generated_by_class = "ClassDef",
1840
+
1841
+ properties = {
1842
+ { category = "General", id = "tags", name = "Tags",
1843
+ editor = "set", default = false, buttons = { {name = "Edit", func = "OpenTagsEditor"}, }, items = function (self) return PresetsCombo(self.TagsListItem, "Default") end, },
1844
+ },
1845
+ TagsListItem = "CommonTags",
1846
+ }
1847
+
1848
+ function TagsProperty:HasTag(tag)
1849
+ return (self.tags or empty_table)[tag] and true or false
1850
+ end
1851
+
1852
+ function TagsProperty:OpenTagsEditor()
1853
+ g_Classes[self.TagsListItem]:OpenEditor()
1854
+ end
1855
+
1856
+ DefineClass.TerrainGrass = {
1857
+ __parents = { "PropertyObject", },
1858
+ __generated_by_class = "ClassDef",
1859
+
1860
+ properties = {
1861
+ { id = "Classes",
1862
+ editor = "string_list", default = {}, item_default = "", items = function (self) return ClassDescendantsCombo("Grass") end, },
1863
+ { id = "SizeFrom", name = "Size From",
1864
+ editor = "number", default = 100, min = 50, max = 200, },
1865
+ { id = "SizeTo", name = "Size To",
1866
+ editor = "number", default = 100, min = 50, max = 200, },
1867
+ { id = "Weight", name = "Weight", help = "For a weighted random selection between multiple grass definitions",
1868
+ editor = "number", default = 100, slider = true, min = 0, max = 100, },
1869
+ { id = "NoiseWeight", name = "Noise Strength", help = "Weight of a random spatial noise modification to density",
1870
+ editor = "number", default = 0, scale = 10, slider = true, min = -1000, max = 1000, },
1871
+ { id = "TiltWithTerrain", name = "TiltWithTerrain", help = "Orient with terrain normal",
1872
+ editor = "bool", default = false, },
1873
+ { id = "PlaceOnWater", name = "PlaceOnWater", help = "Place on water surface",
1874
+ editor = "bool", default = false, },
1875
+ { id = "ColorVarFrom", name = "Color Variation From",
1876
+ editor = "color", default = 4284769380, },
1877
+ { id = "ColorVarTo", name = "Color Variation To",
1878
+ editor = "color", default = 4284769380, },
1879
+ },
1880
+ }
1881
+
1882
+ function TerrainGrass:GetEditorView()
1883
+ local classes = self:GetClassList() or {""}
1884
+ table.replace(classes, "", "No Grass")
1885
+ return table.concat(classes, ", ") .. " (" .. self.Weight .. ")"
1886
+ end
1887
+
1888
+ function TerrainGrass:GetClassList()
1889
+ local classes = table.keys(table.invert(self.Classes or empty_table), true)
1890
+ return #classes > 0 and classes
1891
+ end
1892
+
1893
+ DefineClass.TerrainProps = {
1894
+ __parents = { "PropertyObject", },
1895
+ __generated_by_class = "ClassDef",
1896
+
1897
+ properties = {
1898
+ { id = "TerrainName", name = "Terrain Name",
1899
+ editor = "choice", default = false, items = function (self) return GetTerrainNamesCombo() end, },
1900
+ { id = "TerrainIndex", name = "Terrain Index",
1901
+ editor = "number", default = false, dont_save = true, read_only = true, },
1902
+ { id = "TerrainPreview", name = "Terrain Preview",
1903
+ editor = "image", default = false, dont_save = true, read_only = true, img_size = 100, img_box = 1, base_color_map = true, },
1904
+ },
1905
+ }
1906
+
1907
+ function TerrainProps:GetTerrainPreview()
1908
+ return GetTerrainTexturePreview(self.TerrainName)
1909
+ end
1910
+
1911
+ function TerrainProps:GetTerrainIndex()
1912
+ return GetTerrainTextureIndex(self.TerrainName)
1913
+ end
1914
+
1915
+ DefineClass.TestCombatFilter = {
1916
+ __parents = { "GedFilter", },
1917
+ __generated_by_class = "ClassDef",
1918
+
1919
+ properties = {
1920
+ { id = "ShowInCheats", name = "Shown In Cheats", help = "Filters depending if shown in cheats or not",
1921
+ editor = "choice", default = "", items = function (self) return {"", "true", "false"} end, },
1922
+ },
1923
+ }
1924
+
1925
+ function TestCombatFilter:FilterObject(obj)
1926
+ if self.ShowInCheats ~= "" and obj.show_in_cheats ~= (self.ShowInCheats == "true") then
1927
+ return false
1928
+ end
1929
+
1930
+ return true
1931
+ end
1932
+
1933
+ DefineClass.TextStyle = {
1934
+ __parents = { "Preset", },
1935
+ __generated_by_class = "PresetDef",
1936
+
1937
+ properties = {
1938
+ { category = "Text", id = "TextFont", name = "Font",
1939
+ editor = "text", default = T(202962508484, --[[PresetDef TextStyle default]] "droid, 12"), translate = true, },
1940
+ { category = "Text", id = "TextColor", name = "Color",
1941
+ editor = "color", default = 4280295456, },
1942
+ { category = "Text", id = "RolloverTextColor", name = "Rollover color",
1943
+ editor = "color", default = 4278190080, },
1944
+ { category = "Text", id = "DisabledTextColor", name = "Disabled color",
1945
+ editor = "color", default = 2149589024, },
1946
+ { category = "Text", id = "DisabledRolloverTextColor", name = "Disabled rollover color",
1947
+ editor = "color", default = 2147483648, },
1948
+ { category = "Text", id = "ShadowType", name = "Shadow type",
1949
+ editor = "choice", default = "shadow", items = function (self) return {"shadow", "extrude", "outline", "glow"} end, },
1950
+ { category = "Text", id = "ShadowSize", name = "Shadow size",
1951
+ editor = "number", default = 0, },
1952
+ { category = "Text", id = "ShadowColor", name = "Shadow color",
1953
+ editor = "color", default = 805306368, },
1954
+ { category = "Text", id = "ShadowDir", name = "Shadow dir",
1955
+ editor = "point", default = point(1, 1), },
1956
+ { id = "DarkMode",
1957
+ editor = "preset_id", default = false, preset_class = "TextStyle", },
1958
+ { category = "Text", id = "DisabledShadowColor", name = "Disabled shadow color",
1959
+ editor = "color", default = 805306368, },
1960
+ },
1961
+ HasSortKey = true,
1962
+ GlobalMap = "TextStyles",
1963
+ EditorMenubarName = "Text styles",
1964
+ EditorShortcut = "Ctrl-Alt-T",
1965
+ EditorIcon = "CommonAssets/UI/Icons/detail list view.png",
1966
+ EditorMenubar = "Editors.UI",
1967
+ EditorPreview = Untranslated("<Preview>"),
1968
+ Documentation = "Adds a new text style that can be used by XFontControl and other classes in XTemplate presets.",
1969
+ }
1970
+
1971
+ DefineModItemPreset("TextStyle", { EditorName = "Text style", EditorSubmenu = "Other" })
1972
+
1973
+ function TextStyle:GetFontIdHeightBaseline(scale)
1974
+ local cache = TextStyleCache[self.id]
1975
+ if not cache then
1976
+ cache = {}
1977
+ TextStyleCache[self.id] = cache
1978
+ end
1979
+
1980
+ scale = scale or 1000
1981
+ if cache[scale] then
1982
+ return unpack_params(cache[scale])
1983
+ end
1984
+
1985
+ local font = _InternalTranslate(self.TextFont) or _InternalTranslate(TextStyle.TextFont)
1986
+ font = font:gsub("%d+", function(size)
1987
+ return Max(MulDivRound(tonumber(size) or 1, scale, 1000), 1)
1988
+ end)
1989
+
1990
+ font = GetProjectConvertedFont(font)
1991
+
1992
+ -- Mods can replace fonts with other fonts
1993
+ if g_FontReplaceMap then
1994
+ local font_name = string.match(font, "([^,]+),")
1995
+ if font_name then
1996
+ if g_FontReplaceMap[font_name] then
1997
+ font_name = g_FontReplaceMap[font_name]
1998
+ -- Replace the font name only, leave the size and style
1999
+ font = font:gsub("[^,]+,", font_name .. ",")
2000
+ else
2001
+ -- Match the font if the used name is part of the actual full name
2002
+ for replace, with in pairs(g_FontReplaceMap) do
2003
+ if string.find(replace, font_name) then
2004
+ font_name = g_FontReplaceMap[replace]
2005
+ -- Replace the font name only, leave the size and style
2006
+ font = font:gsub("[^,]+,", font_name .. ",")
2007
+ break
2008
+ end
2009
+ end
2010
+ end
2011
+ end
2012
+ end
2013
+
2014
+ local id = UIL.GetFontID(font)
2015
+ if not id or id < 0 then
2016
+ print("once", "[WARNING] Invalid font", font, "in text style", self.id)
2017
+ return -1, 0, 0
2018
+ end
2019
+ local _, height = UIL.MeasureText("AQj", id)
2020
+ local baseline = height * 8 / 10 -- there is currently no way to get the font's baseline
2021
+ cache[scale] = { id, height, baseline }
2022
+ return id, height, baseline
2023
+ end
2024
+
2025
+ function TextStyle:GetPreview()
2026
+ return string.format("<style %s><%s></style>", self.id, _InternalTranslate(self.TextFont, nil, not "check"))
2027
+ end
2028
+
2029
+ ----- TextStyle globals
2030
+
2031
+ TextStyleCache = {}
2032
+
2033
+ function ClearTextStyleCache() TextStyleCache = {} end
2034
+ OnMsg.EngineOptionsSaved = ClearTextStyleCache
2035
+ OnMsg.ClassesBuilt = ClearTextStyleCache
2036
+ OnMsg.DataLoaded = ClearTextStyleCache
2037
+ OnMsg.DataReload = ClearTextStyleCache
2038
+
2039
+ function LoadTextStyles()
2040
+ local old_text_styles = Presets.TextStyle
2041
+ Presets.TextStyle = {}
2042
+ TextStyles = {}
2043
+ LoadPresets("CommonLua/Data/TextStyle.lua")
2044
+ ForEachLib("Data/TextStyle.lua", function(lib, path) LoadPresets(path) end)
2045
+ LoadPresets("Data/TextStyle.lua")
2046
+ for _, dlc_folder in ipairs(DlcFolders or empty_table) do
2047
+ LoadPresets(dlc_folder .. "/Presets/TextStyle.lua")
2048
+ end
2049
+ TextStyle:SortPresets()
2050
+ for _, group in ipairs(Presets.TextStyle) do
2051
+ for _, preset in ipairs(group) do
2052
+ preset:PostLoad()
2053
+ end
2054
+ end
2055
+ if Platform.developer and not Platform.ged then
2056
+ LoadCollapsedPresetGroups()
2057
+ end
2058
+ GedRebindRoot(old_text_styles, Presets.TextStyle)
2059
+ end
2060
+
2061
+ if FirstLoad or ReloadForDlc then
2062
+ OnMsg.ClassesBuilt = LoadTextStyles
2063
+ OnMsg.DataLoaded = LoadTextStyles
2064
+ end
2065
+
2066
+ DefineClass.WangNoisePreset = {
2067
+ __parents = { "NoisePreset", "WangPerlinNoise", },
2068
+ __generated_by_class = "PresetDef",
2069
+
2070
+ EditorMenubarName = "Noise Editor",
2071
+ }
2072
+
CommonLua/Classes/ClassDefs/ClassDef-StoryBits.generated.lua ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- ========== GENERATED BY ClassDef Editor (Ctrl-Alt-F3) DO NOT EDIT MANUALLY! ==========
2
+
3
+ DefineClass.StoryBit = {
4
+ __parents = { "PresetWithQA", },
5
+ __generated_by_class = "PresetDef",
6
+
7
+ properties = {
8
+ { category = "General", id = "ScriptDone", name = "Script done",
9
+ editor = "bool", default = false, no_edit = function(self) return IsKindOf(self, "ModItem") end, },
10
+ { category = "General", id = "TextReadyForValidation", name = "Text ready for validation",
11
+ editor = "bool", default = false, no_edit = function(self) return IsKindOf(self, "ModItem") end, },
12
+ { category = "General", id = "TextsDone", name = "Texts done",
13
+ editor = "bool", default = false, no_edit = function(self) return IsKindOf(self, "ModItem") end, },
14
+ { category = "Activation", id = "Category",
15
+ editor = "preset_id", default = "FollowUp", preset_class = "StoryBitCategory", extra_item = "FollowUp", },
16
+ { category = "Activation", id = "Trigger",
17
+ editor = "choice", default = "StoryBitTick",
18
+ read_only = function(self) return self.Category ~= "FollowUp" end, items = function (self) return StoryBitTriggersCombo end, },
19
+ { category = "Activation", id = "Enabled",
20
+ editor = "bool", default = false,
21
+ read_only = function(self) return self.Category == "FollowUp" end, },
22
+ { category = "Activation", id = "EnableChance", name = "Enable Chance", help = "Chance to be enabled in a specific playthrough (use sparingly for story bits that occur too often)",
23
+ editor = "number", default = 100,
24
+ no_edit = function(self) return not self.Enabled end, scale = "%", },
25
+ { category = "Activation", id = "InheritsObject", name = "Inherits object", help = "Associate with the object of the story bit that enabled this one",
26
+ editor = "bool", default = true,
27
+ no_edit = function(self) return self.Enabled end, },
28
+ { category = "Activation", id = "OneTime", name = "One-time",
29
+ editor = "bool", default = true,
30
+ read_only = function(self) return self.Category == "FollowUp" end, },
31
+ { category = "Activation", id = "ExpirationTime", name = "Expiration time",
32
+ editor = "number", default = false, scale = "h", },
33
+ { category = "Activation", id = "ExpirationModifier", name = "Expiration modifier",
34
+ editor = "expression", default = function (self, context, obj) return 100 end, params = "self, context, obj", },
35
+ { category = "Activation", id = "SuppressTime", name = "Suppress for", help = "This StoryBit can't trigger for this period after it was enabled",
36
+ editor = "number", default = 0, scale = "h", },
37
+ { category = "Activation", id = "Sets", name = "Sets", help = "Sets this story bit belongs to. These sets can be disabled by game-specific logic.",
38
+ editor = "set", default = false, items = function (self) return PresetsCombo("CooldownDef", "StoryBits") end, },
39
+ { category = "Activation", id = "Prerequisites",
40
+ editor = "nested_list", default = false, base_class = "Condition", },
41
+ { category = "Activation Effects", id = "Disables", help = "List of StoryBits to disable",
42
+ editor = "preset_id_list", default = {}, preset_class = "StoryBit", item_default = "", },
43
+ { category = "Activation Effects", id = "Delay", help = "This StoryBit waits this long after triggering before it gets activated and displayed",
44
+ editor = "number", default = 0, scale = "h", },
45
+ { category = "Activation Effects", id = "DetachObj", name = "Detach object", help = "Detach the story bit related object on delay.",
46
+ editor = "bool", default = false, },
47
+ { category = "Activation Effects", id = "ActivationEffects", name = "Activation Effects",
48
+ editor = "nested_list", default = false, base_class = "Effect", all_descendants = true, },
49
+ { category = "Notification", id = "HasNotification", name = "Has notification",
50
+ editor = "bool", default = true, },
51
+ { category = "Notification", id = "NotificationPriority", name = "Notification priority",
52
+ editor = "choice", default = "StoryBit",
53
+ no_edit = function(self) return not self.HasNotification end, items = function (self) return GetGameNotificationPriorities() end, },
54
+ { category = "Notification", id = "NotificationTitle", name = "Notification Title", help = "Leave empty to use the popup title",
55
+ editor = "text", default = "",
56
+ no_edit = function(self) return not self.HasNotification end, translate = true, lines = 1, },
57
+ { category = "Notification", id = "NotificationText", name = "Notification Text", help = "Leave empty to use the popup title",
58
+ editor = "text", default = "",
59
+ no_edit = function(self) return not self.HasNotification end, translate = true, lines = 1, },
60
+ { category = "Notification", id = "NotificationRolloverTitle", name = "Notification rollover title",
61
+ editor = "text", default = "",
62
+ no_edit = function(self) return not self.HasNotification end, translate = true, lines = 1, },
63
+ { category = "Notification", id = "NotificationRolloverText", name = "Notification rollover text",
64
+ editor = "text", default = "",
65
+ no_edit = function(self) return not self.HasNotification end, translate = true, lines = 1, max_lines = 6, },
66
+ { category = "Notification", id = "NotificationAction", name = "Notification action",
67
+ editor = "choice", default = "complete",
68
+ no_edit = function(self) return not self.HasNotification end, items = function (self) return {"complete", "select object", "callback", "nothing"} end, },
69
+ { category = "Notification", id = "NotificationCallbackFunc", name = "Notification click callback",
70
+ editor = "func", default = function (self, state)
71
+ return true
72
+ end, no_edit = function(self) return self.NotificationAction ~= "callback" end, params = "self, state", },
73
+ { category = "Notification", id = "NotificationExpirationBar", name = "Display expiration bar",
74
+ editor = "bool", default = false,
75
+ no_edit = function(self) return not self.HasNotification end, },
76
+ { category = "Notification", id = "NotificationCanDismiss", name = "Can dismiss",
77
+ editor = "bool", default = true,
78
+ no_edit = function(self) return not self.HasNotification or self.HasPopup end, },
79
+ { category = "Popup", id = "HasPopup", name = "Has popup",
80
+ editor = "bool", default = true, },
81
+ { category = "Notification", id = "FxAction", name = "FX Action", help = "This is used for calling the FX system with given action. Leave it empty to have default notification FX actions.",
82
+ editor = "text", default = "", },
83
+ { category = "Popup", id = "Actor",
84
+ editor = "combo", default = "narrator",
85
+ no_edit = function(self) return not self.HasPopup end, items = function (self) return VoiceActors end, },
86
+ { category = "Popup", id = "Title",
87
+ editor = "text", default = false,
88
+ no_edit = function(self) return not self.HasPopup end, translate = true, lines = 1, },
89
+ { category = "Popup", id = "VoicedText", name = "Voiced Text",
90
+ editor = "text", default = false,
91
+ no_edit = function(self) return not self.HasPopup end, translate = true, lines = 1, max_lines = 256, context = VoicedContextFromField("Actor"), },
92
+ { category = "Popup", id = "Text",
93
+ editor = "text", default = false,
94
+ no_edit = function(self) return not self.HasPopup end, translate = true, lines = 4, max_lines = 256, },
95
+ { category = "Popup", id = "Image",
96
+ editor = "ui_image", default = "",
97
+ no_edit = function(self) return not self.HasPopup end, image_preview_size = 200, },
98
+ { category = "Popup", id = "UseObjectImage", name = "Use object image", help = "Extract a popup image from the context object",
99
+ editor = "bool", default = false,
100
+ no_edit = function(self) return not self.HasPopup end, },
101
+ { category = "Popup", id = "SelectObject", name = "Select Object", help = "Select the object when opening the popup",
102
+ editor = "bool", default = true,
103
+ no_edit = function(self) return not self.HasPopup end, },
104
+ { category = "Popup", id = "PopupFxAction", name = "FX Action", help = "This is used for calling the FX system with given action when opening Popup.",
105
+ editor = "text", default = "", },
106
+ { category = "Completion Effects", id = "Enables", help = "List of StoryBits to enable",
107
+ editor = "preset_id_list", default = {}, preset_class = "StoryBit", item_default = "", },
108
+ { category = "Completion Effects", id = "Effects", name = "Completion Effects",
109
+ editor = "nested_list", default = false, base_class = "Effect", all_descendants = true, },
110
+ { id = "max_reply_id", name = "Max Reply Id",
111
+ editor = "number", default = 0, read_only = true, no_edit = true, },
112
+ },
113
+ HasParameters = true,
114
+ SingleFile = false,
115
+ GlobalMap = "StoryBits",
116
+ ContainerClass = "StoryBitSubItem",
117
+ EditorMenubarName = "Story Bits",
118
+ EditorShortcut = "Ctrl-Alt-E",
119
+ EditorIcon = "CommonAssets/UI/Icons/book 2.png",
120
+ EditorMenubar = "Scripting",
121
+ EditorCustomActions = {
122
+ {
123
+ Name = "Debug",
124
+ },
125
+ {
126
+ FuncName = "GedRpcTestStoryBit",
127
+ Icon = "CommonAssets/UI/Ged/play.tga",
128
+ Menubar = "Debug",
129
+ Name = "TestStoryBit",
130
+ Toolbar = "main",
131
+ },
132
+ {
133
+ FuncName = "GedRpcTestPrerequisitesStoryBit",
134
+ Icon = "CommonAssets/UI/Ged/preview.tga",
135
+ Menubar = "Debug",
136
+ Name = "Test prerequisites",
137
+ Toolbar = "main",
138
+ },
139
+ },
140
+ EditorView = Untranslated("<ChooseColor><id><color 0 128 0><if(not_eq(Trigger,'StoryBitTick'))> (<Trigger>)</if><opt(u(Comment),' ','')><color 128 128 128><opt(u(save_in),' - ','')>"),
141
+ }
142
+
143
+ DefineModItemPreset("StoryBit", { EditorName = "Story bit", EditorSubmenu = "Gameplay" })
144
+
145
+ function StoryBit:OnEditorSetProperty(prop_id, old_value, ged)
146
+ if prop_id == "Category" then
147
+ if self.Category == "FollowUp" or self.Category == "" then
148
+ self.Enabled = false
149
+ self.OneTime = true
150
+ else
151
+ self.Trigger = StoryBitCategories[self.Category].Trigger
152
+ self.Enabled = true
153
+ end
154
+ end
155
+ if prop_id == "Enables" or prop_id == "Effects" or prop_id == "ActivationEffects" then
156
+ StoryBitResetEnabledReferences()
157
+ end
158
+ end
159
+
160
+ function StoryBit:ChooseColor()
161
+ if not self.ScriptDone then return "" end
162
+ local color =
163
+ not self.TextsDone and (self.TextReadyForValidation and RGB(180, 0, 180) or RGB(205, 32, 32)) or
164
+ not self.TextReadyForValidation and RGB(220, 120, 0) or
165
+ not (self.Image and self.Image ~= "" or self.Category == "FollowUp") and RGB(50, 50, 200) or
166
+ RGB(32, 128, 32)
167
+ local r,g,b = GetRGB(color)
168
+ return string.format("<color %s %s %s>", r, g ,b)
169
+ end
170
+
171
+ ----- StoryBit function ModItemStoryBit:TestModItem(ged)
172
+
173
+ if config.Mods then
174
+ function ModItemStoryBit:TestModItem(ged)
175
+ if not GameState.gameplay then return end
176
+ CreateGameTimeThread(function()
177
+ ForceActivateStoryBit(self.id, SelectedObj, "immediate")
178
+ end)
179
+ end
180
+ end
181
+
182
+ ----- StoryBit Add ! marks
183
+
184
+ if Platform.developer then
185
+ local referenced_storybits = false
186
+
187
+ function StoryBitResetEnabledReferences()
188
+ referenced_storybits = false
189
+ ObjModified(Presets.StoryBit)
190
+ end
191
+
192
+ local function add_effect_references(effects)
193
+ for _, effect in ipairs(effects or empty_table) do
194
+ if effect:IsKindOf("StoryBitActivate") then
195
+ referenced_storybits[effect.Id] = true
196
+ end
197
+ if effect:IsKindOf("StoryBitEnableRandom") then
198
+ for _, id in ipairs(effect.StoryBits or empty_table) do
199
+ referenced_storybits[id] = true
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ function OnMsg.MarkReferencedStoryBits(referenced_storybits)
206
+ ForEachPreset(StoryBit, function(storybit)
207
+ for _, id in ipairs(storybit.Enables or empty_table) do
208
+ referenced_storybits[id] = true
209
+ end
210
+ add_effect_references(storybit.Effects)
211
+ add_effect_references(storybit.ActivationEffects)
212
+ for _, child in ipairs(storybit) do
213
+ if child:IsKindOf("StoryBitOutcome") then
214
+ for _, id in ipairs(child.Enables) do
215
+ referenced_storybits[id] = true
216
+ end
217
+ add_effect_references(child.Effects)
218
+ end
219
+ end
220
+ end)
221
+ end
222
+
223
+ function StoryBit:GetError()
224
+ if not referenced_storybits then
225
+ referenced_storybits = {}
226
+ Msg("MarkReferencedStoryBits", referenced_storybits)
227
+ end
228
+ return not (self.Enabled or referenced_storybits[self.id]) and
229
+ "This story bit is not enabled by itself, or from anywhere else."
230
+ end
231
+ else
232
+ function StoryBitResetEnabledReferences()
233
+ end
234
+ end
235
+
236
+ DefineClass.StoryBitOutcome = {
237
+ __parents = { "StoryBitSubItem", },
238
+ __generated_by_class = "ClassDef",
239
+
240
+ properties = {
241
+ { category = "Activation", id = "Prerequisites",
242
+ editor = "nested_list", default = false, base_class = "Condition", },
243
+ { category = "Activation", id = "Weight",
244
+ editor = "number", default = 100, },
245
+ { category = "Popup", id = "Title",
246
+ editor = "text", default = false, translate = true, lines = 1, },
247
+ { category = "Popup", id = "VoicedText", name = "Voiced Text",
248
+ editor = "text", default = false, translate = true, lines = 1, max_lines = 256, context = VoicedContextFromField("Actor"), },
249
+ { category = "Popup", id = "Text",
250
+ editor = "text", default = false, translate = true, lines = 4, max_lines = 256, },
251
+ { category = "Popup", id = "Actor",
252
+ editor = "combo", default = "narrator", items = function (self) return VoiceActors end, },
253
+ { category = "Popup", id = "Image",
254
+ editor = "ui_image", default = "", },
255
+ { id = "Enables", help = "List of StoryBits to enable",
256
+ editor = "preset_id_list", default = {}, preset_class = "StoryBit", item_default = "", },
257
+ { id = "Disables", help = "List of StoryBits to disable",
258
+ editor = "preset_id_list", default = {}, preset_class = "StoryBit", item_default = "", },
259
+ { category = "Effect", id = "StoryBits", help = "A list of storybits with weight. One will be chosen and activated based on weight and met prerequisites.",
260
+ editor = "nested_list", default = false, base_class = "StoryBitWithWeight", all_descendants = true, },
261
+ { category = "Effect", id = "Effects", help = "All effects in the list will be executed even if some storybits have been added to the StoryBits property.",
262
+ editor = "nested_list", default = false, base_class = "Effect", all_descendants = true, },
263
+ },
264
+ EditorName = "New Outcome",
265
+ }
266
+
267
+ function StoryBitOutcome:GetEditorView()
268
+ local result = "<tab 20>Outcome (<Weight>): "
269
+ if self.VoicedText then
270
+ result = result .. "<color 128 128 128><literal(VoicedText)></color>"
271
+ elseif self.Text then
272
+ result = result .. "<color 128 128 128><literal(Text)></color>"
273
+ else
274
+ result = result .. "<color 128 128 128>no display text</color>"
275
+ end
276
+ for _, cond in ipairs(self.Prerequisites or empty_table) do
277
+ result = result .. "\n<tab 20><color 140 64 32>" .. _InternalTranslate(cond:GetProperty("EditorView"), cond, false) .. "</color>"
278
+ end
279
+ for _, effect in ipairs(self.Effects or empty_table) do
280
+ result = result .. "\n<tab 20><color 140 64 32>" .. _InternalTranslate(effect:GetProperty("EditorView"), effect, false) .. "</color>"
281
+ end
282
+ for _, effect in ipairs(self.StoryBits) do
283
+ result = result .. "\n<tab 20><color 140 64 32>" .. _InternalTranslate(effect:GetProperty("EditorView"), effect, false) .. "</color>"
284
+ end
285
+ if next(self.Enables or empty_table) then
286
+ result = result .. "\n<tab 20>Enables: <color 140 64 32>" .. table.concat(self.Enables, ", ") .. "</color>"
287
+ end
288
+ if next(self.Disables or empty_table) then
289
+ result = result .. "\n<tab 20>Disables: <color 140 64 32>" .. table.concat(self.Disables, ", ") .. "</color>"
290
+ end
291
+ return T{Untranslated(result)}
292
+ end
293
+
294
+ function StoryBitOutcome:OnEditorSetProperty(prop_id, old_value, ged)
295
+ if prop_id == "Enables" or prop_id == "Effects" then
296
+ StoryBitResetEnabledReferences()
297
+ end
298
+ end
299
+
300
+ DefineClass.StoryBitReply = {
301
+ __parents = { "StoryBitSubItem", },
302
+ __generated_by_class = "ClassDef",
303
+
304
+ properties = {
305
+ { id = "Text",
306
+ editor = "text", default = false, translate = true, lines = 1, },
307
+ { id = "PrerequisiteText", name = "Prerequisite text",
308
+ editor = "text", default = false, translate = true, },
309
+ { id = "Prerequisites",
310
+ editor = "nested_list", default = false, base_class = "Condition", },
311
+ { id = "Cost",
312
+ editor = "number", default = 0, },
313
+ { id = "HideIfDisabled", name = "Hide if disabled",
314
+ editor = "bool", default = false, },
315
+ { id = "OutcomeText", name = "Outcome Text",
316
+ editor = "choice", default = "none", items = function (self) return { { text = "None", value = "none" }, { text = "Auto (from 1st outcome effects)", value = "auto" }, { text = "Custom", value = "custom" } } end, },
317
+ { id = "CustomOutcomeText", name = "Custom outcome text",
318
+ editor = "text", default = false,
319
+ no_edit = function(self) return self.OutcomeText ~= "custom" end, translate = true, lines = 1, },
320
+ { category = "Comment", id = "Comment",
321
+ editor = "text", default = false, },
322
+ { id = "unique_id", name = "Unique Id",
323
+ editor = "number", default = 0, read_only = true, no_edit = true, },
324
+ },
325
+ EditorName = "New Reply",
326
+ }
327
+
328
+ function StoryBitReply:GetEditorView()
329
+ local conditions = {}
330
+ for _, cond in ipairs(self.Prerequisites) do
331
+ table.insert(conditions, _InternalTranslate(cond:GetProperty("EditorView"), cond, false))
332
+ end
333
+ local cost = self.Cost
334
+ if cost ~= 0 then
335
+ table.insert(conditions, "Cost " .. _InternalTranslate(T(504461186435, "<cost>"), cost, false))
336
+ end
337
+
338
+ local cond_text = #conditions > 0 and "[" .. table.concat(conditions, ", ") .. "] " or ""
339
+ local result = "Reply: <color 0 128 0>" .. cond_text .. "<literal(Text)></color>"
340
+ if self.OutcomeText == "custom" then
341
+ result = result .. "\n<color 128 128 128>(<literal(CustomOutcomeText)>)"
342
+ elseif self.OutcomeText == "auto" then
343
+ result = result .. "\n<color 128 128 128> - auto display of outcome text -"
344
+ end
345
+ if self.Comment and self.Comment ~= "" then
346
+ result = result .. "\n<color 75 105 198>" .. self.Comment .. "</color>"
347
+ end
348
+ if const.TagLookupTable["em"] then
349
+ result = result:gsub(const.TagLookupTable["em"],"")
350
+ result = result:gsub(const.TagLookupTable["/em"],"")
351
+ end
352
+ return T{Untranslated(result)}
353
+ end
354
+
355
+ function StoryBitReply:OnEditorNew(parent, ged, is_paste)
356
+ parent.max_reply_id = parent.max_reply_id + 1
357
+ self.unique_id = parent.max_reply_id
358
+ end
359
+
360
+ DefineClass.StoryBitSubItem = {
361
+ __parents = { "PropertyObject", },
362
+ __generated_by_class = "ClassDef",
363
+
364
+ StoreAsTable = true,
365
+ EditorName = "StoryBit Element",
366
+ }
367
+
CommonLua/Classes/CodeRenderableObject.lua ADDED
@@ -0,0 +1,1375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ local AppendVertex = pstr().AppendVertex
2
+ local GetHeight = terrain.GetHeight
3
+ local height_tile = const.HeightTileSize
4
+ local InvalidZ = const.InvalidZ
5
+ local KeepRefOneFrame = KeepRefOneFrame
6
+
7
+ local SetCustomData
8
+ local GetCustomData
9
+ function OnMsg.Autorun()
10
+ SetCustomData = ComponentCustomData.SetCustomData
11
+ GetCustomData = ComponentCustomData.GetCustomData
12
+ end
13
+
14
+ DefineClass.CodeRenderableObject =
15
+ {
16
+ __parents = { "Object", "ComponentAttach", "ComponentCustomData" },
17
+ entity = "",
18
+ flags = {
19
+ gofAlwaysRenderable = true, cfCodeRenderable = true, cofComponentInterpolation = true, cfConstructible = false,
20
+ efWalkable = false, efCollision = false, efApplyToGrids = false, efSelectable = false, efShadow = false, efSunShadow = false
21
+ },
22
+ depth_test = false,
23
+ zwrite = true,
24
+ }
25
+
26
+ DefineClass.Text =
27
+ {
28
+ --[[
29
+ Custom data layout:
30
+ const.CRTextCCDIndexColorMain - base color, RGBA
31
+ const.CRTextCCDIndexColorShadow - shadow color, RGBA
32
+ const.CRTextCCDIndexFlags - flags: 0: depth test, 1, center
33
+ const.CRTextCCDIndexText - text, as string - you must keep this string from being GCed while the Text object is alive
34
+ const.CRTextCCDIndexFont - font_id, integer as returned by UIL.GetFontID
35
+ const.CRTextCCDIndexShadowOffset - shadow offset
36
+ const.CRTextCCDIndexOpacity - opacity interpolation parameters
37
+ const.CRTextCCDIndexScale - scale interpolation parameters
38
+ const.CRTextCCDIndexZOffset - z offset interpolation parameters
39
+ ]]
40
+ __parents = { "CodeRenderableObject" },
41
+ text = false,
42
+ text_style = false,
43
+ hide_in_editor = true, -- hide using the T button in the editor statusbar (or Alt-Shift-T shortcut)
44
+ }
45
+
46
+ local TextFlag_DepthTest = 1
47
+ local TextFlag_Center = 2
48
+
49
+ function Text:SetColor1(c) SetCustomData(self, const.CRTextCCDIndexColorMain, c) end
50
+ function Text:SetColor2(c) SetCustomData(self, const.CRTextCCDIndexColorShadow, c) end
51
+ function Text:GetColor1(c) return GetCustomData(self, const.CRTextCCDIndexColorMain) end
52
+ function Text:GetColor2(c) return GetCustomData(self, const.CRTextCCDIndexColorShadow) end
53
+
54
+ function Text:SetColor(c)
55
+ self:SetColor1(c)
56
+ self:SetColor2(RGB(0,0,0))
57
+ end
58
+
59
+ function Text:GetColor(c)
60
+ return self:GetColor1()
61
+ end
62
+
63
+ function Text:SetDepthTest(depth_test)
64
+ local flags = GetCustomData(self, const.CRTextCCDIndexFlags)
65
+ if depth_test then
66
+ SetCustomData(self, const.CRTextCCDIndexFlags, FlagSet(flags, TextFlag_DepthTest))
67
+ else
68
+ SetCustomData(self, const.CRTextCCDIndexFlags, FlagClear(flags, TextFlag_DepthTest))
69
+ end
70
+ end
71
+ function Text:GetDepthTest()
72
+ return IsFlagSet(GetCustomData(self, const.CRTextCCDIndexFlags), TextFlag_DepthTest)
73
+ end
74
+
75
+ function Text:SetCenter(c)
76
+ local flags = GetCustomData(self, const.CRTextCCDIndexFlags)
77
+ if c then
78
+ SetCustomData(self, const.CRTextCCDIndexFlags, FlagSet(flags, TextFlag_Center))
79
+ else
80
+ SetCustomData(self, const.CRTextCCDIndexFlags, FlagClear(flags, TextFlag_Center))
81
+ end
82
+ end
83
+ function Text:GetCenter()
84
+ return IsFlagSet(GetCustomData(self, const.CRTextCCDIndexFlags), TextFlag_Center)
85
+ end
86
+
87
+ function Text:SetText(txt)
88
+ KeepRefOneFrame(self.text)
89
+ self.text = txt
90
+ SetCustomData(self, const.CRTextCCDIndexText, self.text)
91
+ end
92
+ function Text:GetText()
93
+ return self.text
94
+ end
95
+
96
+ function Text:SetFontId(id) SetCustomData(self, const.CRTextCCDIndexFont, id) end
97
+ function Text:GetFontId() return GetCustomData(self, const.CRTextCCDIndexFont) end
98
+ function Text:SetShadowOffset(so) SetCustomData(self, const.CRTextCCDIndexShadowOffset, so) end
99
+ function Text:GetShadowOffset() return GetCustomData(self, const.CRTextCCDIndexShadowOffset) end
100
+
101
+ function Text:SetTextStyle(style, scale)
102
+ local style = TextStyles[style]
103
+ if not style then
104
+ assert(false, string.format("Invalid text style '%s'", style))
105
+ return
106
+ end
107
+ scale = scale or terminal.desktop.scale:y()
108
+ local font, height, base_height = style:GetFontIdHeightBaseline(scale)
109
+ self:SetFontId(font, height, base_height)
110
+ self:SetColor(style.TextColor)
111
+ self:SetShadowOffset(style.ShadowSize)
112
+ self.text_style = style
113
+ end
114
+
115
+ function Text:SetOpacityInterpolation(v0, t0, v1, t1)
116
+ -- opacities are 0..100, 7 bits
117
+ -- times are encoded as ms/10, 8 bits each
118
+ v0 = v0 or 100
119
+ v1 = v1 or v0
120
+ t0 = t0 or 0
121
+ t1 = t1 or 0
122
+ SetCustomData(self, const.CRTextCCDIndexOpacity, EncodeBits(v0, 7, v1, 7, t0/10, 8, t1/10, 8))
123
+ end
124
+
125
+ function Text:SetScaleInterpolation(v0, t0, v1, t1)
126
+ -- scales are encoded 0..127, scale in percent/4 - so range 0..500%
127
+ -- times are encoded as ms/10, 8 bits each
128
+ v0 = v0 or 100
129
+ v1 = v1 or v0
130
+ t0 = t0 or 0
131
+ t1 = t1 or 0
132
+ SetCustomData(self, const.CRTextCCDIndexScale, EncodeBits(v0/4, 7, v1/4, 7, t0/10, 8, t1/10, 8))
133
+ end
134
+
135
+ function Text:SetZOffsetInterpolation(v0, t0, v1, t1)
136
+ -- Z offsets are encoded 0..127, in guim/50 - so range 0..6.35 m
137
+ -- times are encoded as ms/10, 8 bits each
138
+ v0 = v0 or 0
139
+ v1 = v1 or v0
140
+ t0 = t0 or 0
141
+ t1 = t1 or 0
142
+ SetCustomData(self, const.CRTextCCDIndexZOffset, EncodeBits(v0/50, 7, v1/50, 7, t0/10, 8, t1/10, 8))
143
+ end
144
+
145
+ function Text:Init()
146
+ self:SetTextStyle(self.text_style or "EditorText")
147
+ end
148
+
149
+ function Text:Done()
150
+ KeepRefOneFrame(self.text)
151
+ self.text = nil
152
+ end
153
+
154
+ function Text:SetCustomData(idx, data)
155
+ assert(idx ~= const.CRTextCCDIndexText, "Use SetText instead")
156
+ return SetCustomData(self, idx, data)
157
+ end
158
+
159
+ DefineClass.TextEditor = {
160
+ __parents = {"Text", "EditorVisibleObject"},
161
+ }
162
+
163
+ function PlaceText(text, pos, color, editor_visibile_only)
164
+ local obj = PlaceObject(editor_visibile_only and "TextEditor" or "Text")
165
+ if pos then
166
+ obj:SetPos(pos)
167
+ end
168
+ obj:SetText(text)
169
+ if color then
170
+ obj:SetColor(color)
171
+ end
172
+ return obj
173
+ end
174
+
175
+ function RemoveAllTexts()
176
+ MapDelete("map", "Text")
177
+ end
178
+
179
+ local function GetMeshFlags()
180
+ local flags = {}
181
+ for name, value in pairs(const) do
182
+ if string.starts_with(name, "mf") then
183
+ flags[value] = name
184
+ end
185
+ end
186
+ return flags
187
+ end
188
+
189
+ DefineClass.MeshParamSet = {
190
+ __parents = { "PropertyObject" },
191
+ properties = {
192
+
193
+ },
194
+ uniforms = false,
195
+ uniforms_size = 0,
196
+ }
197
+
198
+ local uniform_sizes = {
199
+ integer = 4,
200
+ float = 4,
201
+ color = 4,
202
+ point2 = 8,
203
+ point3 = 12,
204
+ }
205
+
206
+ function GetUniformMeta(properties)
207
+ local uniforms = {}
208
+ local offset = 0
209
+ for _, prop in ipairs(properties) do
210
+ local uniform_type = prop.uniform
211
+ if uniform_type then
212
+ if type(uniform_type) ~= "string" then
213
+ if prop.editor == "number" then
214
+ uniform_type = prop.scale and prop.scale ~= 1 and "float" or "integer"
215
+ elseif prop.editor == "point" then
216
+ uniform_type = "point3"
217
+ else
218
+ uniform_type = prop.editor
219
+ end
220
+ end
221
+ local size = uniform_sizes[uniform_type]
222
+ if not size then
223
+ assert(false, "Unknown uniform type.")
224
+ end
225
+ local space = 16 - (offset % 16)
226
+ if space < size then
227
+ table.insert(uniforms, {id = false, type = "padding", offset = offset, size = space})
228
+ offset = offset + space
229
+ end
230
+ table.insert(uniforms, {id = prop.id, type = uniform_type, offset = offset, size = size, scale = prop.scale})
231
+ offset = offset + size
232
+ end
233
+ end
234
+ return uniforms, offset
235
+ end
236
+
237
+ function OnMsg.ClassesPostprocess()
238
+ ClassDescendantsList("MeshParamSet", function(name, def)
239
+ def.uniforms, def.uniforms_size = GetUniformMeta(def:GetProperties())
240
+ end)
241
+ end
242
+
243
+ function MeshParamSet:WriteBuffer(param_pstr, offset, getter)
244
+ if not offset then
245
+ offset = 0
246
+ end
247
+ if not getter then
248
+ getter = self.GetProperty
249
+ end
250
+ param_pstr = param_pstr or pstr("", self.uniforms_size)
251
+ param_pstr:resize(offset)
252
+ for _, prop in ipairs(self.uniforms) do
253
+ local value
254
+ if prop.type == "padding" then
255
+ value = prop.size
256
+ else
257
+ value = getter(self, prop.id)
258
+ end
259
+
260
+ param_pstr:AppendUniform(prop.type, value, prop.scale)
261
+ end
262
+ return param_pstr
263
+ end
264
+
265
+ function MeshParamSet:ComposeBuffer(param_pstr, getter)
266
+ return self:WriteBuffer(param_pstr, 0, getter)
267
+ end
268
+
269
+ DefineClass.Mesh = {
270
+ __parents = { "CodeRenderableObject" },
271
+
272
+ properties = {
273
+ { id = "vertices_len", read_only = true, dont_save = true, editor = "number", default = 0,},
274
+ { id = "CRMaterial", editor = "nested_obj", base_class = "CRMaterial", default = false, },
275
+ { id = "MeshFlags", editor = "flags", default = 0, items = GetMeshFlags },
276
+ { id = "DepthTest", editor = "bool", default = false, read_only = function(s) return not s.shader or s.shader.depth_test ~= "runtime" end, },
277
+ { id = "ShaderName", editor = "choice", default = "default_mesh", items = function() return table.keys2(ProceduralMeshShaders, "sorted") end},
278
+ },
279
+ --[[
280
+ Custom data layout:
281
+ const.CRMeshCCDIndexGeometry - vertices, packed in a pstr
282
+ const.CRMeshCCDIndexPipeline - shader; depth_test >> 31, 0 for off, 1 for on
283
+ const.CRMeshCCDIndexMeshFlags - mesh flags
284
+ const.CRMeshCCDIndexUniforms - uniforms, packed in a pstr
285
+ const.CRMeshCCDIndexTexture0, const.CRMeshCCDIndexTexture1 - textures
286
+ ]]
287
+
288
+ vertices_pstr = false,
289
+ uniforms_pstr = false,
290
+ shader = false,
291
+ textstyle_id = false,
292
+ }
293
+
294
+ function Mesh:GedTreeViewFormat()
295
+ return string.format("%s (%s)", self.class, self.CRMaterial and self.CRMaterial.id or self:GetShaderName())
296
+ end
297
+
298
+ function Mesh:Getvertices_len()
299
+ return self.vertices_pstr and #self.vertices_pstr or 0
300
+ end
301
+
302
+ function Mesh:GetShaderName()
303
+ return self.shader and self.shader.name or ""
304
+ end
305
+
306
+ function Mesh:SetShaderName(value)
307
+ self:SetShader(ProceduralMeshShaders[value])
308
+ end
309
+
310
+ function Mesh:Init()
311
+ self:SetShader(ProceduralMeshShaders.default_mesh)
312
+ end
313
+
314
+ function Mesh:SetMesh(vpstr)
315
+ KeepRefOneFrame(self.vertices_pstr)
316
+ local vertices_pstr = #(vpstr or "") > 0 and vpstr or nil -- an empty string would result in a crash :|
317
+ self.vertices_pstr = vertices_pstr
318
+ SetCustomData(self, const.CRMeshCCDIndexGeometry, vertices_pstr)
319
+ end
320
+
321
+ function Mesh:SetUniformSet(uniform_set)
322
+ self:SetUniformsPstr(uniform_set:ComposeBuffer())
323
+ end
324
+
325
+ function Mesh:SetUniformsPstr(uniforms_pstr)
326
+ KeepRefOneFrame(self.uniforms_pstr)
327
+ self.uniforms_pstr = uniforms_pstr
328
+ SetCustomData(self, const.CRMeshCCDIndexUniforms, uniforms_pstr)
329
+ end
330
+
331
+ function Mesh:SetUniformsList(uniforms, isDouble)
332
+ KeepRefOneFrame(self.uniforms_pstr)
333
+ local count = Max(8, #uniforms)
334
+ local uniforms_pstr = pstr("", count * 4)
335
+ self.uniforms_pstr = uniforms_pstr
336
+ for i = 1, count do
337
+ if isDouble then
338
+ uniforms_pstr:AppendUniform("double", uniforms[i] or 0)
339
+ else
340
+ uniforms_pstr:AppendUniform("float", uniforms[i] or 0, 1000)
341
+ end
342
+ end
343
+ SetCustomData(self, const.CRMeshCCDIndexUniforms, uniforms_pstr)
344
+ end
345
+
346
+ function Mesh:SetUniforms(...)
347
+ return self:SetUniformsList{...}
348
+ end
349
+
350
+ function Mesh:SetDoubleUniforms(...)
351
+ return self:SetUniformsList({...}, true)
352
+ end
353
+
354
+ function Mesh:SetShader(shader, depth_test)
355
+ assert(shader.shaderid)
356
+ assert(shader.defines)
357
+ assert(shader.ref_id > 0)
358
+ if depth_test == nil then
359
+ if shader.depth_test == "always" then
360
+ depth_test = true
361
+ elseif shader.depth_test == "never" then
362
+ depth_test = false
363
+ else
364
+ depth_test = self:GetDepthTest()
365
+ end
366
+ end
367
+ local depth_test_int = 0
368
+ if depth_test then
369
+ depth_test_int = 1
370
+ assert(shader.depth_test == "runtime" or shader.depth_test == "always", "Tried to enable depth test for shader with depth_test = never")
371
+ else
372
+ depth_test_int = 0
373
+ assert(shader.depth_test == "runtime" or shader.depth_test == "never", "Tried to disable depth test for shader with depth_test = always")
374
+ end
375
+ SetCustomData(self, const.CRMeshCCDIndexPipeline, shader.ref_id | (depth_test_int << 31))
376
+ self.shader = shader
377
+ end
378
+
379
+ function Mesh:SetDepthTest(depth_test)
380
+ assert(self.shader)
381
+ self:SetShader(self.shader, depth_test)
382
+ end
383
+
384
+ function Mesh:SetCRMaterial(material)
385
+ if type(material) == "string" then
386
+ local new_material = CRMaterial:GetById(material, true)
387
+ assert(new_material, "CRMaterial not found.")
388
+ material = new_material
389
+ end
390
+ self.CRMaterial = material
391
+ local depth_test = material.depth_test
392
+ if depth_test == "default" then depth_test = nil end
393
+
394
+ CodeRenderableLockCCD(self)
395
+ self:SetShader(material:GetShader(), depth_test)
396
+ self:SetUniformsPstr(material:GetDataPstr())
397
+ CodeRenderableUnlockCCD(self)
398
+ end
399
+
400
+ function Mesh:GetCRMaterial()
401
+ return self.CRMaterial
402
+ end
403
+
404
+ if FirstLoad then
405
+ MeshTextureRefCount = {}
406
+ end
407
+
408
+ local function ModifyMeshTextureRefCount(id, change)
409
+ if id == 0 then return end
410
+ local old = MeshTextureRefCount[id] or 0
411
+ local new = old + change
412
+ if new == 0 then
413
+ MeshTextureRefCount[id] = nil
414
+ ProceduralMeshReleaseResource(id)
415
+ else
416
+ MeshTextureRefCount[id] = new
417
+ end
418
+ end
419
+
420
+ function Mesh:SetTexture(idx, resource_id)
421
+ assert(idx >= 0 and idx <= 1)
422
+ if self:GetTexture(idx) == resource_id then return end
423
+ ModifyMeshTextureRefCount(self:GetTexture(idx), -1)
424
+ SetCustomData(self, const.CRMeshCCDIndexTexture0 + idx, resource_id or 0)
425
+ ModifyMeshTextureRefCount(resource_id, 1)
426
+ end
427
+
428
+ function Mesh:GetTexture(idx)
429
+ assert(idx >= 0 and idx <= 1)
430
+ return GetCustomData(self, const.CRMeshCCDIndexTexture0 + idx) or 0
431
+ end
432
+
433
+ function Mesh:Done()
434
+ KeepRefOneFrame(self.vertices_pstr)
435
+ KeepRefOneFrame(self.uniforms_pstr)
436
+ self.vertices_pstr = nil
437
+ self.uniforms_pstr = nil
438
+ self:SetTexture(0, 0)
439
+ self:SetTexture(1, 0)
440
+ end
441
+
442
+ function OnMsg.DoneMap()
443
+ for key, value in pairs(MeshTextureRefCount) do
444
+ ProceduralMeshReleaseResource(key)
445
+ end
446
+ MeshTextureRefCount = {}
447
+ end
448
+
449
+ function Mesh:SetCustomData(idx, data)
450
+ assert(idx > const.CRMeshCCDIndexGeometry, "Use SetMesh instead!")
451
+ return SetCustomData(self, idx, data)
452
+ end
453
+ function Mesh:GetDepthTest() return (GetCustomData(self, const.CRMeshCCDIndexPipeline) >> 31) == 1 end
454
+ function Mesh:SetMeshFlags(flags) SetCustomData(self, const.CRMeshCCDIndexMeshFlags, flags) end
455
+ function Mesh:GetMeshFlags() return GetCustomData(self, const.CRMeshCCDIndexMeshFlags) end
456
+ function Mesh:AddMeshFlags(flags) self:SetMeshFlags(flags | self:GetMeshFlags()) end
457
+ function Mesh:ClearMeshFlags(flags) self:SetMeshFlags(~flags & self:GetMeshFlags()) end
458
+
459
+ function Mesh.ColorFromTextStyle(id)
460
+ assert(TextStyles[id])
461
+ return TextStyles[id].TextColor
462
+ end
463
+
464
+ function AppendCircleVertices(vpstr, center, radius, color, strip)
465
+ local HSeg = 32
466
+ vpstr = vpstr or pstr("", 1024)
467
+ color = color or RGB(254, 127, 156)
468
+ center = center or point30
469
+ local x0, y0, z0
470
+ for i = 0, HSeg do
471
+ local x, y, z = RotateRadius(radius, MulDivRound(360 * 60, i, HSeg), center, true)
472
+ AppendVertex(vpstr, x, y, z, color)
473
+ if not strip then
474
+ if i ~= 0 then
475
+ AppendVertex(vpstr, x, y, z, color)
476
+ if i == HSeg then
477
+ AppendVertex(vpstr, x0, y0, z0)
478
+ end
479
+ else
480
+ x0, y0, z0 = x, y, z
481
+ end
482
+ end
483
+ end
484
+
485
+ return vpstr
486
+ end
487
+
488
+ function AppendTileVertices(vstr, x, y, z, tile_size, color, offset_z, get_height)
489
+ offset_z = offset_z or 0
490
+ z = z or InvalidZ
491
+ local d = tile_size / 2
492
+ local x1, y1, z1 = x - d, y - d
493
+ local x2, y2, z2 = x + d, y - d
494
+ local x3, y3, z3 = x - d, y + d
495
+ local x4, y4, z4 = x + d, y + d
496
+ get_height = get_height or GetHeight
497
+ if z ~= InvalidZ and z ~= get_height(x, y) then
498
+ z = z + offset_z
499
+ z1, z2, z3, z4 = z, z, z, z
500
+ else
501
+ z1 = get_height(x1, y1) + offset_z
502
+ z2 = get_height(x2, y2) + offset_z
503
+ z3 = get_height(x3, y3) + offset_z
504
+ z4 = get_height(x4, y4) + offset_z
505
+ end
506
+ AppendVertex(vstr, x1, y1, z1, color)
507
+ AppendVertex(vstr, x2, y2, z2, color)
508
+ AppendVertex(vstr, x3, y3, z3, color)
509
+ AppendVertex(vstr, x4, y4, z4, color)
510
+ AppendVertex(vstr, x2, y2, z2, color)
511
+ AppendVertex(vstr, x3, y3, z3, color)
512
+ end
513
+
514
+ function GetSizePstrTile()
515
+ return 6 * const.pstrVertexSize
516
+ end
517
+
518
+ function AppendTorusVertices(vpstr, radius1, radius2, axis, color, normal)
519
+ local HSeg = 32
520
+ local VSeg = 10
521
+ vpstr = vpstr or pstr("", 1024)
522
+ local rad1 = Rotate(axis, 90 * 60)
523
+ rad1 = Cross(axis, rad1)
524
+ rad1 = Normalize(rad1)
525
+ rad1 = MulDivRound(rad1, radius1, 4096)
526
+ for i = 1, HSeg do
527
+ local localCenter1 = RotateAxis(rad1, axis, MulDivRound(360 * 60, i, HSeg))
528
+ local localCenter2 = RotateAxis(rad1, axis, MulDivRound(360 * 60, i - 1, HSeg))
529
+ local lastUpperPt, lastPt
530
+ if not normal or not IsPointInFrontOfPlane(point(0, 0, 0), normal, (localCenter1 + localCenter2) / 2) then
531
+ for j = 0, VSeg do
532
+ local rad2 = MulDivRound(localCenter1, radius2, radius1)
533
+ local localAxis = Cross(rad2, axis)
534
+ local pt = RotateAxis(rad2, localAxis, MulDivRound(360 * 60, j, VSeg))
535
+ pt = localCenter1 + pt
536
+ rad2 = MulDivRound(localCenter2, radius2, radius1)
537
+ localAxis = Cross(rad2, axis)
538
+ local upperPt = RotateAxis(rad2, localAxis, MulDivRound(360 * 60, j, VSeg))
539
+ upperPt = localCenter2 + upperPt
540
+ if j ~= 0 then
541
+ AppendVertex(vpstr, pt, color)
542
+ AppendVertex(vpstr, lastPt)
543
+ AppendVertex(vpstr, upperPt)
544
+ AppendVertex(vpstr, upperPt, color)
545
+ AppendVertex(vpstr, lastUpperPt)
546
+ AppendVertex(vpstr, lastPt)
547
+ end
548
+ lastPt = pt
549
+ lastUpperPt = upperPt
550
+ end
551
+ end
552
+ end
553
+
554
+ return vpstr
555
+ end
556
+
557
+ function AppendConeVertices(vpstr, center, displacement, radius1, radius2, axis, angle, color, offset)
558
+ local HSeg = 10
559
+ vpstr = vpstr or pstr("", 1024)
560
+ center = center or point(0, 0, 0)
561
+ displacement = displacement or point(0, 0, 30 * guim)
562
+ axis = axis or axis_z
563
+ angle = angle or 0
564
+ offset = offset or point(0, 0, 0)
565
+ color = color or RGB(254, 127, 156)
566
+ local lastPt, lastUpperPt
567
+ for i = 0, HSeg do
568
+ local rad = point(radius1, 0, 0)
569
+ local pt = center + Rotate(rad, MulDivRound(360 * 60, i, HSeg))
570
+ local upperRad = point(radius2, 0, 0)
571
+ local upperPt = center + displacement + Rotate(upperRad, MulDivRound(360 * 60, i, HSeg))
572
+ pt = RotateAxis(pt, axis, angle * 60) + offset
573
+ upperPt = RotateAxis(upperPt, axis, angle * 60) + offset
574
+ if i ~= 0 then
575
+ AppendVertex(vpstr, pt, color)
576
+ AppendVertex(vpstr, lastPt)
577
+ AppendVertex(vpstr, upperPt)
578
+ if radius2 ~= 0 then
579
+ AppendVertex(vpstr, upperPt, color)
580
+ AppendVertex(vpstr, lastUpperPt)
581
+ AppendVertex(vpstr, lastPt)
582
+ end
583
+ end
584
+ lastPt = pt
585
+ lastUpperPt = upperPt
586
+ end
587
+
588
+ return vpstr
589
+ end
590
+
591
+ DefineClass.Polyline =
592
+ {
593
+ __parents = { "Mesh" },
594
+ }
595
+
596
+ function Polyline:Init()
597
+ self:SetMeshFlags(const.mfWorldSpace)
598
+ self:SetShader(ProceduralMeshShaders.default_polyline)
599
+ end
600
+
601
+ DefineClass.Vector = {
602
+ __parents = {"Polyline"},
603
+ }
604
+
605
+ function Vector:Set (a, b, col)
606
+ col = col or RGB(255, 255, 255)
607
+ a = ValidateZ(a)
608
+ b = ValidateZ(b)
609
+ self:SetPos(a)
610
+
611
+ local vpstr = pstr("", 1024)
612
+
613
+ AppendVertex(vpstr, a, col)
614
+ AppendVertex(vpstr, b)
615
+
616
+ local ab = b - a
617
+ local cb = (ab * 5) / 100
618
+ local f = cb:Len() / 4
619
+ local c = b - cb
620
+
621
+ local n = 4
622
+ local ps = GetRadialPoints (n, c, cb, f)
623
+ for i = 1 , n/2 do
624
+ AppendVertex(vpstr, ps[i])
625
+ AppendVertex(vpstr, ps[i + n/2])
626
+ AppendVertex(vpstr, b)
627
+ end
628
+ self:SetMesh(vpstr)
629
+ end
630
+
631
+ function Vector:GetA()
632
+ return self:GetPos()
633
+ end
634
+
635
+ function ShowVector(vector, origin, color, time)
636
+ local v = PlaceObject("Vector")
637
+ origin = origin:z() and origin or point(origin:x(), origin:y(), GetWalkableZ(origin))
638
+ vector = vector:z() and vector or point(vector:x(), vector:y(), 0)
639
+ v:Set(origin, origin + vector, color)
640
+ if time then
641
+ CreateGameTimeThread(function()
642
+ Sleep(time)
643
+ DoneObject(v)
644
+ end)
645
+ end
646
+
647
+ return v
648
+ end
649
+
650
+ DefineClass.Segment = {
651
+ __parents = {"Polyline"},
652
+ }
653
+
654
+ function Segment:Init()
655
+ self:SetDepthTest(false)
656
+ end
657
+
658
+ function Segment:Set (a, b, col)
659
+ col = col or RGB(255, 255, 255)
660
+ a = ValidateZ(a)
661
+ b = ValidateZ(b)
662
+ self:SetPos(a)
663
+ local vpstr = pstr("", 1024)
664
+ AppendVertex(vpstr, a, col)
665
+ AppendVertex(vpstr, b)
666
+ self:SetMesh(vpstr)
667
+ end
668
+
669
+ -- After loading the code renderables from C, fix their string custom data in the Lua
670
+ function OnMsg.PersistLoad(_dummy_)
671
+ MapForEach(true, "Text", function(obj)
672
+ SetCustomData(obj, const.CRTextCCDIndexText, obj.text or 0)
673
+ end)
674
+ MapForEach(true, "Mesh", function(obj)
675
+ CodeRenderableLockCCD(obj)
676
+ SetCustomData(obj, const.CRMeshCCDIndexGeometry, obj.vertices_pstr or 0)
677
+ SetCustomData(obj, const.CRMeshCCDIndexUniforms, obj.uniforms_pstr or 0)
678
+ CodeRenderableUnlockCCD(obj)
679
+ end)
680
+ end
681
+
682
+ ----
683
+
684
+ function PlaceTerrainCircle(center, radius, color, step, offset, max_steps)
685
+ step = step or guim
686
+ offset = offset or guim
687
+ local steps = Min(Max(12, (44 * radius) / (7 * step)), max_steps or 360)
688
+ local last_pt
689
+ local mapw, maph = terrain.GetMapSize()
690
+ local vpstr = pstr("", 1024)
691
+ for i = 0,steps do
692
+ local x, y = RotateRadius(radius, MulDivRound(360*60, i, steps), center, true)
693
+ x = Clamp(x, 0, mapw - height_tile)
694
+ y = Clamp(y, 0, maph - height_tile)
695
+ AppendVertex(vpstr, x, y, offset, color)
696
+ end
697
+
698
+ local line = PlaceObject("Polyline")
699
+ line:SetMesh(vpstr)
700
+ line:SetPos(center)
701
+ line:AddMeshFlags(const.mfTerrainDistorted)
702
+ return line
703
+ end
704
+
705
+ local function GetTerrainPointsPStr(vpstr, pt1, pt2, step, offset, color)
706
+ step = step or guim
707
+ offset = offset or guim
708
+ local diff = pt2 - pt1
709
+ local steps = Max(2, 1 + diff:Len2D() / step)
710
+ local mapw, maph = terrain.GetMapSize()
711
+ vpstr = vpstr or pstr("", 1024)
712
+ for i=1,steps do
713
+ local pos = pt1 + MulDivRound(diff, i - 1, steps - 1)
714
+ local x, y = pos:xy()
715
+ x = Clamp(x, 0, mapw - height_tile)
716
+ y = Clamp(y, 0, maph - height_tile)
717
+ AppendVertex(vpstr, x, y, offset, color)
718
+ end
719
+ return vpstr
720
+ end
721
+
722
+ function PlaceTerrainLine(pt1, pt2, color, step, offset)
723
+ local vpstr = GetTerrainPointsPStr(false, pt1, pt2, step, offset, color)
724
+ local line = PlaceObject("Polyline")
725
+ line:SetMesh(vpstr)
726
+ line:SetPos((pt1 + pt2) / 2)
727
+ line:AddMeshFlags(const.mfTerrainDistorted)
728
+ return line
729
+ end
730
+
731
+ function PlaceTerrainBox(box, color, step, offset, mesh_obj, depth_test)
732
+ local p = {box:ToPoints2D()}
733
+ local m
734
+ for i = 1, #p do
735
+ m = GetTerrainPointsPStr(m, p[i], p[i + 1] or p[1], step, offset, color)
736
+ end
737
+ mesh_obj = mesh_obj or PlaceObject("Polyline")
738
+ if depth_test ~= nil then
739
+ mesh_obj:SetDepthTest(depth_test)
740
+ end
741
+ mesh_obj:SetMesh(m)
742
+ mesh_obj:SetPos(box:Center())
743
+ mesh_obj:AddMeshFlags(const.mfTerrainDistorted)
744
+ return mesh_obj
745
+ end
746
+
747
+ function PlaceTerrainPoly(p, color, step, offset, mesh_obj)
748
+ local m
749
+ local center = p[1] + ((p[1] - p[3]) / 2)
750
+ for i = 1, #p do
751
+ m = GetTerrainPointsPStr(m, p[i], p[i + 1] or p[1], step, offset, color)
752
+ end
753
+ mesh_obj = mesh_obj or PlaceObject("Polyline")
754
+ mesh_obj:SetMesh(m)
755
+ mesh_obj:SetPos(center)
756
+ return mesh_obj
757
+ end
758
+
759
+ function PlacePolyLine(pts, clrs, depth_test)
760
+ local line = PlaceObject("Polyline")
761
+ line:SetEnumFlags(const.efVisible)
762
+ if depth_test ~= nil then
763
+ line:SetDepthTest(depth_test)
764
+ end
765
+ local vpstr = pstr("", 1024)
766
+ local clr
767
+ local pt0
768
+ for i, pt in ipairs(pts) do
769
+ if IsValidPos(pt) then
770
+ pt0 = pt0 or pt
771
+ clr = type(clrs) == "table" and clrs[i] or clrs or clr
772
+ AppendVertex(vpstr, pt, clr)
773
+ end
774
+ end
775
+ line:SetMesh(vpstr)
776
+ if pt0 then
777
+ line:SetPos(pt0)
778
+ end
779
+ return line
780
+ end
781
+
782
+ function AppendSplineVertices(spline, color, step, min_steps, max_steps, vpstr)
783
+ step = step or guim
784
+ min_steps = min_steps or 7
785
+ max_steps = max_steps or 1024
786
+ local len = BS3_GetSplineLength3D(spline)
787
+ local steps = Clamp(len / step, min_steps, max_steps)
788
+ vpstr = vpstr or pstr("", (steps + 2) * const.pstrVertexSize)
789
+ local x, y, z
790
+ local x0, y0, z0 = BS3_GetSplinePos(spline, 0)
791
+ AppendVertex(vpstr, x0, y0, z0, color)
792
+ for i = 1,steps-1 do
793
+ local x, y, z = BS3_GetSplinePos(spline, i, steps)
794
+ AppendVertex(vpstr, x, y, z, color)
795
+ end
796
+ local x1, y1, z1 = BS3_GetSplinePos(spline, steps, steps)
797
+ AppendVertex(vpstr, x1, y1, z1, color)
798
+ return vpstr, point((x0 + x1) / 2, (y0 + y1) / 2, (z0 + z1) / 2)
799
+ end
800
+
801
+ function PlaceSpline(spline, color, depth_test, step, min_steps, max_steps)
802
+ local line = PlaceObject("Polyline")
803
+ line:SetEnumFlags(const.efVisible)
804
+ if depth_test ~= nil then
805
+ line:SetDepthTest(depth_test)
806
+ end
807
+ local vpstr, pos = AppendSplineVertices(spline, color, step, min_steps, max_steps)
808
+ line:SetMesh(vpstr)
809
+ line:SetPos(pos)
810
+ return line
811
+ end
812
+
813
+ function PlaceSplines(splines, color, depth_test, start_idx, step, min_steps, max_steps)
814
+ local line = PlaceObject("Polyline")
815
+ line:SetEnumFlags(const.efVisible)
816
+ if depth_test ~= nil then
817
+ line:SetDepthTest(depth_test)
818
+ end
819
+ local count = #(splines or "")
820
+ local pos = point30
821
+ local vpstr = pstr("", count * 128 * const.pstrVertexSize)
822
+ for i = (start_idx or 1), count do
823
+ local _, posi = AppendSplineVertices(splines[i], color, step, min_steps, max_steps, vpstr)
824
+ pos = pos + posi
825
+ end
826
+ if count > 0 then
827
+ pos = pos / count
828
+ end
829
+ line:SetMesh(vpstr)
830
+ line:SetPos(pos)
831
+ return line
832
+ end
833
+
834
+ function PlaceBox(box, color, mesh_obj, depth_test)
835
+ local p1, p2, p3, p4 = box:ToPoints2D()
836
+ local minz, maxz = box:minz(), box:maxz()
837
+ local vpstr = pstr("", 1024)
838
+ if minz and maxz then
839
+ if minz >= maxz - 1 then
840
+ for _, p in ipairs{p1, p2, p3, p4, p1} do
841
+ local x, y = p:xy()
842
+ AppendVertex(vpstr, x, y, minz, color)
843
+ end
844
+ else
845
+ for _, z in ipairs{minz, maxz} do
846
+ for _, p in ipairs{p1, p2, p3, p4, p1} do
847
+ local x, y = p:xy()
848
+ AppendVertex(vpstr, x, y, z, color)
849
+ end
850
+ end
851
+ AppendVertex(vpstr, p2:SetZ(maxz), color)
852
+ AppendVertex(vpstr, p2:SetZ(minz), color)
853
+ AppendVertex(vpstr, p3:SetZ(minz), color)
854
+ AppendVertex(vpstr, p3:SetZ(maxz), color)
855
+ AppendVertex(vpstr, p4:SetZ(maxz), color)
856
+ AppendVertex(vpstr, p4:SetZ(minz), color)
857
+ end
858
+ else
859
+ local z = terrain.GetHeight(p1)
860
+ for _, p in ipairs{p2, p3, p4} do
861
+ z = Max(z, terrain.GetHeight(p))
862
+ end
863
+ for _, p in ipairs{p1, p2, p3, p4, p1} do
864
+ local x, y = p:xy()
865
+ AppendVertex(vpstr, x, y, z, color)
866
+ end
867
+ end
868
+ mesh_obj = mesh_obj or PlaceObject("Polyline")
869
+ if depth_test ~= nil then
870
+ mesh_obj:SetDepthTest(depth_test)
871
+ end
872
+ mesh_obj:SetMesh(vpstr)
873
+ mesh_obj:SetPos(box:Center())
874
+ return mesh_obj
875
+ end
876
+
877
+ function PlaceVector(pos, vec, color, depth_test)
878
+ vec = vec or 10*guim
879
+ vec = type(vec) == "number" and point(0, 0, vec) or vec
880
+ return PlacePolyLine({pos, pos + vec}, color, depth_test)
881
+ end
882
+
883
+ function CreateTerrainCursorCircle(radius, color)
884
+ color = color or RGB(23, 34, 122)
885
+ radius = radius or 30 * guim
886
+
887
+ local line = CreateCircleMesh(radius, color)
888
+ line:SetPos(GetTerrainCursor())
889
+ line:SetMeshFlags(const.mfOffsetByTerrainCursor + const.mfTerrainDistorted + const.mfWorldSpace)
890
+ return line
891
+ end
892
+
893
+ function CreateTerrainCursorSphere(radius, color)
894
+ color = color or RGB(23, 34, 122)
895
+ radius = radius or 30 * guim
896
+
897
+ local line = PlaceObject("Mesh")
898
+ line:SetMesh(CreateSphereVertices(radius, color))
899
+ line:SetShader(ProceduralMeshShaders.mesh_linelist)
900
+ line:SetPos(GetTerrainCursor())
901
+ line:SetMeshFlags(const.mfOffsetByTerrainCursor + const.mfTerrainDistorted + const.mfWorldSpace)
902
+ return line
903
+ end
904
+
905
+ function CreateOrientationMesh(pos)
906
+ local o_mesh = Mesh:new()
907
+ pos = pos or point(0, 0, 0)
908
+ o_mesh:SetShader(ProceduralMeshShaders.mesh_linelist)
909
+ local r = guim/4
910
+ local vpstr = pstr("", 1024)
911
+ AppendVertex(vpstr, point(0, 0, 0), RGB(255, 0, 0))
912
+ AppendVertex(vpstr, point(r, 0, 0))
913
+ AppendVertex(vpstr, point(0, 0, 0), RGB(0, 255, 0))
914
+ AppendVertex(vpstr, point(0, r, 0))
915
+ AppendVertex(vpstr, point(0, 0, 0), RGB(0, 0, 255))
916
+ AppendVertex(vpstr, point(0, 0, r))
917
+ o_mesh:SetMesh(vpstr)
918
+ o_mesh:SetPos(pos)
919
+ return o_mesh
920
+ end
921
+
922
+ function CreateSphereMesh(radius, color, precision)
923
+ local sphere_mesh = Mesh:new()
924
+ sphere_mesh:SetMesh(CreateSphereVertices(radius, color))
925
+ sphere_mesh:SetShader(ProceduralMeshShaders.mesh_linelist)
926
+ return sphere_mesh
927
+ end
928
+
929
+ function PlaceSphere(center, radius, color, depth_test)
930
+ local sphere = CreateSphereMesh(radius, color)
931
+ if depth_test ~= nil then
932
+ sphere:SetDepthTest(depth_test)
933
+ end
934
+ sphere:SetPos(center)
935
+ return sphere
936
+ end
937
+
938
+ function ShowMesh(time, func, ...)
939
+ local ok, meshes = procall(func, ...)
940
+ if not ok or not meshes then
941
+ return
942
+ end
943
+ return CreateRealTimeThread(function(meshes, time)
944
+ Msg("ShowMesh")
945
+ WaitMsg("ShowMesh", time)
946
+ if IsValid(meshes) then
947
+ DoneObject(meshes)
948
+ else
949
+ DoneObjects(meshes)
950
+ end
951
+ end, meshes, time)
952
+ end
953
+
954
+ function CreateCircleMesh(radius, color, center)
955
+ local circle_mesh = Mesh:new()
956
+ circle_mesh:SetMesh(AppendCircleVertices(nil, center, radius, color, true))
957
+ circle_mesh:SetShader(ProceduralMeshShaders.default_polyline)
958
+ return circle_mesh
959
+ end
960
+
961
+ function PlaceCircle(center, radius, color, depth_test)
962
+ local circle = CreateCircleMesh(radius, color)
963
+ if depth_test ~= nil then
964
+ circle:SetDepthTest(depth_test)
965
+ end
966
+ circle:SetPos(center)
967
+ return circle
968
+ end
969
+
970
+ function CreateConeMesh(center, displacement, radius1, radius2, axis, angle, color)
971
+ local circle_mesh = Mesh:new()
972
+ circle_mesh:SetMesh(AppendConeVertices(nil, center, displacement, radius1, radius2, axis, angle, color))
973
+ circle_mesh:SetShader(ProceduralMeshShaders.mesh_linelist)
974
+ return circle_mesh
975
+ end
976
+
977
+ function CreateCylinderMesh(center, displacement, radius, axis, angle, color)
978
+ local circle_mesh = Mesh:new()
979
+ circle_mesh:SetMesh(AppendConeVertices(nil, center, displacement, radius, radius, axis, angle, color))
980
+ circle_mesh:SetShader(ProceduralMeshShaders.default_mesh)
981
+ return circle_mesh
982
+ end
983
+
984
+ function CreateMoveGizmo()
985
+ local g_MoveGizmo = MoveGizmo:new()
986
+ CreateRealTimeThread(function()
987
+ while true do
988
+ g_MoveGizmo:OnMousePos(GetTerrainCursor())
989
+ Sleep(100)
990
+ end
991
+ end)
992
+ end
993
+
994
+ function CreateTerrainCursorTorus(radius1, radius2, axis, angle, color)
995
+ color = color or RGB(255, 0, 0)
996
+ radius1 = radius1 or 2.3 * guim
997
+ radius2 = radius2 or 0.15 * guim
998
+ axis = axis or axis_y
999
+ angle = angle or 90
1000
+
1001
+ local line = PlaceObject("Mesh")
1002
+ local vpstr = pstr("", 1024)
1003
+ local normal = selo():GetPos() - camera.GetEye()
1004
+ local b = selo():GetPos()
1005
+ local bigTorusAxis, bigTorusAngle = GetAxisAngle(normal, axis_z)
1006
+ bigTorusAxis = Normalize(bigTorusAxis)
1007
+ bigTorusAngle = 180 - bigTorusAngle / 60
1008
+ vpstr = AppendTorusVertices(vpstr, point(0, 0, 0), 2.3 * guim, 0.15 * guim, bigTorusAxis, bigTorusAngle, RGB(128, 128, 128))
1009
+ vpstr = AppendTorusVertices(vpstr, point(0, 0, 0), 2.3 * guim, 0.15 * guim, axis_y, 90, RGB(255, 0, 0), normal, b)
1010
+ vpstr = AppendTorusVertices(vpstr, point(0, 0, 0), 2.3 * guim, 0.15 * guim, axis_x, 90, RGB(0, 255, 0), normal, b)
1011
+ vpstr = AppendTorusVertices(vpstr, point(0, 0, 0), 2.3 * guim, 0.15 * guim, axis_z, 0, RGB(0, 0, 255), normal, b)
1012
+ vpstr = AppendTorusVertices(vpstr, point(0, 0, 0), 3.5 * guim, 0.15 * guim, bigTorusAxis, bigTorusAngle, RGB(0, 192, 192))
1013
+ line:SetMesh(vpstr)
1014
+ line:SetPos(selo():GetPos())
1015
+ return line
1016
+ end
1017
+
1018
+ function CreateObjSurfaceMesh(obj, surface_flag, color1, color2)
1019
+ if not IsValidPos(obj) then
1020
+ return
1021
+ end
1022
+ local v_pstr = pstr("", 1024)
1023
+ ForEachSurface(obj, surface_flag, function(pt1, pt2, pt3, v_pstr, color1, color2)
1024
+ local color
1025
+ if color1 and color2 then
1026
+ local rand = xxhash(pt1, pt2, pt3) % 1024
1027
+ color = InterpolateRGB(color1, color2, rand, 1024)
1028
+ end
1029
+ v_pstr:AppendVertex(pt1, color)
1030
+ v_pstr:AppendVertex(pt2, color)
1031
+ v_pstr:AppendVertex(pt3, color)
1032
+ end, v_pstr, color1, color2)
1033
+ local mesh = PlaceObject("Mesh")
1034
+ mesh:SetMesh(v_pstr)
1035
+ mesh:SetPos(obj:GetPos())
1036
+ mesh:SetMeshFlags(const.mfWorldSpace)
1037
+ mesh:SetDepthTest(true)
1038
+ if color1 and not color2 then
1039
+ mesh:SetColorModifier(color1)
1040
+ end
1041
+ return mesh
1042
+ end
1043
+
1044
+ function FlatImageMesh(texture, width, height, glow_size, glow_period, glow_color)
1045
+ local text = PlaceObject("Mesh")
1046
+ local vpstr = pstr("", 1024)
1047
+ local color = RGB(255,255,255)
1048
+ local half_size_x = width or 1000
1049
+ local half_size_y = height or 1000
1050
+ glow_size = glow_size or 0
1051
+ glow_period = glow_period or 0
1052
+ glow_color = glow_color or RGB(255,255,255)
1053
+
1054
+ AppendVertex(vpstr, point(-half_size_x, -half_size_y, 0), color, 0, 0)
1055
+ AppendVertex(vpstr, point(half_size_x, -half_size_y, 0), color, 1, 0)
1056
+ AppendVertex(vpstr, point(-half_size_x, half_size_y, 0), color, 0, 1)
1057
+
1058
+ AppendVertex(vpstr, point(half_size_x, -half_size_y, 0), color, 1, 0)
1059
+ AppendVertex(vpstr, point(half_size_x, half_size_y, 0), color, 1, 1)
1060
+ AppendVertex(vpstr, point(-half_size_x, half_size_y, 0), color, 0, 1)
1061
+
1062
+ text:SetMesh(vpstr)
1063
+
1064
+ if texture then
1065
+ local use_sdf = false
1066
+ local padding = 0
1067
+ local low_edge = 0
1068
+ local high_edge = 0
1069
+ if glow_size > 0 then
1070
+ use_sdf = true
1071
+ padding = 16
1072
+ low_edge = 490
1073
+ high_edge = 510
1074
+ end
1075
+ text:SetTexture(0, ProceduralMeshBindResource("texture", texture, false, 0))
1076
+ if glow_size > 0 then
1077
+ text:SetTexture(1, ProceduralMeshBindResource("texture", texture, true, 0, const.fmt_unorm16_c1))
1078
+ text:SetShader(ProceduralMeshShaders.default_ui_sdf)
1079
+ else
1080
+ text:SetShader(ProceduralMeshShaders.default_ui)
1081
+ end
1082
+ local r, g, b = GetRGB(glow_color)
1083
+ text:SetUniforms(low_edge, high_edge, glow_size, glow_period, r, g, b)
1084
+ end
1085
+
1086
+ return text
1087
+ end
1088
+
1089
+ DefineClass.FlatTextMesh = {
1090
+ __parents = { "Mesh" },
1091
+ properties = {
1092
+ {id = "font_id", editor = "number", read_only = true, default = 0, category = "Rasterize" },
1093
+ {id = "text_style_id", editor = "preset_id", preset_class = "TextStyle", editor_preview = true, default = false, category = "Rasterize" },
1094
+ {id = "text_scale", editor = "number", default = 1000, category = "Rasterize" },
1095
+ {id = "text", editor = "text", default = "", category = "Rasterize" },
1096
+ {id = "padding", editor = "number", default = 0, category = "Rasterize", help = "How much pixels to leave around the text(for effects)"},
1097
+
1098
+ {id = "width", editor = "number", default = 0, category = "Present", help = "In meters. Leave 0 to calculate automatically"},
1099
+ {id = "height", editor = "number", default = 0, category = "Present", help = "In meters. Leave 0 to calculate automatically"},
1100
+ {id = "text_color", editor = "color", default = RGB(255,255,255), category = "Present"},
1101
+ {id = "effect_type", editor = "choice", items = {"none", "glow"}, default = "glow", category = "Present" },
1102
+ {id = "effect_color", editor = "color", default = RGB(255,255,255), category = "Present"},
1103
+ {id = "effect_size", editor = "number", default = 0, help = "In pixels from the rasterized image.", category = "Present" },
1104
+ {id = "effect_period", editor = "number", default = 0, help = "1 pulse per each period seconds. ", category = "Present"},
1105
+ }
1106
+ }
1107
+
1108
+ function FlatTextMesh:Init()
1109
+ self:Recreate()
1110
+ end
1111
+
1112
+ function FlatTextMesh:FetchEffectsFromTextStyle()
1113
+ local text_style = TextStyles[self.text_style_id]
1114
+ if not text_style then return end
1115
+ self.text_color = text_style.TextColor
1116
+ self.effect_type = text_style.ShadowType == "glow" and "glow" or "none"
1117
+ self.effect_color = text_style.ShadowColor
1118
+ self.effect_size = text_style.ShadowSize
1119
+ self.textstyle_id = self.text_style_id
1120
+ end
1121
+
1122
+ function FlatTextMesh:SetColorFromTextStyle(text_style_id)
1123
+ self.text_style_id = text_style_id
1124
+ self.textstyle_id = text_style_id
1125
+ self:FetchEffectsFromTextStyle()
1126
+ self:Recreate()
1127
+ end
1128
+
1129
+ function FlatTextMesh:CalculateSizes(max_width, max_height, default_scale)
1130
+ local width_pixels, height_pixels = UIL.MeasureText(self.text, self.font_id)
1131
+ local scale = 0
1132
+ if max_width == 0 and max_height == 0 then
1133
+ scale = default_scale or 10000
1134
+ elseif max_width == 0 then
1135
+ max_width = 1000000
1136
+ elseif max_height == 0 then
1137
+ max_height = 1000000
1138
+ end
1139
+
1140
+ if scale == 0 then
1141
+ local scale1 = MulDivRound(max_width, 1000, width_pixels)
1142
+ local scale2 = MulDivRound(max_height, 1000, height_pixels)
1143
+ scale = Min(scale1, scale2)
1144
+ end
1145
+
1146
+ self.width = MulDivRound(width_pixels, scale, 1000)
1147
+ self.height = MulDivRound(height_pixels, scale, 1000)
1148
+ end
1149
+
1150
+ function FlatTextMesh:Recreate()
1151
+ local text_style = TextStyles[self.text_style_id]
1152
+ if not text_style then return end
1153
+ local font_id = text_style:GetFontIdHeightBaseline(self.text_scale)
1154
+ self.font_id = font_id
1155
+
1156
+ local effect_type = self.effect_type
1157
+ local use_sdf = false
1158
+ local padding = 0
1159
+ if effect_type == "glow" then
1160
+ use_sdf = true
1161
+ padding = 16
1162
+ end
1163
+
1164
+ local width_pixels, height_pixels = UIL.MeasureText(self.text, font_id)
1165
+ local width = self.width
1166
+ local height = self.height
1167
+ if width == 0 and height == 0 then
1168
+ local default_scale = 10000
1169
+ width = MulDivRound(width_pixels, default_scale, 1000)
1170
+ height = MulDivRound(height_pixels, default_scale, 1000)
1171
+ end
1172
+ if width == 0 then
1173
+ width = MulDivRound(width_pixels, height, height_pixels)
1174
+ end
1175
+ if height == 0 then
1176
+ height = MulDivRound(height_pixels, width, width_pixels)
1177
+ end
1178
+
1179
+ --add for padding
1180
+ width = width + MulDivRound(width, padding * 2 * 1000, width_pixels * 1000)
1181
+ height = height + MulDivRound(height, padding * 2 * 1000, height_pixels * 1000)
1182
+
1183
+
1184
+ local vpstr = pstr("", 1024)
1185
+ local half_size_x = (width or 1000) / 2
1186
+ local half_size_y = (height or 1000) / 2
1187
+ local color = self.text_color
1188
+ AppendVertex(vpstr, point(-half_size_x, -half_size_y, 0), color, 0, 0)
1189
+ AppendVertex(vpstr, point(half_size_x, -half_size_y, 0), color, 1, 0)
1190
+ AppendVertex(vpstr, point(-half_size_x, half_size_y, 0), color, 0, 1)
1191
+
1192
+ AppendVertex(vpstr, point(half_size_x, -half_size_y, 0), color, 1, 0)
1193
+ AppendVertex(vpstr, point(half_size_x, half_size_y, 0), color, 1, 1)
1194
+ AppendVertex(vpstr, point(-half_size_x, half_size_y, 0), color, 0, 1)
1195
+
1196
+ self:SetMesh(vpstr)
1197
+
1198
+ self:SetTexture(0, ProceduralMeshBindResource("text", self.text, font_id, use_sdf, padding))
1199
+ local r, g, b = GetRGB(self.effect_color)
1200
+ self:SetUniforms(use_sdf and 1000 or 0, 0, self.effect_size * 1000, self.effect_period, r, g, b)
1201
+ self:SetShader(ProceduralMeshShaders.default_ui)
1202
+ end
1203
+
1204
+ function TestUIRenderables()
1205
+ local pt = GetTerrainCursor() + point(0, 0, 100)
1206
+ for i = 0, 4 do
1207
+ local height = 700
1208
+ local space = 5000
1209
+
1210
+ local text = FlatTextMesh:new({
1211
+ text_style_id = "ProcMeshDefault",
1212
+ text_scale = 500 + 400 * i,
1213
+ text = "Hello world",
1214
+ height = height,
1215
+ })
1216
+ text:SetPos(pt + point(i * space, 0, 0))
1217
+
1218
+ text = FlatTextMesh:new({
1219
+ text_style_id = "ProcMeshDefaultFX",
1220
+ text_scale = 500 + 400 * i,
1221
+ text = "Hello world",
1222
+ height = height,
1223
+ effect_type = "glow",
1224
+ effect_size = 8,
1225
+ effect_period = 200,
1226
+ effect_color = RGB(255, 0, 0),
1227
+ })
1228
+ text:SetPos(pt + point(i * space, 3000, 0))
1229
+ text:SetGameFlags(const.gofRealTimeAnim)
1230
+
1231
+ local mesh = FlatImageMesh("UI/MercsPortraits/Buns", 1000, 1000, 200 * i, 1000, RGB(255, 255, 255))
1232
+ mesh:SetPos(pt + point(i * space, 6000, 0))
1233
+
1234
+ mesh = FlatImageMesh("UI/MercsPortraits/Buns", 1000, 1000)
1235
+ mesh:SetPos(pt + point(i * space, 9000, 0))
1236
+ end
1237
+ end
1238
+
1239
+ function DebugShowMeshes()
1240
+ local meshes = MapGet("map", "Mesh")
1241
+ OpenGedGameObjectEditor(meshes, true)
1242
+ end
1243
+
1244
+ -- Represents combination of shader & the data the shader accepts. Provides "properties" interface for the underlying raw bits sent to the shader.
1245
+ -- Inherits from PersistedRenderVars(is a preset) and provides minimalistic default logic for updating meshes on the fly.
1246
+ local function depth_test_values(obj)
1247
+ local tbl = {{value = "default", text = "default"}}
1248
+ local shader_id = obj.shader_id
1249
+ local shader_data = ProceduralMeshShaders[shader_id]
1250
+ if shader_data then
1251
+ if shader_data.depth_test == "runtime" or shader_data.depth_test == "never" then
1252
+ table.insert(tbl, {value = false, text = "never"})
1253
+ end
1254
+ if shader_data.depth_test == "runtime" or shader_data.depth_test == "always" then
1255
+ table.insert(tbl, {value = true, text = "always"})
1256
+ end
1257
+ end
1258
+ return tbl
1259
+ end
1260
+ DefineClass.CRMaterial = {
1261
+ __parents = {"PersistedRenderVars", "MeshParamSet"},
1262
+
1263
+ properties = {
1264
+ { id = "ShaderName", editor = "choice", default = "default_mesh", items = function() return table.keys2(ProceduralMeshShaders, "sorted") end, read_only = true,},
1265
+ { id = "depth_test", editor = "choice", items = depth_test_values },
1266
+ },
1267
+ group = "CRMaterial",
1268
+ depth_test = "default",
1269
+ cloned_from = false,
1270
+ shader_id = "default_mesh",
1271
+ shader = false,
1272
+ pstr_buffer = false,
1273
+ dirty = false,
1274
+ }
1275
+
1276
+ function CRMaterial:GetError()
1277
+ if not self.shader_id then
1278
+ return "CRMaterial without a shader_id."
1279
+ end
1280
+ if not ProceduralMeshShaders[self.shader_id] then
1281
+ return "ShaderID " .. self.shader_id .. " is not valid."
1282
+ end
1283
+ end
1284
+
1285
+ function CRMaterial:GetShader()
1286
+ if self.shader then
1287
+ return self.shader
1288
+ end
1289
+ if self.shader_id then
1290
+ return ProceduralMeshShaders[self.shader_id]
1291
+ end
1292
+ return false
1293
+ end
1294
+
1295
+ ------- Prevent triggering Preset logic on cloned materials. Probably should be implemented smarter, maybe by __index table reference, so even clones are live updated by editor
1296
+ function CRMaterial:SetId(value)
1297
+ if self.cloned_from then
1298
+ self.id = value
1299
+ else
1300
+ PersistedRenderVars.SetId(self, value)
1301
+ end
1302
+ end
1303
+
1304
+ function CRMaterial:SetGroup(value)
1305
+ if self.cloned_from then
1306
+ self.group = value
1307
+ else
1308
+ PersistedRenderVars.SetGroup(self, value)
1309
+ end
1310
+ end
1311
+
1312
+ function CRMaterial:Register(...)
1313
+ if self.cloned_from then
1314
+ return
1315
+ end
1316
+ return PersistedRenderVars.Register(self, ...)
1317
+ end
1318
+
1319
+ function CRMaterial:Clone()
1320
+ local obj = _G[self.class]:new({cloned_from = self.id})
1321
+ obj:CopyProperties(self)
1322
+ return obj
1323
+ end
1324
+
1325
+ function CRMaterial:GetDataPstr()
1326
+ if self.dirty or not self.pstr_buffer then
1327
+ self:Recreate()
1328
+ end
1329
+ return self.pstr_buffer
1330
+ end
1331
+
1332
+ function CRMaterial:GetShaderName()
1333
+ return self.shader and self.shader.id or self.shader_id
1334
+ end
1335
+
1336
+ function CRMaterial:Recreate()
1337
+ self.dirty = false
1338
+ self.pstr_buffer = self:WriteBuffer()
1339
+ end
1340
+
1341
+ function CRMaterial:OnPreSave()
1342
+ self.pstr_buffer = nil
1343
+ end
1344
+
1345
+ function CRMaterial:Apply()
1346
+ self:Recreate()
1347
+ if CurrentMap ~= "" then
1348
+ MapGet("map", "Mesh", function(o)
1349
+ local omtrl = o.CRMaterial
1350
+ if omtrl == self then
1351
+ o:SetCRMaterial(self)
1352
+ elseif (omtrl and omtrl.id == self.id) then
1353
+ for _, prop in ipairs(omtrl:GetProperties()) do
1354
+ local value = rawget(omtrl, prop.id)
1355
+ if value == nil or (not prop.read_only and not prop.no_edit) then
1356
+ omtrl:SetProperty(prop.id, self:GetProperty(prop.id))
1357
+ end
1358
+ end
1359
+ omtrl:Recreate()
1360
+ o:SetCRMaterial(omtrl)
1361
+ end
1362
+ end)
1363
+ end
1364
+ end
1365
+
1366
+
1367
+
1368
+ DefineClass.CRM_DebugMeshMaterial = {
1369
+ __parents = {"CRMaterial"},
1370
+
1371
+ shader_id = "debug_mesh",
1372
+ properties = {
1373
+ },
1374
+ }
1375
+
CommonLua/Classes/CollectionAnimator.lua ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DefineClass.CollectionAnimator = {
2
+ __parents = { "Object", "EditorEntityObject", "EditorObject" },
3
+ --entity = "WayPoint",
4
+ editor_entity = "WayPoint",
5
+ properties = {
6
+ { name = "Rotate Speed", id = "rotate_speed", category = "Animator", editor = "number", default = 0, scale = 100, help = "Revolutions per minute" },
7
+ { name = "Oscillate Offset", id = "oscillate_offset", category = "Animator", editor = "point", default = point30, scale = "m", help = "Map offset acceleration movement up and down (in meters)" },
8
+ { name = "Oscillate Cycle", id = "oscillate_cycle", category = "Animator", editor = "number", default = 0, help = "Full cycle time in milliseconds" },
9
+ { name = "Locked Orientation", id = "LockedOrientation", category = "Animator", editor = "bool", default = false },
10
+ },
11
+ animated_obj = false,
12
+ rotation_thread = false,
13
+ move_thread = false,
14
+ }
15
+
16
+ DefineClass.CollectionAnimatorObj = {
17
+ __parents = { "Object", "ComponentAttach" },
18
+ flags = { cofComponentInterpolation = true, efWalkable = false, efApplyToGrids = false, efCollision = false },
19
+ properties = {
20
+ -- exclude properties to not copy them
21
+ { id = "Pos" },
22
+ { id = "Angle" },
23
+ { id = "Axis" },
24
+ { id = "Walkable" },
25
+ { id = "ApplyToGrids" },
26
+ { id = "Collision" },
27
+ { id = "OnCollisionWithCamera" },
28
+ { id = "CollectionIndex" },
29
+ { id = "CollectionName" },
30
+ },
31
+ }
32
+
33
+ function CollectionAnimator:GameInit()
34
+ self:StartAnimate()
35
+ end
36
+
37
+ function CollectionAnimator:Done()
38
+ self:StopAnimate()
39
+ end
40
+
41
+ function CollectionAnimator:StartAnimate()
42
+ if self.animated_obj then
43
+ return -- already started
44
+ end
45
+ if not self:AttachObjects() then
46
+ return
47
+ end
48
+ -- rotation
49
+ if not self.rotation_thread and self.rotate_speed ~= 0 then
50
+ self.rotation_thread = CreateGameTimeThread(function()
51
+ local obj = self.animated_obj
52
+ obj:SetAxis(self:RotateAxis(0,0,4096))
53
+ local a = 162*60*(self.rotate_speed < 0 and -1 or 1)
54
+ local t = 27000 * 100 / abs(self.rotate_speed)
55
+ while true do
56
+ obj:SetAngle(obj:GetAngle() + a, t)
57
+ Sleep(t)
58
+ end
59
+ end)
60
+ end
61
+ -- movement
62
+ if not self.move_thread and self.oscillate_cycle >= 100 and self.oscillate_offset:Len() > 0 then
63
+ self.move_thread = CreateGameTimeThread(function()
64
+ local obj = self.animated_obj
65
+ local pos = self:GetVisualPos()
66
+ local vec = self.oscillate_offset
67
+ local t = self.oscillate_cycle/4
68
+ local acc = self:GetAccelerationAndStartSpeed(pos+vec, 0, t)
69
+ while true do
70
+ obj:SetAcceleration(acc)
71
+ obj:SetPos(pos+vec, t)
72
+ Sleep(t)
73
+ obj:SetAcceleration(-acc)
74
+ obj:SetPos(pos, t)
75
+ Sleep(t)
76
+ obj:SetAcceleration(acc)
77
+ obj:SetPos(pos-vec, t)
78
+ Sleep(t)
79
+ obj:SetAcceleration(-acc)
80
+ obj:SetPos(pos, t)
81
+ Sleep(t)
82
+ end
83
+ end)
84
+ end
85
+ end
86
+
87
+ function CollectionAnimator:StopAnimate()
88
+ DeleteThread(self.rotation_thread)
89
+ self.rotation_thread = nil
90
+ DeleteThread(self.move_thread)
91
+ self.move_thread = nil
92
+ self:RestoreObjects()
93
+ end
94
+
95
+ function CollectionAnimator:AttachObjects()
96
+ local col = self:GetCollection()
97
+ if not col then
98
+ return false
99
+ end
100
+ SuspendPassEdits("CollectionAnimator")
101
+ local obj = PlaceObject("CollectionAnimatorObj")
102
+ self.animated_obj = obj
103
+ local pos = self:GetPos()
104
+ local max_offset = 0
105
+ MapForEach (col.Index, false, "map", "attached", false, function(o)
106
+ if o == self then return end
107
+ local o_pos, o_axis, o_angle = o:GetVisualPos(), o:GetAxis(), o:GetAngle()
108
+ local o_offset = o_pos - pos
109
+ --if o:IsKindOf("ComponentAttach") then
110
+ o:DetachFromMap()
111
+ o:SetAngle(0) -- fixes a problem when attaching
112
+ obj:Attach(o)
113
+ --else
114
+ -- local clone = PlaceObject("CollectionAnimatorObj")
115
+ -- clone:ChangeEntity(o:GetEntity())
116
+ -- clone:CopyProperties(o)
117
+ --end
118
+ o:SetAttachAxis(o_axis)
119
+ o:SetAttachAngle(o_angle)
120
+ o:SetAttachOffset(o_offset)
121
+ max_offset = Max(max_offset, o_offset:Len())
122
+ end)
123
+ if max_offset > 20*guim then
124
+ obj:SetGameFlags(const.gofAlwaysRenderable)
125
+ end
126
+ if self.LockedOrientation then
127
+ obj:SetHierarchyGameFlags(const.gofLockedOrientation)
128
+ end
129
+ obj:ClearHierarchyEnumFlags(const.efWalkable + const.efApplyToGrids + const.efCollision)
130
+ obj:SetPos(pos)
131
+ ResumePassEdits("CollectionAnimator")
132
+ return true
133
+ end
134
+
135
+ function CollectionAnimator:RestoreObjects()
136
+ local obj = self.animated_obj
137
+ if not obj then
138
+ return
139
+ end
140
+ SuspendPassEdits("CollectionAnimator")
141
+ self.animated_obj = nil
142
+ obj:SetPos(self:GetPos())
143
+ obj:SetAxis(axis_z)
144
+ obj:SetAngle(0)
145
+ for i = obj:GetNumAttaches(), 1, -1 do
146
+ local o = obj:GetAttach(i)
147
+ local o_pos, o_axis, o_angle = o:GetAttachOffset(), o:GetAttachAxis(), o:GetAttachAngle()
148
+ o:Detach()
149
+ o:SetPos(o:GetPos() + o_pos)
150
+ o:SetAxis(o_axis)
151
+ o:SetAngle(o_angle)
152
+ o:ClearGameFlags(const.gofLockedOrientation)
153
+ end
154
+ DoneObject(obj)
155
+ ResumePassEdits("CollectionAnimator")
156
+ end
157
+
158
+ function CollectionAnimator:EditorEnter()
159
+ self:StopAnimate()
160
+ end
161
+
162
+ function CollectionAnimator:EditorExit()
163
+ self:StartAnimate()
164
+ end
165
+
166
+ function OnMsg.PreSaveMap()
167
+ MapForEach("map", "CollectionAnimator", function(obj) obj:StopAnimate() end)
168
+ end
169
+
170
+ function OnMsg.PostSaveMap()
171
+ MapForEach("map", "CollectionAnimator", function(obj) obj:StartAnimate() end)
172
+ end
CommonLua/Classes/ColorModifierReason.lua ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ColorModifierReasons = {
2
+ --override this in your project
3
+ --{id = "reason_name", weight = default reason weight number, color = default reason color},
4
+ }
5
+
6
+ MapVar("ColorModifierReasonsData", false)
7
+ local table_find = table.find
8
+ local SetColorModifier = CObject.SetColorModifier
9
+ local GetColorModifier = CObject.GetColorModifier
10
+ local clrNoModifier = const.clrNoModifier
11
+ local default_color_modifier = RGBA(100, 100, 100, 0)
12
+
13
+ function SetColorModifierReason(obj, reason, color, weight, blend, skip_attaches)
14
+ assert(reason)
15
+ if not reason then
16
+ return
17
+ end
18
+ local color_value = color
19
+
20
+ if obj:GetRadius() > 0 then
21
+ local data = ColorModifierReasonsData
22
+ if not data then
23
+ data = {}
24
+ ColorModifierReasonsData = data
25
+ end
26
+ local mrt = data[obj]
27
+ local orig_color
28
+ if not mrt then
29
+ orig_color = GetColorModifier(obj)
30
+ mrt = { orig_color = orig_color }
31
+ data[obj] = mrt
32
+ end
33
+
34
+ local rt = ColorModifierReasons
35
+ local idx = table_find(rt, "id", reason)
36
+ local rdata = idx and rt[idx] or false
37
+
38
+ color = color or rdata and rdata.color or nil
39
+ if not color then
40
+ printf("[WARNING] SetColorModifierReason no color! reason %s, color %s, weight %s", reason, tostring(color), tostring(weight))
41
+ return
42
+ end
43
+ weight = weight or rdata and rdata.weight or const.DefaultColorModWeight
44
+ if not weight then
45
+ printf("[WARNING] SetColorModifierReason no weight! reason %s, color %s, weight %s", reason, tostring(color), tostring(weight))
46
+ return
47
+ end
48
+
49
+ if blend then
50
+ orig_color = orig_color or mrt.orig_color
51
+ if orig_color ~= clrNoModifier then
52
+ color = InterpolateRGB(orig_color, color, blend, 100)
53
+ end
54
+ end
55
+ local idx = table_find(mrt, "reason", reason)
56
+ local entry = idx and mrt[idx]
57
+ if entry then
58
+ entry.weight = weight
59
+ entry.color = color
60
+ else
61
+ entry = { reason = reason, weight = weight, color = color }
62
+ table.insert(mrt, entry)
63
+ end
64
+ table.stable_sort(mrt, function(a, b)
65
+ return a.weight < b.weight
66
+ end)
67
+ SetColorModifier(obj, mrt[#mrt].color)
68
+ end
69
+ if skip_attaches then
70
+ return
71
+ end
72
+ obj:ForEachAttach(SetColorModifierReason, reason, color_value, weight, blend)
73
+ end
74
+
75
+ function SetOrigColorModifier(obj, color, skip_attaches)
76
+ local data = ColorModifierReasonsData
77
+ local mrt = data and data[obj]
78
+ if not mrt then
79
+ SetColorModifier(obj, color)
80
+ else
81
+ mrt.orig_color = color
82
+ end
83
+ if skip_attaches then return end
84
+ obj:ForEachAttach(SetOrigColorModifier, color)
85
+ end
86
+
87
+ function GetOrigColorModifier(obj)
88
+ local modifier = GetColorModifier(obj)
89
+ return modifier == default_color_modifier and table.get(ColorModifierReasonsData, obj, "orig_color") or modifier
90
+ end
91
+
92
+ function ValidateColorReasons()
93
+ table.validate_map(ColorModifierReasonsData)
94
+ end
95
+
96
+ function ClearColorModifierReason(obj, reason, skip_color_change, skip_attaches)
97
+ assert(reason)
98
+ if not reason then
99
+ return
100
+ end
101
+ local data = ColorModifierReasonsData
102
+ local mrt = data and data[obj]
103
+ if mrt then
104
+ if not IsValid(obj) then
105
+ data[obj] = nil
106
+ return
107
+ end
108
+ local idx = table_find(mrt, "reason", reason)
109
+ if not idx then
110
+ return
111
+ end
112
+ local update = idx == #mrt
113
+ table.remove(mrt, idx)
114
+ if #mrt == 0 then
115
+ data[obj] = nil
116
+ DelayedCall(1000, ValidateColorReasons)
117
+ if not next(data) then
118
+ ColorModifierReasonsData = false
119
+ end
120
+ end
121
+ if update and not skip_color_change then
122
+ local active = mrt[#mrt]
123
+ local color = active and active.color or mrt.orig_color or const.clrNoModifier
124
+ SetColorModifier(obj, color)
125
+ end
126
+ end
127
+ if skip_attaches then
128
+ return
129
+ end
130
+ obj:ForEachAttach(ClearColorModifierReason, reason, skip_color_change)
131
+ end
132
+
133
+ function ClearColorModifierReasons(obj)
134
+ local data = ColorModifierReasonsData
135
+ local mrt = data and data[obj]
136
+ if not mrt then
137
+ return
138
+ end
139
+ if IsValid(obj) then
140
+ SetColorModifier(obj, mrt.orig_color or const.clrNoModifier)
141
+ obj:ForEachAttach(ClearColorModifierReasons)
142
+ end
143
+ data[obj] = nil
144
+ end
145
+
146
+ OnMsg.StartSaveGame = ValidateColorReasons
147
+
148
+ ----
149
+
150
+ MapVar("InvisibleReasons", {}, weak_keys_meta)
151
+
152
+ local efVisible = const.efVisible
153
+
154
+ function SetInvisibleReason(obj, reason)
155
+ local invisible_reasons = InvisibleReasons
156
+ local obj_reasons = invisible_reasons[obj]
157
+ if obj_reasons then
158
+ obj_reasons[reason] = true
159
+ return
160
+ end
161
+ invisible_reasons[obj] = { [reason] = true }
162
+ obj:ClearHierarchyEnumFlags(efVisible)
163
+ end
164
+
165
+ function ClearInvisibleReason(obj, reason)
166
+ local invisible_reasons = InvisibleReasons
167
+ local obj_reasons = invisible_reasons[obj]
168
+ if not obj_reasons or not obj_reasons[reason] then
169
+ return
170
+ end
171
+ obj_reasons[reason] = nil
172
+ if next(obj_reasons) then
173
+ return
174
+ end
175
+ invisible_reasons[obj] = nil
176
+ obj:SetHierarchyEnumFlags(efVisible)
177
+ end
178
+
179
+ function ClearInvisibleReasons(obj)
180
+ local invisible_reasons = InvisibleReasons
181
+ if not invisible_reasons[obj] then
182
+ return
183
+ end
184
+ invisible_reasons[obj] = nil
185
+ obj:SetHierarchyEnumFlags(efVisible)
186
+ end
187
+
188
+ ----
CommonLua/Classes/Colorization.lua ADDED
@@ -0,0 +1,854 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ if FirstLoad then
2
+ g_DefaultColorsPalette = "Default colors"
3
+ end
4
+
5
+ local prop_cat = "Colorization Palette"
6
+
7
+ DefineClass.ColorizableObject = {
8
+ __parents = { "PropertyObject" },
9
+ flags = { cofComponentColorizationMaterial = true },
10
+ properties = {
11
+ { category = "Colorization Palette", id = "ColorizationPalette", name = "Colorization Palette",
12
+ editor = "choice",
13
+ default = g_DefaultColorsPalette,
14
+ preset_class = "ColorizationPalettePreset", -- For GedRpcEditPreset
15
+ items = function(self)
16
+ -- Filters out ColorizationPropSet and EnvironmentColorEntry (and other descendants of ColorizableObject that are not CObjects)
17
+ if not IsValid(self) then
18
+ return false
19
+ end
20
+
21
+ local entity = self:GetEntity() ~= "" and self:GetEntity() or self.class
22
+ local palettes = g_EntityToColorPalettes_Cache[entity]
23
+ palettes = palettes and table.map(palettes, function(pal) return pal.PaletteName end) or {}
24
+ palettes[#palettes + 1] = g_DefaultColorsPalette
25
+ palettes[#palettes + 1] = ""
26
+ return palettes
27
+ end,
28
+ no_edit = function(self)
29
+ return self:ColorizationPropsNoEdit("palette") and true
30
+ end,
31
+ dont_save = function(self)
32
+ return self:ColorizationPropsDontSave("palette") and true
33
+ end,
34
+ read_only = function(self)
35
+ return self:ColorizationReadOnlyReason("palette") and true
36
+ end,
37
+ buttons = {{
38
+ name = "Edit",
39
+ is_hidden = function(self)
40
+ return not IsValid(self) or self:GetColorizationPalette() == ""
41
+ end,
42
+ -- Open the editor which can edit the colors currently used on the object
43
+ func = function(self, root, prop_id, socket)
44
+ local palette_value = self:GetColorizationPalette()
45
+ local preset_obj
46
+
47
+ if palette_value == "" then return end
48
+
49
+ -- If palette is "Default colors" => open Art Spec editor
50
+ if palette_value == g_DefaultColorsPalette then
51
+ local entity = self:GetEntity() ~= "" and self:GetEntity() or self.class
52
+
53
+ ForEachPreset("EntitySpec", function(preset)
54
+ if preset.id == entity then
55
+ preset_obj = preset
56
+ return "break"
57
+ end
58
+ end)
59
+
60
+ local ged = OpenPresetEditor("EntitySpec")
61
+ if ged then
62
+ ged:SetSelection("root", PresetGetPath(preset_obj))
63
+ end
64
+
65
+ return
66
+ end
67
+
68
+ -- If palette is something else => open Colorization Palette editor
69
+ local select_idx
70
+ ForEachPreset("ColorizationPalettePreset", function(preset)
71
+ for idx, entry in ipairs(preset) do
72
+ if entry.class == "CPPaletteEntry" and entry.PaletteName == palette_value then
73
+ preset_obj = preset
74
+ select_idx = idx
75
+ return "break"
76
+ end
77
+ end
78
+ end)
79
+
80
+ if not preset_obj then
81
+ return
82
+ end
83
+
84
+ GedRpcEditPreset(socket, "SelectedObject", prop_id, preset_obj.id)
85
+
86
+ local ged = FindPresetEditor("ColorizationPalettePreset")
87
+ if ged then
88
+ ged:SetSelection("SelectedPreset", { select_idx })
89
+ end
90
+ end
91
+ }}
92
+ },
93
+ },
94
+ env_colorized = false,
95
+ }
96
+
97
+ -- Returns if a given entity (by name) can be colorized through the Object editor
98
+ local function CanEntityBeColorized(entity)
99
+ -- Filters out ColorizationPropSet and EnvironmentColorEntry (and other descendants of ColorizableObject that are not CObjects)
100
+ local entity_data = EntityData[entity]
101
+ if not entity_data then
102
+ return false
103
+ end
104
+
105
+ return ColorizationMaterialsCount(entity) > 0 and not (entity_data.entity and entity_data.entity.env_colorized)
106
+ end
107
+
108
+ function ColorizableObject:CanBeColorized()
109
+ local entity = self:GetEntity() ~= "" and self:GetEntity() or self.class
110
+ return not self.env_colorized and CanEntityBeColorized(entity)
111
+ end
112
+
113
+ function ColorizableObject:GetColorizationPalette()
114
+ -- Filters out ColorizationPropSet and EnvironmentColorEntry (and other descendants of ColorizableObject that are not CObjects)
115
+ if not IsValid(self) then
116
+ return
117
+ end
118
+
119
+ -- Get the value from C++
120
+ return self:GetColorizationPaletteName()
121
+ end
122
+
123
+ function ColorizableObject:SetColorsFromTable(colors)
124
+ if colors.EditableColor1 then
125
+ self:SetEditableColor1(colors.EditableColor1)
126
+ end
127
+ if colors.EditableColor2 then
128
+ self:SetEditableColor2(colors.EditableColor2)
129
+ end
130
+ if colors.EditableColor3 then
131
+ self:SetEditableColor3(colors.EditableColor3)
132
+ end
133
+
134
+ if colors.EditableRoughness1 then
135
+ self:SetEditableRoughness1(colors.EditableRoughness1)
136
+ end
137
+ if colors.EditableRoughness2 then
138
+ self:SetEditableRoughness2(colors.EditableRoughness2)
139
+ end
140
+ if colors.EditableRoughness3 then
141
+ self:SetEditableRoughness3(colors.EditableRoughness3)
142
+ end
143
+
144
+ if colors.EditableMetallic1 then
145
+ self:SetEditableMetallic1(colors.EditableMetallic1)
146
+ end
147
+ if colors.EditableMetallic2 then
148
+ self:SetEditableMetallic2(colors.EditableMetallic2)
149
+ end
150
+ if colors.EditableMetallic3 then
151
+ self:SetEditableMetallic3(colors.EditableMetallic3)
152
+ end
153
+ end
154
+
155
+ -- Colorizes the object with the colors from the palette with the given name
156
+ -- name == "" or nil => apply previous palette colors
157
+ -- name == "Default colors" => apply default entity colors
158
+ function ColorizableObject:SetColorsByColorizationPaletteName(palette_name, previous_palette)
159
+ -- If we're removing the palette, set the colors to those from the previous palette so they can be easily adjusted
160
+ if not palette_name or palette_name == "" then
161
+ palette_name = previous_palette or ""
162
+ end
163
+
164
+ if palette_name == g_DefaultColorsPalette then
165
+ -- Set to the Default entity colors defined in the Art Spec editor
166
+ local default_colors = self:GetDefaultColorizationSet()
167
+ if default_colors then
168
+ self:SetColorsFromTable(default_colors)
169
+ return
170
+ end
171
+
172
+ -- Set all the color properties to their default values from the prop meta
173
+ self:SetEditableColor1(self:GetDefaultPropertyValue("EditableColor1"))
174
+ self:SetEditableColor2(self:GetDefaultPropertyValue("EditableColor2"))
175
+ self:SetEditableColor3(self:GetDefaultPropertyValue("EditableColor3"))
176
+
177
+ self:SetEditableRoughness1(self:GetDefaultPropertyValue("EditableRoughness1"))
178
+ self:SetEditableRoughness2(self:GetDefaultPropertyValue("EditableRoughness2"))
179
+ self:SetEditableRoughness3(self:GetDefaultPropertyValue("EditableRoughness3"))
180
+
181
+ self:SetEditableMetallic1(self:GetDefaultPropertyValue("EditableMetallic1"))
182
+ self:SetEditableMetallic2(self:GetDefaultPropertyValue("EditableMetallic2"))
183
+ self:SetEditableMetallic3(self:GetDefaultPropertyValue("EditableMetallic3"))
184
+ return
185
+ end
186
+
187
+ -- If not empty or default => find the palette colors and apply them on the object
188
+ local entity = self:GetEntity() ~= "" and self:GetEntity() or self.class
189
+ for _, palette in ipairs(g_EntityToColorPalettes_Cache[entity]) do
190
+ if palette.PaletteName == palette_name and palette.PaletteColors then
191
+ self:SetColorsFromTable(palette.PaletteColors)
192
+ break
193
+ end
194
+ end
195
+ end
196
+
197
+ local function real_set_modifier(object, setter, value, ...)
198
+ -- Filters out ColorizationPropSet and EnvironmentColorEntry (and other descendants of ColorizableObject that are not CObjects)
199
+ if IsValid(object) then
200
+ object[setter](object, value, ...)
201
+ end
202
+ end
203
+
204
+ function ColorizableObject:SetColorizationPalette(palette_name)
205
+ -- Filters out ColorizationPropSet and EnvironmentColorEntry (and other descendants of ColorizableObject that are not CObjects)
206
+ if not IsValid(self) then
207
+ return
208
+ end
209
+
210
+ palette_name = palette_name or ""
211
+
212
+ -- Set the palette name in C++
213
+ self:SetColorizationPaletteName(palette_name)
214
+
215
+ -- Apply the colors of the chosen palette
216
+ self:SetColorsByColorizationPaletteName(palette_name)
217
+ end
218
+
219
+ function ColorizableObject:ColorizationPropsNoEdit(i)
220
+ if type(i) == "number" then return i > self:GetMaxColorizationMaterials() end
221
+ return false
222
+ end
223
+
224
+ function ColorizableObject:GetMaxColorizationMaterials()
225
+ if not IsValid(self) or self.env_colorized then
226
+ return const.MaxColorizationMaterials
227
+ end
228
+ return Min(const.MaxColorizationMaterials, ColorizationMaterialsCount(self))
229
+ end
230
+
231
+ function ColorizableObject:OnEditorSetProperty(prop_id, old_value, ged, multi)
232
+ -- When pasting a color/roughness/metallic prop in the editor, remove the palette and keep the colors for edit
233
+ if string.match(prop_id, "Editable") and self:GetColorizationPalette() ~= "" then
234
+ local new_value = self:GetProperty(prop_id)
235
+
236
+ -- Removing the palette will set the colors to the palette colors so we have to manually set the property again
237
+ self:SetColorizationPalette("")
238
+ self:SetProperty(prop_id, new_value)
239
+ end
240
+ end
241
+
242
+ function ColorizableObject:ColorizationReadOnlyReason(usage)
243
+ if IsValid(self) and self:GetParent() then
244
+ return "Object is an attached one. AutoAttaches are not persisted and colorization is either inherited from the parent or set explicitly in the AutoAttach editor."
245
+ end
246
+
247
+ local palette_value = self:GetColorizationPalette()
248
+ if palette_value and palette_value ~= "" and usage ~= "palette" then
249
+ return "A Colorization Palette preset is chosen and the colors are loaded from there."
250
+ end
251
+
252
+ if IsKindOf(self, "AppearanceObject") then
253
+ return "AppearanceObjects are managed in the Appearance Editor."
254
+ end
255
+
256
+ return false
257
+ end
258
+
259
+ function ColorizableObject:ColorizationReadOnlyText()
260
+ local reason = self:ColorizationReadOnlyReason()
261
+ return reason and "Colorization is read only:\n"..reason
262
+ end
263
+
264
+ function ColorizableObject:ColorizationPropsDontSave(i)
265
+ local no_edit_result = self:ColorizationPropsNoEdit(i)
266
+ if no_edit_result then
267
+ return true
268
+ end
269
+ if self:ColorizationReadOnlyReason() then
270
+ return true -- if they are readonly they probably don't have to be saved(and are initialized by someone else)
271
+ end
272
+ if type(i) == "number" then
273
+ local palette_value = self:GetColorizationPalette()
274
+ if palette_value and palette_value ~= "" then
275
+ return true
276
+ end
277
+ end
278
+ return false
279
+ end
280
+
281
+ local default_color = const.ColorPaletteWhitePoint
282
+ local default_roughness = 0
283
+ local default_metallic = 0
284
+
285
+ for i = 1, const.MaxColorizationMaterials or 0 do
286
+ local color = string.format("Color%d", i)
287
+ local roughness = string.format("Roughness%d", i)
288
+ local metallic = string.format("Metallic%d", i)
289
+ local color_prop = string.format("Editable%s", color)
290
+ local roughness_prop = string.format("Editable%s", roughness)
291
+ local metallic_prop = string.format("Editable%s", metallic)
292
+ local reset = string.format("ResetColorizationMaterial%d", i)
293
+
294
+ _G[reset] = function(parentEditor, object, property, ...)
295
+ object:SetProperty(color_prop, default_color)
296
+ object:SetProperty(roughness_prop, default_roughness)
297
+ object:SetProperty(metallic_prop, default_metallic)
298
+ ObjModified(object)
299
+ end
300
+
301
+ local no_edit = function(self)
302
+ return self:ColorizationPropsNoEdit(i) and true
303
+ end
304
+ local no_save = function(self)
305
+ return self:ColorizationPropsDontSave(i) and true
306
+ end
307
+ table.iappend( ColorizableObject.properties, {
308
+ {
309
+ id = color_prop,
310
+ category = prop_cat,
311
+ name = string.format("%d: Base Color", i),
312
+ editor = "color", default = default_color,
313
+ no_edit = no_edit,
314
+ dont_save = no_save,
315
+ alpha = false,
316
+ buttons = {{name = "Reset", func = reset, is_hidden = function(obj)
317
+ if IsKindOf(obj, "GedMultiSelectAdapter") then
318
+ for _, o in ipairs(obj.__objects) do
319
+ if IsKindOf(0, "ColorizableObject") and o:ColorizationReadOnlyReason() then
320
+ return true
321
+ end
322
+ end
323
+ return false
324
+ end
325
+ return obj:ColorizationReadOnlyReason()
326
+ end}},
327
+ autoattach_prop = true,
328
+ read_only = function(obj)
329
+ return obj:ColorizationReadOnlyReason() and true
330
+ end,
331
+ help = ColorizableObject.ColorizationReadOnlyText,
332
+ },
333
+ {
334
+ id = roughness_prop,
335
+ category = prop_cat,
336
+ name = string.format("%d: Roughness", i),
337
+ editor = "number", default = default_roughness, slider = true,
338
+ min = -128, max = 127,
339
+ no_edit = no_edit,
340
+ dont_save = no_save,
341
+ autoattach_prop = true,
342
+ read_only = function(obj)
343
+ return obj:ColorizationReadOnlyReason() and true
344
+ end,
345
+ help = ColorizableObject.ColorizationReadOnlyText,
346
+ },
347
+ {
348
+ id = metallic_prop,
349
+ category = prop_cat,
350
+ name = string.format("%d: Metallic", i),
351
+ editor = "number", default = default_metallic, slider = true,
352
+ min = -128, max = 127,
353
+ no_edit = no_edit,
354
+ dont_save = no_save,
355
+ autoattach_prop = true,
356
+ read_only = function(obj)
357
+ return obj:ColorizationReadOnlyReason() and true
358
+ end,
359
+ help = ColorizableObject.ColorizationReadOnlyText,
360
+ },
361
+ })
362
+ ColorizableObject[color_prop] = default_color
363
+ ColorizableObject[roughness_prop] = default_roughness
364
+ ColorizableObject[metallic_prop] = default_metallic
365
+
366
+ local set_func_color = string.format("Set%s", color)
367
+ ColorizableObject["Set" .. color_prop] = function(object, property, ...)
368
+ -- Filters out ColorizationPropSet and EnvironmentColorEntry (and other descendants of ColorizableObject that are not CObjects)
369
+ if not IsValid(object) then
370
+ object[color_prop] = property
371
+ return
372
+ end
373
+
374
+ return object[set_func_color](object, property, ...)
375
+ end
376
+
377
+ local get_func_color = string.format("Get%s", color)
378
+ ColorizableObject["Get" .. color_prop] = function(object, property, ...)
379
+ -- Filters out ColorizationPropSet and EnvironmentColorEntry (and other descendants of ColorizableObject that are not CObjects)
380
+ if not IsValid(object) then
381
+ return object[color_prop]
382
+ end
383
+
384
+ return object[get_func_color](object)
385
+ end
386
+
387
+ local set_func_roughness = string.format("Set%s", roughness)
388
+ ColorizableObject["Set" .. roughness_prop] = function(object, property, ...)
389
+ -- Filters out ColorizationPropSet and EnvironmentColorEntry (and other descendants of ColorizableObject that are not CObjects)
390
+ if not IsValid(object) then
391
+ object[roughness_prop] = property
392
+ return
393
+ end
394
+
395
+ return object[set_func_roughness](object, property, ...)
396
+ end
397
+
398
+ local get_func_roughness = string.format("Get%s", roughness)
399
+ ColorizableObject["Get" .. roughness_prop] = function(object, property, ...)
400
+ -- Filters out ColorizationPropSet and EnvironmentColorEntry (and other descendants of ColorizableObject that are not CObjects)
401
+ if not IsValid(object) then
402
+ return object[roughness_prop]
403
+ end
404
+
405
+ return object[get_func_roughness](object)
406
+ end
407
+
408
+ local set_func_metallic = string.format("Set%s", metallic)
409
+ ColorizableObject["Set" .. metallic_prop] = function(object, property, ...)
410
+ -- Filters out ColorizationPropSet and EnvironmentColorEntry (and other descendants of ColorizableObject that are not CObjects)
411
+ if not IsValid(object) then
412
+ object[metallic_prop] = property
413
+ return
414
+ end
415
+
416
+ return object[set_func_metallic](object, property, ...)
417
+ end
418
+
419
+ local get_func_metallic = string.format("Get%s", metallic)
420
+ ColorizableObject["Get" .. metallic_prop] = function(object, property, ...)
421
+ -- Filters out ColorizationPropSet and EnvironmentColorEntry (and other descendants of ColorizableObject that are not CObjects)
422
+ if not IsValid(object) then
423
+ return object[metallic_prop]
424
+ end
425
+
426
+ return object[get_func_metallic](object)
427
+ end
428
+ end
429
+
430
+ local function GeneratePropNames(prefixes, count)
431
+ local t = {}
432
+ for i = 1, count do
433
+ for _, prefix in ipairs(prefixes) do
434
+ table.insert(t, prefix .. i)
435
+ end
436
+ end
437
+ return t
438
+ end
439
+
440
+ local setter_names = GeneratePropNames({"SetEditableColor", "SetEditableRoughness", "SetEditableMetallic"}, const.MaxColorizationMaterials)
441
+ local getter_names = GeneratePropNames({"GetEditableColor", "GetEditableRoughness", "GetEditableMetallic"}, const.MaxColorizationMaterials)
442
+ local prop_names = GeneratePropNames({"EditableColor", "EditableRoughness", "EditableMetallic"}, const.MaxColorizationMaterials)
443
+ local defaults = {}
444
+ for i = 1, const.MaxColorizationMaterials do
445
+ table.iappend(defaults, {default_color, default_roughness, default_metallic})
446
+ end
447
+
448
+ function ColorizableObject:AreColorsModified()
449
+ local count = const.MaxColorizationMaterials
450
+ for i = 1, count * 3 do
451
+ if not self:IsPropertyDefault(prop_names[i]) then
452
+ return true
453
+ end
454
+ end
455
+ return false
456
+ end
457
+
458
+ function SetColorizationNoSetter(dst, src)
459
+ local count = const.MaxColorizationMaterials
460
+ for i = 1, count * 3 do
461
+ local getter_name = getter_names[i]
462
+ local value = src[getter_name](src)
463
+ dst[prop_names[i]] = value
464
+ end
465
+ end
466
+
467
+ function SetColorizationNoGetter(dst, src)
468
+ local count = const.MaxColorizationMaterials
469
+ for i = 1, count * 3 do
470
+ local setter_name = setter_names[i]
471
+ local value = src[prop_names[i]]
472
+ dst[setter_name](dst, value)
473
+ end
474
+ end
475
+
476
+
477
+ function ColorizableObject:GetColorsAsTable()
478
+ if not self[getter_names[1]] then
479
+ return
480
+ end
481
+ local ret = nil
482
+ local count = self:GetMaxColorizationMaterials()
483
+ for i = 1, count * 3 do
484
+ local getter_name = getter_names[i]
485
+ local prop_name = prop_names[i]
486
+ local value = self[getter_name](self)
487
+ if value ~= defaults[i] then
488
+ ret = ret or {}
489
+ ret[prop_name] = value
490
+ end
491
+ end
492
+
493
+ return ret
494
+ end
495
+
496
+
497
+ function ColorizableObject:SetColorization(obj, ignore_his_max)
498
+ if obj then
499
+ if not obj[getter_names[1]] then
500
+ self:SetColorizationPalette(obj["ColorizationPalette"] or "")
501
+ SetColorizationNoGetter(self, obj)
502
+ return
503
+ end
504
+ local his_max = IsKindOf(obj, "ColorizableObject") and obj:GetMaxColorizationMaterials() or const.MaxColorizationMaterials
505
+ local count = not ignore_his_max and Min(self:GetMaxColorizationMaterials(), his_max) or self:GetMaxColorizationMaterials()
506
+ self:SetColorizationPalette(obj:GetColorizationPalette() or "")
507
+ for i = 1, count * 3 do
508
+ local setter_name = setter_names[i]
509
+ local getter_name = getter_names[i]
510
+ local value = obj[getter_name](obj)
511
+ self[setter_name](self, value)
512
+ end
513
+ else
514
+ self:SetColorizationPalette("")
515
+ local count = self:GetMaxColorizationMaterials()
516
+ for i = 1, count * 3 do
517
+ self[ setter_names[i] ] ( self, defaults[i] )
518
+ end
519
+ end
520
+ end
521
+
522
+ function ColorizableObject:SetMaterialColor(idx, value) self[setter_names[idx * 3 - 2]](self, value) end
523
+ function ColorizableObject:SetMaterialRougness(idx, value) self[setter_names[idx * 3 - 1]](self, value) end
524
+ function ColorizableObject:SetMaterialMetallic(idx, value) self[setter_names[idx * 3]] (self, value) end
525
+
526
+ function ColorizableObject:GetMaterialColor(idx, value) return self[getter_names[idx * 3 - 2]](self, value) end
527
+ function ColorizableObject:GetMaterialRougness(idx, value) return self[getter_names[idx * 3 - 1]](self, value) end
528
+ function ColorizableObject:GetMaterialMetallic(idx, value) return self[getter_names[idx * 3]] (self, value) end
529
+
530
+ if Platform.developer then
531
+ if FirstLoad then
532
+ ColorizationMatrixObjects = {}
533
+ end
534
+ function CreateGameObjectColorizationMatrix()
535
+ for key, value in ipairs(ColorizationMatrixObjects) do
536
+ DoneObject(value)
537
+ end
538
+ ColorizationMatrixObjects = {}
539
+
540
+ local selected = editor.GetSel()
541
+ if not selected or #selected == 0 then
542
+ print("Please, select a valid object.")
543
+ return false
544
+ end
545
+
546
+ local first = selected[1]
547
+ if not IsValid(first) then
548
+ print("Object was invalid.")
549
+ return false
550
+ end
551
+ local width = first:GetEntityBBox():sizex()
552
+ local length = first:GetEntityBBox():sizey()
553
+
554
+ local start_pos = first:GetPos()
555
+ local colors = { RGB(0, 0, 0), RGB(200, 200, 200), RGB(100, 100, 100), RGB(120, 10, 10), RGB(10, 120, 10), RGB(10, 10, 120), RGB(90, 90, 30), RGB(90, 30, 90), RGB(30, 90, 90) }
556
+ local roughness_metallic = { point(0, 0), point(-80, 0), point(0, -80), point(80, 0), point(0, 80), point(80, 80), point(-80, -80) }
557
+ for x, rm in ipairs(roughness_metallic) do
558
+ for idx, color in ipairs(colors) do
559
+ local obj = PlaceObject("Shapeshifter")
560
+ obj:ChangeEntity(first:GetEntity())
561
+ obj:SetPos(start_pos + point(x * width, idx * length))
562
+ for c = 1, const.MaxColorizationMaterials do
563
+ local method_name = "SetEditableColor" .. c
564
+ obj[method_name](obj, colors[((idx + c - 2) % #colors) + 1])
565
+ method_name = "SetEditableRoughness" .. c
566
+ obj[method_name](obj, rm:x())
567
+ method_name = "SetEditableMetallic" .. c
568
+ obj[method_name](obj, rm:y())
569
+ end
570
+ table.insert(ColorizationMatrixObjects, obj)
571
+ end
572
+ end
573
+ end
574
+ end
575
+
576
+
577
+
578
+ DefineClass.ColorizationPropSet = {
579
+ __parents = {"ColorizableObject"},
580
+ }
581
+
582
+ function ColorizationPropSet:GetEditorView()
583
+ local clrs = {}
584
+ local count = self:GetMaxColorizationMaterials()
585
+ for i=1,count do
586
+ local color_get = string.format("GetEditableColor%d", i)
587
+ local color = self[color_get] and self[color_get](self)
588
+ local r, g, b = GetRGB(color)
589
+ clrs[#clrs + 1] = string.format("<color %d %d %d>C%d</color>", r, g, b, i)
590
+ end
591
+ return Untranslated(table.concat(clrs, " "))
592
+ end
593
+
594
+ function ColorizationPropSet:Clone()
595
+ local result = g_Classes[self.class]:new({})
596
+ result:CopyProperties(self)
597
+ result:SetColorization(self)
598
+ return result
599
+ end
600
+
601
+ function ColorizationPropSet:OnEditorSetProperty(prop_id, old_value, ged)
602
+ -- TODO: this should be a native ged functionality - modifying props with sub objects have to notify the prop owner as well
603
+ local parent = ged.selected_object
604
+ if not parent then return end
605
+ local list, parent_prop_id = parent:FindSubObjectLocation(self)
606
+ if list ~= parent then return end
607
+ return parent:OnEditorSetProperty(parent_prop_id, nil, ged)
608
+ end
609
+
610
+ function ColorizationPropSet:GetError()
611
+ if not AreBinAssetsLoaded() then
612
+ return "Entities not loaded yet - load a map to edit colors."
613
+ end
614
+ end
615
+
616
+ function ColorizationPropSet:EqualsByValue(other)
617
+ if rawequal(self, other) then return true end
618
+
619
+ if not IsKindOf(self, "ColorizationPropSet") then
620
+ return false
621
+ end
622
+ if not IsKindOf(other, "ColorizationPropSet") then
623
+ return false
624
+ end
625
+ for i = 1, const.MaxColorizationMaterials or 0 do
626
+ local color_get = string.format("GetEditableColor%d", i)
627
+ local roughness_get = string.format("GetEditableRoughness%d", i)
628
+ local metallic_get = string.format("GetEditableMetallic%d", i)
629
+
630
+ if self[color_get] and other[color_get] and self[color_get](self) ~= other[color_get](other) then
631
+ return false
632
+ end
633
+ if self[roughness_get] and other[roughness_get] and self[roughness_get](self) ~= other[roughness_get](other) then
634
+ return false
635
+ end
636
+ if self[metallic_get] and other[metallic_get] and self[metallic_get](self) ~= other[metallic_get](other) then
637
+ return false
638
+ end
639
+ end
640
+ return true
641
+ end
642
+
643
+ ColorizationPropSet.__eq = ColorizationPropSet.EqualsByValue
644
+
645
+
646
+ function GetEnvColorizedGroups() -- Stub
647
+ return {}
648
+ end
649
+
650
+ function EnvColorizedTerrainColor(terrain_obj) -- Called from C
651
+ local color_mod = terrain_obj.color_modifier
652
+ return color_mod
653
+ end
654
+
655
+ local function GetDefaultColorizationSet(entity_name)
656
+ if not entity_name then return end
657
+ local entity_data = EntityData[entity_name]
658
+ if not entity_data then return end
659
+ local default_colors = entity_data.default_colors
660
+ if default_colors and next(default_colors) then
661
+ return default_colors
662
+ end
663
+ end
664
+ ColorizableObject.GetDefaultColorizationSet = function(obj) return GetDefaultColorizationSet(obj:GetEntity()) end
665
+
666
+ if Platform.developer then
667
+ function OnMsg.EditorCallback(id, objects, reason)
668
+ if (id == "EditorCallbackPlace" or id == "EditorCallbackPlaceCursor")
669
+ and reason ~= "undo"
670
+ then
671
+ for i = 1, #objects do
672
+ local obj = objects[i]
673
+ -- NOTE: Light should not be ColorizableObject since it treats its Color properties differently
674
+ -- so we ignore them here because the palette will overwrite their copy/pasted colors
675
+ local colorizable = obj and IsKindOf(obj, "ColorizableObject") and not IsKindOf(obj, "Light")
676
+ if colorizable and not obj:ColorizationReadOnlyReason("palette") and obj:GetColorizationPaletteName() == g_DefaultColorsPalette then
677
+ -- Newly placed objects have the "Default colors" color palette
678
+ -- which inherits the Default colors from the Art spec editor
679
+ obj:SetColorizationPalette(g_DefaultColorsPalette)
680
+ end
681
+ end
682
+ end
683
+ end
684
+ end
685
+
686
+ -- Applies the latest colors to objects with a chosen palette when the ColorizationPalettePreset is saved
687
+ -- This allows the person creating new palettes to immediately see how the latest colors look on the object
688
+ local function ApplyLatestColorPalettes()
689
+ if GetMap() == "" then return end
690
+ MapForEach("map", "CObject", function(obj)
691
+ local palette_value = obj:GetColorizationPalette()
692
+ if palette_value and palette_value ~= "" then
693
+ obj:SetColorsByColorizationPaletteName(palette_value)
694
+ end
695
+ end)
696
+ end
697
+
698
+ if FirstLoad then
699
+ g_EntityToColorPalettes_Cache = {} -- for preset dropdown in entity object editor
700
+ end
701
+
702
+ DefineClass.ColorizationPalettePreset = {
703
+ __parents = { "Preset" },
704
+ properties = {},
705
+
706
+ GlobalMap = "ColorizationPalettePresets",
707
+ EditorMenubarName = "Colorization Palettes Editor",
708
+ EditorMenubar = "Editors.Art",
709
+ EditorIcon = "CommonAssets/UI/Icons/colour creativity palette.png",
710
+
711
+ ContainerClass = "CPEntry",
712
+ --ValidateAfterSave = true,
713
+ }
714
+
715
+ -- CP = ColorizationPalette
716
+ DefineClass.CPEntry = {
717
+ __parents = { "InitDone" },
718
+ }
719
+
720
+ DefineClass.CPPaletteEntry = {
721
+ __parents = { "CPEntry" },
722
+
723
+ properties = {
724
+ { id = "PaletteName", name = "Palette Name", editor = "text", default = false },
725
+ { id = "PaletteColors", name = "Color Palette", editor = "nested_obj", base_class = "ColorizationPropSet", auto_expand = true, inclusive = true, default = false, },
726
+ },
727
+
728
+ EditorView = Untranslated("<color 0 143 0>Palette</color> - <PaletteName> <GetColorsPreviewString>")
729
+ }
730
+
731
+ function CPPaletteEntry:OnEditorSetProperty(prop_id, old_value, ged)
732
+ -- Called by ColorizationPropSet:OnEditorSetProperty
733
+ ApplyLatestColorPalettes()
734
+ end
735
+
736
+ function CPPaletteEntry:GetColorsPreviewString()
737
+ if not self.PaletteColors then
738
+ return ""
739
+ end
740
+
741
+ local c1, c2, c3 = "", "", ""
742
+
743
+ if self.PaletteColors.EditableColor1 then
744
+ local r, g, b = GetRGB(self.PaletteColors.EditableColor1)
745
+ c1 = string.format("<color %s %s %s>C1</color>", r, g, b)
746
+ end
747
+ if self.PaletteColors.EditableColor2 then
748
+ local r, g, b = GetRGB(self.PaletteColors.EditableColor2)
749
+ c2 = string.format("<color %s %s %s>C2</color>", r, g, b)
750
+ end
751
+ if self.PaletteColors.EditableColor3 then
752
+ local r, g, b = GetRGB(self.PaletteColors.EditableColor3)
753
+ c3 = string.format("<color %s %s %s>C3</color>", r, g, b)
754
+ end
755
+
756
+ return string.format("- %s %s %s", c1, c2, c3)
757
+ end
758
+
759
+ local function GetColorizableEntities()
760
+ local result = {}
761
+ for entity_name, entity_data in pairs(EntityData) do
762
+ if CanEntityBeColorized(entity_name) then
763
+ result[#result + 1] = entity_name
764
+ end
765
+ end
766
+ return result
767
+ end
768
+
769
+ DefineClass.CPEntityEntry = {
770
+ __parents = { "CPEntry" },
771
+
772
+ properties = {
773
+ { id = "ForEntity", name = "For Entity", editor = "choice", items = GetColorizableEntities, default = false },
774
+ },
775
+
776
+ EditorView = Untranslated("<color 143 0 0>Entity</color> - <ForEntity>")
777
+ }
778
+
779
+
780
+ local function RebuildCPMappingCaches()
781
+ g_EntityToColorPalettes_Cache = {}
782
+
783
+ -- Rebuild the mapping caches
784
+ ForEachPreset("ColorizationPalettePreset", function(preset)
785
+ local palette_names = {}
786
+ local palettes = {}
787
+ local entities = {}
788
+ for _, entry in ipairs(preset) do
789
+ if entry.class == "CPPaletteEntry" and entry.PaletteName and entry.PaletteColors then
790
+ palettes[#palettes + 1] = entry
791
+ elseif entry.class == "CPEntityEntry" and entry.ForEntity then
792
+ entities[#entities + 1] = entry
793
+ end
794
+ end
795
+
796
+ for _, entity in ipairs(entities) do
797
+ g_EntityToColorPalettes_Cache[entity.ForEntity] = palettes
798
+ end
799
+ end)
800
+ end
801
+
802
+ function OnMsg.PresetSave(class)
803
+ local classdef = g_Classes[class]
804
+ if IsKindOf(classdef, "ColorizationPalettePreset") then
805
+ RebuildCPMappingCaches()
806
+ ApplyLatestColorPalettes()
807
+ elseif IsKindOf(classdef, "EntitySpec") then
808
+ ApplyLatestColorPalettes()
809
+ end
810
+ end
811
+ -- Initial Presets load
812
+ OnMsg.DataLoaded = RebuildCPMappingCaches
813
+ -- Presets reload
814
+ OnMsg.DataReloadDone = RebuildCPMappingCaches
815
+
816
+
817
+ -- Colorizes objects on map load based on default colors as setters were not called!
818
+ function OnMsg.NewMapLoaded()
819
+ MapForEach("map", "Object", const.efRoot, function(obj)
820
+ -- Skip objects that can't be colorized (EnvColorized or have no Colorization Materials)
821
+ if not obj:CanBeColorized() then
822
+ return
823
+ end
824
+ -- Only g_DefaultColorsPalette colors were not updated!
825
+ local palette_value = obj:GetColorizationPalette()
826
+ if palette_value == g_DefaultColorsPalette then
827
+ obj:SetColorsByColorizationPaletteName(palette_value)
828
+ end
829
+ end)
830
+ end
831
+
832
+ -- called by C when initializing CObjects with palettes
833
+ function GetColorsByColorizationPaletteName(entity, palette_value)
834
+ if palette_value == g_DefaultColorsPalette then
835
+ -- Set to the Default entity colors defined in the Art Spec editor
836
+ local default_colors = GetDefaultColorizationSet(entity)
837
+ if default_colors then
838
+ return RGBRM(default_colors.EditableColor1, default_colors.EditableRoughness1, default_colors.EditableMetallic1),
839
+ RGBRM(default_colors.EditableColor2, default_colors.EditableRoughness2, default_colors.EditableMetallic2),
840
+ RGBRM(default_colors.EditableColor3, default_colors.EditableRoughness3, default_colors.EditableMetallic3)
841
+ end
842
+
843
+ end
844
+
845
+ -- If not empty or default => find the palette colors and apply them on the object
846
+ for _, palette in ipairs(g_EntityToColorPalettes_Cache[entity] or empty_table) do
847
+ if palette.PaletteName == palette_name and palette.PaletteColors then
848
+ local colors = palette.PaletteColors
849
+ return RGBRM(colors.EditableColor1, colors.EditableRoughness1, colors.EditableMetallic1),
850
+ RGBRM(colors.EditableColor2, colors.EditableRoughness2, colors.EditableMetallic2),
851
+ RGBRM(colors.EditableColor3, colors.EditableRoughness3, colors.EditableMetallic3)
852
+ end
853
+ end
854
+ end
CommonLua/Classes/CommandObject.lua ADDED
@@ -0,0 +1,704 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ local DebugCommand = (Platform.developer or Platform.asserts) and not Platform.console
2
+ local Trace_SetCommand = DebugCommand and "log"
3
+
4
+ local CommandImportance = const.CommandImportance or empty_table
5
+ local WeakImportanceThreshold = CommandImportance.WeakImportanceThreshold
6
+
7
+ --[[@@@
8
+ @class CommandObject
9
+ It is often necessary to ensure that an object is doing one thing – and one thing only. The command system is used to accomplish just that.
10
+
11
+ A CommandObject has a single thread executing its current command (if any). A command is just a function. When the current command finishes (the function returs), the current command changes to "Idle". A call to SetCommand (or a similar function) interrupts the currently executed command (deletes the thread) and creates a new thread to run the new command.
12
+
13
+ For example, imagine a `Citizen` called Hulio who is walking to work and gets murdered by a `Soldier`. We'd like to have Hulio fall on the ground – dead – and interrupt his workday for good.
14
+
15
+ ~~~~ Lua
16
+ -- This sets Hulio's command to "CmdGoWork"
17
+ function Citizen:FindWork()
18
+ ...
19
+ self:SetCommand("CmdGoWork", workplace)
20
+ end
21
+
22
+ -- This is called by the soldier who will kill Hulio
23
+ function Soldier:DoKill(obj)
24
+ ...
25
+ if not IsDead(obj) then
26
+ -- This cancels Hulio's "GoWork" command
27
+ obj:SetCommand("CmdGoDie", "Eliminated")
28
+ end
29
+ end
30
+ ~~~~
31
+
32
+
33
+ ## Destructors
34
+
35
+ When a command gets interrupted, the object can remain in an unpredictable state. Destructors solve that problem.
36
+
37
+ Each command or a function called from a command can push one or more destructors and *must* later pop them. If the command gets interrupted, any active destructors get executed from the most recently pushed one to the oldest one.
38
+
39
+ ~~~~ Lua
40
+ function Citizen:CmdUseWaterDispenser(dispencer)
41
+ assert(dispencer.in_use == false)
42
+ dispencer.in_use = true
43
+ self:PushDestructor(function(self) -- run this in case someone interrupts the Citizen (e.g. kidnaps him) while using the dispenser
44
+ dispenser.in_use = false
45
+ end)
46
+ self:Goto(dispenser) -- Goto probably pushes (and pops) its own destructor
47
+ self:SetAnim("UseWaterDispenser")
48
+ Sleep(self:TimeToAnimEnd())
49
+ self:PopAndCallDestructor() -- removes and executes the destuctor above, which will get also executed if the command is interrupted before reaching this code
50
+ end
51
+ ~~~~
52
+
53
+
54
+ ## Importance of commands
55
+
56
+ A CommandObject can execute hundreds of different commands it is quite difficult to figure out if an event should interrupt the current command or not.
57
+
58
+ We assign *importance* to each command - a number in most cases taken from const.CommandImportance[command], although some functions take *importance* as a parameter.
59
+ This allows us to implement methods such as TrySetCommand(cmd, ...) and CanInterruptCommand(cmd).
60
+
61
+ For example, if a stone hits a Citizen going to work, the Citizen should hold his head and scream with pain. If the Citizen is unconscious, nothing should happen. Command importance provides an elegant way to do that.
62
+
63
+ ~~~~ Lua
64
+ -- a stone has hit a citizen
65
+ citizen:TrySetCommand("CmdInPain") -- will be set only if running a less important command than CmdInPain
66
+ ~~~~
67
+
68
+ In the example above, if CmdGoDie has higher importance than CmdInPain (as it should) it will not be interrupted while CmdUseWaterDispenser will be correctly interrupted.
69
+
70
+ ## Queue
71
+
72
+ Commands can be queued for execution after the current command completes.
73
+
74
+ For example, a unit should complete something important (run from an enemy) and then return to whatever it was doing.
75
+
76
+ Another example is when the player has given a unit several commands to execute in order: kill this guy then kill that guy then return to the base for repairs.
77
+
78
+ --]]
79
+ DefineClass.CommandObject =
80
+ {
81
+ __parents = { "InitDone" },
82
+
83
+ command = false,
84
+ command_queue = false,
85
+ dont_clear_queue = false,
86
+ command_destructors = false,
87
+ command_thread = false,
88
+ thread_running_destructors = false,
89
+ command_call_stack = false,
90
+ forced_cmd_importance = false,
91
+ trace_setcmd = Trace_SetCommand,
92
+ last_error_time = false,
93
+ uninterruptable_importance = false,
94
+
95
+ CreateThread = CreateGameTimeThread,
96
+ IsValid = IsValid,
97
+ }
98
+
99
+ DefineClass.RealTimeCommandObject =
100
+ {
101
+ __parents = { "CommandObject" },
102
+
103
+ CreateThread = CreateRealTimeThread,
104
+ IsValid = function() return true end,
105
+ NetUpdateHash = function () end,
106
+ }
107
+
108
+ function RealTimeCommandObject:Done()
109
+ self.IsValid = empty_func
110
+ end
111
+
112
+ --[[@@@
113
+ When deleted, the command object interrupts the currently executed command. All present destructors will be called in another thread.
114
+ @function void CommandObject:Done()
115
+ --]]
116
+ function CommandObject:Done()
117
+ if self.command and CurrentThread() ~= self.command_thread then
118
+ self:SetCommand(false)
119
+ end
120
+ self.command_queue = nil
121
+ end
122
+
123
+ function CommandObject:Idle()
124
+ self[false](self)
125
+ end
126
+
127
+ function CommandObject:CmdInterrupt()
128
+ end
129
+
130
+ CommandObject[false] = function(self)
131
+ self.command = nil
132
+ self.command_thread = nil
133
+ self.command_destructors = nil
134
+ self.thread_running_destructors = nil
135
+ Halt()
136
+ end
137
+
138
+ --[[@@@
139
+ Called whenever a new command starts executing. It might be faster to do some simple cleanup here instead of pushing a destructor often.
140
+ @function void CommandObject:OnCommandStart()
141
+ --]]
142
+ AutoResolveMethods.OnCommandStart = true
143
+ CommandObject.OnCommandStart = empty_func
144
+
145
+ local SetCommandErrorChecks = empty_func
146
+ local SleepOnInfiniteLoop = empty_func
147
+
148
+ local function GetNextDestructor(obj, destructors)
149
+ local count = destructors[1]
150
+ if count == 0 then
151
+ return empty_func
152
+ end
153
+ local dstor = destructors[count + 1]
154
+ destructors[count + 1] = false
155
+ destructors[1] = count - 1
156
+
157
+ if type(dstor) == "string" then
158
+ assert(obj[dstor], string.format("Missing destructor: %s.%s", obj.class, dstor))
159
+ dstor = obj[dstor] or empty_func
160
+ elseif type(dstor) == "table" then
161
+ assert(type(dstor[1]) == "string")
162
+ assert(obj[dstor[1]], string.format("Missing destructor: %s.%s", obj.class, dstor[1]))
163
+ assert(#dstor == table.maxn(dstor)) -- make sure table.unpack works properly
164
+ return obj[dstor[1]] or empty_func, obj, table.unpack(dstor, 2)
165
+ end
166
+ return dstor, obj
167
+ end
168
+
169
+ local function CommandThreadProc(self, command, ...)
170
+ dbg(SleepOnInfiniteLoop(self))
171
+
172
+ -- wait the thread calling destructors to finish
173
+ local destructors = self.command_destructors
174
+ local thread_running_destructors = self.thread_running_destructors
175
+ if thread_running_destructors then
176
+ while IsValidThread(self.thread_running_destructors) and not WaitMsg(destructors, 100) do
177
+ end
178
+ end
179
+ local thread = CurrentThread()
180
+ if self.command_thread ~= thread then return end
181
+ assert(not self.uninterruptable_importance)
182
+ assert(not self.thread_running_destructors)
183
+
184
+ local command_func = type(command) == "function" and command or self[command]
185
+ local packed_command
186
+ while true do
187
+
188
+ if destructors and destructors[1] > 0 then
189
+ self.thread_running_destructors = thread
190
+ while destructors[1] > 0 do
191
+ sprocall(GetNextDestructor(self, destructors))
192
+ end
193
+ self.thread_running_destructors = false
194
+ if self.command_thread ~= thread then
195
+ Msg(destructors)
196
+ return
197
+ end
198
+ end
199
+ if not self:IsValid() then
200
+ return
201
+ end
202
+
203
+ self:NetUpdateHash("Command", type(command) == "function" and "function" or command, ...)
204
+ self:OnCommandStart()
205
+ local success, err
206
+ if packed_command == nil then
207
+ success, err = sprocall(command_func, self, ...)
208
+ else
209
+ success, err = sprocall(command_func, self, unpack_params(packed_command, 3))
210
+ end
211
+ assert(self.command_thread == thread)
212
+ if not success and not IsBeingDestructed(self) then
213
+ if self.last_error_time == now() then
214
+ -- throttle in case of an error right after another error to avoid infinite loops
215
+ Sleep(1000)
216
+ end
217
+ self.last_error_time = now()
218
+ end
219
+ local forced_cmd_importance
220
+ local queue = self.command_queue
221
+ packed_command = queue and table.remove(queue, 1)
222
+ if packed_command then
223
+ if type(packed_command) == "table" then
224
+ forced_cmd_importance = packed_command[1] or nil
225
+ command = packed_command[2]
226
+ else
227
+ command = packed_command
228
+ end
229
+ command_func = type(command) == "function" and command or self[command]
230
+ else
231
+ dbg(not success or SetCommandErrorChecks(self, "->Idle", ...))
232
+ command = "Idle"
233
+ command_func = self.Idle
234
+ end
235
+ self.forced_cmd_importance = forced_cmd_importance
236
+ self.command = command
237
+ destructors = self.command_destructors
238
+ end
239
+ self.command_thread = nil
240
+ end
241
+
242
+ --[[@@@
243
+ Changes the current command unconditionally. Any present destructors form the previous command will be called before executing it. The method can fail if the current command thread cannot be deleted. When invoked, the self is passed as a first param.
244
+ @function bool CommandObject:SetCommand(string command, ...)
245
+ @function bool CommandObject:SetCommand(function command_func, ...)
246
+ @param string command - Name of the command. Should be an object's method name.
247
+ @param function command_func - Alternatively, the command to execute can be provided as a function param.
248
+ @result bool - Command change success.
249
+ --]]
250
+ function CommandObject:SetCommand(command, ...)
251
+ return self:DoSetCommand(nil, command, ...)
252
+ end
253
+
254
+ -- Use with SetCommand or SetCommandImportance
255
+ function CommandObject:DoSetCommand(importance, command, ...)
256
+ self:NetUpdateHash("SetCommand", type(command) == "function" and "function" or command, ...)
257
+ dbg(SetCommandErrorChecks(self, command, ...))
258
+ self.command = command or nil
259
+ if not self.dont_clear_queue then
260
+ self.command_queue = nil
261
+ end
262
+ self.dont_clear_queue = nil
263
+ local old_thread = self.command_thread
264
+ local new_thread = self.CreateThread(CommandThreadProc, self, command, ...)
265
+ self.command_thread = new_thread
266
+ self.forced_cmd_importance = importance or nil
267
+ ThreadsSetThreadSource(new_thread, "Command", command)
268
+ if old_thread == self.thread_running_destructors then
269
+ local uninterruptable_importance = self.uninterruptable_importance
270
+ if not uninterruptable_importance then
271
+ -- wait the current thread to finish destructor execution
272
+ return true
273
+ end
274
+ local test_importance = importance or CommandImportance[command or false] or 0
275
+ if uninterruptable_importance >= test_importance then
276
+ -- wait the current thread to finish uninterruptable execution
277
+ return true
278
+ end
279
+ self.uninterruptable_importance = false
280
+ self.thread_running_destructors = false
281
+ end
282
+
283
+ DeleteThread(old_thread, true)
284
+ if old_thread == CurrentThread() then
285
+ -- the old thread failed to be deleted, revert!!!
286
+ DeleteThread(new_thread)
287
+ self.command_thread = old_thread
288
+ return false
289
+ end
290
+ return true
291
+ end
292
+
293
+ function CommandObject:TestInfiniteLoop()
294
+ self:SetCommand("TestInfiniteLoop2")
295
+ end
296
+
297
+ function CommandObject:TestInfiniteLoop2()
298
+ self:SetCommand("TestInfiniteLoop")
299
+ end
300
+
301
+ function CommandObject:GetCommandText()
302
+ return tostring(self.command)
303
+ end
304
+
305
+ local function IsCommandThread(self, thread)
306
+ thread = thread or CurrentThread()
307
+ return thread and (thread == self.command_thread or thread == self.thread_running_destructors)
308
+ end
309
+ CommandObject.IsCommandThread = IsCommandThread
310
+
311
+ --[[@@@
312
+ Pushes a destructor to be executed if the command is interrupted. The destructor stack is a LIFO structure. When invoked, the self is passed as a first param.
313
+ @function int CommandObject:PushDestructor(function dtor)
314
+ @function int CommandObject:PushDestructor(string dtor)
315
+ @function int CommandObject:PushDestructor(table dtor)
316
+ @param function dtor - Destructor function.
317
+ @param string dtor - Destructor name. Should be an object's method name.
318
+ @param table dtor - Destructor table, containing a method name and the params to be passed.
319
+ @result number - The count of the destructors pushed in the destructor stack.
320
+ Example:
321
+ ~~~~
322
+ local orig_name = unit.name
323
+ unit:PushDestructor(function(unit)
324
+ unit.name = orig_name
325
+ end)
326
+ ~~~~
327
+ --]]
328
+ function CommandObject:PushDestructor(dtor)
329
+ assert(IsCommandThread(self))
330
+ local destructors = self.command_destructors
331
+ if destructors then
332
+ destructors[1] = destructors[1] + 1
333
+ destructors[destructors[1] + 1] = dtor
334
+ return destructors[1]
335
+ else
336
+ self.command_destructors = { 1, dtor }
337
+ return 1
338
+ end
339
+ end
340
+
341
+ --[[@@@
342
+ Pops and calls the last pushed destructor to be executed if the command is interrupted.
343
+ @function void CommandObject:PopAndCallDestructor(int check_count = false)
344
+ @param int check_count - And optional param used to check for destructor stack consistency.
345
+ --]]
346
+ function CommandObject:PopAndCallDestructor(check_count)
347
+ local destructors = self.command_destructors
348
+
349
+ assert(destructors and destructors[1] > 0)
350
+ assert(not check_count or check_count == destructors[1])
351
+ assert(IsCommandThread(self))
352
+
353
+ local old_thread_running_destructors = self.thread_running_destructors
354
+ if not IsValidThread(old_thread_running_destructors) then
355
+ self.thread_running_destructors = CurrentThread()
356
+ assert(not old_thread_running_destructors)
357
+ old_thread_running_destructors = false
358
+ end
359
+ sprocall(GetNextDestructor(self, destructors))
360
+
361
+ if not old_thread_running_destructors then
362
+ self.thread_running_destructors = false
363
+ if self.command_thread ~= CurrentThread() then
364
+ Msg(destructors)
365
+ Halt()
366
+ end
367
+ end
368
+ end
369
+
370
+ --[[@@@
371
+ Same as PopAndCallDestructor but the destructor isn't invoked.
372
+ @function void CommandObject:PopDestructor(int check_count)
373
+ --]]
374
+ function CommandObject:PopDestructor(check_count)
375
+ local destructors = self.command_destructors
376
+
377
+ assert(destructors and destructors[1] > 0)
378
+ assert(not check_count or check_count == destructors[1])
379
+ assert(IsCommandThread(self))
380
+
381
+ destructors[destructors[1] + 1] = false
382
+ destructors[1] = destructors[1] - 1
383
+ end
384
+
385
+ function CommandObject:GetDestructorsCount()
386
+ local destructors = self.command_destructors
387
+ return destructors and destructors[1] or 0
388
+ end
389
+
390
+ --[[@@@
391
+ Executes a function, interruptable only by commands with higher importance than the specified one. The execution immitates a destructor call, meaning that if the new command fails to interrupt, that will happen immediately after the uninterruptable execution terminates. The self is pased as a first param when called.
392
+ @function void CommandObject:ExecuteUninterruptableImportance(int importance, function func, ...)
393
+ @function void CommandObject:ExecuteUninterruptableImportance(int importance, string method_name, ...)
394
+ @param int importance - Command importance threshold.
395
+ @param function func - Function to be executed.
396
+ @param string method_name - Alternatively, the function to execute can be provided as a object's method name.
397
+ --]]
398
+ function CommandObject:ExecuteUninterruptableImportance(importance, func, ...)
399
+ local thread = CurrentThread()
400
+ local func_to_execute = type(func) == "function" and func or self[func]
401
+
402
+ if self.command_thread ~= thread or self.thread_running_destructors then
403
+ assert((self.uninterruptable_importance or max_int) >= (importance or max_int))
404
+ sprocall(func_to_execute, self, ...)
405
+ return
406
+ end
407
+
408
+ local destructors = self.command_destructors
409
+ if not destructors then
410
+ -- the destructors table is needed to sync command threads
411
+ destructors = { 0 }
412
+ self.command_destructors = destructors
413
+ end
414
+
415
+ self.uninterruptable_importance = importance
416
+ self.thread_running_destructors = thread
417
+
418
+ sprocall(func_to_execute, self, ...)
419
+
420
+ self.uninterruptable_importance = false
421
+ self.thread_running_destructors = false
422
+
423
+ if self.command_thread == thread then
424
+ return
425
+ end
426
+
427
+ Msg(destructors)
428
+ Halt()
429
+ end
430
+
431
+ --[[@@@
432
+ A shortcut to invoke [ExecuteUninterruptableImportance](#CommandObject:ExecuteUninterruptableImportance) with maximum importance, disallowing interruption by any commands
433
+ @function void CommandObject:ExecuteUninterruptable(function func, ...)
434
+ --]]
435
+ function CommandObject:ExecuteUninterruptable(func, ...)
436
+ return self:ExecuteUninterruptableImportance(nil, func, ...)
437
+ end
438
+
439
+ --[[@@@
440
+ A shortcut to invoke [ExecuteUninterruptableImportance](#CommandObject:ExecuteUninterruptableImportance) with WeakImportanceThreshold, allowing interruption by all commands with higher importance.
441
+ @function void CommandObject:ExecuteWeakUninterruptable(function func, ...)
442
+ --]]
443
+ function CommandObject:ExecuteWeakUninterruptable(func, ...)
444
+ assert(WeakImportanceThreshold)
445
+ return self:ExecuteUninterruptableImportance(WeakImportanceThreshold, func, ...)
446
+ end
447
+
448
+ function CommandObject:IsIdleCommand()
449
+ return (self.command or "Idle") == "Idle"
450
+ end
451
+
452
+ local function InsertCommand(self, index, forced_importance, command, ...)
453
+ if self:IsIdleCommand() then
454
+ return self:SetCommand(command, ...)
455
+ end
456
+ local packed_command = not forced_importance and count_params(...) == 0 and command or pack_params(forced_importance or false, command or false, ...)
457
+ local queue = self.command_queue
458
+ if not queue then
459
+ self.command_queue = { packed_command }
460
+ else
461
+ if index then
462
+ table.insert(queue, index, packed_command)
463
+ else
464
+ queue[#queue + 1] = packed_command
465
+ end
466
+ end
467
+ end
468
+
469
+ -- queue command to be executed after the current and all other queued commands complete
470
+ function CommandObject:QueueCommand(command, ...)
471
+ return InsertCommand(self, false, false, command, ...)
472
+ end
473
+
474
+ function CommandObject:QueueCommandImportance(forced_importance, command, ...)
475
+ return InsertCommand(self, false, forced_importance, command, ...)
476
+ end
477
+
478
+ -- insert command at the specified place in the queue to be executed right after the current one completes
479
+ -- this is often used with 1 to place a command to be executed ASAP before continuing with the rest of the queue
480
+ function CommandObject:InsertCommand(index, forced_importance, command, ...)
481
+ return InsertCommand(self, index, forced_importance, command, ...)
482
+ end
483
+
484
+ -- Like setcommand, but without clearing the queue. Useful when we want current command to terminate immediately,
485
+ -- regardless of current stack position, start the new command and preserve the queue.
486
+ function CommandObject:SetCommandKeepQueue(command, ...)
487
+ self.dont_clear_queue = true
488
+ self:SetCommand(command, ...)
489
+ end
490
+
491
+ function CommandObject:HasCommandsInQueue()
492
+ return #(self.command_queue or "") > 0
493
+ end
494
+
495
+ function CommandObject:ClearCommandQueue()
496
+ self.command_queue = nil
497
+ end
498
+
499
+
500
+ ----- Command importance
501
+
502
+ function CommandObject:GetCommandImportance(command)
503
+ if not command then
504
+ return self.forced_cmd_importance or CommandImportance[self.command]
505
+ else
506
+ return CommandImportance[command or false]
507
+ end
508
+ end
509
+
510
+ --[[@@@
511
+ Checks if the current command can be changed by the given one.
512
+ @function bool CommandObject:CanSetCommand(string command, int importance = false)
513
+ @param string command - Name of the command to test.
514
+ @param int importance - Optional custom importance.
515
+ @result bool - Command change test success.
516
+ --]]
517
+ function CommandObject:CanSetCommand(command, importance)
518
+ assert(not importance or type(importance) == "number")
519
+ local current_importance = self.forced_cmd_importance or CommandImportance[self.command] or 0
520
+ importance = importance or CommandImportance[command or false] or 0
521
+ return current_importance <= importance
522
+ end
523
+
524
+ --[[@@@
525
+ Same as [SetCommand](#CommandObject:SetCommand) but may fail if the current command has a higher importance.
526
+ @function bool CommandObject:TrySetCommand(string command, ...)
527
+ --]]
528
+ function CommandObject:TrySetCommand(cmd, ...)
529
+ if not self:CanSetCommand(cmd) then
530
+ return
531
+ end
532
+ return self:SetCommand(cmd, ...)
533
+ end
534
+
535
+ --[[@@@
536
+ Same as [SetCommand](#CommandObject:SetCommand) but a custom importance is forced. The command importances are specified in the CommandImportance const group.
537
+ @function bool CommandObject:SetCommandImportance(int importance, string command, ...)
538
+ @param int importance - A custom importance to replace the default command importance.
539
+ --]]
540
+ function CommandObject:SetCommandImportance(importance, cmd, ...)
541
+ assert(not importance or type(importance) == "number")
542
+ return self:DoSetCommand(importance or nil, cmd, ...)
543
+ end
544
+
545
+ --[[@@@
546
+ See [SetCommandImportance](#CommandObject:SetCommandImportance), [TrySetCommand](#CommandObject:TrySetCommand)
547
+ @function bool CommandObject:TrySetCommandImportance(int importance, string command, ...)
548
+ --]]
549
+ function CommandObject:TrySetCommandImportance(importance, cmd, ...)
550
+ if not self:CanSetCommand(cmd, importance) then
551
+ return
552
+ end
553
+ return self:SetCommandImportance(importance, cmd, ...)
554
+ end
555
+
556
+ function CommandObject:ExecuteInCommand(method_name, ...)
557
+ if CanYield() and IsCommandThread(self) then
558
+ self[method_name](self, ...)
559
+ return true
560
+ end
561
+ return self:TrySetCommand(method_name, ...)
562
+ end
563
+
564
+ SuspendCommandObjectInfiniteChangeDetection = empty_func
565
+ ResumeCommandObjectInfiniteChangeDetection = empty_func
566
+
567
+ ----
568
+
569
+ if DebugCommand then
570
+
571
+ CommandObject.command_change_prev = false
572
+ CommandObject.command_change_count = 0
573
+ CommandObject.command_change_gtime = 0
574
+ CommandObject.command_change_rtime = 0
575
+ CommandObject.command_change_loops = 0
576
+
577
+ local lCommandChangeLoopDetection = true
578
+
579
+ function SuspendCommandObjectInfiniteChangeDetection()
580
+ lCommandChangeLoopDetection = false
581
+ end
582
+
583
+ function ResumeCommandObjectInfiniteChangeDetection()
584
+ lCommandChangeLoopDetection = true
585
+ end
586
+
587
+ local infinite_command_changes = 10
588
+
589
+ SleepOnInfiniteLoop = function(self)
590
+ if not lCommandChangeLoopDetection then return end
591
+
592
+ local rtime, gtime = RealTime(), GameTime()
593
+ if self.command_change_rtime ~= rtime or self.command_change_gtime ~= gtime then
594
+ self.command_change_rtime = rtime -- real time to avoid false positive on paused game
595
+ self.command_change_gtime = gtime -- game time to avoid false positive on falling behind gametime
596
+ self.command_change_count = nil
597
+ return
598
+ end
599
+ local command_change_count = self.command_change_count
600
+ if command_change_count <= infinite_command_changes then
601
+ self.command_change_count = command_change_count + 1
602
+ return
603
+ end
604
+ self.command_change_loops = self.command_change_loops + 1
605
+ Sleep(50 * self.command_change_loops)
606
+ self.command_change_count = nil
607
+ end
608
+
609
+ SetCommandErrorChecks = function(self, command, ...)
610
+ local destructors = self.command_destructors
611
+ local prev_command = self.command
612
+ if command == "->Idle" and destructors and destructors[1] > 0 then -- the command should pop all its destructors
613
+ print("Command", self.class .. "." .. tostring(prev_command), "remaining destructors:")
614
+ for i = 1,destructors[1] do
615
+ local destructor = destructors[i + 1]
616
+ if type(destructor) == "string" then
617
+ printf("\t%d. %s.%s", i, self.class, destructor)
618
+ elseif type(destructor) == "table" then
619
+ printf("\t%d. %s.%s", i, self.class, destructor[1])
620
+ else
621
+ local info = debug.getinfo(destructor, "S") or empty_table
622
+ local source = info.source or "Unknown"
623
+ local line = info.linedefined or -1
624
+ printf("\t%d. %s(%d)", i, source, line)
625
+ end
626
+ end
627
+ error(string.format("Command %s.%s did not pop its destructors.", self.class, tostring(self.command)), 2)
628
+ -- remove the remaining destructors to avoid having the error all the time
629
+ while destructors[1] > 0 do
630
+ self:PopDestructor()
631
+ end
632
+ end
633
+ if command and command ~= "->Idle" then
634
+ if type(command) ~= "function" and not self:HasMember(command) then
635
+ error(string.format("Invalid command %s:%s", self.class, tostring(command)), 3)
636
+ end
637
+ if IsBeingDestructed(self) then
638
+ error(string.format("%s:SetCommand('%s') called from Done() or delete()", self.class, tostring(command)), 3)
639
+ end
640
+ end
641
+ if command ~= "->Idle" or prev_command ~= "Idle" then
642
+ self.command_call_stack = GetStack(3)
643
+ if self.trace_setcmd then
644
+ if self.trace_setcmd == "log" then
645
+ self:Trace("SetCommand {1}", tostring(command), self.command_call_stack, ...)
646
+ else
647
+ error(string.format("%s:SetCommand(%s) time %d, old command %s", self.class, concat_params(", ", tostring(command), ...), GameTime(), tostring(self.command)), 3)
648
+ end
649
+ end
650
+ end
651
+ if self.command_change_count == infinite_command_changes then
652
+ assert(false, string.format("Infinite command change in %s: %s -> %s -> %s", self.class, tostring(self.command_change_prev), tostring(prev_command), tostring(command)))
653
+ --StoreErrorSource(self, "Infinite command change") Pause("Debug")
654
+ end
655
+ self.command_change_prev = prev_command
656
+ end
657
+
658
+ local function __DbgForEachMethod(passed, obj, callback, ...)
659
+ if not obj then
660
+ return
661
+ end
662
+ for name, value in pairs(obj) do
663
+ if type(value) == "function" and not passed[name] then
664
+ passed[name] = true
665
+ callback(name, value, ...)
666
+ end
667
+ end
668
+ return __DbgForEachMethod(passed, getmetatable(obj), callback, ...)
669
+ end
670
+
671
+ function DbgForEachMethod(obj, callback, ...)
672
+ return __DbgForEachMethod({}, obj, callback, ...)
673
+ end
674
+
675
+ function DbgBreakRemove(obj)
676
+ DbgForEachMethod(obj, function(name, value, obj)
677
+ obj[name] = nil
678
+ end, obj)
679
+ end
680
+
681
+ function DbgBreakSchedule(obj, methods)
682
+ DbgBreakRemove(obj)
683
+ if methods == "string" then methods = { methods } end
684
+ DbgForEachMethod(obj, function(name, value, obj)
685
+ if not methods or table.find(methods, name) then
686
+ local new_value = function(...)
687
+ if IsCommandThread(obj) then
688
+ DbgBreakRemove(obj)
689
+ print("Break removed")
690
+ bp(true, 1)
691
+ end
692
+ return value(...)
693
+ end
694
+ obj[name] = new_value
695
+ end
696
+ end, obj)
697
+ print("Break schedule")
698
+ end
699
+
700
+ function CommandObject:AsyncCheatDebugger()
701
+ DbgBreakSchedule(self)
702
+ end
703
+
704
+ end -- DebugCommand
CommonLua/Classes/Common.lua ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DefineClass.CameraFacingObject = {
2
+ __parents = { "CObject", "ComponentExtraTransform" },
3
+
4
+ properties = {
5
+ { id = "CameraFacing", name="Camera facing", default = false, editor = "bool", help = "Let object use camera facing, specified in its class" },
6
+ },
7
+ SetCameraFacing = function( self, value )
8
+ if value then
9
+ self:SetSpecialOrientation(const.soFacing)
10
+ else
11
+ self:SetSpecialOrientation()
12
+ end
13
+ end,
14
+ GetCameraFacing = function(self)
15
+ return self:GetSpecialOrientation() == const.soFacing
16
+ end,
17
+ }
18
+
19
+ function DepositionTypesItems(obj)
20
+ local deposition = obj:GetDepositionSupported()
21
+ local items = {
22
+ { value = "", text = "None"},
23
+ }
24
+ if deposition == "terrainchunk" or deposition == "all" then
25
+ table.insert(items, {value = "terrainchunk", text = "Terrain Chunk"})
26
+ end
27
+
28
+ if deposition == "terraintype" or deposition == "all" then
29
+ local subitems = { }
30
+ ForEachPreset("TerrainObj", function(preset)
31
+ table.insert(subitems, { value = preset.material_name, text = preset.material_name })
32
+ end)
33
+ table.sort(subitems, function(a, b) return a.value < b.value end)
34
+ table.append(items, subitems)
35
+ end
36
+
37
+ return items;
38
+ end
39
+
40
+ DefineClass.Deposition = {
41
+ __parents = { "CObject", "ComponentCustomData" },
42
+ flags = { efSelectable = false, },
43
+ properties = {
44
+ { category = "Deposition", id = "DepositionType", editor = "dropdownlist", default = "", items = DepositionTypesItems, help = "The type of material that is going to be applied on top of this object." },
45
+ { category = "Deposition", id = "DepositionScale", editor = "number", default = 10, min = 1, max = 100, scale = 10, slider = true, help = "The scale of all textures extracted from the material.", no_edit=function(obj) return obj:IsTerrainChunkDeposition() end },
46
+ { category = "Deposition", id = "DepositionAxis", editor = "point", default = point(0, 0, 127), helper = "relative_pos", helper_origin = true, helper_outside_object = true, helper_scale_with_parent = true, help = "The axis used for determining where the deposition must be applied.", no_edit=function(obj) return obj:IsTerrainChunkDeposition() end },
47
+ { category = "Deposition", id = "DepositionFadeStart", editor = "number", default = 40, min = 0, max = 100, scale = 1, slider = true, help = "At which point relative to the axis the deposition must be completely invisible." },
48
+ { category = "Deposition", id = "DepositionFadeEnd", editor = "number", default = 60, min = 0, max = 100, scale = 1, slider = true, help = "At which point relative to the axis the deposition must be completely visible." },
49
+ { category = "Deposition", id = "DepositionFadeCurve", editor = "number", default = 10, min = 1, max = 100, scale = 10, slider = true, help = "Determines the hardness of the transition between areas with and without deposition." },
50
+ { category = "Deposition", id = "DepositionAlphaStart", editor = "number", default = 40, min = 0, max = 100, scale = 1, slider = true, help = "At which point relative to the axis the alpha of the diffuse texture is completely applied. You can use it to create sparse deposition or improve the transition between areas with and without deposition.",
51
+ no_edit=function(obj) return obj:IsTerrainChunkDeposition() end },
52
+ { category = "Deposition", id = "DepositionAlphaEnd", editor = "number", default = 60, min = 0, max = 100, scale = 1, slider = true, help = "At which point relative to the axis the alpha of the diffuse texture is not applied. You can use it to create sparse deposition or improve the transition between areas with and without deposition.",
53
+ no_edit=function(obj) return obj:IsTerrainChunkDeposition() end },
54
+ { category = "Deposition", id = "DepositionNoiseGamma", editor = "number", default = 0, min = 0, max = 255, scale = 128, slider = true, help = "How much of the noise to apply. 0 to disable.", },
55
+ { category = "Deposition", id = "DepositionNoiseFreq", editor = "number", default = 0, min = 0, max = 255, scale = 64, slider = true, help = "Noise frequency", },
56
+ }
57
+ }
58
+
59
+ function Deposition:IsTerrainChunkDeposition()
60
+ local deposition = self:GetDepositionType()
61
+ return deposition == "terrainchunk"
62
+ end
63
+
64
+ function OnMsg.BinAssetsLoaded()
65
+ UpdateDepositionMaterialLUT()
66
+ UpdateDustMaterial(const.DustMaterialExterior, "TerrainSand_01_mesh.mtl")
67
+ UpdateDustMaterial(const.DustMaterialInterior, "DustRust_mesh.mtl")
68
+ end
69
+
70
+ DefineClass.Mirrorable = {
71
+ __parents = {"CObject"},
72
+ properties = {
73
+ }
74
+ }
CommonLua/Classes/Components.lua ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ local function not_attached(obj)
2
+ return not obj:GetParent()
3
+ end
4
+
5
+ --[[@@@
6
+ @class ComponentAttach
7
+ Objects inheriting this class can attach other objects or be attached to other objects
8
+ --]]
9
+
10
+ DefineClass.ComponentAttach = {
11
+ __parents = { "CObject" },
12
+ flags = { cofComponentAttach = true },
13
+ properties = {
14
+ { category = "Child", id = "AttachOffset", name = "Attached Offset", editor = "point", default = point30, no_edit = not_attached, dont_save = true },
15
+ { category = "Child", id = "AttachAxis", name = "Attached Axis", editor = "point", default = axis_z, no_edit = not_attached, dont_save = true },
16
+ { category = "Child", id = "AttachAngle", name = "Attached Angle", editor = "number", default = 0, no_edit = not_attached, dont_save = true, min = -180*60, max = 180*60, slider = true, scale = "deg" },
17
+ { category = "Child", id = "AttachSpotName", name = "Attached At", editor = "text", default = "", no_edit = not_attached, dont_save = true, read_only = true },
18
+ { category = "Child", id = "Parent", name = "Attached To", editor = "object", default = false, no_edit = not_attached, dont_save = true, read_only = true },
19
+ { category = "Child", id = "TopmostParent", name = "Topmost Parent", editor = "object", default = false, no_edit = not_attached, dont_save = true, read_only = true },
20
+ { category = "Child", id = "AngleLocal", name = "Local Angle", editor = "number", default = 0, no_edit = not_attached, dont_save = true, min = -180*60, max = 180*60, slider = true, scale = "deg" },
21
+ { category = "Child", id = "AxisLocal", name = "Local Axis", editor = "point", default = axis_z, no_edit = not_attached, dont_save = true },
22
+ },
23
+ }
24
+
25
+ ComponentAttach.SetAngleLocal = CObject.SetAngle
26
+ ComponentAttach.SetAxisLocal = CObject.SetAxis
27
+
28
+ DefineClass.StripComponentAttachProperties = {
29
+ __parents = { "ComponentAttach" },
30
+ properties = {
31
+ { id = "AttachOffset", },
32
+ { id = "AttachAxis", },
33
+ { id = "AttachAngle", },
34
+ { id = "AttachSpotName", },
35
+ { id = "Parent", },
36
+ },
37
+ }
38
+
39
+ function ComponentAttach:GetAttachSpotName()
40
+ local parent = self:GetParent()
41
+ return parent and parent:GetSpotName(self:GetAttachSpot())
42
+ end
43
+
44
+ DefineClass.ComponentCustomData = {
45
+ __parents = { "CObject" },
46
+ flags = { cofComponentCustomData = true },
47
+ -- when inheriting ComponentCustomData from multiple parents you have to:
48
+ -- 1. review if its use is not conflicting
49
+ -- 2. add member CustomDataType = "<class-name>" to suppress the class system error
50
+
51
+ GetCustomData = _GetCustomData,
52
+ SetCustomData = _SetCustomData,
53
+ GetCustomString = _GetCustomString,
54
+ SetCustomString = _SetCustomString,
55
+ }
56
+
57
+ if Platform.developer then
58
+ function OnMsg.ClassesPreprocess(classdefs)
59
+ for name, class in pairs(classdefs) do
60
+ if table.find(class.__parents, "ComponentCustomData") then
61
+ if not class.CustomDataType then
62
+ class.CustomDataType = name
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ function SpecialOrientationItems()
70
+ local SpecialOrientationNames = { "soTerrain", "soTerrainLarge", "soFacing", "soFacingY", "soFacingVertical", "soVelocity", "soZOffset", "soTerrainPitch", "soTerrainPitchLarge" }
71
+ table.sort(SpecialOrientationNames)
72
+ local items = {}
73
+ for i, name in ipairs(SpecialOrientationNames) do
74
+ items[i] = { text = name, value = const[name] }
75
+ end
76
+ table.insert(items, 1, { text = "", value = const.soNone })
77
+ return items
78
+ end
79
+
80
+ DefineClass.ComponentExtraTransform = {
81
+ __parents = { "CObject" },
82
+ flags = { cofComponentExtraTransform = true },
83
+ properties = {
84
+ { id = "SpecialOrientation", name = "Special Orientation", editor = "choice", default = const.soNone, items = SpecialOrientationItems },
85
+ },
86
+ }
87
+
88
+ DefineClass.ComponentInterpolation = {
89
+ __parents = { "CObject" },
90
+ flags = { cofComponentInterpolation = true },
91
+ }
92
+
93
+ DefineClass.ComponentCurvature = {
94
+ __parents = { "CObject" },
95
+ flags = { cofComponentCurvature = true },
96
+ }
97
+
98
+ DefineClass.ComponentAnim = {
99
+ __parents = { "CObject" },
100
+ flags = { cofComponentAnim = true },
101
+ }
102
+
103
+ DefineClass.ComponentSound = {
104
+ __parents = { "CObject" },
105
+ flags = { cofComponentSound = true },
106
+ properties = {
107
+ { category = "Sound", id = "SoundBank", name = "Bank", editor = "preset_id", default = "", preset_class = "SoundPreset", dont_save = true },
108
+ { category = "Sound", id = "SoundType", name = "Type", editor = "preset_id", default = "", preset_class = "SoundTypePreset", dont_save = true, read_only = true },
109
+ { category = "Sound", id = "Sound", name = "Sample", editor = "text", default = "", dont_save = true, read_only = true },
110
+ { category = "Sound", id = "SoundDuration", name = "Duration", editor = "number", default = -1, dont_save = true, read_only = true },
111
+ { category = "Sound", id = "SoundHandle", name = "Handle", editor = "number", default = -1, dont_save = true, read_only = true },
112
+ },
113
+ }
114
+
115
+ function ComponentSound:GetSoundBank()
116
+ local sname, sbank, stype, shandle, sduration, stime = self:GetSound()
117
+ return sbank or ""
118
+ end
119
+ function ComponentSound:GetSoundType()
120
+ local sname, sbank, stype, shandle, sduration, stime = self:GetSound()
121
+ return stype or ""
122
+ end
123
+ function ComponentSound:GetSoundHandle()
124
+ local sname, sbank, stype, shandle, sduration, stime = self:GetSound()
125
+ return shandle or -1
126
+ end
127
+ function ComponentSound:GetSoundDuration()
128
+ local sname, sbank, stype, shandle, sduration, stime = self:GetSound()
129
+ return sduration or -1
130
+ end
CommonLua/Classes/Composite.lua ADDED
@@ -0,0 +1,503 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ----- Composite objects with components of base class CompositeClass that can be turned on and off
2
+ --
3
+ -- create the specific classes, setting their components and properties, using the Ged editor that will appear
4
+ -- properties of all components that have template = true in their metadata are editable in the Ged editor
5
+ -- use AutoResolveMethod to defind how to combine methods present in multiple components
6
+
7
+ const.ComponentsPropCategory = "Components"
8
+
9
+ DefineClass.CompositeDef = {
10
+ __parents = { "Preset" },
11
+ properties = {
12
+ { category = "Preset", id = "object_class", name = "Object Class", editor = "choice", default = "", items = function(self) return ClassDescendantsCombo(self.ObjectBaseClass, true) end, },
13
+ { category = "Preset", id = "code", name = "Global Code", editor = "func", default = false, lines = 1, max_lines = 100, params = "",
14
+ no_edit = function(self) return IsKindOf(self, "ModItem") end,
15
+ },
16
+ },
17
+
18
+ -- Preset settings
19
+ GeneratesClass = true,
20
+ SingleFile = false,
21
+ GedShowTemplateProps = true,
22
+
23
+ -- CompositeDef settings
24
+ ObjectBaseClass = false,
25
+ ComponentClass = false,
26
+
27
+ components_cache = false,
28
+ components_sorting = false,
29
+ properties_cache = false,
30
+ EditorMenubarName = false,
31
+
32
+ EditorViewPresetPostfix = Untranslated(" <style GedSmall><color 164 128 64><object_class></color></style>"),
33
+ Documentation = "This is a preset that results in a composite class definition. You can look at it as a template from which objects are created.\n\nThe generated class will inherit the specified Object Class and all component classes.",
34
+ }
35
+
36
+ function CompositeDef.new(class, obj)
37
+ local object = Preset.new(class, obj)
38
+ object.object_class = CompositeDef.GetObjectClass(object)
39
+ return object
40
+ end
41
+
42
+ function CompositeDef:GetObjectClass()
43
+ return self.object_class ~= "" and self.object_class or self.ObjectBaseClass
44
+ end
45
+
46
+ function CompositeDef:GetComponents(filter)
47
+ if not self.ComponentClass then return empty_table end
48
+
49
+ local components_cache = self.components_cache
50
+ if not components_cache then
51
+ local sorting_keys = {}
52
+ local component_class = g_Classes[self.ComponentClass]
53
+ local blacklist = component_class.BlackListBaseClasses
54
+ components_cache = ClassDescendantsList(self.ComponentClass, function(classname, class, base_class, base_def, sorting_keys, blacklist)
55
+ if class:IsKindOf(base_class) or base_def:IsKindOf(classname)
56
+ or IsKindOf(g_Classes[class.__generated_by_class or false], "CompositeDef")
57
+ or class:IsKindOfClasses(blacklist) then
58
+ return
59
+ end
60
+ if (class.ComponentSortKey or 0) ~= 0 then
61
+ sorting_keys[classname] = class.ComponentSortKey
62
+ end
63
+ return true
64
+ end, self.ObjectBaseClass, g_Classes[self.ObjectBaseClass], sorting_keys, blacklist)
65
+ local classdef = g_Classes[self.class]
66
+ rawset(classdef, "components_cache", components_cache)
67
+ rawset(classdef, "components_sorting", sorting_keys)
68
+ end
69
+ if filter == "active" then
70
+ return table.ifilter(components_cache, function(_, classname) return self:GetProperty(classname) end)
71
+ elseif filter == "inactive" then
72
+ return table.ifilter(components_cache, function(_, classname) return not self:GetProperty(classname) end)
73
+ end
74
+ return components_cache
75
+ end
76
+
77
+ function CompositeDef:GetProperties()
78
+ local object_class = self:GetObjectClass()
79
+ local object_def = g_Classes[object_class]
80
+ assert(not object_class or object_def)
81
+ if not object_def then
82
+ return self.properties
83
+ end
84
+
85
+ local cache = self.properties_cache or {}
86
+ if not cache[object_class] then
87
+ local props, prop_data = {}, {}
88
+ local function add_prop(prop, default, class)
89
+ local added
90
+ if not prop_data[prop.id] then
91
+ added = true
92
+ if prop.default ~= default then
93
+ prop = table.copy(prop)
94
+ prop.default = default
95
+ end
96
+ props[#props + 1] = prop
97
+ else
98
+ assert(prop_data[prop.id].default == default,
99
+ string.format("Default value conflict for property '%s' in classes '%s' and '%s'", prop.id, prop_data[prop.id].class, class))
100
+ end
101
+ prop_data[prop.id] = { default = default, class = class }
102
+ return added and prop or table.find_value(props, "id", prop.id)
103
+ end
104
+
105
+ for _, prop in ipairs(self.properties) do
106
+ if prop.id ~= "code" then add_prop(prop, prop.default, self.class) end
107
+ end
108
+ for _, prop in ipairs(object_def.properties) do
109
+ if prop.template then
110
+ add_prop(prop, object_def:GetDefaultPropertyValue(prop.id), self.class)
111
+ end
112
+ end
113
+
114
+ local components = self:GetComponents()
115
+ for _, classname in ipairs(components) do
116
+ local inherited = object_def:IsKindOf(classname) or false
117
+ local help = inherited and "Inherited from the base class"
118
+ local prop = { category = const.ComponentsPropCategory, id = classname, editor = "bool", default = inherited, read_only = inherited, help = help }
119
+ add_prop(prop, inherited, self.class)
120
+ end
121
+ add_prop(table.find_value(self.properties, "id", "code"), self:GetDefaultPropertyValue("code"), self.class)
122
+ for _, classname in ipairs(components) do
123
+ if not object_def:IsKindOf(classname) then
124
+ local component_def = g_Classes[classname]
125
+ for _, prop in ipairs(component_def.properties) do
126
+ local category = prop.category or classname
127
+ local no_edit = prop.no_edit
128
+ prop = table.copy(prop, "deep")
129
+ prop.category = category
130
+ prop = add_prop(prop, component_def:GetDefaultPropertyValue(prop.id), classname)
131
+ local composite_owner_classes = prop.composite_owner_classes or {}
132
+ composite_owner_classes[#composite_owner_classes + 1] = classname
133
+ prop.composite_owner_classes = composite_owner_classes
134
+ prop.no_edit = function(self, ...)
135
+ if no_edit == true or type(no_edit) == "function" and no_edit(self, ...) then return true end
136
+ local prop_meta = select(1, ...)
137
+ for _, name in ipairs(prop_meta.composite_owner_classes or empty_table) do
138
+ if rawget(self, name) then
139
+ return
140
+ end
141
+ end
142
+ return true
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ -- store the cache in the class, this auto-invalidates it on Lua reload
149
+ rawset(g_Classes[self.class], "properties_cache", cache)
150
+ rawset(cache, object_class, props)
151
+ return props
152
+ end
153
+
154
+ return cache[object_class]
155
+ end
156
+
157
+ function CompositeDef:SetProperty(prop_id, value)
158
+ local prop_meta = self:GetPropertyMetadata(prop_id)
159
+ if prop_meta and prop_meta.template and prop_meta.setter then
160
+ return prop_meta.setter(self, value, prop_id, prop_meta)
161
+ end
162
+ if table.find(CompositeDef.properties, "id", prop_id) then
163
+ return Preset.SetProperty(self, prop_id, value)
164
+ end
165
+ if value and table.find(self:GetComponents(), prop_id) and _G[prop_id]:HasMember("OnEditorNew") then
166
+ _G[prop_id].OnEditorNew(self) -- OnEditorNew can initialize component property defaults of e.g. nested_obj/list component properties
167
+ end
168
+ rawset(self, prop_id, value)
169
+ end
170
+
171
+ function CompositeDef:GetProperty(prop_id)
172
+ local prop_meta = self:GetPropertyMetadata(prop_id)
173
+ if prop_meta and prop_meta.template and prop_meta.getter then
174
+ return prop_meta.getter(self, prop_id, prop_meta)
175
+ end
176
+ local value = Preset.GetProperty(self, prop_id)
177
+ if value ~= nil then
178
+ return value
179
+ end
180
+ return prop_meta and prop_meta.default
181
+ end
182
+
183
+ function CompositeDef:OnEditorSetProperty(prop_id, old_value, ged)
184
+ local prop_meta = self:GetPropertyMetadata(prop_id)
185
+ if prop_meta and prop_meta.template and prop_meta.edited then
186
+ return prop_meta.edited(self, old_value, prop_id, prop_meta)
187
+ end
188
+ return Preset.OnEditorSetProperty(self, prop_id, old_value, ged)
189
+ end
190
+
191
+ function CompositeDef:__toluacode(...)
192
+ -- clear properties of the inactive components
193
+ local properties = self:GetProperties()
194
+ local find = table.find
195
+ local rawget = rawget
196
+ for _, classname in ipairs(self:GetComponents("inactive")) do
197
+ for _, prop in ipairs(g_Classes[classname].properties) do
198
+ if rawget(self, prop.id) ~= nil and not find(properties, "id", prop.id) then
199
+ self[prop.id] = nil
200
+ end
201
+ end
202
+ end
203
+ return Preset.__toluacode(self, ...)
204
+ end
205
+
206
+ -- supports generating a different class for each DLC, including property values for this DLC; see PresetDLCSplitting.lua
207
+ -- return a table with <key, file_name> pairs to generate multiple companion files, where key = dlc
208
+ function CompositeDef:GetCompanionFilesList(save_path)
209
+ local files = { }
210
+ for _, prop in pairs(self:GetProperties()) do
211
+ local save_in = prop.dlc or ""
212
+ if not files[save_in] then
213
+ -- GetSavePath depends on self.group and self.id
214
+ files[save_in] = self:GetCompanionFileSavePath(prop.dlc and self:GetSavePath(prop.dlc) or save_path)
215
+ end
216
+ end
217
+ return files
218
+ end
219
+
220
+ function CompositeDef:GenerateCompanionFileCode(code, dlc)
221
+ local class_exists_err = self:CheckIfIdExistsInGlobal()
222
+ if class_exists_err then
223
+ return class_exists_err
224
+ end
225
+
226
+ code:appendf("UndefineClass('%s')\nDefineClass.%s = {\n", self.id, self.id)
227
+ self:GenerateParents(code)
228
+ self:AppendGeneratedByProps(code)
229
+ self:GenerateFlags(code)
230
+ self:GenerateConsts(code, dlc)
231
+ code:append("}\n\n")
232
+ self:GenerateGlobalCode(code)
233
+ end
234
+
235
+ function CompositeDef:GenerateParents(code)
236
+ local object_class = self:GetObjectClass()
237
+
238
+ local list = self:GetComponents("active")
239
+ if #list > 0 then
240
+ assert(list ~= self.components_cache)
241
+ local object_def = g_Classes[object_class]
242
+ assert(object_def)
243
+ if object_def then
244
+ list = table.ifilter(list, function(_, classname) return not object_def:IsKindOf(classname) end)
245
+ end
246
+ end
247
+ if #list == 0 then
248
+ code:appendf('\t__parents = { "%s" },\n', object_class)
249
+ return
250
+ end
251
+
252
+ if next(self.components_sorting) then
253
+ table.insert(list, 1, object_class)
254
+ local sorting_keys = self.components_sorting
255
+ table.stable_sort(list, function(class1, class2)
256
+ return (sorting_keys[class1] or 0) < (sorting_keys[class2] or 0)
257
+ end)
258
+ code:append('\t__parents = { "', table.concat(list, '", "'), '" },\n')
259
+ else
260
+ code:appendf('\t__parents = { "%s", "', object_class)
261
+ code:append(table.concat(list, '", "'))
262
+ code:append('" },\n')
263
+ end
264
+ end
265
+
266
+ ClassNonInheritableMembers.composite_flags = true
267
+
268
+ function CompositeDef:GenerateFlags(code)
269
+ local object_def = g_Classes[self:GetObjectClass()]
270
+ assert(object_def)
271
+ if not object_def then return end
272
+
273
+ local flags = table.copy(object_def.composite_flags or empty_table)
274
+ for _, component in ipairs(self:GetComponents("active")) do
275
+ for flag, set in pairs(g_Classes[component].composite_flags) do
276
+ assert(flags[flag] == nil)
277
+ flags[flag] = set
278
+ end
279
+ end
280
+ if not next(flags) then
281
+ return
282
+ end
283
+ code:append('\tflags = { ')
284
+ for flag, set in sorted_pairs(flags) do
285
+ code:appendf("%s = %s, ", flag, set and "true" or "false")
286
+ end
287
+ code:append('},\n')
288
+ end
289
+
290
+ function CompositeDef:IncludePropAs(prop, dlc)
291
+ local id = prop.id
292
+ if Preset:GetPropertyMetadata(id) or id == "code" then
293
+ return false
294
+ end
295
+ if not prop.dlc and not (dlc ~= "" and prop.dlc_override) or prop.dlc == dlc then
296
+ return prop.maingame_prop_id or prop.id
297
+ end
298
+ end
299
+
300
+ function CompositeDef:GenerateConsts(code, dlc)
301
+ local props = self:GetProperties()
302
+ code:append(#props > 0 and "\n" or "")
303
+ local has_embedded_objects = false
304
+ for _, prop in ipairs(props) do
305
+ local id = prop.id
306
+ local include_as = self:IncludePropAs(prop, dlc)
307
+ if include_as then
308
+ local value = rawget(self, id)
309
+ if not self:IsDefaultPropertyValue(id, prop, value) then
310
+ code:append("\t", include_as, " = ")
311
+ ValueToLuaCode(value, 1, code, {} --[[ enable property injection ]])
312
+ code:append(",\n")
313
+ end
314
+ end
315
+ end
316
+ return has_embedded_objects
317
+ end
318
+
319
+ function CompositeDef:GenerateGlobalCode(code)
320
+ if self.code and self.code ~= "" then
321
+ code:append("\n")
322
+ local name, params, body = GetFuncSource(self.code)
323
+ if type(body) == "table" then
324
+ for _, line in ipairs(body) do
325
+ code:append(line, "\n")
326
+ end
327
+ elseif type(body) == "string" then
328
+ code:append(body)
329
+ end
330
+ code:append("\n")
331
+ end
332
+ end
333
+
334
+ function CompositeDef:GetObjectClassLuaFilePath(path)
335
+ if self.save_in == "" then
336
+ return string.format("Lua/%s/__%s.generated.lua", self.class, self.ObjectBaseClass)
337
+ elseif self.save_in == "Common" then
338
+ return string.format("CommonLua/Classes/%s/__%s.generated.lua", self.class, self.ObjectBaseClass)
339
+ elseif self.save_in:starts_with("Libs/") then -- lib
340
+ return string.format("CommonLua/%s/%s/__%s.generated.lua", self.save_in, self.class, self.ObjectBaseClass)
341
+ else -- save_in is a DLC name
342
+ return string.format("svnProject/Dlc/%s/Presets/%s/__%s.generated.lua", self.save_in, self.class, self.ObjectBaseClass)
343
+ end
344
+ end
345
+
346
+ function CompositeDef:GetWarning()
347
+ if not g_Classes[self.id] then
348
+ return "The class for this preset has not been generated yet.\nIt needs to be saved before it can be used or referenced from elsewhere."
349
+ end
350
+ end
351
+
352
+ function CompositeDef:GetError()
353
+ for _, component in ipairs(self:GetComponents()) do
354
+ if self[component] then
355
+ local err = g_Classes[component].GetError(self)
356
+ if err then
357
+ return err
358
+ end
359
+ end
360
+ end
361
+ end
362
+
363
+ function OnMsg.ClassesPreprocess(classdefs)
364
+ for name, classdef in pairs(classdefs) do
365
+ if classdef.__parents and classdef.__parents[1] == "CompositeDef" then
366
+ classdefs[classdef.ObjectBaseClass].__hierarchy_cache = true
367
+ end
368
+ end
369
+ end
370
+
371
+ function OnMsg.ClassesBuilt()
372
+ ClassDescendants("CompositeDef", function(class_name, class)
373
+ if IsKindOf(class, "ModItem") then return end
374
+
375
+ local objclass = class.ObjectBaseClass
376
+ local path = class:GetObjectClassLuaFilePath()
377
+
378
+ -- can't generate the file in packed builds, as we can't get Lua source for func properties
379
+ if config.RunUnpacked and Platform.developer and not Platform.console then
380
+ -- Map all component methods => list of components they are defined in
381
+ local methods = {}
382
+ for _, component in ipairs(class:GetComponents()) do
383
+ for name, member in pairs(g_Classes[component]) do
384
+ if type(member) == "function" and not RecursiveCallMethods[name] then
385
+ local classlist = methods[name]
386
+ if classlist then
387
+ classlist[#classlist + 1] = component
388
+ else
389
+ methods[name] = { component }
390
+ end
391
+ end
392
+ end
393
+ end
394
+
395
+ -- Generate the code for the CompositeDef's object class here
396
+ local code = pstr(exported_files_header_warning, 16384)
397
+ code:appendf("function __%sExtraDefinitions()\n", objclass)
398
+
399
+ -- a) make GetComponents callable from the object class
400
+ code:appendf("\t%s.components_cache = false\n", objclass)
401
+ code:appendf("\t%s.GetComponents = %s.GetComponents\n", objclass, class_name)
402
+ code:appendf("\t%s.ComponentClass = %s.ComponentClass\n", objclass, class_name)
403
+ code:appendf("\t%s.ObjectBaseClass = %s.ObjectBaseClass\n\n", objclass, class_name)
404
+
405
+ -- b) add default property values for ALL component properites, so accessing them is fine from the object class
406
+ local objprops = _G[objclass].properties
407
+ for _, prop in ipairs(class:GetProperties()) do
408
+ if not table.find(class.properties, "id", prop.id) and not table.find(objprops, "id", prop.id) then
409
+ code:append("\t", objclass, ".", prop.id, " = ")
410
+ ValueToLuaCode(class:GetDefaultPropertyValue(prop.id, prop), nil, code, {} --[[ enable property injection ]])
411
+ code:append("\n")
412
+ end
413
+ end
414
+ code:append("end\n\n")
415
+ code:appendf("function OnMsg.ClassesBuilt() __%sExtraDefinitions() end\n", objclass)
416
+
417
+ -- Save the code and execute it now
418
+ local err = SaveSVNFile(path, code, class.LocalPreset)
419
+ if err then
420
+ printf("Error '%s' saving %s", tostring(err), path)
421
+ return
422
+ end
423
+ end
424
+
425
+ if io.exists(path) then
426
+ dofile(path)
427
+ _G[string.format("__%sExtraDefinitions", objclass)]()
428
+ else
429
+ -- saved in a DLC folder, in a pack file mounted somewhere in DlcFolders
430
+ assert(path:starts_with("svnProject/Dlc/"))
431
+ for _, dlc_folder in ipairs(rawget(_G, "DlcFolders")) do
432
+ local path = string.format("%s/Presets/%s/__%s.generated.lua", dlc_folder, class_name, objclass)
433
+ if io.exists(path) then
434
+ dofile(path)
435
+ _G[string.format("__%sExtraDefinitions", objclass)]()
436
+ return
437
+ end
438
+ end
439
+ assert(false, "Unable to find and execute " .. path .. " from a DLC folder.")
440
+ end
441
+ end)
442
+ end
443
+
444
+
445
+ ----- Test/sample code below
446
+
447
+ --[[DefineClass.TestClass = {
448
+ __parents = { "PropertyObject" },
449
+ properties = {
450
+ { category = "General", id = "BaseProp1", editor = "text", default = "", translate = true, lines = 1, max_lines = 10, },
451
+ { category = "General", id = "BaseProp2", editor = "bool", default = true, },
452
+ },
453
+ Value = true,
454
+ TestMethod = true,
455
+ }
456
+
457
+ DefineClass.TestClassComponent = {
458
+ __parents = { "PropertyObject" }
459
+ }
460
+
461
+ DefineClass.TestClassComponent1 = {
462
+ __parents = { "TestClassComponent" },
463
+ properties = {
464
+ { id = "Component1Prop1", editor = "text", default = "", translate = true, lines = 1, max_lines = 10 },
465
+ { id = "Component1Prop2", editor = "bool", default = true },
466
+ },
467
+ }
468
+
469
+ function TestClassComponent1:Value()
470
+ return 1
471
+ end
472
+
473
+ function TestClassComponent1:TestMethod()
474
+ return 1
475
+ end
476
+
477
+ DefineClass.TestClassComponent2 = {
478
+ __parents = { "TestClassComponent" },
479
+ properties = {
480
+ { id = "Component2Prop", editor = "number", default = 0 },
481
+ },
482
+ }
483
+
484
+ function TestClassComponent2:Value()
485
+ return 2
486
+ end
487
+
488
+ RecursiveCallMethods.Value = "+"
489
+ RecursiveCallMethods.TestMethod = "call"
490
+
491
+ DefineClass.TestCompositeDef = {
492
+ __parents = { "CompositeDef" },
493
+
494
+ -- composite def
495
+ ObjectBaseClass = "TestClass",
496
+ ComponentClass = "TestClassComponent",
497
+
498
+ -- preset
499
+ EditorMenubarName = "TestClass Composite Objects Editor",
500
+ EditorMenubar = "Editors",
501
+ EditorShortcut = "Ctrl-T",
502
+ GlobalMap = "TestCompositeDefs",
503
+ }]]
CommonLua/Classes/CompositeBody.lua ADDED
@@ -0,0 +1,1428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function GetSpotOffset(obj, name, idx, state, phase)
2
+ assert(obj)
3
+ if not IsValid(obj) then
4
+ return 0, 0, 0, "obj"
5
+ end
6
+ idx = idx or obj:GetSpotBeginIndex(name) or -1
7
+ if idx == -1 then
8
+ return 0, 0, 0, "spot"
9
+ end
10
+ state = state or "idle"
11
+ phase = phase or 0
12
+ local x, y, z = GetEntitySpotPos(obj, state, phase, idx, idx, true):xyz()
13
+ local s = obj:GetWorldScale()
14
+ if s ~= 100 then
15
+ x, y, z = x * s / 100, y * s / 100, z * s / 100
16
+ end
17
+ return x, y, z
18
+ end
19
+
20
+ function GetLocalAngleDiff(attach, local_angle)
21
+ return abs(AngleDiff(attach:GetVisualAngleLocal(), local_angle))
22
+ end
23
+
24
+ function GetLocalRotationTime(attach, local_angle, speed)
25
+ return MulDivRound(1000, GetLocalAngleDiff(attach, local_angle), speed)
26
+ end
27
+
28
+ function GetLocalAngle(obj, angle)
29
+ return AngleDiff(angle, obj:GetAngle())
30
+ end
31
+
32
+ ----
33
+
34
+ DefineClass.CompositeBodyPart = {
35
+ __parents = { "ComponentAnim", "ComponentAttach", "ColorizableObject" },
36
+ flags = { gofSyncState = true, efWalkable = false, efApplyToGrids = false, efCollision = false, efSelectable = true },
37
+ }
38
+
39
+ function CompositeBodyPart:GetName()
40
+ local parent = self:GetParent()
41
+ while IsValid(parent) do
42
+ if IsKindOf(parent, "CompositeBody") then
43
+ for name, part in pairs(parent.attached_parts) do
44
+ if part == self then
45
+ return name
46
+ end
47
+ end
48
+ return
49
+ else
50
+ parent = parent:GetParent()
51
+ end
52
+ end
53
+ end
54
+
55
+ local function RecomposeBody(obj)
56
+ for name, part in pairs(obj.attached_parts) do
57
+ if part ~= obj then
58
+ obj:RemoveBodyPart(part, name)
59
+ end
60
+ end
61
+ obj.attached_parts = nil
62
+ obj:ComposeBodyParts()
63
+ end
64
+
65
+ local function EditorRecomposeBodiesOnMap(obj, root, prop_id, ged)
66
+ if IsValid(obj) then
67
+ RecomposeBody(obj)
68
+ elseif obj.object_class then
69
+ MapForEach("map", obj.object_class, RecomposeBody)
70
+ end
71
+ end
72
+
73
+ local function get_body_parts_count(self)
74
+ local class_name = self.id
75
+ local class = g_Classes[class_name] or empty_table
76
+ local target = self.composite_part_target or class.composite_part_target or class_name
77
+ local composite_part_groups = self.composite_part_groups or class.composite_part_groups or { class_name }
78
+ local part_presets = Presets.CompositeBodyPreset
79
+ local count = 0
80
+ for _, part_name in ipairs(self.composite_part_names or class.composite_part_names) do
81
+ for _, part_group in ipairs(composite_part_groups) do
82
+ for _, part_preset in ipairs(part_presets[part_group] or empty_table) do
83
+ if (not target or part_preset.Target == target) and (part_preset.Parts or empty_table)[part_name] then
84
+ count = count + 1
85
+ end
86
+ end
87
+ end
88
+ end
89
+ return count
90
+ end
91
+
92
+ -- Composite bodies change the entity, scale and colors of the unit."
93
+ DefineClass.CompositeBody = {
94
+ __parents = { "Object", "CompositeBodyPart" },
95
+
96
+ properties = {
97
+ { category = "Composite Body", id = "recompose", name = "Recompose", editor = "buttons", default = false, template = true, buttons = { { name = "Recompose", func = function(...) return EditorRecomposeBodiesOnMap(...) end, } } },
98
+ { category = "Composite Body", id = "composite_part_names", name = "Parts", editor = "string_list", template = true, help = "Composite body parts. Each body preset may cover one or more parts. Each part may have another part as a parent and a custom attach spot.", body_part_match = true },
99
+ { category = "Composite Body", id = "composite_part_main", name = "Main Part", editor = "choice", items = PropGetter("composite_part_names"), template = true, help = "Main body part to be applied directly to the composite object." },
100
+ { category = "Composite Body", id = "composite_part_target", name = "Target", editor = "text", template = true, help = "Will match composite body presets having the same target. If not specified, the class name is used.", body_part_match = true },
101
+ { category = "Composite Body", id = "composite_part_groups", name = "Groups", editor = "string_list", items = PresetGroupsCombo("CompositeBodyPreset"), template = true, help = "Will match composite body presets from those groups. If not specified, the class name is used as a group name.", body_part_match = true },
102
+ { category = "Composite Body", id = "CompositePartCount", name = "Parts Found", editor = "number", template = true, default = 0, dont_save = true, read_only = 0, getter = get_body_parts_count },
103
+ { category = "Composite Body", id = "composite_part_parent", name = "Parent", editor = "prop_table", read_only = true, template = true, help = "Defines custom parent for each body part." },
104
+ { category = "Composite Body", id = "composite_part_spots", name = "Spots", editor = "prop_table", read_only = true, template = true, help = "Defines custom attach spots for each body part." },
105
+ { category = "Composite Body", id = "cycle_colors", name = "Cycle Colors", editor = "bool", default = false, template = true, help = "If you can cycle through the composite body colors during construction.", },
106
+ },
107
+
108
+ flags = { gofSyncState = false, gofPropagateState = true },
109
+
110
+ composite_seed = false,
111
+ colorization_offset = 0,
112
+ composite_part_target = false,
113
+ composite_part_names = { "Body" },
114
+ composite_part_spots = false,
115
+ composite_part_parent = false,
116
+ composite_part_main = "Body",
117
+ composite_part_groups = false,
118
+
119
+ attached_parts = false,
120
+ override_parts = false,
121
+ override_parts_spot = false,
122
+
123
+ InitBodyParts = empty_func,
124
+ SetAutoAttachMode = empty_func,
125
+ ChangeEntityDisabled = empty_func,
126
+ }
127
+
128
+ function CompositeBody:CheatCompose()
129
+ self:ComposeBodyParts()
130
+ end
131
+
132
+ local props = CompositeBody.properties
133
+ for i=1,10 do
134
+ local category = "Composite Body Hierarchy"
135
+ local function no_edit(self)
136
+ local names = self:GetProperty("composite_part_names") or empty_table
137
+ local name = names[i]
138
+ return not name or name == self:GetProperty("composite_part_main")
139
+ end
140
+ local function GetPartName(self)
141
+ local names = self:GetProperty("composite_part_names")
142
+ return names[i] or ""
143
+ end
144
+ local function GetSpotName(self)
145
+ local name = GetPartName(self)
146
+ return name .. " Spot"
147
+ end
148
+ local function GetParentName(self)
149
+ local name = GetPartName(self)
150
+ return name .. " Parent"
151
+ end
152
+ local spot_id = "composite_part_spot_" .. i
153
+ local parent_id = "composite_part_parent_" .. i
154
+ local function getter(self, prop_id)
155
+ local target_id
156
+ if prop_id == spot_id then
157
+ target_id = "composite_part_spots"
158
+ elseif prop_id == parent_id then
159
+ target_id = "composite_part_parent"
160
+ else
161
+ return ""
162
+ end
163
+ local name = GetPartName(self)
164
+ local map = self:GetProperty(target_id)
165
+ return map and map[name] or ""
166
+ end
167
+ local function setter(self, value, prop_id)
168
+ local target_id
169
+ if prop_id == spot_id then
170
+ target_id = "composite_part_spots"
171
+ elseif prop_id == parent_id then
172
+ target_id = "composite_part_parent"
173
+ else
174
+ return
175
+ end
176
+ local name = GetPartName(self)
177
+ local map = self:GetProperty(target_id) or empty_table
178
+ map = table.raw_copy(map)
179
+ map[name] = (value or "") ~= "" and value or nil
180
+ rawset(self, target_id, map)
181
+ end
182
+ local function GetParentItems(self)
183
+ local names = self:GetProperty("composite_part_names") or empty_table
184
+ if names[i] then
185
+ names = table.icopy(names)
186
+ table.remove_value(names, names[i])
187
+ end
188
+ return names, return_true
189
+ end
190
+ table.iappend(props, {
191
+ { category = category, id = spot_id, name = GetSpotName, editor = "text", default = "", dont_save = true, getter = getter, setter = setter, no_edit = no_edit, template = true },
192
+ { category = category, id = parent_id, name = GetParentName, editor = "choice", default = "", items = GetParentItems, dont_save = true, getter = getter, setter = setter, no_edit = no_edit, template = true },
193
+ })
194
+ CompositeBody["Get" .. spot_id] = function(self)
195
+ return getter(self, spot_id)
196
+ end
197
+ CompositeBody["Get" .. parent_id] = function(self)
198
+ return getter(self, parent_id)
199
+ end
200
+ CompositeBody["Set" .. spot_id] = function(self, value)
201
+ return setter(self, spot_id, value)
202
+ end
203
+ CompositeBody["Set" .. parent_id] = function(self, value)
204
+ return setter(self, parent_id, value)
205
+ end
206
+ end
207
+
208
+ function CompositeBody:Done()
209
+ -- allow garbage collection of CompositeBody objects which otherwise have a non-weak reference to themselves
210
+ self.attached_parts = nil
211
+ self.override_parts = nil
212
+ end
213
+
214
+ function CompositeBody:GetPart(name)
215
+ local parts = self.attached_parts
216
+ return parts and parts[name]
217
+ end
218
+
219
+ function CompositeBody:GetPartName(part_to_find)
220
+ for name, part in pairs(self.attached_parts) do
221
+ if part == part_to_find then
222
+ return name
223
+ end
224
+ end
225
+ end
226
+
227
+ function CompositeBody:ForEachBodyPart(func, ...)
228
+ local attached_parts = self.attached_parts or empty_table
229
+ for _, name in ipairs(self.composite_part_names) do
230
+ local part = attached_parts[name]
231
+ if part then
232
+ func(part, self, ...)
233
+ end
234
+ end
235
+ end
236
+
237
+ function CompositeBody:UpdateEntity()
238
+ return self:ComposeBodyParts()
239
+ end
240
+
241
+ local function ResolveCompositeMainEntity(classdef)
242
+ if not classdef then return end
243
+ local composite_part_groups = classdef.composite_part_groups
244
+ local composite_part_group = composite_part_groups and composite_part_groups[1] or classdef.class
245
+ local part_presets = table.get(Presets, "CompositeBodyPreset", composite_part_group)
246
+ if next(part_presets) then
247
+ local composite_part_target = classdef.composite_part_target
248
+ local composite_part_main = classdef.composite_part_main or "Body"
249
+ for _, part_preset in ipairs(part_presets) do
250
+ if not composite_part_target or composite_part_target == part_preset.Target then
251
+ if (part_preset.Parts or empty_table)[composite_part_main] then
252
+ return part_preset.Entity
253
+ end
254
+ end
255
+ end
256
+ end
257
+ return classdef.entity or classdef.class
258
+ end
259
+
260
+ function ResolveTemplateEntity(self)
261
+ local entity = IsValid(self) and self:GetEntity()
262
+ if IsValidEntity(entity) then
263
+ return entity
264
+ end
265
+ local class = self.id or self.class
266
+ local classdef = g_Classes[class]
267
+ if not classdef then return end
268
+ entity = ResolveCompositeMainEntity(classdef)
269
+ return IsValidEntity(entity) and entity
270
+ end
271
+
272
+ function TemplateSpotItems(self)
273
+ local entity = ResolveTemplateEntity(self)
274
+ if not entity then return {} end
275
+ local spots = {{ value = false, text = "" }}
276
+ local seen = {}
277
+ local spbeg, spend = GetAllSpots(entity)
278
+ for spot = spbeg, spend do
279
+ local name = GetSpotName(entity, spot)
280
+ if not seen[name] then
281
+ seen[name] = true
282
+ spots[#spots + 1] = { value = name, text = name }
283
+ end
284
+ end
285
+ table.sortby_field(spots, "text")
286
+ return spots
287
+ end
288
+
289
+ function CompositeBody:CollectBodyParts(part_to_preset, seed)
290
+ local target = self.composite_part_target or self.class
291
+ local composite_part_groups = self.composite_part_groups or { self.class }
292
+ local part_presets = Presets.CompositeBodyPreset
293
+ for _, part_name in ipairs(self.composite_part_names) do
294
+ if not part_to_preset[part_name] then
295
+ local matched_preset, matched_presets
296
+ for _, part_group in ipairs(composite_part_groups) do
297
+ for _, part_preset in ipairs(part_presets[part_group]) do
298
+ if (not target or part_preset.Target == target) and (part_preset.Parts or empty_table)[part_name] then
299
+ local matched = true
300
+ for _, filter in ipairs(part_preset.Filters) do
301
+ if not filter:Match(self) then
302
+ matched = false
303
+ break
304
+ end
305
+ end
306
+ if matched then
307
+ if not matched_preset or matched_preset.ZOrder < part_preset.ZOrder then
308
+ matched_preset = part_preset
309
+ matched_presets = nil
310
+ elseif matched_preset.ZOrder == part_preset.ZOrder then
311
+ if matched_presets then
312
+ matched_presets[#matched_presets + 1] = part_preset
313
+ else
314
+ matched_presets = { matched_preset, part_preset }
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
320
+ end
321
+ if matched_presets then
322
+ seed = self:ComposeBodyRand(seed)
323
+ matched_preset = table.weighted_rand(matched_presets, "Weight", seed)
324
+ end
325
+ if matched_preset then
326
+ part_to_preset[part_name] = matched_preset
327
+ end
328
+ end
329
+ end
330
+ return seed
331
+ end
332
+
333
+ function CompositeBody:GetConstructionCopyObjectData(copy_data)
334
+ table.rawset_values(copy_data, self, "composite_seed", "colorization_offset")
335
+ end
336
+
337
+ function CompositeBody:GetConstructionCursorDynamicData(controller, cursor_data)
338
+ table.rawset_values(cursor_data, controller, "composite_seed", "colorization_offset")
339
+ end
340
+
341
+ function CompositeBody:GetConstructionControllerDynamicData(controller_data)
342
+ table.rawset_values(controller_data, self, "composite_seed", "colorization_offset")
343
+ end
344
+
345
+ function OnMsg.GatherConstructionInitData(construction_init_data)
346
+ rawset(construction_init_data, "composite_seed", true)
347
+ rawset(construction_init_data, "colorization_offset", true)
348
+ end
349
+
350
+ function CompositeBody:ComposeBodyRand(seed, ...)
351
+ seed = seed or self.composite_seed or self:RandSeed("Body")
352
+ self.composite_seed = self.composite_seed or seed
353
+ return BraidRandom(seed, ...)
354
+ end
355
+
356
+ function CompositeBody:GetPartFXTarget(part)
357
+ return self
358
+ end
359
+
360
+ function CompositeBody:ComposeBodyParts(seed)
361
+ if self:ChangeEntityDisabled() then
362
+ return
363
+ end
364
+ local part_to_preset = { }
365
+ -- collect the best matched body presets for the remaining parts without equipment
366
+ seed = self:CollectBodyParts(part_to_preset, seed) or seed
367
+
368
+ -- apply the main body entity (all others are attached to this one)
369
+ local main_name = self.composite_part_main
370
+ local main_preset = main_name and part_to_preset[main_name]
371
+ if not main_preset and not IsValidEntity(self:GetEntity()) then
372
+ return
373
+ end
374
+ local applied_presets = {}
375
+ local changed
376
+ if main_preset then
377
+ local changed_i, seed_i = self:ApplyBodyPart(self, main_preset, main_name, seed)
378
+ assert(IsValidEntity(self:GetEntity()))
379
+ changed = changed_i or changed
380
+ seed = seed_i or seed
381
+ applied_presets = { [main_preset] = true }
382
+ end
383
+
384
+ local last_part_class, part_def
385
+
386
+ local override_parts = self.override_parts or empty_table
387
+ -- apply all the remaining as attaches (removing the unused ones from the previous procedure)
388
+ local attached_parts = self.attached_parts or {}
389
+ attached_parts[main_name] = self
390
+ self.attached_parts = attached_parts
391
+ for _, part_name in ipairs(self.composite_part_names) do
392
+ if part_name == main_name then
393
+ goto continue
394
+ end
395
+ local part_obj = attached_parts[part_name]
396
+ --body part overriding
397
+ local override = override_parts[part_name]
398
+ if override then
399
+ if override ~= part_obj then
400
+ if part_obj then
401
+ self:RemoveBodyPart(part_obj, part_name)
402
+ end
403
+ attached_parts[part_name] = override
404
+ local parent = self
405
+ if override:GetParent() ~= parent then
406
+ local spot = self.override_parts_spot and self.override_parts_spot[part_name]
407
+ spot = spot or self.composite_part_spots[part_name]
408
+ local spot_idx = spot and parent:GetSpotBeginIndex(spot)
409
+ parent:Attach(override, spot_idx)
410
+ end
411
+ end
412
+ goto continue
413
+ end
414
+ --preset search
415
+ local preset = part_to_preset[part_name]
416
+ if preset and not applied_presets[preset] then
417
+ applied_presets[preset] = true
418
+ if preset.Entity ~= "" then
419
+ local part_class = preset.PartClass or "CompositeBodyPart"
420
+ if not IsValid(part_obj) or part_obj.class ~= part_class then
421
+ if last_part_class ~= part_class then
422
+ last_part_class = part_class
423
+ part_def = g_Classes[part_class]
424
+ assert(part_def)
425
+ part_def = part_def or CompositeBodyPart
426
+ end
427
+ DoneObject(part_obj)
428
+ part_obj = part_def:new()
429
+ attached_parts[part_name] = part_obj
430
+ changed = true
431
+ end
432
+ local changed_i, seed_i = self:ApplyBodyPart(part_obj, preset, part_name, seed)
433
+ changed = changed_i or changed
434
+ seed = seed_i or seed
435
+ goto continue
436
+ end
437
+ end
438
+ -- 1) body part preset not found
439
+ -- 2) part already covered, should be removed
440
+ -- 3) part used to specify a missing part
441
+ if part_obj then
442
+ attached_parts[part_name] = nil
443
+ self:RemoveBodyPart(part_obj, part_name)
444
+ end
445
+ ::continue::
446
+ end
447
+ if changed then
448
+ self:NetUpdateHash("BodyChanged", seed)
449
+ end
450
+ self:InitBodyParts()
451
+ return changed
452
+ end
453
+
454
+ local def_scale = range(100, 100)
455
+
456
+ function CompositeBody:ChangeBodyPartEntity(part, preset, name)
457
+ local entity = preset.Entity
458
+ if (preset.AffectedBy or "") ~= "" and (preset.EntityWhenAffected or "") ~= "" and self.attached_parts[preset.AffectedBy] then
459
+ entity = preset.EntityWhenAffected
460
+ end
461
+
462
+ local current_entity = part:GetEntity()
463
+ if current_entity == entity or not IsValidEntity(entity) then
464
+ return
465
+ end
466
+ if current_entity ~= "" then
467
+ PlayFX("ApplyBodyPart", "end", part, self:GetPartFXTarget(part))
468
+ end
469
+ local state = part:GetGameFlags(const.gofSyncState) == 0 and EntityStates.idle or nil
470
+ part:ChangeEntity(entity, state)
471
+ return true
472
+ end
473
+
474
+ function CompositeBody:ChangeBodyPartScale(part, name, scale)
475
+ if part:GetScale() ~= scale then
476
+ part:SetScale(scale)
477
+ return true
478
+ end
479
+ end
480
+
481
+ function CompositeBody:ApplyBodyPart(part, preset, name, seed)
482
+ -- entity
483
+ local changed_entity = self:ChangeBodyPartEntity(part, preset, name)
484
+ local changed = changed_entity
485
+ -- mirrored
486
+ if part:GetMirrored() ~= preset.Mirrored then
487
+ part:SetMirrored(preset.Mirrored)
488
+ changed = true
489
+ end
490
+ -- scale
491
+ local scale = 100
492
+ local scale_range = preset.Scale
493
+ if scale_range ~= def_scale then
494
+ local scale_min, scale_max = scale_range.from, scale_range.to
495
+ if scale_min == scale_max then
496
+ scale = scale_min
497
+ else
498
+ scale, seed = self:ComposeBodyRand(seed, scale_min, scale_max)
499
+ end
500
+ end
501
+ if self:ChangeBodyPartScale(part, name, scale) then
502
+ changed = true
503
+ end
504
+ -- color
505
+ seed = self:ColorizeBodyPart(part, preset, name, seed) or seed
506
+ -- attach
507
+ if part ~= self then
508
+ local axis = preset.Axis
509
+ if axis and part:GetAxisLocal() ~= axis then
510
+ part:SetAxis(axis)
511
+ changed = true
512
+ end
513
+ local angle = preset.Angle
514
+ if angle and part:GetAngleLocal() ~= angle then
515
+ part:SetAngle(angle)
516
+ changed = true
517
+ end
518
+ local spot_name = preset.AttachSpot or ""
519
+ if spot_name == "" then
520
+ local spots = self.composite_part_spots
521
+ spot_name = spots and spots[name] or ""
522
+ if spot_name == "" then
523
+ spot_name = "Origin"
524
+ end
525
+ end
526
+ local sync_state = preset.SyncState
527
+ if sync_state == "auto" then
528
+ sync_state = spot_name == "Origin"
529
+ end
530
+ if not sync_state then
531
+ part:ClearGameFlags(const.gofSyncState)
532
+ else
533
+ part:SetGameFlags(const.gofSyncState)
534
+ end
535
+ local prev_parent, prev_spot_idx = part:GetParent(), part:GetAttachSpot()
536
+ local parents = self.composite_part_parent
537
+ local parent_part = preset.Parent or parents and parents[name] or ""
538
+ local parent = parent_part ~= "" and self.attached_parts[parent_part] or self
539
+ local spot_idx = parent:GetSpotBeginIndex(spot_name)
540
+ assert(spot_idx ~= -1, string.format("Failed to attach body part %s to spot %s of %s with state %s", name, spot_name, parent:GetEntity(), parent:GetStateText()))
541
+ if prev_parent ~= parent or prev_spot_idx ~= spot_idx then
542
+ parent:Attach(part, spot_idx)
543
+ changed = true
544
+ end
545
+ local attach_offset = preset.AttachOffset or point30
546
+ local attach_axis = preset.AttachAxis or axis_z
547
+ local attach_angle = preset.AttachAngle or 0
548
+ if attach_offset ~= part:GetAttachOffset() or attach_axis ~= part:GetAttachAxis() or attach_angle ~= part:GetAttachAngle() then
549
+ part:SetAttachOffset(attach_offset)
550
+ part:SetAttachAxis(attach_axis)
551
+ part:SetAttachAngle(attach_angle)
552
+ changed = true
553
+ end
554
+ end
555
+
556
+ local changed_fx
557
+ local fx_actor_class = (preset.FxActor or "") ~= "" and preset.FxActor or nil
558
+ local current_fx_actor = rawget(part, "fx_actor_class") -- avoid clearing class fx actor with the default FxActor value
559
+ if current_fx_actor ~= fx_actor_class then
560
+ if current_fx_actor then
561
+ PlayFX("ApplyBodyPart", "end", part, self:GetPartFXTarget(part))
562
+ end
563
+ part.fx_actor_class = fx_actor_class
564
+ changed_fx = true
565
+ end
566
+
567
+ if changed_fx or changed_entity then
568
+ PlayFX("ApplyBodyPart", "start", part, self:GetPartFXTarget(part))
569
+ end
570
+
571
+ return changed, seed
572
+ end
573
+
574
+ function CompositeBody:ColorizeBodyPart(part, preset, name, seed)
575
+ local inherit_from = preset.ColorInherit
576
+ local colorization = inherit_from ~= "" and table.get(self.attached_parts, inherit_from)
577
+ if not colorization then
578
+ seed = self:ComposeBodyRand(seed)
579
+ local colors = preset.Colors or empty_table
580
+ local idx
581
+ colorization, idx = table.weighted_rand(colors, "Weight", seed)
582
+ local offset = self.colorization_offset
583
+ if idx and offset then
584
+ idx = ((idx + offset - 1) % #colors) + 1
585
+ colorization = colors[idx]
586
+ end
587
+ end
588
+ part:SetColorization(colorization)
589
+ return seed
590
+ end
591
+
592
+ function CompositeBody:SetColorizationOffset(offset)
593
+ local part_to_preset = {}
594
+ local seed = self.composite_seed
595
+ self:CollectBodyParts(part_to_preset, seed)
596
+ local attached_parts = self.attached_parts
597
+ self.colorization_offset = offset
598
+ for _, part_name in ipairs(self.composite_part_names) do
599
+ local preset = part_to_preset[part_name]
600
+ if preset then
601
+ local part = attached_parts[part_name]
602
+ self:ColorizeBodyPart(part, preset, part_name, seed, offset)
603
+ end
604
+ end
605
+ end
606
+
607
+ function CompositeBody:RemoveBodyPart(part, name)
608
+ DoneObject(part)
609
+ end
610
+
611
+ function CompositeBody:OverridePart(name, obj, spot)
612
+ if not IsValid(self) or IsBeingDestructed(self) then
613
+ return
614
+ end
615
+ assert(table.find(self.composite_part_names, name), "Invalid part name")
616
+ if type(obj) == "string" and IsValidEntity(obj) then
617
+ local entity = obj
618
+ obj = CompositeBodyPart:new()
619
+ obj:ChangeEntity(entity)
620
+ AutoAttachObjects(obj)
621
+ end
622
+ if IsValid(obj) then
623
+ self.override_parts = self.override_parts or {}
624
+ assert(not self.override_parts[name], "Part already overridden")
625
+ self.override_parts[name] = obj
626
+ self.override_parts_spot = self.override_parts_spot or {}
627
+ self.override_parts_spot[name] = spot
628
+ elseif self.override_parts then
629
+ obj = self.override_parts[name]
630
+ if self.attached_parts[name] == obj then
631
+ self.attached_parts[name] = nil
632
+ end
633
+ self.override_parts[name] = nil
634
+ self.override_parts_spot[name] = nil
635
+ end
636
+ self:ComposeBodyParts()
637
+ return obj
638
+ end
639
+
640
+ function CompositeBody:RemoveOverridePart(name)
641
+ local part = self:OverridePart(name, false)
642
+ if IsValid(part) then
643
+ self:RemoveBodyPart(part)
644
+ end
645
+ end
646
+
647
+ local composite_body_targets, composite_body_filters, composite_body_parts, composite_body_defs
648
+
649
+ function CompositeBody:OnEditorSetProperty(prop_id, old_value, ged)
650
+ local prop_meta = self:GetPropertyMetadata(prop_id) or empty_table
651
+ if prop_meta.body_part_match then
652
+ composite_body_targets = nil
653
+ end
654
+ if prop_meta.body_part_filter then
655
+ self:ComposeBodyParts()
656
+ end
657
+ return Object.OnEditorSetProperty(self, prop_id, old_value, ged)
658
+ end
659
+
660
+ ----
661
+ -- Editor only code:
662
+
663
+ local function UpdateItems()
664
+ if composite_body_targets then
665
+ return
666
+ end
667
+ composite_body_filters, composite_body_parts, composite_body_defs = {}, {}, {}
668
+ ClassDescendantsList("CompositeBody", function(class, def)
669
+ local target = def.composite_part_target or class
670
+
671
+ local filters = composite_body_filters[target] or {}
672
+ for _, prop in ipairs(def:GetProperties()) do
673
+ if prop.body_part_filter then
674
+ filters[prop.id] = filters[prop.id] or prop
675
+ end
676
+ end
677
+ composite_body_filters[target] = filters
678
+
679
+ local defs = composite_body_defs[target] or {}
680
+ if not defs[class] then
681
+ defs[class] = true
682
+ table.insert(defs, def)
683
+ end
684
+ composite_body_defs[target] = defs
685
+
686
+ local parts = composite_body_parts[target] or {}
687
+ for _, part in ipairs(def.composite_part_names) do
688
+ table.insert_unique(parts, part)
689
+ end
690
+ composite_body_parts[target] = parts
691
+ end, "")
692
+ composite_body_targets = table.keys2(composite_body_parts, true, "")
693
+ end
694
+
695
+ function GetBodyPartEntityItems()
696
+ local items = {}
697
+ for entity in pairs(GetAllEntities()) do
698
+ local data = EntityData[entity]
699
+ if data then
700
+ items[#items + 1] = entity
701
+ end
702
+ end
703
+ table.sort(items)
704
+ table.insert(items, 1, "")
705
+ return items
706
+ end
707
+
708
+ function GetBodyPartNameItems(preset)
709
+ UpdateItems()
710
+ return composite_body_parts[preset.Target]
711
+ end
712
+
713
+ function GetBodyPartNameCombo(preset)
714
+ local items = table.copy(GetBodyPartNameItems(preset) or empty_table)
715
+ table.insert(items, 1, "")
716
+ return items
717
+ end
718
+
719
+ function GetBodyPartTargetItems(preset)
720
+ UpdateItems()
721
+ return composite_body_targets
722
+ end
723
+
724
+ function EntityStatesCombo(entity, ...)
725
+ entity = entity or ""
726
+ if entity == "" then
727
+ return { ... }
728
+ end
729
+ local anims = GetStates(entity)
730
+ table.sort(anims)
731
+ table.insert(anims, 1, "")
732
+ return anims
733
+ end
734
+
735
+ function EntityStateMomentsCombo(entity, anim, ...)
736
+ entity = entity or ""
737
+ anim = anim or ""
738
+ if entity == "" or anim == "" then
739
+ return { ... }
740
+ end
741
+ local moments = GetStateMomentsNames(entity, anim)
742
+ table.insert(moments, 1, "")
743
+ return moments
744
+ end
745
+
746
+ ----
747
+
748
+ DefineClass.CompositeBodyPreset = {
749
+ __parents = { "Preset" },
750
+ properties = {
751
+ { id = "Target", name = "Target", editor = "choice", default = "", items = GetBodyPartTargetItems },
752
+ { id = "Parts", name = "Covered Parts", editor = "set", default = false, items = GetBodyPartNameItems },
753
+ { id = "CustomMatch", name = "Custom Match", editor = "bool", default = false, },
754
+ { id = "BodiesFound", name = "Bodies Found", editor = "text", default = "", dont_save = true, read_only = 0, lines = 1, max_lines = 3, no_edit = PropChecker("CustomMatch", true) },
755
+ { id = "Parent", name = "Parent Part", editor = "choice", default = false, items = GetBodyPartNameItems },
756
+ { id = "Entity", name = "Entity", editor = "choice", default = "", items = GetBodyPartEntityItems },
757
+ { id = "PartClass", name = "Custom Class", editor = "text", default = false, translate = false, validate = function(self) return self.PartClass and not g_Classes[self.PartClass] and "Invalid class" end },
758
+ { id = "AttachSpot", name = "Attach Spot", editor = "text", default = "", translate = false, help = "Force attach spot" },
759
+ { id = "Scale", name = "Scale", editor = "range", default = def_scale },
760
+ { id = "Axis", name = "Axis", editor = "point", default = false, help = "Force a specific axis" },
761
+ { id = "Angle", name = "Angle", editor = "number", default = false, scale = "deg", min = -180*60, max = 180*60, slider = true, help = "Force a specific angle" },
762
+ { id = "Mirrored", name = "Mirrored", editor = "bool", default = false },
763
+ { id = "SyncState", name = "Sync State", editor = "choice", default = "auto", items = {true, false, "auto"}, help = "Force sync state" },
764
+ { id = "ZOrder", name = "ZOrder", editor = "number", default = 0, },
765
+ { id = "Weight", name = "Weight", editor = "number", default = 1000, min = 0, scale = 10 },
766
+ { id = "FxActor", name = "Fx Actor", editor = "combo", default = "", items = ActorFXClassCombo },
767
+ { id = "Filters", name = "Filters", editor = "nested_list", default = false, base_class = "CompositeBodyPresetFilter", inclusive = true },
768
+ { id = "ColorInherit", name = "Color Inherit", editor = "choice", default = "", items = GetBodyPartNameCombo },
769
+ { id = "Colors", name = "Colors", editor = "nested_list", default = false, base_class = "CompositeBodyPresetColor", inclusive = true, no_edit = function(self) return self.ColorInherit ~= "" end },
770
+ { id = "Lights", name = "Lights", editor = "nested_list", default = false, base_class = "CompositeBodyPresetLight", inclusive = true },
771
+ { id = "AffectedBy", name = "Affected by", editor = "choice", default = "", items = GetBodyPartNameCombo },
772
+ { id = "EntityWhenAffected", name = "Entity when affected", editor = "choice", default = "", items = GetBodyPartEntityItems, no_edit = function(o) return not o.AffectedBy end },
773
+ { id = "AttachOffset", name = "Attach Offset", editor = "point", default = point30, },
774
+ { id = "AttachAxis", name = "Attach Axis", editor = "point", default = axis_z, },
775
+ { id = "AttachAngle", name = "Attach Angle", editor = "number", default = 0, scale = "deg", min = -180*60, max = 180*60, slider = true },
776
+
777
+ { id = "ApplyAnim", name = "Apply Anim", editor = "choice", default = "", items = function(self) return EntityStatesCombo(self.AnimTestEntity, "") end },
778
+ { id = "UnapplyAnim", name = "Unapply Anim", editor = "choice", default = "", items = function(self) return EntityStatesCombo(self.AnimTestEntity, "") end },
779
+ { id = "ApplyAnimMoment", name = "Apply Anim Moment", editor = "choice", default = "hit", items = function(self) return EntityStateMomentsCombo(self.AnimTestEntity, self.ApplyAnim, "", "hit") end, },
780
+ { id = "AnimTestEntity", name = "Anim Test Entity", editor = "text", default = false },
781
+ },
782
+ GlobalMap = "CompositeBodyPresets",
783
+ EditorMenubar = "Editors.Art",
784
+ EditorMenubarName = "Composite Body Parts",
785
+ EditorIcon = "CommonAssets/UI/Icons/atom molecule science.png",
786
+
787
+ StoreAsTable = false,
788
+ }
789
+
790
+ CompositeBodyPreset.Documentation = [[The composite body system is a matching system for attaching parts to a body.
791
+
792
+ A body collects its potential parts not from all part presets, but from a specified preset <style GedHighlight>Group</style>. The matched parts are those having the same <style GedHighlight>Target</style> property as the body target property.
793
+
794
+ If no matching information is specified in the body, then its class name is used instead for all matching.
795
+
796
+ Each part can contain filters for additional conditions during the matching process.
797
+
798
+ Each part covers a specific named location on the body specified by <style GedHighlight>Covered Parts</style> property. If several parts are matched for the same location, a single one is chosen based on the <style GedHighlight>ZOrder</style> property. If there are still multiple parts with equal ZOrder, then a part is randomly selected based on the <style GedHighlight>Weight</style> property.]]
799
+
800
+ function CompositeBodyPreset:GetError()
801
+ if self.CustomMatch then
802
+ return
803
+ end
804
+ local parts = self.Parts
805
+ if not next(parts) then
806
+ return "No covered parts specified!"
807
+ end
808
+ UpdateItems()
809
+ local defs = composite_body_defs[self.Target]
810
+ if not defs then
811
+ return string.format("No composite bodies found with target '%s'", self.Target)
812
+ end
813
+ local group = self.group
814
+ local count_group = 0
815
+ local count_part = 0
816
+ for _, def in ipairs(defs) do
817
+ local composite_part_groups = def.composite_part_groups or { def.class }
818
+ if table.find(composite_part_groups, group) then
819
+ count_group = count_group + 1
820
+ for _, part_name in ipairs(def.composite_part_names) do
821
+ if parts[part_name] then
822
+ count_part = count_part + 1
823
+ break
824
+ end
825
+ end
826
+ end
827
+ end
828
+ if count_group == 0 then
829
+ return string.format("No composite bodies found with group '%s'", tostring(group))
830
+ end
831
+ if count_part == 0 then
832
+ return string.format("No composite bodies found with parts %s", table.concat(table.keys(parts, true)))
833
+ end
834
+ end
835
+
836
+ function CompositeBodyPreset:GetBodiesFound()
837
+ UpdateItems()
838
+ local parts = self.Parts
839
+ if not next(parts) then
840
+ return 0
841
+ end
842
+ local found = {}
843
+ for _, def in ipairs(composite_body_defs[self.Target]) do
844
+ local composite_part_groups = def.composite_part_groups or { def.class }
845
+ if table.find(composite_part_groups, self.group) then
846
+ for _, part_name in ipairs(def.composite_part_names) do
847
+ if parts[part_name] then
848
+ found[def.class] = true
849
+ break
850
+ end
851
+ end
852
+ end
853
+ end
854
+ return table.concat(table.keys(found, true), ", ")
855
+ end
856
+
857
+ function CompositeBodyPreset:OnEditorSetProperty(prop_id, old_value, ged)
858
+ if prop_id == "Entity" then
859
+ for _, obj in ipairs(self.Colors) do
860
+ ObjModified(obj) -- properties for modifiable colors have changed
861
+ end
862
+ end
863
+ end
864
+
865
+ local function FindParentPreset(obj, member)
866
+ return GetParentTableOfKind(obj, "CompositeBodyPreset")
867
+ end
868
+
869
+ function OnMsg.ClassesGenerate()
870
+ DefineModItemPreset("CompositeBodyPreset", {
871
+ EditorSubmenu = "Other",
872
+ EditorName = "Composite body",
873
+ EditorShortcut = false,
874
+ })
875
+ end
876
+
877
+ ----
878
+
879
+ local function GetBodyFilters(filter)
880
+ UpdateItems()
881
+ local parent = FindParentPreset(filter)
882
+ local props = parent and composite_body_filters[parent.Target]
883
+ if not props then
884
+ return {}
885
+ end
886
+ local filters = {}
887
+ for _, def in ipairs(composite_body_defs[parent.Target]) do
888
+ for name, prop in pairs(props) do
889
+ local items
890
+ if prop.items then
891
+ items = prop_eval(prop.items, def, prop)
892
+ elseif prop.preset_class then
893
+ local filter = prop.preset_filter
894
+ items = {}
895
+ ForEachPreset(prop.preset_class, function(preset, group, items)
896
+ if not filter or filter(preset) then
897
+ items[#items + 1] = preset.id
898
+ end
899
+ end, items)
900
+ table.sort(items)
901
+ end
902
+ if items and #items > 0 then
903
+ local prev_filters = filters[name]
904
+ if not prev_filters then
905
+ filters[name] = items
906
+ else
907
+ for _, value in ipairs(items) do
908
+ table.insert_unique(prev_filters, value)
909
+ end
910
+ end
911
+ end
912
+ end
913
+ end
914
+ return filters
915
+ end
916
+
917
+ local function GetFilterNameItems(filter)
918
+ local filters = GetBodyFilters(filter)
919
+ local items = filters and table.keys(filters, true)
920
+ if items[1] ~= "" then
921
+ table.insert(items, 1, "")
922
+ end
923
+ return items
924
+ end
925
+
926
+ local function GetFilterValueItems(filter)
927
+ local filters = GetBodyFilters(filter)
928
+ return filters and filters[filter.Name] or {""}
929
+ end
930
+
931
+ DefineClass.CompositeBodyPresetFilter = {
932
+ __parents = { "PropertyObject" },
933
+ properties = {
934
+ { id = "Name", name = "Name", editor = "choice", default = "", items = GetFilterNameItems, },
935
+ { id = "Value", name = "Value", editor = "choice", default = "", items = GetFilterValueItems, },
936
+ { id = "Test", name = "Test", editor = "choice", default = "=", items = {"=", ">", "<"}, },
937
+ },
938
+ EditorView = Untranslated("<Name> <Test> <Value>"),
939
+ }
940
+
941
+ function CompositeBodyPresetFilter:Match(obj)
942
+ local obj_value, value, test = obj[self.Name], self.Value, self.Test
943
+ if test == '=' then
944
+ return obj_value == value
945
+ elseif test == '>' then
946
+ return obj_value > value
947
+ elseif test == '<' then
948
+ return obj_value < value
949
+ end
950
+ end
951
+
952
+ ----
953
+
954
+ DefineClass.CompositeBodyPresetColor = {
955
+ __parents = { "ColorizationPropSet" },
956
+ properties = {
957
+ { id = "Weight", name = "Weight", editor = "number", default = 1000, min = 0, scale = 10 },
958
+ },
959
+ }
960
+
961
+ function CompositeBodyPresetColor:GetMaxColorizationMaterials()
962
+ PopulateParentTableCache(self)
963
+ if not ParentTableCache[self] then
964
+ return ColorizationPropSet.GetMaxColorizationMaterials(self)
965
+ end
966
+ local parent = FindParentPreset(self)
967
+ return parent and ColorizationMaterialsCount(parent.Entity) or 0
968
+ end
969
+
970
+ function CompositeBodyPresetColor:GetError()
971
+ if self:GetMaxColorizationMaterials() == 0 then
972
+ local parent = FindParentPreset(self)
973
+ if not parent or parent.Entity == "" then
974
+ return "The composite body entity is not set."
975
+ else
976
+ return "There are no modifiable colors in the composite body entity."
977
+ end
978
+ end
979
+ end
980
+
981
+ ----
982
+
983
+ local light_props = {}
984
+ function OnMsg.ClassesBuilt()
985
+ local function RegisterProps(class, classdef)
986
+ local props = {}
987
+ for _, prop in ipairs(classdef:GetProperties()) do
988
+ if prop.category == "Visuals"
989
+ and not prop_eval(prop.no_edit, classdef, prop)
990
+ and not prop_eval(prop.read_only, classdef, prop) then
991
+ props[#props + 1] = prop
992
+ props[prop.id] = classdef:GetDefaultPropertyValue(prop.id, prop)
993
+ end
994
+ end
995
+ light_props[class] = props
996
+ end
997
+ RegisterProps("Light", Light)
998
+ ClassDescendants("Light", RegisterProps)
999
+ end
1000
+
1001
+ function OnMsg.GatherFXActors(list)
1002
+ for _, preset in pairs(CompositeBodyPresets) do
1003
+ if (preset.FxActor or "") ~= "" then
1004
+ list[#list + 1] = preset.FxActor
1005
+ end
1006
+ end
1007
+ end
1008
+
1009
+ function OnMsg.DataLoaded()
1010
+ PopulateParentTableCache(Presets.CompositeBodyPreset)
1011
+ end
1012
+
1013
+ local function GetEntitySpotsItems(light)
1014
+ local parent = FindParentPreset(light)
1015
+ local entity = parent and parent.Entity or ""
1016
+ local states = IsValidEntity(entity) and GetStates(entity) or ""
1017
+ if #states == 0 then return empty_table end
1018
+ local idx = table.find(states, "idle")
1019
+ local spots = {}
1020
+ local spbeg, spend = GetAllSpots(entity, states[idx] or states[1])
1021
+ for spot = spbeg, spend do
1022
+ spots[GetSpotName(entity, spot)] = true
1023
+ end
1024
+ return table.keys(spots, true)
1025
+ end
1026
+
1027
+ DefineClass.CompositeBodyPresetLight = {
1028
+ __parents = { "PropertyObject" },
1029
+ properties = {
1030
+ { id = "LightType", name = "Light Type", editor = "choice", default = "Light", items = ToCombo(light_props) },
1031
+ { id = "LightSpot", name = "Light Spot", editor = "combo", default = "Origin", items = GetEntitySpotsItems },
1032
+ { id = "LightSIEnable", name = "SI Apply", editor = "bool", default = true },
1033
+ { id = "LightSIModulation", name = "SI Modulation", editor = "number", default = 255, min = 0, max = 255, slider = true, no_edit = function(self) return not self.LightSIEnable end },
1034
+ { id = "night_mode", name = "Night mode", editor = "dropdownlist", items = { "Off", "On" }, default = "On" },
1035
+ { id = "day_mode", name = "Day mode", editor = "dropdownlist", items = { "Off", "On" }, default = "Off" },
1036
+ },
1037
+ EditorView = Untranslated("<LightType>: <LightSpot>"),
1038
+ }
1039
+
1040
+ function CompositeBodyPresetLight:GetError()
1041
+ if not light_props[self.LightType] then
1042
+ return "Invalid light type selected!"
1043
+ end
1044
+ end
1045
+
1046
+ function CompositeBodyPresetLight:ApplyToLight(light)
1047
+ local props = light_props[self.LightType] or empty_table
1048
+ for _, prop in ipairs(props) do
1049
+ local prop_id = prop.id
1050
+ local prop_value = rawget(self, prop_id)
1051
+ if prop_value ~= nil then
1052
+ light:SetProperty(prop_id, prop_value)
1053
+ end
1054
+ end
1055
+ end
1056
+
1057
+ function CompositeBodyPresetLight:GetProperties()
1058
+ local props = table.icopy(self.properties)
1059
+ table.iappend(props, light_props[self.LightType] or empty_table)
1060
+ return props
1061
+ end
1062
+
1063
+ function CompositeBodyPresetLight:GetDefaultPropertyValue(prop_id, prop_meta)
1064
+ local def = table.get(light_props, self.LightType, prop_id)
1065
+ if def ~= nil then
1066
+ return def
1067
+ end
1068
+ return PropertyObject.GetDefaultPropertyValue(self, prop_id, prop_meta)
1069
+ end
1070
+
1071
+ DefineClass.BaseLightObject = {
1072
+ __parents = { "Object" },
1073
+ }
1074
+
1075
+ function BaseLightObject:UpdateLight(lm, delayed)
1076
+ end
1077
+
1078
+ function BaseLightObject:GameInit()
1079
+ Game:AddToLabel("Lights", self)
1080
+ end
1081
+
1082
+ function BaseLightObject:Done()
1083
+ Game:RemoveFromLabel("Lights", self)
1084
+ end
1085
+
1086
+ if FirstLoad then
1087
+ UpdateLightsThread = false
1088
+ end
1089
+
1090
+ function OnMsg.DoneMap()
1091
+ UpdateLightsThread = false
1092
+ end
1093
+
1094
+ function UpdateLights(lm, delayed)
1095
+ local lights = table.get(Game, "labels", "Lights")
1096
+ for _, obj in ipairs(lights) do
1097
+ obj:UpdateLight(lm, delayed)
1098
+ end
1099
+ end
1100
+
1101
+ function UpdateLightsDelayed(lm, delayed_time)
1102
+ DeleteThread(UpdateLightsThread)
1103
+ UpdateLightsThread = false
1104
+ if delayed_time > 0 then
1105
+ UpdateLightsThread = CreateGameTimeThread(function(lm, delayed_time)
1106
+ Sleep(delayed_time)
1107
+ UpdateLights(lm, true)
1108
+ UpdateLightsThread = false
1109
+ end, lm, delayed_time)
1110
+ else
1111
+ UpdateLights(lm)
1112
+ end
1113
+ end
1114
+
1115
+ function OnMsg.LightmodelChange(view, lm, time)
1116
+ UpdateLightsDelayed(lm, time/2)
1117
+ end
1118
+
1119
+ function OnMsg.GatherAllLabels(labels)
1120
+ labels.Lights = true
1121
+ end
1122
+
1123
+ DefineClass.CompositeLightObject = {
1124
+ __parents = { "CompositeBody", "BaseLightObject" },
1125
+
1126
+ light_parts = false,
1127
+ light_objs = false,
1128
+ }
1129
+
1130
+ function CompositeLightObject:ComposeBodyParts(seed)
1131
+ self.light_parts = nil
1132
+
1133
+ local changed = CompositeBody.ComposeBodyParts(self, seed)
1134
+
1135
+ local light_parts = self.light_parts
1136
+ local light_objs = self.light_objs
1137
+ for i = #(light_objs or ""),1,-1 do
1138
+ local config = light_objs[i]
1139
+ local part = light_parts and light_parts[config]
1140
+ if not part then
1141
+ DoneObject(light_objs[config])
1142
+ light_objs[config] = nil
1143
+ table.remove_value(light_objs, config)
1144
+ end
1145
+ end
1146
+ for _, config in ipairs(light_parts) do
1147
+ light_objs = light_objs or {}
1148
+ if light_objs[config] == nil then
1149
+ light_objs[config] = false
1150
+ light_objs[#light_objs + 1] = config
1151
+ end
1152
+ end
1153
+ self.light_objs = light_objs
1154
+
1155
+ return changed
1156
+ end
1157
+
1158
+ function CompositeLightObject:ApplyBodyPart(part, preset, ...)
1159
+ local light_parts = self.light_parts
1160
+ for _, config in ipairs(preset.Lights) do
1161
+ light_parts = light_parts or {}
1162
+ light_parts[config] = part
1163
+ light_parts[#light_parts + 1] = config
1164
+ end
1165
+ self.light_parts = light_parts
1166
+
1167
+ return CompositeBody.ApplyBodyPart(self, part, preset, ...)
1168
+ end
1169
+
1170
+ function CompositeLightObject:IsBodyPartLightOn(config)
1171
+ local mode = GameState.Night and config.night_mode or config.day_mode
1172
+ return mode == "On"
1173
+ end
1174
+
1175
+ function CompositeLightObject:UpdateLight(delayed)
1176
+ local light_objs = self.light_objs or empty_table
1177
+ local IsBodyPartLightOn = self.IsBodyPartLightOn
1178
+ for _, config in ipairs(light_objs) do
1179
+ local light = light_objs[config]
1180
+ local part = self.light_parts[config]
1181
+ local turned_on = IsBodyPartLightOn(self, config)
1182
+ if turned_on and not light then
1183
+ light = PlaceObject(config.LightType)
1184
+ config:ApplyToLight(light)
1185
+ part:Attach(light, GetSpotBeginIndex(part, config.LightSpot))
1186
+ light_objs[config] = light
1187
+ elseif not turned_on and light then
1188
+ DoneObject(light)
1189
+ light_objs[config] = false
1190
+ end
1191
+ if config.LightSIEnable then
1192
+ part:SetSIModulation(turned_on and config.LightSIModulation or 0)
1193
+ end
1194
+ end
1195
+ end
1196
+
1197
+ ----
1198
+
1199
+ DefineClass.BlendedCompositeBody = {
1200
+ __parents = { "CompositeBody", "Object" },
1201
+ composite_part_blend = false,
1202
+
1203
+ blended_body_parts_params = false,
1204
+ blended_body_parts = false,
1205
+ }
1206
+
1207
+ function BlendedCompositeBody:Init()
1208
+ self.blended_body_parts_params = { }
1209
+ self.blended_body_parts = { }
1210
+ end
1211
+
1212
+ function BlendedCompositeBody:ForceComposeBlendedBodyParts()
1213
+ self.blended_body_parts_params = { }
1214
+ self.blended_body_parts = { }
1215
+ self:ComposeBodyParts()
1216
+ end
1217
+
1218
+ function BlendedCompositeBody:ForceRevertBlendedBodyParts()
1219
+ if next(self.attached_parts) then
1220
+ local part_to_preset = {}
1221
+ self:CollectBodyParts(part_to_preset)
1222
+ for name,preset in sorted_pairs(part_to_preset) do
1223
+ local part = self.attached_parts[name]
1224
+ local entity = preset.Entity
1225
+ if IsValid(part) and IsValidEntity(entity) then
1226
+ Msg("RevertBlendedBodyPart", part)
1227
+ part:ChangeEntity(entity)
1228
+ end
1229
+ end
1230
+ end
1231
+ end
1232
+
1233
+ function BlendedCompositeBody:UpdateBlendPartParams(params, part, preset, name, seed)
1234
+ return part:GetEntity()
1235
+ end
1236
+
1237
+ function BlendedCompositeBody:ShouldBlendPart(params, part, preset, name, seed)
1238
+ return false
1239
+ end
1240
+
1241
+ if FirstLoad then
1242
+ g_EntityBlendLocks = { }
1243
+ --g_EntityBlendLog = { }
1244
+ end
1245
+
1246
+ local function BlendedEntityLocksGet(entity_name)
1247
+ return g_EntityBlendLocks[entity_name] or 0
1248
+ end
1249
+
1250
+ function BlendedEntityIsLocked(entity_name)
1251
+ --table.insert(g_EntityBlendLog, GameTime() .. " lock " .. entity_name)
1252
+ return BlendedEntityLocksGet(entity_name) > 0
1253
+ end
1254
+
1255
+ function BlendedEntityLock(entity_name)
1256
+ --table.insert(g_EntityBlendLog, GameTime() .. " unlock " .. entity_name)
1257
+ g_EntityBlendLocks[entity_name] = BlendedEntityLocksGet(entity_name) + 1
1258
+ end
1259
+
1260
+ function BlendedEntityUnlock(entity_name)
1261
+ local locks_count = BlendedEntityLocksGet(entity_name)
1262
+ assert(locks_count >= 1, "Unlocking a blended entity that isn't locked")
1263
+ if locks_count > 1 then
1264
+ g_EntityBlendLocks[entity_name] = locks_count - 1
1265
+ else
1266
+ g_EntityBlendLocks[entity_name] = nil
1267
+ end
1268
+ end
1269
+
1270
+ function WaitBlendEntityLocks(obj, entity_name)
1271
+ while BlendedEntityIsLocked(entity_name) do
1272
+ if obj and not IsValid(obj) then
1273
+ return false
1274
+ end
1275
+ WaitNextFrame(1)
1276
+ end
1277
+
1278
+ return true
1279
+ end
1280
+
1281
+ function BlendedCompositeBody:BlendEntity(t, e1, e2, e3, w1, w2, w3, m2, m3)
1282
+ --table.insert(g_EntityBlendLog, GameTime() .. " " .. self.class .. " blend " .. t)
1283
+ assert(BlendedEntityIsLocked(t), "To blend an entity you must lock it using BlendedEntityLock")
1284
+ assert(t ~= e1 and t ~= e2 and t ~= e3)
1285
+
1286
+ SetMaterialBlendMaterials(
1287
+ GetEntityIdleMaterial(t), --target
1288
+ GetEntityIdleMaterial(e1), --base
1289
+ m2, GetEntityIdleMaterial(e2), --weight 1, material
1290
+ m3, GetEntityIdleMaterial(e3)) --weight 2, material
1291
+ WaitNextFrame(1)
1292
+
1293
+ local err = AsyncOpWait(nil, nil, "AsyncMeshBlend",
1294
+ t, 0, --target, LOD
1295
+ e1, w1, --entity 1, weight
1296
+ e2, w2, --entity 2, weight
1297
+ e3, w3) --entity 3, weight
1298
+ if err then print("Failed to blend meshes: ", err) end
1299
+ end
1300
+
1301
+ function BlendedCompositeBody:AsyncBlendEntity(obj, t, e1, e2, e3, w1, w2, w3, m2, m3, callback)
1302
+ return CreateRealTimeThread(function(self, obj, t, e1, e2, e3, w1, w2, w3, m2, m3, callback)
1303
+ WaitBlendEntityLocks(obj, t)
1304
+ BlendedEntityLock(t)
1305
+
1306
+ self:BlendEntity(t, e1, e2, e3, w1, w2, w3, m2, m3)
1307
+
1308
+ if callback then
1309
+ callback(self, obj, t, e1, e2, e3, w1, w2, w3, m2, m3)
1310
+ end
1311
+
1312
+ BlendedEntityUnlock(t)
1313
+ end, self, obj, t, e1, e2, e3, w1, w2, w3, m2, m3, callback)
1314
+ end
1315
+
1316
+ function BlendedCompositeBody:ApplyBlendBodyPart(blended_entity, part, preset, name, seed)
1317
+ return CompositeBody.ApplyBodyPart(self, preset, name, seed)
1318
+ end
1319
+
1320
+ function BlendedCompositeBody:BlendBodyPartFailed(blended_entity, part, preset, name, seed)
1321
+ return CompositeBody.ApplyBodyPart(self, part, preset, name, seed)
1322
+ end
1323
+
1324
+ -- if the body part is declared as "to be blended"
1325
+ function BlendedCompositeBody:IsBlendBodyPart(name)
1326
+ return self.composite_part_blend and self.composite_part_blend[name]
1327
+ end
1328
+
1329
+ -- if the body part is using a blended entity or is being blended at the moment
1330
+ function BlendedCompositeBody:IsCurrentlyBlendedBodyPart(name)
1331
+ return self.blended_body_parts and self.blended_body_parts[name]
1332
+ end
1333
+
1334
+ function BlendedCompositeBody:ColorizeBodyPart(part, preset, name, seed)
1335
+ if self:IsCurrentlyBlendedBodyPart(name) then
1336
+ return
1337
+ end
1338
+ return CompositeBody.ColorizeBodyPart(self, part, preset, name, seed)
1339
+ end
1340
+
1341
+ function BlendedCompositeBody:ChangeBodyPartEntity(part, preset, name)
1342
+ if self:IsCurrentlyBlendedBodyPart(name) then
1343
+ return
1344
+ end
1345
+ return CompositeBody.ChangeBodyPartEntity(self, part, preset, name)
1346
+ end
1347
+
1348
+ function BlendedCompositeBody:ApplyBodyPart(part, preset, name, seed)
1349
+ if self:IsBlendBodyPart(name) then
1350
+ self.blended_body_parts_params = self.blended_body_parts_params or { }
1351
+ local params = self.blended_body_parts_params[name]
1352
+ if not params or self:ShouldBlendPart(params, part, preset, name, seed) then
1353
+ params = params or { }
1354
+ local blended_entity = self:UpdateBlendPartParams(params, part, preset, name, seed)
1355
+ if IsValidEntity(blended_entity) then
1356
+ self.blended_body_parts_params[name] = params
1357
+ self.blended_body_parts[name] = (self.blended_body_parts[name] or 0) + 1
1358
+ return self:ApplyBlendBodyPart(blended_entity, part, preset, name, seed)
1359
+ else
1360
+ self.blended_body_parts[name] = nil
1361
+ return self:BlendBodyPartFailed(blended_entity, part, preset, name, seed)
1362
+ end
1363
+ end
1364
+ end
1365
+
1366
+ return CompositeBody.ApplyBodyPart(self, part, preset, name, seed)
1367
+ end
1368
+
1369
+ function BlendedCompositeBody:RemoveBodyPart(part, name)
1370
+ if self:IsBlendBodyPart(name) and self.blended_body_parts_params then
1371
+ self.blended_body_parts_params[name] = nil
1372
+ end
1373
+ return CompositeBody.RemoveBodyPart(self, part, name)
1374
+ end
1375
+
1376
+ function ForceRecomposeAllBlendedBodies()
1377
+ local objs = MapGet("map", "BlendedCompositeBody")
1378
+ for i,obj in ipairs(objs) do
1379
+ obj:ForceRevertBlendedBodyParts()
1380
+ end
1381
+ for i,obj in ipairs(objs) do
1382
+ obj:ForceComposeBlendedBodyParts()
1383
+ end
1384
+ end
1385
+
1386
+ function OnMsg.PostLoadGame()
1387
+ ForceRecomposeAllBlendedBodies()
1388
+ end
1389
+
1390
+ function OnMsg.AdditionalEntitiesLoaded()
1391
+ if type(__cobjectToCObject) ~= "table" then return end
1392
+ ForceRecomposeAllBlendedBodies()
1393
+ end
1394
+
1395
+ local body_to_states
1396
+ function CompositeBodyAnims(classdef)
1397
+ local id = classdef.id or classdef.class
1398
+ body_to_states = body_to_states or {}
1399
+ local states = body_to_states[id]
1400
+ if not states then
1401
+ local entity = ResolveTemplateEntity(classdef)
1402
+ states = IsValidEntity(entity) and GetStates(entity) or empty_table
1403
+ table.sort(states)
1404
+ body_to_states[id] = states
1405
+ end
1406
+ return states
1407
+ end
1408
+
1409
+ function SavegameFixups.BlendedBodyPartsList()
1410
+ MapForEach(true, "BlendedCompositeBody", function(obj)
1411
+ obj.blended_body_parts = {}
1412
+ end)
1413
+ end
1414
+
1415
+ function SavegameFixups.BlendedBodyBlendIDs()
1416
+ MapForEach(true, "BlendedCompositeBody", function(obj)
1417
+ for name in pairs(obj.blended_body_parts) do
1418
+ obj.blended_body_parts[name] = 1
1419
+ end
1420
+ end)
1421
+ end
1422
+
1423
+ function SavegameFixups.FixSyncStateFlag2()
1424
+ MapForEach(true, "CompositeBody", "Building", function(obj)
1425
+ obj:ClearGameFlags(const.gofSyncState)
1426
+ obj:SetGameFlags(const.gofPropagateState)
1427
+ end)
1428
+ end
CommonLua/Classes/Context.lua ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --- An object which can resolve a key to a value.
2
+ -- Context objects can be nested to create a complex value resolution structure.
3
+ -- The global function ResolveValue allows resolving a tuple to a value in an arbitrary context.
4
+
5
+ DefineClass.Context = {
6
+ __parents = {},
7
+ __hierarchy_cache = true,
8
+ }
9
+
10
+ function Context:new(obj)
11
+ return setmetatable(obj or {}, self)
12
+ end
13
+
14
+ function Context:ResolveValue(key)
15
+ local value = rawget(self, key)
16
+ if value ~= nil then return value end
17
+ for _, sub_context in ipairs(self) do
18
+ value = ResolveValue(sub_context, key)
19
+ if value ~= nil then return value end
20
+ end
21
+ end
22
+
23
+ -- change __index method to allow full member resolution without warning
24
+ function OnMsg.ClassesBuilt()
25
+ local context_class = g_Classes.Context
26
+ context_class.__index = function (self, key)
27
+ if type(key) == "string" then
28
+ return rawget(context_class, key) or context_class.ResolveValue(self, key)
29
+ end
30
+ end
31
+ end
32
+
33
+ function Context:IsKindOf(class)
34
+ if IsKindOf(self, class) then return true end
35
+ for _, sub_context in ipairs(self) do
36
+ if IsKindOf(sub_context, "Context") and sub_context:IsKindOf(class) or IsKindOf(sub_context, class) then
37
+ return true
38
+ end
39
+ end
40
+ end
41
+
42
+ function Context:IsKindOfClasses(...)
43
+ if IsKindOfClasses(self, ...) then return true end
44
+ for _, sub_context in ipairs(self) do
45
+ if IsKindOf(sub_context, "Context") and sub_context:IsKindOfClasses(...) or IsKindOfClasses(sub_context, ...) then
46
+ return true
47
+ end
48
+ end
49
+ end
50
+
51
+ function ForEachObjInContext(context, f, ...)
52
+ if not context then return end
53
+ if IsKindOf(context, "Context") then
54
+ for _, sub_context in ipairs(context) do
55
+ ForEachObjInContext(sub_context, f, ...)
56
+ end
57
+ else
58
+ f(context, ...)
59
+ end
60
+ end
61
+
62
+ function SubContext(context, t)
63
+ assert(not IsKindOf(t, "PropertyObject"))
64
+ t = t or {}
65
+ if IsKindOf(context, "PropertyObject") or type(context) ~= "table" then
66
+ t[#t + 1] = context
67
+ elseif type(context) == "table" then
68
+ for _, obj in ipairs(context) do
69
+ t[#t + 1] = obj
70
+ end
71
+ for k, v in pairs(context) do
72
+ if rawget(t, k) == nil then
73
+ t[k] = v
74
+ end
75
+ end
76
+ end
77
+ return Context:new(t)
78
+ end
79
+
80
+ function ResolveValue(context, key, ...)
81
+ if key == nil then return context end
82
+ if type(context) == "table" then
83
+ if IsKindOfClasses(context, "Context", "PropertyObject") then
84
+ return ResolveValue(context:ResolveValue(key), ...)
85
+ end
86
+ return ResolveValue(rawget(context, key), ...)
87
+ end
88
+ end
89
+
90
+ function ResolveFunc(context, key)
91
+ if key == nil then return end
92
+ if type(context) == "table" then
93
+ if IsKindOf(context, "Context") then
94
+ local f = rawget(context, key)
95
+ if type(f) == "function" then
96
+ return f
97
+ end
98
+ for _, sub_context in ipairs(context) do
99
+ local f, obj = ResolveFunc(sub_context, key)
100
+ if f ~= nil then return f, obj end
101
+ end
102
+ return
103
+ end
104
+ if IsKindOf(context, "PropertyObject") and context:HasMember(key) then
105
+ local f = context[key]
106
+ if type(f) == "function" then return f, context end
107
+ else
108
+ local f = rawget(context, key)
109
+ if f == false or type(f) == "function" then return f end
110
+ end
111
+ end
112
+ end
113
+
114
+ function ResolvePropObj(context)
115
+ if IsKindOf(context, "PropertyObject") then
116
+ return context
117
+ end
118
+ if IsKindOf(context, "Context") then
119
+ for _, sub_context in ipairs(context) do
120
+ local obj = ResolvePropObj(sub_context)
121
+ if obj then return obj end
122
+ end
123
+ end
124
+ end
125
+
CommonLua/Classes/ContinuousEffect.lua ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ local hintColor = RGB(210, 255, 210)
2
+
3
+
4
+ ----- ContinuousEffect
5
+
6
+ DefineClass.ContinuousEffect = {
7
+ __parents = { "Effect" },
8
+ properties = {
9
+ { id = "Id", editor = "text", help = "A unique Id allowing you to later stop this effect using StopEffect/StopGlobalEffect; optional", default = "",
10
+ no_edit = function(obj) return obj.Id:starts_with("autoid") end,
11
+ },
12
+ },
13
+ CreateInstance = false,
14
+ EditorExcludeAsNested = true,
15
+ container = false, -- restored in ModifiersPreset:PostLoad(), won't be valid if the ContinuousEffect is not stored in a ModifiersPreset
16
+ }
17
+
18
+ function ContinuousEffect:Execute(object, ...)
19
+ self:ValidateObject(object)
20
+ assert(IsKindOf(object, "ContinuousEffectContainer"))
21
+ object:StartEffect(self, ...)
22
+ end
23
+
24
+ if FirstLoad then
25
+ g_MaxContinuousEffectId = 0
26
+ end
27
+
28
+ function ContinuousEffect:OnEditorNew(parent, ged, is_paste)
29
+ -- ContinuousEffects embedded in a parent ContinuousEffect are managed by
30
+ -- the parent effect and have an auto-generated internal uneditable Id
31
+ local obj = ged:GetParentOfKind(parent, "PropertyObject")
32
+ if obj and (obj:IsKindOf("ContinuousEffect") or obj:HasMember("ManagesContinuousEffects") and obj.ManagesContinuousEffects) then
33
+ g_MaxContinuousEffectId = g_MaxContinuousEffectId + 1
34
+ self.Id = "autoid" .. tostring(g_MaxContinuousEffectId)
35
+ elseif self.Id:starts_with("autoid") then
36
+ self.Id = ""
37
+ elseif ged.app_template:starts_with("Mod") then
38
+ local mod_item = IsKindOf(parent, "ModItem") and parent or ged:GetParentOfKind(parent, "ModItem")
39
+ local mod_def = mod_item.mod
40
+ self.Id = mod_def:GenerateModItemId(self)
41
+ end
42
+ self.container = obj
43
+ end
44
+
45
+ function ContinuousEffect:__fromluacode(table)
46
+ local obj = Effect.__fromluacode(self, table)
47
+ local id = obj.Id
48
+ if id:starts_with("autoid") then
49
+ g_MaxContinuousEffectId = Max(g_MaxContinuousEffectId, tonumber(id:sub(7, -1)))
50
+ end
51
+ return obj
52
+ end
53
+
54
+ function ContinuousEffect:__toluacode(...)
55
+ local old = self.container
56
+ self.container = nil -- restored in ModifiersPreset:PostLoad()
57
+ local ret = Effect.__toluacode(self, ...)
58
+ self.container = old
59
+ return ret
60
+ end
61
+
62
+
63
+ ----- ContinuousEffectDef
64
+
65
+ DefineClass.ContinuousEffectDef = {
66
+ __parents = { "EffectDef" },
67
+ group = "ContinuousEffects",
68
+ DefParentClassList = { "ContinuousEffect" },
69
+ GedEditor = "ClassDefEditor",
70
+ }
71
+
72
+ function ContinuousEffectDef:OnEditorNew(parent, ged, is_paste)
73
+ if is_paste then return end
74
+
75
+ -- remove Execute/__exec metod
76
+ for i = #self, 1, -1 do
77
+ if IsKindOf(self[i], "ClassMethodDef") and (self[i].name == "Execute" and self[i].name == "__exec" )then
78
+ table.remove(self, i)
79
+ break
80
+ end
81
+ end
82
+ -- add CreateInstance, Start, Stop, and Id
83
+ local idx = #self + 1
84
+ self[idx] = self[idx] or ClassMethodDef:new{ name = "OnStart", params = "obj, context"}
85
+ idx = idx + 1
86
+ self[idx] = self[idx] or ClassMethodDef:new{ name = "OnStop", params = "obj, context"}
87
+ table.insert(self, 1, ClassConstDef:new{ id = "CreateInstance", name = "CreateInstance" , type = "bool", })
88
+ end
89
+
90
+ function ContinuousEffectDef:CheckExecMethod()
91
+ local start = self:FindSubitem("Start")
92
+ local stop = self:FindSubitem("Stop")
93
+ if start and (start.class ~= "ClassMethodDef" or start.code == ClassMethodDef.code) or
94
+ stop and (stop.class ~= "ClassMethodDef" or stop.code == ClassMethodDef.code) then
95
+ return {[[--== Start & Stop ==--
96
+ Add Start and Stop methods that implement the effect.
97
+ ]], hintColor, table.find(self, start), table.find(self, stop) }
98
+ end
99
+ end
100
+
101
+ function ContinuousEffectDef:GetError()
102
+ local id = self:FindSubitem("CreateInstance")
103
+ if not id then
104
+ return "The CreateInstance constant is required for ContinuousEffects."
105
+ end
106
+ end
107
+
108
+
109
+ ----- ContinuousEffectContainer
110
+
111
+ DefineClass.ContinuousEffectContainer = {
112
+ __parents = {"InitDone"},
113
+ effects = false,
114
+ }
115
+
116
+ function ContinuousEffectContainer:Done()
117
+ for _, effect in ipairs(self.effects or empty_table) do
118
+ effect:OnStop(self)
119
+ end
120
+ self.effects = false
121
+ end
122
+
123
+ function ContinuousEffectContainer:StartEffect(effect, context)
124
+ self.effects = self.effects or {}
125
+
126
+ local id = effect.Id or ""
127
+ if id == "" then
128
+ id = effect
129
+ end
130
+ if self.effects[id] then
131
+ -- TODO: Add an AllowReplace property and assert whether AllowReplace is true?
132
+ self:StopEffect(id)
133
+ end
134
+ if effect.CreateInstance then
135
+ effect = effect:Clone()
136
+ end
137
+ self.effects[id] = effect
138
+ self.effects[#self.effects + 1] = effect
139
+ effect:OnStart(self, context)
140
+ Msg("OnEffectStarted", self, effect)
141
+ assert(effect.CreateInstance or not effect:HasNonPropertyMembers()) -- please set the CreateInstance class constant to 'true' to use dynamic members
142
+ end
143
+
144
+ function ContinuousEffectContainer:StopEffect(id)
145
+ if not self.effects then return end
146
+ local effect = self.effects[id]
147
+ if not effect then return end
148
+ effect:OnStop(self)
149
+ table.remove_entry(self.effects, effect)
150
+ self.effects[id] = nil
151
+ Msg("OnEffectEnded", self, effect)
152
+ end
153
+
154
+ ----- InfopanelMessage Effects
155
+
156
+ MapVar("g_AdditionalInfopanelSectionText", {})
157
+ function GetAdditionalInfopanelSectionText(sectionId, obj)
158
+ if not sectionId or sectionId=="" then
159
+ return ""
160
+ end
161
+ local section = g_AdditionalInfopanelSectionText[sectionId]
162
+ if not section or not next(section) then
163
+ return ""
164
+ end
165
+ local texts = {}
166
+ for label, text in pairs(section) do
167
+ if label== "__AllSections" or IsKindOf(obj, label) then
168
+ texts[#texts + 1] = text
169
+ end
170
+ end
171
+ if not next(texts)then
172
+ return ""
173
+ end
174
+ return table.concat(texts, "\n")
175
+ end
176
+
177
+ function AddAdditionalInfopanelSectionText(sectionId, label, text, color, object, context)
178
+ local style = "Infopanel"
179
+ if color == "red" then
180
+ style = "InfopanelError"
181
+ elseif color == "green" then
182
+ style = "InfopanelBonus"
183
+ end
184
+ local section = g_AdditionalInfopanelSectionText[sectionId] or {}
185
+ label = label or "__AllSections"
186
+ section[label] = T{410957252932, "<textcolor><text></color>", textcolor = "<color " .. style .. ">", text = T{text, object, context}}
187
+ g_AdditionalInfopanelSectionText[sectionId] = section
188
+ end
189
+
190
+ function RemoveAdditionalInfopanelSectionText(sectionId, label)
191
+ if g_AdditionalInfopanelSectionText[sectionId] then
192
+ label = label or "__AllSections"
193
+ g_AdditionalInfopanelSectionText[sectionId][label]= nil
194
+ end
195
+ end
CommonLua/Classes/Decal.lua ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- base class required for filtering in map editor
2
+ DefineClass.Decal = {
3
+ __parents = { "CObject" },
4
+ flags = { efSelectable = false, efSunShadow = false, efShadow = false, cofComponentColorizationMaterial = true, },
5
+
6
+ properties = {
7
+ { category = "Decal", id = "sort_priority", name = "SortPriority", editor = "number", default = 0, max = 3, min = -4, template = true }
8
+ }
9
+ }
10
+
11
+ function Decal:SetShadowOnly(bSet)
12
+ if g_CMTPaused then return end
13
+ if bSet then
14
+ self:SetHierarchyGameFlags(const.gofSolidShadow)
15
+ else
16
+ self:ClearHierarchyGameFlags(const.gofSolidShadow)
17
+ end
18
+ end
19
+
20
+ DefineClass.TerrainDecal =
21
+ {
22
+ __parents = { "Decal", "EntityClass" },
23
+ flags = { cfDecal = true },
24
+ }
25
+
26
+ DefineClass.BakedTerrainDecal =
27
+ {
28
+ __parents = { "TerrainDecal", "InvisibleObject" },
29
+ flags = { cfConstructible = false, efBakedTerrainDecal = true },
30
+ max_allowed_radius = hr.TR_DecalSearchRadius * guim,
31
+ }
32
+
33
+ function BakedTerrainDecal:ConfigureInvisibleObjectHelper(helper)
34
+ helper:SetColorModifier(RGBRM(60, 60, 60, 127, 127))
35
+ helper:SetScale(35)
36
+ self:SetVisible(true)
37
+ end
38
+
39
+ DefineClass.BakedTerrainDecalLarge =
40
+ {
41
+ __parents = { "BakedTerrainDecal" },
42
+ flags = { efBakedTerrainDecalLarge = true },
43
+ }
44
+
45
+ DefineClass.BakedTerrainDecalDetailed =
46
+ {
47
+ __parents = { "BakedTerrainDecal" },
48
+ flags = { gofDetailedDecal = true },
49
+ max_allowed_radius = hr.TR_DetailedDecalSearchRadius * guim,
50
+ }
CommonLua/Classes/DeveloperOptions.lua ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DefineClass.DeveloperOptions = {
2
+ __parents = { "PropertyObject" },
3
+ option_name = "",
4
+ }
5
+
6
+ function DeveloperOptions:GetProperty(property)
7
+ local meta = table.find_value(self.properties, "id", property)
8
+ if meta and not prop_eval(meta.dont_save, self, meta) then
9
+ return GetDeveloperOption(property, self.class, self.option_name, meta.default)
10
+ end
11
+ return PropertyObject.GetProperty(self, property)
12
+ end
13
+
14
+ function DeveloperOptions:SetProperty(property, value)
15
+ local meta = table.find_value(self.properties, "id", property)
16
+ if meta and not prop_eval(meta.dont_save, self, meta) then
17
+ return SetDeveloperOption(property, value, self.class, self.option_name)
18
+ end
19
+ return PropertyObject.SetProperty(self, property, value)
20
+ end
21
+
22
+ function GetDeveloperOption(option, storage, substorage, default)
23
+ storage = storage or "Developer"
24
+ substorage = substorage or "General"
25
+ local ds = LocalStorage and LocalStorage[storage]
26
+ return ds and ds[substorage] and ds[substorage][option] or default or false
27
+ end
28
+
29
+ function SetDeveloperOption(option, value, storage, substorage)
30
+ if not LocalStorage then
31
+ print("no local storage available!")
32
+ return
33
+ end
34
+ storage = storage or "Developer"
35
+ substorage = substorage or "General"
36
+ value = value or nil
37
+ local infos = LocalStorage[storage] or {}
38
+ local info = infos[substorage] or {}
39
+ info[option] = value
40
+ infos[substorage] = info
41
+ LocalStorage[storage] = infos
42
+ Msg("DeveloperOptionsChanged", storage, substorage, option, value)
43
+ DelayedCall(0, SaveLocalStorage)
44
+ end
45
+
46
+ function GetDeveloperHistory(class, name)
47
+ if not LocalStorage then
48
+ return {}
49
+ end
50
+
51
+ local history = LocalStorage.History or {}
52
+ LocalStorage.History = history
53
+
54
+ history[class] = history[class] or {}
55
+ local list = history[class][name] or {}
56
+ history[class][name] = list
57
+
58
+ return list
59
+ end
60
+
61
+ function AddDeveloperHistory(class, name, entry, max_size, accept_empty)
62
+ max_size = max_size or 20
63
+ if not LocalStorage or not accept_empty and (entry or "") == "" then
64
+ return
65
+ end
66
+ local history = GetDeveloperHistory(class, name)
67
+ table.remove_entry(history, entry)
68
+ table.insert(history, 1, entry)
69
+ while #history > max_size do
70
+ table.remove(history)
71
+ end
72
+ SaveLocalStorageDelayed()
73
+ end
CommonLua/Classes/DuckingParams.lua ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DefineClass.DuckingParam = {
2
+ __parents = { "Preset" },
3
+ GlobalMap = "DuckingParams",
4
+
5
+ properties = {
6
+ { id = "Name", name = "Name", editor = "text", default = "" , help = "The name with which this ducking tier will appear in the sound type editor." },
7
+ { id = "Tier", name = "Tier", editor = "number", default = 0, min = -1, max = 100, help = "Which tiers will be affected by this one - lower tiers affect higher ones." },
8
+ { id = "Strength", name = "Strength", editor = "number", default = 100, min = 0, max = 1000, scale = 1, slider = true, help = "How much will this tier duck the ones below it." },
9
+ { id = "Attack", name = "Attack Duration", editor = "number", default = 100, min = 0, max = 1000, scale = 1, slider = true, help = "How long will this tier take to go from no effect to full ducking in ms." },
10
+ { id = "Release", name = "Release Duration", editor = "number", default = 100, min = 0, max = 1000, scale = 1, slider = true, help = "How long will this tier take to go from full ducking to no effect in ms." },
11
+ { id = "Hold", name = "Hold Duration", editor = "number", default = 100, min = 0, max = 5000, scale = 1, slider = true, help = "How long will this tier take, before starting to decay the ducking strength, after the sound strength decreases." },
12
+ { id = "Envelope", name = "Use side chain", editor = "bool", default = true, help = "Should the sounds in this preset modify the other sounds based on the current strength of their sound, or apply a constant static effect." },
13
+ },
14
+
15
+ OnEditorSetProperty = function(properties)
16
+ ReloadDucking()
17
+ end,
18
+
19
+ Apply = function(self)
20
+ ReloadDucking()
21
+ end,
22
+
23
+ EditorMenubarName = "Ducking Editor",
24
+ EditorMenubar = "Editors.Audio",
25
+ EditorIcon = "CommonAssets/UI/Icons/church.png",
26
+ }
27
+
28
+ function ReloadDucking()
29
+ local names = {}
30
+ local tiers = {}
31
+ local strengths = {}
32
+ local attacks = {}
33
+ local releases = {}
34
+ local hold = {}
35
+ local envelopes = {}
36
+ local i = 1
37
+ for _, p in pairs(DuckingParams) do
38
+ names[i] = p.id
39
+ tiers[i] = p.Tier
40
+ strengths[i] = p.Strength
41
+ attacks[i] = p.Attack
42
+ releases[i] = p.Release
43
+ hold[i] = p.Hold
44
+ envelopes[i] = p.Envelope and 1 or 0
45
+ i = i + 1
46
+ end
47
+ LoadDuckingParams(names, tiers, strengths, attacks, releases, hold, envelopes)
48
+ ReloadSoundTypes()
49
+ end
50
+
51
+ function ChangeDuckingPreset(id, tier, str, attack, release, hold)
52
+ if tier then
53
+ DuckingParams[id].Tier = tier
54
+ end
55
+ if str then
56
+ DuckingParams[id].Strength = str
57
+ end
58
+ if attack then
59
+ DuckingParams[id].Attack = attack
60
+ end
61
+ if release then
62
+ DuckingParams[id].Release = release
63
+ end
64
+ if hold then
65
+ DuckingParams[id].Hold = hold
66
+ end
67
+ ReloadDucking()
68
+ end
69
+
70
+ OnMsg.DataLoaded = ReloadDucking
CommonLua/Classes/DumbAI.lua ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ local ai_debug = Platform.developer and Platform.pc
2
+ local bias_base = 1000000 -- fixed point value equivalent to 1 or 100%
3
+
4
+ DefineClass.DumbAIPlayer = {
5
+ __parents = { "InitDone" },
6
+
7
+ actions = false,
8
+ action_log = false,
9
+ log_size = 10,
10
+ running_actions = false,
11
+ biases = false,
12
+ resources = false,
13
+ display_name = false,
14
+
15
+ absolute_actions = 10,
16
+ absolute_threshold = 10000,
17
+ relative_threshold = 50, -- percent of the highest eval
18
+ think_interval = 1000,
19
+
20
+ seed = 0,
21
+ think_thread = false,
22
+ ai_start = 0,
23
+
24
+ GedEditor = "DumbAIDebug",
25
+
26
+ -- production
27
+ production_interval = 60000,
28
+ next_production = 0,
29
+ production_rules = false,
30
+ next_production_times = false,
31
+ }
32
+
33
+ function DumbAIPlayer:Init()
34
+ self.actions = {}
35
+ self.action_log = {}
36
+ self.running_actions = {}
37
+ self.biases = {}
38
+ self.resources = {}
39
+ for _, def in ipairs(Presets.AIResource.Default) do
40
+ self.resources[def.id] = 0
41
+ end
42
+ self.ai_start = GameTime()
43
+ self.production_rules = {}
44
+ self.next_production = GameTime()
45
+ self.next_production_times = setmetatable({}, weak_keys_meta)
46
+ end
47
+
48
+ function DumbAIPlayer:Done()
49
+ DeleteThread(self.think_thread)
50
+ GedObjectDeleted(self)
51
+ end
52
+
53
+ function DumbAIPlayer:AddAIDef(ai_def)
54
+ if not ai_def then return end
55
+ local actions = self.actions
56
+ for _, action in ipairs(ai_def) do
57
+ actions[#actions + 1] = action
58
+ end
59
+ local resources = self.resources
60
+ for _, res in ipairs(ai_def.initial_resources) do
61
+ local resource = res.resource
62
+ resources[resource] = resources[resource] + res:Amount()
63
+ end
64
+ local production_rules = self.production_rules
65
+ for _, rule in ipairs(ai_def.production_rules or empty_table) do
66
+ production_rules[#production_rules + 1] = rule
67
+ end
68
+ local label = "AIDef " .. ai_def.id
69
+ for _, bias in ipairs(ai_def.biases) do
70
+ self:AddBias(bias.tag, bias.bias, nil, label)
71
+ end
72
+ end
73
+
74
+ function DumbAIPlayer:RemoveAIDef(ai_def)
75
+ if not ai_def then return end
76
+ local actions = self.actions
77
+ for _, action in ipairs(ai_def) do
78
+ table.remove_entry(actions, action)
79
+ end
80
+ local production_rules = self.production_rules
81
+ for _, rule in ipairs(ai_def.production_rules or empty_table) do
82
+ table.remove_entry(production_rules, rule)
83
+ end
84
+ local label = "AIDef " .. ai_def.id
85
+ for _, bias in ipairs(ai_def.biases) do
86
+ self:RemoveBias(bias.tag, nil, label)
87
+ end
88
+ end
89
+
90
+
91
+ -- AI bias
92
+
93
+ local function recalc_bias(tag_biases)
94
+ local acc = bias_base
95
+ for _, bias in ipairs(tag_biases) do
96
+ acc = MulDivRound(acc, bias.change, bias_base)
97
+ end
98
+ tag_biases.acc = acc
99
+ end
100
+
101
+ function DumbAIPlayer:AddBias(tag, change, source, label)
102
+ local tag_biases = self.biases[tag]
103
+ if not tag_biases then
104
+ tag_biases = { acc = bias_base }
105
+ self.biases[tag] = tag_biases
106
+ end
107
+ if label then
108
+ local idx = table.find(tag_biases, "label", label)
109
+ if idx then
110
+ table.remove(tag_biases, idx)
111
+ end
112
+ end
113
+ local bias = {
114
+ change = change,
115
+ label = label or nil,
116
+ source = ai_debug and source or nil,
117
+ }
118
+ tag_biases[#tag_biases + 1] = bias
119
+ recalc_bias(tag_biases)
120
+ return bias
121
+ end
122
+
123
+ function DumbAIPlayer:RemoveBias(tag, bias, label)
124
+ local tag_biases = self.biases[tag]
125
+ if tag_biases then
126
+ table.remove_entry(tag_biases, bias)
127
+ local idx = table.find(tag_biases, "label", label)
128
+ if idx then
129
+ table.remove(tag_biases, idx)
130
+ end
131
+ recalc_bias(tag_biases)
132
+ end
133
+ end
134
+
135
+ function DumbAIPlayer:BiasValue(value, tags)
136
+ local biases = self.biases
137
+ for _, tag in ipairs(tags or empty_table) do
138
+ local tag_biases = biases[tag]
139
+ if tag_biases then
140
+ value = MulDivRound(value, tag_biases.acc, bias_base)
141
+ end
142
+ end
143
+ return value
144
+ end
145
+
146
+ function DumbAIPlayer:BiasValueByTag(value, tag)
147
+ local tag_biases = self.biases[tag]
148
+ if tag_biases then
149
+ value = MulDivRound(value, tag_biases.acc, bias_base)
150
+ end
151
+ return value
152
+ end
153
+
154
+ -- AI main loop
155
+
156
+ function DumbAIPlayer:AIUpdate(seed)
157
+ local resources = self.resources
158
+ for _, rule in ipairs(self.production_rules) do
159
+ local time = self.next_production_times[rule] or 0
160
+ if GameTime() >= time then
161
+ self.next_production_times[rule] = time + rule.production_interval
162
+ procall(rule.Run, rule, resources, self)
163
+ end
164
+ end
165
+ end
166
+
167
+ function DumbAIPlayer:LogAction(action)
168
+ table.insert(self.action_log, {action = action, time = GameTime()})
169
+ while #self.action_log > self.log_size do
170
+ table.remove(self.action_log, 1)
171
+ end
172
+ end
173
+
174
+ function DumbAIPlayer:GetDisplayName()
175
+ return self.display_name or ""
176
+ end
177
+
178
+ function DumbAIPlayer:AIStartAction(action)
179
+ self.running_actions[action] = (self.running_actions[action] or 0) + 1
180
+ local resources = self.resources
181
+ for _, res in ipairs(action.required_resources) do
182
+ local resource = res.resource
183
+ resources[resource] = resources[resource] - res.amount
184
+ end
185
+ CreateGameTimeThread(function(self, action, ai_debug)
186
+ sprocall(action.Run, action, self)
187
+ Sleep(self:BiasValueByTag(action.delay, "action_delay"))
188
+ if (action.log_entry or "") ~= "" then
189
+ self:LogAction(action)
190
+ end
191
+ local resources = self.resources
192
+ for _, res in ipairs(action.resulting_resources) do
193
+ local resource = res.resource
194
+ resources[resource] = resources[resource] + res:Amount()
195
+ end
196
+ sprocall(action.OnEnd, action, self)
197
+ assert((self.running_actions[action] or 0) > 0)
198
+ self.running_actions[action] = (self.running_actions[action] or 0) - 1
199
+ if ai_debug then
200
+ ObjModified(self)
201
+ end
202
+ end, self, action, ai_debug)
203
+ end
204
+
205
+ function DumbAIPlayer:AILimitActions(actions)
206
+ local active_actions = {}
207
+ local resources = self.resources
208
+ local running_actions = self.running_actions
209
+ for _, action in ipairs(actions) do
210
+ if (running_actions[action] or 0) < action.max_running then
211
+ for _, res in ipairs(action.required_resources) do
212
+ assert(res:Amount() == res.amount, "randomized amounts are not supported for required_resources")
213
+ if resources[res.resource] < res.amount then
214
+ action = nil
215
+ break
216
+ end
217
+ end
218
+ if action and action:IsAllowed(self) then
219
+ local eval = action:Eval(self) or action.base_eval
220
+ action.eval = self:BiasValue(eval, action.tags)
221
+ active_actions[#active_actions + 1] = action
222
+ end
223
+ end
224
+ end
225
+ table.sortby_field_descending(active_actions, "eval")
226
+ -- limit by number of actions
227
+ local count = self:BiasValueByTag(self.absolute_actions, "ai_absolute_actions")
228
+ count = Min(count, #active_actions)
229
+ if count < 1 then
230
+ return active_actions, 0
231
+ end
232
+ -- limit by evaluation
233
+ local threshold = self:BiasValueByTag(self.absolute_threshold, "ai_absolute_threshold")
234
+ local rel_threshold = self:BiasValueByTag(self.relative_threshold, "ai_relative_threshold")
235
+ threshold = Max(threshold, MulDivRound(active_actions[1].eval, rel_threshold, 100))
236
+
237
+ while count > 0
238
+ and active_actions[count].eval < threshold do
239
+ count = count - 1
240
+ end
241
+ return active_actions, count
242
+ end
243
+
244
+ function DumbAIPlayer:AIThink(seed)
245
+ seed = seed or AsyncRand()
246
+ self:AIUpdate(seed)
247
+ local actions, count = self:AILimitActions(self.actions)
248
+ local action = actions[BraidRandom(seed, count) + 1]
249
+ if action then
250
+ self:AIStartAction(action)
251
+ end
252
+ if ai_debug then
253
+ if #self > 40 then -- remove entries beyond 40
254
+ for i = 1, #self do
255
+ self[i] = self[i + 1]
256
+ end
257
+ end
258
+ if #self > 0 and not self[#self][3] then
259
+ self[#self] = nil -- replace last entry if there was no action selected
260
+ end
261
+ self[#self + 1] = {
262
+ GameTime() - self.ai_start,
263
+ seed,
264
+ action or false,
265
+ actions,
266
+ count,
267
+ table.copy(self.resources),
268
+ action and action.eval,
269
+ }
270
+ ObjModified(self)
271
+ end
272
+ return action
273
+ end
274
+
275
+ function DumbAIPlayer:CreateAIThinkThread()
276
+ DeleteThread(self.think_thread)
277
+ self.think_thread = CreateGameTimeThread(function(self)
278
+ local rand, think_seed = BraidRandom(self.seed)
279
+ while true do
280
+ Sleep(self:BiasValueByTag(self.think_interval, "ai_think_interval"))
281
+ rand, think_seed = BraidRandom(think_seed)
282
+ self:AIThink(rand)
283
+ end
284
+ end, self)
285
+ end
286
+
287
+ -- AI Debug
288
+
289
+ if ai_debug then
290
+
291
+ local function format_bias(n)
292
+ return string.format("%d.%02d", n / bias_base, (n % bias_base) * 100 / bias_base)
293
+ end
294
+
295
+ local function DumbAIDebugActions(texts, actions, count, eval)
296
+ texts[#texts + 1] = "<style GedTitleSmall><center>Actions selection</style>"
297
+ for i, action in ipairs(actions) do
298
+ if i == count + 1 then
299
+ texts[#texts + 1] = ""
300
+ texts[#texts + 1] = "<style GedTitleSmall><center>Low evaluation</style>"
301
+ end
302
+ if eval then
303
+ texts[#texts + 1] = string.format("<left>%s<right>%s", action.id, format_bias(action.eval))
304
+ else
305
+ texts[#texts + 1] = string.format("<left>%s", action.id)
306
+ end
307
+ end
308
+ end
309
+
310
+ local function DumbAIDebugResources(texts, resources)
311
+ texts[#texts + 1] = "<style GedTitleSmall><center>Resources</style>"
312
+ for _, def in ipairs(Presets.AIResource.Default) do
313
+ local resource = def.id
314
+ texts[#texts + 1] = string.format("<left>%s<right>%d", resource, resources[resource])
315
+ end
316
+ end
317
+
318
+ function GedDumbAIDebugState(ai_player)
319
+ local texts = {}
320
+ DumbAIDebugResources(texts, ai_player.resources)
321
+ texts[#texts + 1] = ""
322
+ texts[#texts + 1] = "<style GedTitleSmall><center>Tag biases</style>"
323
+ for _, def in ipairs(Presets.AITag.Default) do
324
+ local tag = def.id
325
+ local tag_biases = ai_player.biases[tag]
326
+ if tag_biases then
327
+ texts[#texts + 1] = string.format("<left>%s<right>%d%%", tag, MulDivRound(tag_biases.acc, 100, bias_base))
328
+ end
329
+ end
330
+
331
+ texts[#texts + 1] = ""
332
+ local actions, count = ai_player:AILimitActions(ai_player.actions)
333
+ DumbAIDebugActions(texts, actions, count, true)
334
+ return table.concat(texts, "\n")
335
+ end
336
+
337
+ local function time(time)
338
+ time = tonumber(time)
339
+ if time then
340
+ local sign = time < 0 and "-" or ""
341
+ local sec = abs(time) / 1000
342
+ local min = sec / 60
343
+ local hours = min / 60
344
+ local days = hours / 24
345
+ if days > 0 then
346
+ return string.format("%s%dd%02dh%02dm%02ds", sign, days, hours % 24, min % 60, sec % 60)
347
+ else
348
+ return string.format("%s%dh%02dm%02ds", sign, hours, min % 60, sec % 60)
349
+ end
350
+ end
351
+ end
352
+
353
+ function GedDumbAIDebugLog(ai_player)
354
+ local list = {}
355
+ for i, entry in ipairs(ai_player) do
356
+ local t, seed, action, actions, count, resources, eval = table.unpack(entry)
357
+ list[i] = string.format("%s %s %s", time(t) or "???", action and action.id or "---", action and format_bias(eval) or "")
358
+ end
359
+ return list
360
+ end
361
+
362
+ function GedDumbAIDebugLogEntry(entry)
363
+ local texts = {}
364
+ local time, seed, action, actions, count, resources = table.unpack(entry)
365
+ DumbAIDebugResources(texts, resources)
366
+ texts[#texts + 1] = ""
367
+ DumbAIDebugActions(texts, actions, count)
368
+ return table.concat(texts, "\n")
369
+ end
370
+
371
+ -- Test
372
+
373
+ __TestAI = false
374
+
375
+ function TestAI()
376
+ if __TestAI then __TestAI:delete() end
377
+ __TestAI = DumbAIPlayer:new{
378
+ think_interval = const.HourDuration,
379
+ production_interval = const.DayDuration,
380
+ }
381
+ __TestAI:AddAIDef(Presets.DumbAIDef.Default.default)
382
+ __TestAI:AddAIDef(Presets.DumbAIDef.MissionSponsors.IMM)
383
+ __TestAI:CreateAIThinkThread()
384
+ __TestAI:OpenEditor()
385
+ Resume()
386
+ end
387
+
388
+ end
389
+
390
+ function DumbAIPlayer:GetCurrentStanding()
391
+ return self.resources.standing
392
+ end
CommonLua/Classes/EditorBase.lua ADDED
@@ -0,0 +1,543 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ DefineClass.EditorObject = {
2
+ __parents = { "CObject" },
3
+
4
+ EditorEnter = empty_func,
5
+ EditorExit = empty_func,
6
+ }
7
+
8
+ function EditorObject:PostLoad()
9
+ if IsEditorActive() then
10
+ self:EditorEnter()
11
+ end
12
+ end
13
+
14
+ RecursiveCallMethods.EditorEnter = "procall"
15
+ RecursiveCallMethods.EditorExit = "procall_parents_last"
16
+
17
+ DefineClass.EditorCallbackObject = {
18
+ __parents = { "CObject" },
19
+ flags = { cfEditorCallback = true },
20
+
21
+ -- all callbacks receive no parameters, except EditorCallbackClone, which receives the original object
22
+ EditorCallbackPlace = empty_func,
23
+ EditorCallbackPlaceCursor = empty_func,
24
+ EditorCallbackDelete = empty_func,
25
+ EditorCallbackRotate = empty_func,
26
+ EditorCallbackMove = empty_func,
27
+ EditorCallbackScale = empty_func,
28
+ EditorCallbackClone = empty_func, -- function(orig) end,
29
+ EditorCallbackGenerate = empty_func, -- function(generator, object_source, placed_objects, prefab_list) end,
30
+ }
31
+
32
+ AutoResolveMethods.EditorCallbackPlace = true
33
+ AutoResolveMethods.EditorCallbackPlaceCursor = true
34
+ AutoResolveMethods.EditorCallbackDelete = true
35
+ AutoResolveMethods.EditorCallbackRotate = true
36
+ AutoResolveMethods.EditorCallbackMove = true
37
+ AutoResolveMethods.EditorCallbackScale = true
38
+ AutoResolveMethods.EditorCallbackClone = true
39
+ AutoResolveMethods.EditorCallbackGenerate = true
40
+
41
+ function OnMsg.ChangeMapDone()
42
+ --CObjects that are EditorVisibleObject will get saved as efVisible == true and pop up on first map load
43
+ if GetMap() == "" then return end
44
+ if not IsEditorActive() then
45
+ MapForEach("map", "EditorVisibleObject", const.efVisible, function(o)
46
+ o:ClearEnumFlags(const.efVisible)
47
+ end)
48
+ end
49
+ end
50
+
51
+ DefineClass.EditorVisibleObject = {
52
+ __parents = { "EditorObject" },
53
+ flags = { efVisible = false },
54
+ properties = {
55
+ { id = "OnCollisionWithCamera" },
56
+ },
57
+ }
58
+
59
+ function EditorVisibleObject:EditorEnter()
60
+ self:SetEnumFlags(const.efVisible)
61
+ end
62
+
63
+ function EditorVisibleObject:EditorExit()
64
+ self:ClearEnumFlags(const.efVisible)
65
+ end
66
+
67
+ ----
68
+
69
+ DefineClass.EditorColorObject = {
70
+ __parents = { "EditorObject" },
71
+ editor_color = false,
72
+ orig_color = false,
73
+ }
74
+
75
+ function EditorColorObject:EditorGetColor()
76
+ return self.editor_color
77
+ end
78
+
79
+ function EditorColorObject:EditorEnter()
80
+ local editor_color = self:EditorGetColor()
81
+ if editor_color then
82
+ self.orig_color = self:GetColorModifier()
83
+ self:SetColorModifier(editor_color)
84
+ end
85
+ end
86
+
87
+ function EditorColorObject:EditorExit()
88
+ if self.orig_color then
89
+ self:SetColorModifier(self.orig_color)
90
+ self.orig_color = false
91
+ end
92
+ end
93
+
94
+ function EditorColorObject:GetColorModifier()
95
+ if self.orig_color then
96
+ return self.orig_color
97
+ end
98
+ return EditorObject.GetColorModifier(self)
99
+ end
100
+
101
+ ----
102
+
103
+ DefineClass.EditorEntityObject = {
104
+ __parents = { "EditorCallbackObject", "EditorColorObject" },
105
+ entity = "",
106
+ editor_entity = "",
107
+ orig_scale = false,
108
+ editor_scale = false,
109
+ }
110
+
111
+ function EditorEntityObject:EditorCanPlace()
112
+ return true
113
+ end
114
+
115
+ function EditorEntityObject:SetEditorEntity(set)
116
+ if (self.editor_entity or "") ~= "" then
117
+ self:ChangeEntity(set and self.editor_entity or g_Classes[self.class]:GetEntity())
118
+ end
119
+ if self.editor_scale then
120
+ if set then
121
+ self.orig_scale = self:GetScale()
122
+ self:SetScale(self.editor_scale)
123
+ elseif self.orig_scale then
124
+ self:SetScale(self.orig_scale)
125
+ self.orig_scale = false
126
+ end
127
+ end
128
+ end
129
+ function EditorEntityObject:GetScale()
130
+ if self.orig_scale then
131
+ return self.orig_scale
132
+ end
133
+ return EditorObject.GetScale(self)
134
+ end
135
+
136
+ function EditorEntityObject:EditorEnter()
137
+ self:SetEditorEntity(true)
138
+ end
139
+ function EditorEntityObject:EditorExit()
140
+ self:SetEditorEntity(false)
141
+ end
142
+ function OnMsg.EditorCallback(id, objects, ...)
143
+ if id == "EditorCallbackPlace" or id == "EditorCallbackPlaceCursor" then
144
+ for i = 1, #objects do
145
+ local obj = objects[i]
146
+ if obj:IsKindOf("EditorEntityObject") then
147
+ obj:SetEditorEntity(true)
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ ----
154
+
155
+ DefineClass.EditorTextObject = {
156
+ __parents = { "EditorObject", "ComponentAttach" },
157
+ editor_text_spot = "Label",
158
+ editor_text_color = RGBA(255,255,255,255),
159
+ editor_text_offset = point(0,0,3*guim),
160
+ editor_text_style = false,
161
+ editor_text_depth_test = true,
162
+ editor_text_ctarget = "SetColor",
163
+ editor_text_obj = false,
164
+ editor_text_member = "class",
165
+ editor_text_class = "Text",
166
+ }
167
+
168
+ function EditorTextObject:EditorEnter()
169
+ self:EditorTextUpdate(true)
170
+ end
171
+
172
+ function EditorTextObject:EditorExit()
173
+ DoneObject(self.editor_text_obj)
174
+ self.editor_text_obj = nil
175
+ end
176
+
177
+ AutoResolveMethods.EditorGetText = ".."
178
+
179
+ function EditorTextObject:EditorGetText()
180
+ return self[self.editor_text_member]
181
+ end
182
+
183
+ function EditorTextObject:EditorGetTextColor()
184
+ return self.editor_text_color
185
+ end
186
+
187
+ function EditorTextObject:EditorGetTextStyle()
188
+ return self.editor_text_style
189
+ end
190
+
191
+ function EditorTextObject:Clone(class, ...)
192
+ local clone = EditorObject.Clone(self, class or self.class, ...)
193
+ if IsKindOf(clone, "EditorTextObject") then
194
+ clone:EditorTextUpdate(true)
195
+ end
196
+ return clone
197
+ end
198
+
199
+ function EditorTextObject:EditorTextUpdate(create)
200
+ if not IsValid(self) then
201
+ return
202
+ end
203
+ local obj = self.editor_text_obj
204
+ if not IsValid(obj) and not create then return end
205
+ local is_hidden = GetDeveloperOption("Hidden", "EditorHiddenTextOptions", self.class)
206
+ local text = not is_hidden and self:EditorGetText()
207
+ if not text then
208
+ DoneObject(obj)
209
+ return
210
+ end
211
+ if not IsValid(obj) then
212
+ obj = PlaceObject(self.editor_text_class, {text_style = self:EditorGetTextStyle()})
213
+ obj:SetDepthTest(self.editor_text_depth_test)
214
+ local spot = self.editor_text_spot
215
+ if spot and self:HasSpot(spot) then
216
+ self:Attach(obj, self:GetSpotBeginIndex(spot))
217
+ else
218
+ self:Attach(obj)
219
+ end
220
+ local offset = self.editor_text_offset
221
+ if offset then
222
+ obj:SetAttachOffset(offset)
223
+ end
224
+ self.editor_text_obj = obj
225
+ end
226
+ obj:SetText(text)
227
+ local color = self:EditorGetTextColor()
228
+ if color then
229
+ obj[self.editor_text_ctarget](obj, color)
230
+ end
231
+ end
232
+
233
+ function EditorTextObject:OnEditorSetProperty(prop_id)
234
+ if prop_id == self.editor_text_member then
235
+ self:EditorTextUpdate(true)
236
+ end
237
+ return EditorObject.OnEditorSetProperty(self, prop_id)
238
+ end
239
+
240
+ DefineClass.NoteMarker = {
241
+ __parents = { "Object", "EditorVisibleObject", "EditorTextObject" },
242
+ properties = {
243
+ { id = "MantisID", editor = "number", default = 0, important = true , buttons = {{name = "OpenMantis", func = "OpenMantisFromMarker"}}},
244
+ { id = "Text", editor = "text", lines = 5, default = "", important = true },
245
+ { id = "TextColor", editor = "color", default = RGB(255,255,255), important = true },
246
+ { id = "TextStyle", editor = "text", default = "InfoText", important = true },
247
+ -- disabled properties
248
+ { id = "Angle", editor = false},
249
+ { id = "Axis", editor = false},
250
+ { id = "Opacity", editor = false},
251
+ { id = "StateCategory", editor = false},
252
+ { id = "StateText", editor = false},
253
+ { id = "Groups", editor = false},
254
+ { id = "Mirrored", editor = false},
255
+ { id = "ColorModifier", editor = false},
256
+ { id = "Occludes", editor = false},
257
+ { id = "Walkable", editor = false},
258
+ { id = "ApplyToGrids", editor = false},
259
+ { id = "Collision", editor = false},
260
+ { id = "OnCollisionWithCamera", editor = false},
261
+ { id = "CollectionIndex", editor = false},
262
+ { id = "CollectionName", editor = false},
263
+ },
264
+ editor_text_offset = point(0,0,4*guim),
265
+ editor_text_member = "Text",
266
+ }
267
+
268
+ for i = 1, const.MaxColorizationMaterials do
269
+ table.iappend( NoteMarker.properties, {
270
+ { id = string.format("Color%d", i), editor = false },
271
+ { id = string.format("Roughness%d", i), editor = false },
272
+ { id = string.format("Metallic%d", i), editor = false },
273
+ })
274
+ end
275
+
276
+ function NoteMarker:EditorGetTextColor()
277
+ return self.TextColor
278
+ end
279
+
280
+ function NoteMarker:EditorGetTextStyle()
281
+ return self.TextStyle
282
+ end
283
+
284
+ function OpenMantisFromMarker(parentEditor, object, prop_id, ...)
285
+ local mantisID = object:GetProperty(prop_id)
286
+ if mantisID and mantisID ~= "" and mantisID ~= 0 then
287
+ local url = "http://mantis.haemimontgames.com/view.php?id="..mantisID
288
+ OpenUrl(url, "force external browser")
289
+ end
290
+ end
291
+
292
+ if not Platform.editor then
293
+
294
+ function OnMsg.ClassesPreprocess(classdefs)
295
+ for name, class in pairs(classdefs) do
296
+ class.EditorCallbackPlace = nil
297
+ class.EditorCallbackPlaceCursor = nil
298
+ class.EditorCallbackDelete = nil
299
+ class.EditorCallbackRotate = nil
300
+ class.EditorCallbackMove = nil
301
+ class.EditorCallbackScale = nil
302
+ class.EditorCallbackClone = nil
303
+ class.EditorCallbackGenerate = nil
304
+
305
+ class.EditorEnter = nil
306
+ class.EditorExit = nil
307
+
308
+ class.EditorGetText = nil
309
+ class.EditorGetTextColor = nil
310
+ class.EditorGetTextStyle = nil
311
+ class.EditorGetTextFont = nil
312
+
313
+ class.editor_text_obj = nil
314
+ class.editor_text_spot = nil
315
+ class.editor_text_color = nil
316
+ class.editor_text_offset = nil
317
+ class.editor_text_style = nil
318
+ end
319
+ end
320
+
321
+ function OnMsg.Autorun()
322
+ MsgClear("EditorCallback")
323
+ MsgClear("GameEnterEditor")
324
+ MsgClear("GameExitEditor")
325
+ end
326
+
327
+ end
328
+
329
+ ----
330
+
331
+ local update_thread
332
+ function UpdateEditorTexts()
333
+ if not IsEditorActive() or IsValidThread(update_thread) then
334
+ return
335
+ end
336
+ update_thread = CreateRealTimeThread(function()
337
+ MapForEach("map", "EditorTextObject", function(obj)
338
+ obj:EditorTextUpdate(true)
339
+ end)
340
+ end)
341
+ end
342
+
343
+
344
+ function OnMsg.DeveloperOptionsChanged(storage, name, id, value)
345
+ if storage == "EditorHiddenTextOptions" then
346
+ UpdateEditorTexts()
347
+ end
348
+ end
349
+
350
+ ----
351
+
352
+ DefineClass.ForcedTemplate =
353
+ {
354
+ __parents = { "EditorObject" },
355
+ template_class = "Template",
356
+ }
357
+
358
+ function GetTemplateBase(class_name)
359
+ local class = g_Classes[class_name]
360
+ return class and class.template_class or ""
361
+ end
362
+
363
+ MapVar("ForcedTemplateObjs", {})
364
+
365
+ function ForcedTemplate:EditorEnter()
366
+ if self:GetGameFlags(const.gofPermanent) == 0 and self:GetEnumFlags(const.efVisible) ~= 0 then
367
+ ForcedTemplateObjs[self] = true
368
+ self:ClearEnumFlags(const.efVisible)
369
+ end
370
+ end
371
+
372
+ function ForcedTemplate:EditorExit()
373
+ if ForcedTemplateObjs[self] then
374
+ self:SetEnumFlags(const.efVisible)
375
+ end
376
+ end
377
+
378
+
379
+ ---- EditorSelectedObject --------------------------------------
380
+
381
+ MapVar("l_editor_selection", empty_table)
382
+
383
+ DefineClass.EditorSelectedObject = {
384
+ __parents = { "CObject" },
385
+ }
386
+
387
+ function EditorSelectedObject:EditorSelect(selected)
388
+ end
389
+
390
+ function EditorSelectedObject:EditorIsSelected(check_helpers)
391
+ if l_editor_selection[self] then
392
+ return true
393
+ end
394
+ if check_helpers then
395
+ local helpers = PropertyHelpers and PropertyHelpers[self] or empty_table
396
+ for prop_id, helper in pairs(helpers) do
397
+ if editor.IsSelected(helper) then
398
+ return true
399
+ end
400
+ end
401
+ end
402
+ return false
403
+ end
404
+
405
+ function UpdateEditorSelectedObjects(selection)
406
+ local new_selection = setmetatable({}, weak_keys_meta)
407
+ local old_selection = l_editor_selection
408
+ l_editor_selection = new_selection
409
+ for i=1,#(selection or "") do
410
+ local obj = selection[i]
411
+ if IsKindOf(obj, "EditorSelectedObject") then
412
+ new_selection[obj] = true
413
+ if not old_selection[obj] then
414
+ obj:EditorSelect(true)
415
+ end
416
+ end
417
+ end
418
+ for obj in pairs(old_selection or empty_table) do
419
+ if not new_selection[obj] then
420
+ obj:EditorSelect(false)
421
+ end
422
+ end
423
+ end
424
+
425
+ function OnMsg.EditorSelectionChanged(selection)
426
+ UpdateEditorSelectedObjects(selection)
427
+ end
428
+
429
+ function OnMsg.GameEnterEditor()
430
+ UpdateEditorSelectedObjects(editor.GetSel())
431
+ end
432
+
433
+ function OnMsg.GameExitEditor()
434
+ UpdateEditorSelectedObjects()
435
+ end
436
+
437
+
438
+ ---- EditorSubVariantObject --------------------------------------
439
+
440
+ DefineClass.EditorSubVariantObject = {
441
+ __parents = { "PropertyObject" },
442
+ properties = {
443
+ { name = "Subvariant", id = "subvariant", editor = "number", default = -1,
444
+ buttons = {
445
+ { name = "Next", func = "CycleEntityBtn" },
446
+ },
447
+ },
448
+ },
449
+ }
450
+
451
+ function EditorSubVariantObject:CycleEntityBtn()
452
+ self:CycleEntity()
453
+ end
454
+
455
+ function EditorSubVariantObject:Setsubvariant(val)
456
+ self.subvariant = val
457
+ end
458
+
459
+ function EditorSubVariantObject:PreviousEntity()
460
+ self:CycleEntity(-1)
461
+ end
462
+
463
+ function EditorSubVariantObject:NextEntity()
464
+ self:CycleEntity(-1)
465
+ end
466
+
467
+ local maxEnt = 20
468
+ function EditorSubVariantObject:CycleEntity(delta)
469
+ delta = delta or 1
470
+ local curE = self:GetEntity()
471
+ local nxt = self.subvariant == -1 and (tonumber(string.match(curE, "%d+$")) or 1) or self.subvariant
472
+ nxt = nxt + delta
473
+
474
+ local nxtE = string.gsub(curE, "%d+$", (nxt < 10 and "0" or "") .. tostring(nxt))
475
+ if not IsValidEntity(nxtE) then
476
+ if delta > 0 then
477
+ --going up, reset to first
478
+ nxt = 1
479
+ nxtE = string.gsub(curE, "%d+$", (nxt < 10 and "0" or "") .. tostring(nxt))
480
+ else
481
+ --going down, reset to last, whichever that is..
482
+ nxt = maxEnt + 1
483
+ while not IsValidEntity(nxtE) and nxt > 0 do
484
+ nxt = nxt - 1
485
+ nxtE = string.gsub(curE, "%d+$", (nxt < 10 and "0" or "") .. tostring(nxt))
486
+ end
487
+ end
488
+
489
+ if not IsValidEntity(nxtE) then
490
+ nxtE = curE
491
+ nxt = -1
492
+ end
493
+ end
494
+
495
+ if self.subvariant ~= nxt then
496
+ self.subvariant = nxt
497
+ self:ChangeEntity(nxtE)
498
+ ObjModified(self)
499
+ return true
500
+ end
501
+ return false
502
+ end
503
+
504
+ function EditorSubVariantObject:ResetSubvariant()
505
+ self.subvariant = -1
506
+ end
507
+
508
+ function EditorSubVariantObject.OnShortcut(delta)
509
+ local sel = editor.GetSel()
510
+ if sel and #sel > 0 then
511
+ XEditorUndo:BeginOp{ objects = sel }
512
+ for i = 1, #sel do
513
+ if IsKindOf(sel[i], "EditorSubVariantObject") then
514
+ sel[i]:CycleEntity(delta)
515
+ end
516
+ end
517
+ XEditorUndo:EndOp(sel)
518
+ end
519
+ end
520
+
521
+ function CycleObjSubvariant(obj, dir)
522
+ if IsKindOf(obj, "EditorSubVariantObject") then
523
+ obj:CycleEntity(dir)
524
+ else
525
+ local class = obj.class
526
+ local num = tonumber(class:sub(-2, -1))
527
+ if num then
528
+ local list = {}
529
+ for i = 0, 99 do
530
+ local class_name = class:sub(1, -3) .. (i <= 9 and "0" or "") .. tostring(i)
531
+ if g_Classes[class_name] and IsValidEntity(g_Classes[class_name]:GetEntity()) then
532
+ list[#list + 1] = class_name
533
+ end
534
+ end
535
+
536
+ local idx = table.find(list, class) + dir
537
+ if idx == 0 then idx = #list elseif idx > #list then idx = 1 end
538
+ obj = editor.ReplaceObject(obj, list[idx])
539
+ end
540
+ end
541
+
542
+ return obj
543
+ end
CommonLua/Classes/EntityClass.lua ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ if Platform.ged then
2
+ DefineClass("EntityClass", "CObject")
3
+ return
4
+ end
5
+
6
+ DefineClass.EntityClass = {
7
+ flags = { efCameraRepulse = true, efSelectable = false },
8
+ __hierarchy_cache = true,
9
+ __parents = { "CObject" },
10
+ }
11
+
12
+ local detail_flags = {
13
+ ["Default"] = { gofDetailClass0 = false, gofDetailClass1 = false },
14
+ ["Essential"] = { gofDetailClass0 = false, gofDetailClass1 = true },
15
+ ["Optional"] = { gofDetailClass0 = true, gofDetailClass1 = false },
16
+ ["Eye Candy"] = { gofDetailClass0 = true, gofDetailClass1 = true },
17
+ }
18
+
19
+ function CopyDetailFlagsFromEntity(class, entity, name)
20
+ local detail_class = entity and entity.DetailClass or "Essential"
21
+ local flags = detail_flags[detail_class]
22
+ if class.flags then
23
+ for k, v in pairs(flags or empty_table) do
24
+ class.flags[k] = v
25
+ end
26
+ else
27
+ class.flags = flags
28
+ end
29
+ end
30
+
31
+ function OnMsg.ClassesGenerate(classdefs)
32
+ local all_entities = GetAllEntities()
33
+ local EntityData = EntityData
34
+ local CopyDetailFlagsFromEntity = CopyDetailFlagsFromEntity
35
+ local UseCameraCollision = not config.NoCameraCollision
36
+ local UseColliders = const.maxCollidersPerObject > 0
37
+
38
+ for name in pairs(EntityData) do
39
+ all_entities[name] = true
40
+ end
41
+
42
+ for name, class in pairs(classdefs) do
43
+ local entity = class.entity or name
44
+ local cls_data = entity and (EntityData[entity] or empty_table).entity
45
+ for id, value in pairs(cls_data) do
46
+ if not rawget(class, id) and id ~= "class_parent" then
47
+ class[id] = value
48
+ end
49
+ end
50
+ local flags = class.flags
51
+ if not flags or flags.gofDetailClass0 == nil and flags.gofDetailClass1 == nil then
52
+ CopyDetailFlagsFromEntity(class, cls_data, name)
53
+ end
54
+
55
+ all_entities[name] = nil -- exclude used entities
56
+ if rawget(class, "prevent_entity_class_creation") then
57
+ all_entities[entity or false] = nil
58
+ end
59
+ end
60
+ all_entities["StatesSpots"] = nil
61
+ all_entities["error"] = nil
62
+ all_entities[""] = nil
63
+
64
+ local __parent_tables = { }
65
+
66
+ for name in pairs(all_entities) do
67
+ local entity_data = EntityData[name]
68
+ local cls_data = entity_data and entity_data.entity
69
+ local class = cls_data and table.copy(cls_data) or { }
70
+ local parent = class.class_parent or "EntityClass"
71
+ if parent ~= "NoClass" then
72
+ class.class_parent = nil
73
+
74
+ local __parents = __parent_tables[parent]
75
+ if not __parents then
76
+ if parent ~= "EntityClass" and parent:find(",") then
77
+ __parents = string.split(parent, "[^%w]+")
78
+ table.remove_value(__parents, "")
79
+ else
80
+ __parents = { parent }
81
+ end
82
+ __parent_tables[parent] = __parents
83
+ end
84
+ class.__parents = __parents
85
+
86
+ CopyDetailFlagsFromEntity(class, cls_data, name)
87
+
88
+ if UseCameraCollision then
89
+ local entity_occ = cls_data and cls_data.on_collision_with_camera or "no action"
90
+ local occ_flags = OCCtoFlags[entity_occ]
91
+ if occ_flags then
92
+ if class.flags then
93
+ for k, v in pairs(occ_flags) do
94
+ class.flags[k] = v
95
+ end
96
+ else
97
+ class.flags = occ_flags
98
+ end
99
+ end
100
+ end
101
+
102
+ if UseColliders and IsValidEntity(name) and not HasColliders(name) then
103
+ class.flags = class.flags and table.copy(class.flags) or {}
104
+ class.flags.cofComponentCollider = false
105
+ end
106
+ class.entity = false
107
+ class.__generated_by_class = "EntityClass"
108
+ classdefs[name] = class
109
+ end
110
+ end
111
+
112
+ Msg("BeforeClearEntityData") -- game specific hook to give the game the chance to do something with the entities
113
+ MsgClear("BeforeClearEntityData")
114
+
115
+ CreateRealTimeThread(ReloadFadeCategories, true)
116
+ end
117
+
118
+ function ReloadFadeCategories(apply_to_objects)
119
+ if const.UseDistanceFading and rawget(_G, "EntityData") then
120
+ for name,entity_data in pairs(EntityData) do
121
+ local fade = FadeCategories[entity_data.entity and entity_data.entity.fade_category or false]
122
+ SetEntityFadeDistances(name, fade and fade.min or 0, fade and fade.max or 0)
123
+ end
124
+ if apply_to_objects and __cobjectToCObject then
125
+ MapForEach("map", function(x)
126
+ x:GenerateFadeDistances()
127
+ end)
128
+ end
129
+ end
130
+ end
131
+
132
+ AnimatedTextureObjectTypes = {
133
+ { value = pbo.Normal, text = "Normal" },
134
+ { value = pbo.PingPong, text = "Ping-Pong" },
135
+ }
136
+ DefineClass.AnimatedTextureObject =
137
+ {
138
+ __parents = { "ComponentCustomData", "Object" },
139
+
140
+ properties = {
141
+ { category = "Animated Texture", id = "anim_type", name = "Pick frame by", editor = "choice", items = function() return AnimatedTextureObjectTypes end, template = true },
142
+ { category = "Animated Texture", id = "anim_speed", name = "Speed Multiplier", editor = "number", max = 4095, min = 0, template = true },
143
+ { category = "Animated Texture", id = "sequence_time_remap", name = "Sequence time", editor = "curve4", max = 63, scale = 63, max_x = 15, scale_x = 15, template = true },
144
+ },
145
+
146
+ anim_type = pbo.Normal,
147
+ anim_speed = 1000,
148
+ sequence_time_remap = MakeLine(0, 63, 15),
149
+ }
150
+
151
+ function AnimatedTextureObject:Setanim_type(value)
152
+ self:SetFrameAnimationPlaybackOrder(value)
153
+ end
154
+
155
+ function AnimatedTextureObject:Getanim_type()
156
+ return self:GetFrameAnimationPlaybackOrder()
157
+ end
158
+
159
+ function AnimatedTextureObject:Setanim_speed(value)
160
+ self:SetFrameAnimationSpeed(value)
161
+ end
162
+
163
+ function AnimatedTextureObject:Getanim_speed()
164
+ return self:GetFrameAnimationSpeed()
165
+ end
166
+
167
+ function AnimatedTextureObject:Setsequence_time_remap(curve)
168
+ local value = (curve[1]:y()) |
169
+ (curve[2]:y() << 6) |
170
+ (curve[3]:y() << 12) |
171
+ (curve[4]:y() << 18) |
172
+ (curve[2]:x() << 24) |
173
+ (curve[3]:x() << 28)
174
+ self:SetFrameAnimationPackedCurve(value)
175
+ end
176
+
177
+ function AnimatedTextureObject:Getsequence_time_remap()
178
+ local value = self:GetFrameAnimationPackedCurve()
179
+ local curve = {
180
+ point(0, value & 0x3F),
181
+ point((value >> 24) & 0xF, (value >> 6) & 0x3F),
182
+ point((value >> 28) & 0xF, (value >> 12) & 0x3F),
183
+ point(15, (value >> 18) & 0x3F),
184
+ }
185
+ for i = 1, 4 do
186
+ curve[i] = point(curve[i]:x(), curve[i]:y(), curve[i]:y())
187
+ end
188
+ return curve
189
+ end
190
+
191
+ function AnimatedTextureObject:Init()
192
+ self:InitTextureAnimation()
193
+ self:Setanim_type(self.anim_type)
194
+ self:Setanim_speed(self.anim_speed)
195
+ self:Setsequence_time_remap(self.sequence_time_remap)
196
+ end