Upload 1816 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -0
- CommonLua/AccountStorage.lua +254 -0
- CommonLua/AlignedObj.lua +96 -0
- CommonLua/ArtTest.lua +448 -0
- CommonLua/AtmosphericParticles.lua +115 -0
- CommonLua/BaseClasses.lua +147 -0
- CommonLua/Billboards.lua +380 -0
- CommonLua/BufferedProcess.lua +298 -0
- CommonLua/CMT.lua +155 -0
- CommonLua/Camera.lua +1005 -0
- CommonLua/CameraControlUtils.lua +149 -0
- CommonLua/CameraMakeTransparent.lua +454 -0
- CommonLua/CanonizeFilename.lua +95 -0
- CommonLua/Classes/Achievement.lua +160 -0
- CommonLua/Classes/ActionFX.lua +0 -0
- CommonLua/Classes/AnimMomentHook.lua +409 -0
- CommonLua/Classes/AppearanceObject.lua +274 -0
- CommonLua/Classes/AttachViaProp.lua +262 -0
- CommonLua/Classes/AutoAttach.lua +1355 -0
- CommonLua/Classes/BaseObjects.lua +376 -0
- CommonLua/Classes/BlendEntityObj.lua +115 -0
- CommonLua/Classes/CameraEditor.lua +261 -0
- CommonLua/Classes/CharacterControl.lua +1065 -0
- CommonLua/Classes/ClassDef.lua +336 -0
- CommonLua/Classes/ClassDefFunctionObjects.lua +930 -0
- CommonLua/Classes/ClassDefSubItem.lua +1476 -0
- CommonLua/Classes/ClassDefs/ClassDef-Common.generated.lua +68 -0
- CommonLua/Classes/ClassDefs/ClassDef-Conditions.generated.lua +521 -0
- CommonLua/Classes/ClassDefs/ClassDef-Config.generated.lua +1354 -0
- CommonLua/Classes/ClassDefs/ClassDef-Default.generated.lua +193 -0
- CommonLua/Classes/ClassDefs/ClassDef-Effects.generated.lua +495 -0
- CommonLua/Classes/ClassDefs/ClassDef-PresetDefs.generated.lua +2072 -0
- CommonLua/Classes/ClassDefs/ClassDef-StoryBits.generated.lua +367 -0
- CommonLua/Classes/CodeRenderableObject.lua +1375 -0
- CommonLua/Classes/CollectionAnimator.lua +172 -0
- CommonLua/Classes/ColorModifierReason.lua +188 -0
- CommonLua/Classes/Colorization.lua +854 -0
- CommonLua/Classes/CommandObject.lua +704 -0
- CommonLua/Classes/Common.lua +74 -0
- CommonLua/Classes/Components.lua +130 -0
- CommonLua/Classes/Composite.lua +503 -0
- CommonLua/Classes/CompositeBody.lua +1428 -0
- CommonLua/Classes/Context.lua +125 -0
- CommonLua/Classes/ContinuousEffect.lua +195 -0
- CommonLua/Classes/Decal.lua +50 -0
- CommonLua/Classes/DeveloperOptions.lua +73 -0
- CommonLua/Classes/DuckingParams.lua +70 -0
- CommonLua/Classes/DumbAI.lua +392 -0
- CommonLua/Classes/EditorBase.lua +543 -0
- 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
|