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