Line data Source code
1 : /* Copyright (C) 2022 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "MapGenerator.h"
21 :
22 : #include "graphics/MapIO.h"
23 : #include "graphics/Patch.h"
24 : #include "graphics/Terrain.h"
25 : #include "lib/external_libraries/libsdl.h"
26 : #include "lib/status.h"
27 : #include "lib/timer.h"
28 : #include "lib/file/vfs/vfs_path.h"
29 : #include "maths/MathUtil.h"
30 : #include "ps/CLogger.h"
31 : #include "ps/FileIo.h"
32 : #include "ps/Profile.h"
33 : #include "ps/TaskManager.h"
34 : #include "ps/scripting/JSInterface_VFS.h"
35 : #include "scriptinterface/FunctionWrapper.h"
36 : #include "scriptinterface/ScriptContext.h"
37 : #include "scriptinterface/ScriptConversions.h"
38 : #include "scriptinterface/ScriptInterface.h"
39 : #include "scriptinterface/JSON.h"
40 : #include "simulation2/helpers/MapEdgeTiles.h"
41 :
42 : #include <string>
43 : #include <vector>
44 :
45 : // TODO: Maybe this should be optimized depending on the map size.
46 : constexpr int RMS_CONTEXT_SIZE = 96 * 1024 * 1024;
47 :
48 : extern bool IsQuitRequested();
49 :
50 : static bool
51 0 : MapGeneratorInterruptCallback(JSContext* UNUSED(cx))
52 : {
53 : // This may not use SDL_IsQuitRequested(), because it runs in a thread separate to SDL, see SDL_PumpEvents
54 0 : if (IsQuitRequested())
55 : {
56 0 : LOGWARNING("Quit requested!");
57 0 : return false;
58 : }
59 :
60 0 : return true;
61 : }
62 :
63 6 : CMapGeneratorWorker::CMapGeneratorWorker(ScriptInterface* scriptInterface) :
64 6 : m_ScriptInterface(scriptInterface)
65 : {
66 : // If something happens before we initialize, that's a failure
67 6 : m_Progress = -1;
68 6 : }
69 :
70 12 : CMapGeneratorWorker::~CMapGeneratorWorker()
71 : {
72 : // Cancel or wait for the task to end.
73 6 : m_WorkerThread.CancelOrWait();
74 6 : }
75 :
76 0 : void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings)
77 : {
78 0 : std::lock_guard<std::mutex> lock(m_WorkerMutex);
79 :
80 : // Set progress to positive value
81 0 : m_Progress = 1;
82 0 : m_ScriptPath = scriptFile;
83 0 : m_Settings = settings;
84 :
85 : // Start generating the map asynchronously.
86 0 : m_WorkerThread = Threading::TaskManager::Instance().PushTask([this]() {
87 0 : PROFILE2("Map Generation");
88 :
89 0 : std::shared_ptr<ScriptContext> mapgenContext = ScriptContext::CreateContext(RMS_CONTEXT_SIZE);
90 :
91 : // Enable the script to be aborted
92 0 : JS_AddInterruptCallback(mapgenContext->GetGeneralJSContext(), MapGeneratorInterruptCallback);
93 :
94 0 : m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenContext);
95 :
96 : // Run map generation scripts
97 0 : if (!Run() || m_Progress > 0)
98 : {
99 : // Don't leave progress in an unknown state, if generator failed, set it to -1
100 0 : std::lock_guard<std::mutex> lock(m_WorkerMutex);
101 0 : m_Progress = -1;
102 : }
103 :
104 0 : SAFE_DELETE(m_ScriptInterface);
105 :
106 : // At this point the random map scripts are done running, so the thread has no further purpose
107 : // and can die. The data will be stored in m_MapData already if successful, or m_Progress
108 : // will contain an error value on failure.
109 0 : });
110 0 : }
111 :
112 0 : bool CMapGeneratorWorker::Run()
113 : {
114 0 : ScriptRequest rq(m_ScriptInterface);
115 :
116 : // Parse settings
117 0 : JS::RootedValue settingsVal(rq.cx);
118 0 : if (!Script::ParseJSON(rq, m_Settings, &settingsVal) && settingsVal.isUndefined())
119 : {
120 0 : LOGERROR("CMapGeneratorWorker::Run: Failed to parse settings");
121 0 : return false;
122 : }
123 :
124 : // Prevent unintentional modifications to the settings object by random map scripts
125 0 : if (!Script::FreezeObject(rq, settingsVal, true))
126 : {
127 0 : LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings");
128 0 : return false;
129 : }
130 :
131 : // Init RNG seed
132 0 : u32 seed = 0;
133 0 : if (!Script::HasProperty(rq, settingsVal, "Seed") ||
134 0 : !Script::GetProperty(rq, settingsVal, "Seed", seed))
135 0 : LOGWARNING("CMapGeneratorWorker::Run: No seed value specified - using 0");
136 :
137 0 : InitScriptInterface(seed);
138 :
139 0 : RegisterScriptFunctions_MapGenerator();
140 :
141 : // Copy settings to global variable
142 0 : JS::RootedValue global(rq.cx, rq.globalValue());
143 0 : if (!Script::SetProperty(rq, global, "g_MapSettings", settingsVal, true, true))
144 : {
145 0 : LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings");
146 0 : return false;
147 : }
148 :
149 : // Load RMS
150 0 : LOGMESSAGE("Loading RMS '%s'", m_ScriptPath.string8());
151 0 : if (!m_ScriptInterface->LoadGlobalScriptFile(m_ScriptPath))
152 : {
153 0 : LOGERROR("CMapGeneratorWorker::Run: Failed to load RMS '%s'", m_ScriptPath.string8());
154 0 : return false;
155 : }
156 :
157 0 : return true;
158 : }
159 :
160 : #define REGISTER_MAPGEN_FUNC(func) \
161 : ScriptFunction::Register<&CMapGeneratorWorker::func, ScriptInterface::ObjectFromCBData<CMapGeneratorWorker>>(rq, #func);
162 : #define REGISTER_MAPGEN_FUNC_NAME(func, name) \
163 : ScriptFunction::Register<&CMapGeneratorWorker::func, ScriptInterface::ObjectFromCBData<CMapGeneratorWorker>>(rq, name);
164 :
165 6 : void CMapGeneratorWorker::InitScriptInterface(const u32 seed)
166 : {
167 6 : m_ScriptInterface->SetCallbackData(static_cast<void*>(this));
168 :
169 6 : m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG);
170 6 : m_MapGenRNG.seed(seed);
171 :
172 : // VFS
173 6 : JSI_VFS::RegisterScriptFunctions_ReadOnlySimulationMaps(*m_ScriptInterface);
174 :
175 : // Globalscripts may use VFS script functions
176 6 : m_ScriptInterface->LoadGlobalScripts();
177 :
178 : // File loading
179 12 : ScriptRequest rq(m_ScriptInterface);
180 6 : REGISTER_MAPGEN_FUNC_NAME(LoadScripts, "LoadLibrary");
181 6 : REGISTER_MAPGEN_FUNC_NAME(LoadHeightmap, "LoadHeightmapImage");
182 6 : REGISTER_MAPGEN_FUNC(LoadMapTerrain);
183 :
184 : // Engine constants
185 :
186 : // Length of one tile of the terrain grid in metres.
187 : // Useful to transform footprint sizes to the tilegrid coordinate system.
188 6 : m_ScriptInterface->SetGlobal("TERRAIN_TILE_SIZE", static_cast<int>(TERRAIN_TILE_SIZE));
189 :
190 : // Number of impassable tiles at the map border
191 6 : m_ScriptInterface->SetGlobal("MAP_BORDER_WIDTH", static_cast<int>(MAP_EDGE_TILES));
192 6 : }
193 :
194 0 : void CMapGeneratorWorker::RegisterScriptFunctions_MapGenerator()
195 : {
196 0 : ScriptRequest rq(m_ScriptInterface);
197 :
198 : // Template functions
199 0 : REGISTER_MAPGEN_FUNC(GetTemplate);
200 0 : REGISTER_MAPGEN_FUNC(TemplateExists);
201 0 : REGISTER_MAPGEN_FUNC(FindTemplates);
202 0 : REGISTER_MAPGEN_FUNC(FindActorTemplates);
203 :
204 : // Progression and profiling
205 0 : REGISTER_MAPGEN_FUNC(SetProgress);
206 0 : REGISTER_MAPGEN_FUNC(GetMicroseconds);
207 0 : REGISTER_MAPGEN_FUNC(ExportMap);
208 0 : }
209 :
210 : #undef REGISTER_MAPGEN_FUNC
211 : #undef REGISTER_MAPGEN_FUNC_NAME
212 :
213 0 : int CMapGeneratorWorker::GetProgress()
214 : {
215 0 : std::lock_guard<std::mutex> lock(m_WorkerMutex);
216 0 : return m_Progress;
217 : }
218 :
219 0 : double CMapGeneratorWorker::GetMicroseconds()
220 : {
221 0 : return JS_Now();
222 : }
223 :
224 0 : Script::StructuredClone CMapGeneratorWorker::GetResults()
225 : {
226 0 : std::lock_guard<std::mutex> lock(m_WorkerMutex);
227 0 : return m_MapData;
228 : }
229 :
230 0 : void CMapGeneratorWorker::ExportMap(JS::HandleValue data)
231 : {
232 : // Copy results
233 0 : std::lock_guard<std::mutex> lock(m_WorkerMutex);
234 0 : m_MapData = Script::WriteStructuredClone(ScriptRequest(m_ScriptInterface), data);
235 0 : m_Progress = 0;
236 0 : }
237 :
238 0 : void CMapGeneratorWorker::SetProgress(int progress)
239 : {
240 : // Copy data
241 0 : std::lock_guard<std::mutex> lock(m_WorkerMutex);
242 :
243 0 : if (progress >= m_Progress)
244 0 : m_Progress = progress;
245 : else
246 0 : LOGWARNING("The random map script tried to reduce the loading progress from %d to %d", m_Progress, progress);
247 0 : }
248 :
249 0 : CParamNode CMapGeneratorWorker::GetTemplate(const std::string& templateName)
250 : {
251 0 : const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetOnlyChild();
252 0 : if (!templateRoot.IsOk())
253 0 : LOGERROR("Invalid template found for '%s'", templateName.c_str());
254 :
255 0 : return templateRoot;
256 : }
257 :
258 0 : bool CMapGeneratorWorker::TemplateExists(const std::string& templateName)
259 : {
260 0 : return m_TemplateLoader.TemplateExists(templateName);
261 : }
262 :
263 0 : std::vector<std::string> CMapGeneratorWorker::FindTemplates(const std::string& path, bool includeSubdirectories)
264 : {
265 0 : return m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES);
266 : }
267 :
268 0 : std::vector<std::string> CMapGeneratorWorker::FindActorTemplates(const std::string& path, bool includeSubdirectories)
269 : {
270 0 : return m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES);
271 : }
272 :
273 24 : bool CMapGeneratorWorker::LoadScripts(const VfsPath& libraryName)
274 : {
275 : // Ignore libraries that are already loaded
276 24 : if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end())
277 0 : return true;
278 :
279 : // Mark this as loaded, to prevent it recursively loading itself
280 24 : m_LoadedLibraries.insert(libraryName);
281 :
282 48 : VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath();
283 48 : VfsPaths pathnames;
284 :
285 : // Load all scripts in mapgen directory
286 24 : Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames);
287 24 : if (ret == INFO::OK)
288 : {
289 240 : for (const VfsPath& p : pathnames)
290 : {
291 216 : LOGMESSAGE("Loading map generator script '%s'", p.string8());
292 :
293 216 : if (!m_ScriptInterface->LoadGlobalScriptFile(p))
294 : {
295 0 : LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8());
296 0 : return false;
297 : }
298 : }
299 : }
300 : else
301 : {
302 : // Some error reading directory
303 : wchar_t error[200];
304 0 : LOGERROR("CMapGeneratorWorker::LoadScripts: Error reading scripts in directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error))));
305 0 : return false;
306 : }
307 :
308 24 : return true;
309 : }
310 :
311 0 : JS::Value CMapGeneratorWorker::LoadHeightmap(const VfsPath& filename)
312 : {
313 0 : std::vector<u16> heightmap;
314 0 : if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK)
315 : {
316 0 : LOGERROR("Could not load heightmap file '%s'", filename.string8());
317 0 : return JS::UndefinedValue();
318 : }
319 :
320 0 : ScriptRequest rq(m_ScriptInterface);
321 0 : JS::RootedValue returnValue(rq.cx);
322 0 : Script::ToJSVal(rq, &returnValue, heightmap);
323 0 : return returnValue;
324 : }
325 :
326 : // See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering
327 0 : JS::Value CMapGeneratorWorker::LoadMapTerrain(const VfsPath& filename)
328 : {
329 0 : ScriptRequest rq(m_ScriptInterface);
330 :
331 0 : if (!VfsFileExists(filename))
332 : {
333 0 : ScriptException::Raise(rq, "Terrain file \"%s\" does not exist!", filename.string8().c_str());
334 0 : return JS::UndefinedValue();
335 : }
336 :
337 0 : CFileUnpacker unpacker;
338 0 : unpacker.Read(filename, "PSMP");
339 :
340 0 : if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION)
341 : {
342 0 : ScriptException::Raise(rq, "Could not load terrain file \"%s\" too old version!", filename.string8().c_str());
343 0 : return JS::UndefinedValue();
344 : }
345 :
346 : // unpack size
347 0 : ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize();
348 0 : size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1;
349 :
350 : // unpack heightmap
351 0 : std::vector<u16> heightmap;
352 0 : heightmap.resize(SQR(verticesPerSide));
353 0 : unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16));
354 :
355 : // unpack texture names
356 0 : size_t textureCount = unpacker.UnpackSize();
357 0 : std::vector<std::string> textureNames;
358 0 : textureNames.reserve(textureCount);
359 0 : for (size_t i = 0; i < textureCount; ++i)
360 : {
361 0 : CStr texturename;
362 0 : unpacker.UnpackString(texturename);
363 0 : textureNames.push_back(texturename);
364 : }
365 :
366 : // unpack texture IDs per tile
367 0 : ssize_t tilesPerSide = patchesPerSide * PATCH_SIZE;
368 0 : std::vector<CMapIO::STileDesc> tiles;
369 0 : tiles.resize(size_t(SQR(tilesPerSide)));
370 0 : unpacker.UnpackRaw(&tiles[0], sizeof(CMapIO::STileDesc) * tiles.size());
371 :
372 : // reorder by patches and store and save texture IDs per tile
373 0 : std::vector<u16> textureIDs;
374 0 : for (ssize_t x = 0; x < tilesPerSide; ++x)
375 : {
376 0 : size_t patchX = x / PATCH_SIZE;
377 0 : size_t offX = x % PATCH_SIZE;
378 0 : for (ssize_t y = 0; y < tilesPerSide; ++y)
379 : {
380 0 : size_t patchY = y / PATCH_SIZE;
381 0 : size_t offY = y % PATCH_SIZE;
382 : // m_Priority and m_Tex2Index unused
383 0 : textureIDs.push_back(tiles[(patchY * patchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)].m_Tex1Index);
384 : }
385 : }
386 :
387 0 : JS::RootedValue returnValue(rq.cx);
388 :
389 0 : Script::CreateObject(
390 : rq,
391 : &returnValue,
392 : "height", heightmap,
393 : "textureNames", textureNames,
394 : "textureIDs", textureIDs);
395 :
396 0 : return returnValue;
397 : }
398 :
399 : //////////////////////////////////////////////////////////////////////////////////
400 : //////////////////////////////////////////////////////////////////////////////////
401 :
402 0 : CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker(nullptr))
403 : {
404 0 : }
405 :
406 0 : CMapGenerator::~CMapGenerator()
407 : {
408 0 : delete m_Worker;
409 0 : }
410 :
411 0 : void CMapGenerator::GenerateMap(const VfsPath& scriptFile, const std::string& settings)
412 : {
413 0 : m_Worker->Initialize(scriptFile, settings);
414 0 : }
415 :
416 0 : int CMapGenerator::GetProgress()
417 : {
418 0 : return m_Worker->GetProgress();
419 : }
420 :
421 0 : Script::StructuredClone CMapGenerator::GetResults()
422 : {
423 0 : return m_Worker->GetResults();
424 3 : }
|