Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Controllers/GameSettingsController.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Controllers/GameSettingsController.js +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Controllers/GameSettingsController.js @@ -184,7 +184,7 @@ return; this.loading = loading; for (let handler of this.loadingChangeHandlers) - handler(); + handler(loading); } /** Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/Controls/AISelection.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/Controls/AISelection.js +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/Controls/AISelection.js @@ -15,7 +15,7 @@ ]); this.dropdown.list = this.values.Title; - this.dropdown.list_data = this.values.Id; + this.dropdown.list_data = this.values.Id.map(x => x || "undefined"); } render() Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js @@ -111,7 +111,7 @@ let selected = this.dropdown.list_data?.[this.dropdown.selected]; this.dropdown.list = this.values.Caption; - this.dropdown.list_data = this.values.Value; + this.dropdown.list_data = this.values.Value.map(x => x || "undefined"); this.setSelectedValue(selected); Engine.ProfileStop(); } Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js @@ -41,8 +41,8 @@ { name = translate(name); } - - this.playerName.caption = name; + if (name) + this.playerName.caption = name; } }; Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Sliders/SeaLevelRiseTime.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Sliders/SeaLevelRiseTime.js +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Sliders/SeaLevelRiseTime.js @@ -14,8 +14,11 @@ render() { - this.setHidden(g_GameSettings.seaLevelRise.value === undefined); + let hidden = g_GameSettings.seaLevelRise.value === undefined; + this.setHidden(hidden); this.setEnabled(g_GameSettings.map.type != "scenario"); + if (hidden) + return; let value = g_GameSettings.seaLevelRise.value; this.sprintfValue.minutes = value; Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -251,6 +251,9 @@ { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + if (!ent) + return null; + // All units must have a template; if not then it's a nonexistent entity id. let template = cmpTemplateManager.GetCurrentTemplateName(ent); if (!template) Index: ps/trunk/source/ps/tests/test_ModIo.h =================================================================== --- ps/trunk/source/ps/tests/test_ModIo.h +++ ps/trunk/source/ps/tests/test_ModIo.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -140,10 +140,10 @@ TS_ASSERT_PARSE("{\"data\": [{\"foo\":\"bar\"}]}", "Failed to get name from el."); TS_ASSERT_PARSE("{\"data\": [{\"name\":null}]}", "Failed to get name from el."); - TS_ASSERT_PARSE("{\"data\": [{\"name\":42}]}", "Failed to get name_id from el."); // no conversion warning, but converting numbers to strings and vice-versa seems ok - TS_ASSERT_PARSE("{\"data\": [{\"name\":false}]}", "Failed to get name_id from el."); // also some script value conversion check warning - TS_ASSERT_PARSE("{\"data\": [{\"name\":{}}]}", "Failed to get name_id from el."); // also some script value conversion check warning - TS_ASSERT_PARSE("{\"data\": [{\"name\":[]}]}", "Failed to get name_id from el."); // also some script value conversion check warning + TS_ASSERT_PARSE("{\"data\": [{\"name\":42}]}", "Failed to get name_id from el."); // 'name' works, integers implicitly convertible to string. + TS_ASSERT_PARSE("{\"data\": [{\"name\":false}]}", "Failed to get name_id from el."); // 'name' works, booleans implicitly convertible to string. + TS_ASSERT_PARSE("{\"data\": [{\"name\":{}}]}", "Failed to get name from el."); // Fails at 'name', not convertible to string. + TS_ASSERT_PARSE("{\"data\": [{\"name\":[]}]}", "Failed to get name from el."); // Fails at 'name', not convertible to string. TS_ASSERT_PARSE("{\"data\": [{\"name\":\"foobar\"}]}", "Failed to get name_id from el."); TS_ASSERT_PARSE("{\"data\": [{\"name\":\"\",\"name_id\":\"\",\"summary\":\"\"}]}", "modfile not an object."); Index: ps/trunk/source/scriptinterface/ScriptConversions.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptConversions.cpp +++ ps/trunk/source/scriptinterface/ScriptConversions.cpp @@ -18,6 +18,7 @@ #include "precompiled.h" #include "ScriptConversions.h" +#include "ScriptExceptions.h" #include "ScriptExtraHeaders.h" #include "graphics/Entity.h" @@ -26,14 +27,19 @@ #include "ps/CLogger.h" #include "ps/CStr.h" -#define FAIL(msg) STMT(LOGERROR(msg); return false) +// Catch the raised exception right away to ensure the stack trace gets printed. +#define FAIL(msg) STMT(ScriptException::Raise(rq, msg); ScriptException::CatchPending(rq); return false) -// Implicit type conversions often hide bugs, so warn about them -#define WARN_IF_NOT(c, v) STMT(if (!(c)) { JS::WarnUTF8(rq.cx, "Script value conversion check failed: %s (got type %s)", #c, JS::InformalValueTypeName(v)); }) +// Implicit type conversions often hide bugs, so fail. +#define FAIL_IF_NOT(c, v) STMT(if (!(c)) { \ + ScriptException::Raise(rq, "Script value conversion check failed: %s (got type %s)", #c, JS::InformalValueTypeName(v)); \ + ScriptException::CatchPending(rq); \ + return false; \ +}) template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, bool& out) { - WARN_IF_NOT(v.isBoolean(), v); + FAIL_IF_NOT(v.isBoolean(), v); out = JS::ToBoolean(v); return true; } @@ -41,7 +47,7 @@ template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, float& out) { double tmp; - WARN_IF_NOT(v.isNumber(), v); + FAIL_IF_NOT(v.isNumber(), v); if (!JS::ToNumber(rq.cx, v, &tmp)) return false; out = tmp; @@ -50,7 +56,7 @@ template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, double& out) { - WARN_IF_NOT(v.isNumber(), v); + FAIL_IF_NOT(v.isNumber(), v); if (!JS::ToNumber(rq.cx, v, &out)) return false; return true; @@ -58,7 +64,7 @@ template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, i32& out) { - WARN_IF_NOT(v.isNumber(), v); + FAIL_IF_NOT(v.isNumber(), v); if (!JS::ToInt32(rq.cx, v, &out)) return false; return true; @@ -66,7 +72,7 @@ template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, u32& out) { - WARN_IF_NOT(v.isNumber(), v); + FAIL_IF_NOT(v.isNumber(), v); if (!JS::ToUint32(rq.cx, v, &out)) return false; return true; @@ -74,7 +80,7 @@ template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, u16& out) { - WARN_IF_NOT(v.isNumber(), v); + FAIL_IF_NOT(v.isNumber(), v); if (!JS::ToUint16(rq.cx, v, &out)) return false; return true; @@ -83,7 +89,7 @@ template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, u8& out) { u16 tmp; - WARN_IF_NOT(v.isNumber(), v); + FAIL_IF_NOT(v.isNumber(), v); if (!JS::ToUint16(rq.cx, v, &tmp)) return false; out = (u8)tmp; @@ -92,7 +98,7 @@ template<> bool ScriptInterface::FromJSVal(const ScriptRequest& rq, JS::HandleValue v, std::wstring& out) { - WARN_IF_NOT(v.isString() || v.isNumber() || v.isBoolean(), v); // allow implicit boolean/number conversions + FAIL_IF_NOT(v.isString() || v.isNumber() || v.isBoolean(), v); // allow implicit boolean/number conversions JS::RootedString str(rq.cx, JS::ToString(rq.cx, v)); if (!str) FAIL("Argument must be convertible to a string"); @@ -318,4 +324,4 @@ } #undef FAIL -#undef WARN_IF_NOT +#undef FAIL_IF_NOT Index: ps/trunk/source/scriptinterface/ScriptExceptions.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptExceptions.cpp +++ ps/trunk/source/scriptinterface/ScriptExceptions.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -22,6 +22,7 @@ #include "ps/CLogger.h" #include "ps/CStr.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" bool ScriptException::IsPending(const ScriptRequest& rq) @@ -36,11 +37,11 @@ JS::RootedValue excn(rq.cx); ENSURE(JS_GetPendingException(rq.cx, &excn)); + JS_ClearPendingException(rq.cx); if (excn.isUndefined()) { LOGERROR("JavaScript error: (undefined)"); - JS_ClearPendingException(rq.cx); return true; } @@ -50,7 +51,6 @@ CStr error; ScriptInterface::FromJSVal(rq, excn, error); LOGERROR("JavaScript error: %s", error); - JS_ClearPendingException(rq.cx); return true; } @@ -62,7 +62,6 @@ CStr error; ScriptInterface::FromJSVal(rq, excn, error); LOGERROR("JavaScript error: %s", error); - JS_ClearPendingException(rq.cx); return true; } @@ -78,10 +77,11 @@ JS::RootedObject stackObj(rq.cx, ExceptionStackOrNull(excnObj)); JS::RootedValue stackVal(rq.cx, JS::ObjectOrNullValue(stackObj)); + if (!stackVal.isNull()) { std::string stackText; - ScriptInterface::FromJSVal(rq, stackVal, stackText); + ScriptFunction::Call(rq, stackVal, "toString", stackText); std::istringstream stream(stackText); for (std::string line; std::getline(stream, line);) @@ -93,7 +93,6 @@ // When running under Valgrind, print more information in the error message // VALGRIND_PRINTF_BACKTRACE("->"); - JS_ClearPendingException(rq.cx); return true; } @@ -101,6 +100,10 @@ { va_list ap; va_start(ap, format); - JS_ReportErrorUTF8(rq.cx, format, ap); + // SM is single-threaded, so a static thread_local buffer needs no locking. + thread_local static char buffer[256]; + vsprintf_s(buffer, ARRAY_SIZE(buffer), format, ap); va_end(ap); + // Rather annoyingly, there are no va_list versions of this function, hence the preformatting above. + JS_ReportErrorUTF8(rq.cx, "%s", buffer); }