1 #include "ScriptingExtensions.h"
2
3 #define SOL_CHECK_ARGUMENTS 1
4 #define SOL_PRINT_ERRORS 1
5 #define SOL_ALL_SAFETIES_ON 1
6 #include <sol/sol.hpp> // this needs to be included first
7 #include "STStringHandler.h"
8
9 #include "Campaign_Types.h"
10 #include "ContentManager.h"
11 #include "FileMan.h"
12 #include "FunctionsLibrary.h"
13 #include "Game_Events.h"
14 #include "GameInstance.h"
15 #include "GameSettings.h"
16 #include "Logger.h"
17 #include "Overhead.h"
18 #include "Quests.h"
19 #include "StrategicMap.h"
20 #include "Structure.h"
21 #include <set>
22 #include <stdexcept>
23 #include <string>
24 #include <string_theory/format>
25 #include <string_theory/string>
26
27 #define SCRIPTS_DIR "scripts"
28 #define ENTRYPOINT_SCRIPT "main.lua"
29
30 /*! \struct GAME_OPTIONS
31 \brief Options which the current game was started with */
32 struct GAME_OPTIONS;
33 /*! \struct TacticalStatusType
34 \brief Status information of the game
35 \details Accessible via the gTacticalStatusType global variable
36 */
37 struct TacticalStatusType;
38
39 static std::set<std::string> loadedScripts;
40 static bool isLuaInitialized = false;
41 static bool isLuaDisabled = false;
42 static sol::state lua;
43
44 // an increment counter used to generate unique keys for listeners
45 static unsigned int counter;
46
47 static void RegisterUserTypes();
48 static void RegisterGlobals();
49 static void RegisterLogger();
50 static void RegisterListener(std::string observable, std::string luaFunctionName);
51 static void UnregisterListener(std::string observable, std::string key);
52
JA2Require(std::string scriptFileName)53 void JA2Require(std::string scriptFileName)
54 {
55 if (isLuaInitialized)
56 {
57 throw std::runtime_error("JA2Require is not allowed after initialization");
58 }
59
60 if (loadedScripts.find(scriptFileName) != loadedScripts.end())
61 {
62 STLOGW("Script file '{}' has already been loaded", scriptFileName);
63 return;
64 }
65
66 STLOGD("Loading LUA script file: {}", scriptFileName);
67 std::string scriptbody = FileMan::fileReadText(
68 AutoSGPFile(GCM->openGameResForReading(SCRIPTS_DIR "/" + scriptFileName))
69 ).to_std_string();
70 lua.script(scriptbody, ST::format("@{}", scriptFileName).to_std_string());
71 }
72
InitScriptingEngine()73 void InitScriptingEngine()
74 {
75 loadedScripts.clear();
76 isLuaInitialized = false;
77 isLuaDisabled = false;
78 counter = 0;
79
80 if (!GCM->doesGameResExists(SCRIPTS_DIR "/" ENTRYPOINT_SCRIPT))
81 {
82 return;
83 }
84
85 try
86 {
87 SLOGD("Initializing Lua/Sol2 scripting engine");
88
89 lua = sol::state();
90 lua.open_libraries(
91 sol::lib::base,
92 sol::lib::math,
93 sol::lib::string,
94 sol::lib::table
95 );
96
97 RegisterUserTypes();
98 RegisterGlobals();
99 RegisterLogger();
100
101 JA2Require(ENTRYPOINT_SCRIPT);
102
103 isLuaInitialized = true;
104 }
105 catch (const std::exception &ex)
106 {
107 STLOGE("Lua script engine has failed to initialize:\n {}", ex.what());
108 ST::string err = "The game cannot be started due to an error in the mod scripts. Check the logs for more details.";
109 std::throw_with_nested(std::runtime_error(err.to_std_string()));
110 }
111 }
112
RegisterUserTypes()113 static void RegisterUserTypes()
114 {
115 lua.new_usertype<SECTORINFO>("SECTORINFO",
116 "ubNumAdmins", &SECTORINFO::ubNumAdmins,
117 "ubNumTroops", &SECTORINFO::ubNumTroops,
118 "ubNumElites", &SECTORINFO::ubNumElites,
119 "uiFlags", &SECTORINFO::uiFlags
120 );
121
122 lua.new_usertype<UNDERGROUND_SECTORINFO>("UNDERGROUND_SECTORINFO",
123 "ubNumAdmins", &UNDERGROUND_SECTORINFO::ubNumAdmins,
124 "ubNumTroops", &UNDERGROUND_SECTORINFO::ubNumTroops,
125 "ubNumElites", &UNDERGROUND_SECTORINFO::ubNumElites,
126 "uiFlags", &UNDERGROUND_SECTORINFO::uiFlags
127 );
128
129 lua.new_usertype<OBJECTTYPE>("OBJECTTYPE",
130 "usItem", &OBJECTTYPE::usItem,
131 "bTrap", &OBJECTTYPE::bTrap
132 );
133
134 lua.new_usertype<STRUCTURE>("STRUCTURE",
135 "sGridNo", &STRUCTURE::sGridNo,
136 "uiFlags", &STRUCTURE::fFlags
137 );
138
139 lua.new_usertype<StrategicMapElement>("StrategicMapElement",
140 "bNameId", &StrategicMapElement::bNameId,
141 "fEnemyControlled", &StrategicMapElement::fEnemyControlled,
142 "fEnemyAirControlled", &StrategicMapElement::fEnemyAirControlled
143 );
144
145 lua.new_usertype<TacticalStatusType>("TacticalStatusType",
146 "fEnemyInSector", &TacticalStatusType::fEnemyInSector,
147 "fDidGameJustStart", &TacticalStatusType::fDidGameJustStart
148 );
149
150 lua.new_usertype<STRATEGICEVENT>("STRATEGICEVENT",
151 "uiTimeStamp", &STRATEGICEVENT::uiTimeStamp,
152 "uiParam", &STRATEGICEVENT::uiParam,
153 "uiTimeOffset", &STRATEGICEVENT::uiTimeOffset,
154 "ubEventFrequency", &STRATEGICEVENT::ubEventType,
155 "ubEventKind", &STRATEGICEVENT::ubCallbackID
156 );
157
158 lua.new_usertype<GAME_OPTIONS>("GAME_OPTIONS",
159 "fGunNut", &GAME_OPTIONS::fGunNut,
160 "fSciFi", &GAME_OPTIONS::fSciFi,
161 "ubDifficultyLevel", &GAME_OPTIONS::ubDifficultyLevel,
162 "fTurnTimeLimit", &GAME_OPTIONS::fTurnTimeLimit,
163 "ubGameSaveMode", &GAME_OPTIONS::ubGameSaveMode
164 );
165
166 lua.new_usertype<SOLDIERTYPE>("SOLDIERTYPE",
167 "ubID", &SOLDIERTYPE::ubID,
168 "ubProfile", &SOLDIERTYPE::ubProfile,
169 "ubBodyType", &SOLDIERTYPE::ubBodyType,
170 "ubSoldierClass", &SOLDIERTYPE::ubSoldierClass,
171 "bTeam", &SOLDIERTYPE::bTeam,
172 "ubCivilianGroup", &SOLDIERTYPE::ubCivilianGroup,
173 "bNeutral", &SOLDIERTYPE::bNeutral,
174
175 "bLifeMax", &SOLDIERTYPE::bLifeMax,
176 "bLife", &SOLDIERTYPE::bLife,
177 "bBreath", &SOLDIERTYPE::bBreath,
178 "bBreathMax", &SOLDIERTYPE::bBreathMax,
179 "bCamo", &SOLDIERTYPE::bCamo,
180
181 "bAgility", &SOLDIERTYPE::bAgility,
182 "bDexterity", &SOLDIERTYPE::bDexterity,
183 "bExplosive", &SOLDIERTYPE::bExplosive,
184 "bLeadership", &SOLDIERTYPE::bLeadership,
185 "bMarksmanship", &SOLDIERTYPE::bMarksmanship,
186 "bMechanical", &SOLDIERTYPE::bMechanical,
187 "bMedical", &SOLDIERTYPE::bMedical,
188 "bStrength", &SOLDIERTYPE::bStrength,
189 "bWisdom", &SOLDIERTYPE::bWisdom,
190
191 "bExpLevel", &SOLDIERTYPE::bExpLevel,
192 "ubSkillTrait1", &SOLDIERTYPE::ubSkillTrait1,
193 "ubSkillTrait2", &SOLDIERTYPE::ubSkillTrait2,
194
195 "HeadPal", &SOLDIERTYPE::HeadPal,
196 "PantsPal", &SOLDIERTYPE::PantsPal,
197 "VestPal", &SOLDIERTYPE::VestPal,
198 "SkinPal", &SOLDIERTYPE::SkinPal,
199
200 "ubBattleSoundID", &SOLDIERTYPE::ubBattleSoundID
201 );
202
203 lua.new_usertype<BOOLEAN_S>("BOOLEAN_S",
204 "val", &BOOLEAN_S::val
205 );
206 lua.new_usertype<UINT8_S>("UINT8_S",
207 "val", &UINT8_S::val
208 );
209 }
210
RegisterGlobals()211 static void RegisterGlobals()
212 {
213 lua["gTacticalStatus"] = &gTacticalStatus;
214 lua["gubQuest"] = &gubQuest;
215 lua["gubFact"] = &gubFact;
216 lua["gGameOptions"] = &gGameOptions;
217
218 lua.set_function("GetCurrentSector", GetCurrentSector);
219 lua.set_function("GetSectorInfo", GetSectorInfo);
220 lua.set_function("GetUndergroundSectorInfo", GetUndergroundSectorInfo);
221
222 lua.set_function("CreateItem", CreateItem);
223 lua.set_function("CreateMoney", CreateMoney);
224 lua.set_function("PlaceItem", PlaceItem);
225
226 lua.set_function("JA2Require", JA2Require);
227 lua.set_function("require", []() { throw std::logic_error("require is not allowed. Use JA2Require instead"); });
228 lua.set_function("dofile", []() { throw std::logic_error("dofile is not allowed. Use JA2Require instead"); });
229 lua.set_function("loadfile", []() { throw std::logic_error("loadfile is not allowed. Use JA2Require instead"); });
230
231 lua.set_function("___noop", []() {});
232 lua.set_function("RegisterListener", RegisterListener);
233 lua.set_function("UnregisterListener", UnregisterListener);
234 }
235
LogLuaMessage(LogLevel level,std::string msg)236 static void LogLuaMessage(LogLevel level, std::string msg) {
237 lua_Debug info;
238 // Stack position 0 is the c function we are in
239 // Stack position 1 is the calling lua script
240 lua_getstack(lua, 1, &info);
241 lua_getinfo(lua, "S", &info);
242 LogMessage(false, level, info.short_src, msg);
243 }
244
RegisterLogger()245 static void RegisterLogger()
246 {
247 sol::table log = lua["log"].get_or_create<sol::table>();
248 log["debug"] = [](std::string msg) { LogLuaMessage(LogLevel::Debug, msg); };
249 log["info"] = [](std::string msg) { LogLuaMessage(LogLevel::Info, msg); };
250 log["warn"] = [](std::string msg) { LogLuaMessage(LogLevel::Warn, msg); };
251 log["error"] = [](std::string msg) { LogLuaMessage(LogLevel::Error, msg); };
252
253 // overrides the default print()
254 lua.set_function("print", [](std::string msg) { LogLuaMessage(LogLevel::Info, msg); });
255 }
256
257 /**
258 * Invokes a Lua function by name
259 */
260 template<typename ...A>
InvokeFunction(ST::string functionName,A...args)261 static void InvokeFunction(ST::string functionName, A... args)
262 {
263 if (isLuaDisabled)
264 {
265 SLOGE("Scripting engine has been disabled due to a previous error");
266 return;
267 }
268
269 sol::protected_function func = lua[functionName.to_std_string()];
270 if (!func.valid())
271 {
272 STLOGE("Function {} is not defined", functionName);
273 isLuaDisabled = true;
274 return;
275 }
276
277 auto result = func.call(args...);
278 if (!result.valid())
279 {
280 sol::error err = result;
281 STLOGE("Lua script had an error. Scripting engine is now DISABLED. The error was:\n{}", err.what());
282 isLuaDisabled = true;
283 }
284 }
285
286 // Creates a typed std::function out of a Lua function
287 template<typename ...A>
wrap(std::string luaFunc)288 static std::function<void(A...)> wrap(std::string luaFunc)
289 {
290 return [luaFunc](A... args) {
291 InvokeFunction(luaFunc, args...);
292 };
293 }
294
_RegisterListener(std::string observable,std::string luaFunc,ST::string key)295 static void _RegisterListener(std::string observable, std::string luaFunc, ST::string key)
296 {
297 if (isLuaInitialized)
298 {
299 throw std::runtime_error("RegisterListener is not allowed after initialization");
300 }
301
302 if (observable == "OnStructureDamaged") OnStructureDamaged.addListener(key, wrap<INT16, INT16, INT8, INT16, STRUCTURE*, UINT8, BOOLEAN>(luaFunc));
303 else if (observable == "BeforeStructureDamaged") BeforeStructureDamaged.addListener(key, wrap<INT16, INT16, INT8, INT16, STRUCTURE*, UINT32, BOOLEAN_S*>(luaFunc));
304 else if (observable == "OnAirspaceControlUpdated") OnAirspaceControlUpdated.addListener(key, wrap<>(luaFunc));
305 else if (observable == "BeforePrepareSector") BeforePrepareSector.addListener(key, wrap<>(luaFunc));
306 else if (observable == "OnSoldierCreated") OnSoldierCreated.addListener(key, wrap<SOLDIERTYPE*>(luaFunc));
307 else {
308 ST::string err = ST::format("There is no observable named '{}'", observable);
309 throw std::logic_error(err.to_std_string());
310 }
311 }
312
313 /**
314 * Registers a callback listener with an Observable, to receive notifications in Lua scripts.
315 * This function can only be used during initialization.
316 * @param observable the name of an Observable
317 * @param luaFunc name of the function handling callback
318 * @ingroup funclib-general
319 */
RegisterListener(std::string observable,std::string luaFunc)320 static void RegisterListener(std::string observable, std::string luaFunc)
321 {
322 ST::string key = ST::format("mod:{03d}", counter++);
323 _RegisterListener(observable, luaFunc, key);
324 }
325
326 /**
327 * Unregisters a listener from the Observable.
328 * This function can only be used during initialization.
329 * @param observable
330 * @param key
331 * @ingroup funclib-general
332 */
UnregisterListener(std::string observable,std::string key)333 static void UnregisterListener(std::string observable, std::string key)
334 {
335 _RegisterListener(observable, "___noop", key);
336 }
337