diff --git a/src/client/component/updater.cpp b/src/client/component/updater.cpp new file mode 100644 index 00000000..9700d72f --- /dev/null +++ b/src/client/component/updater.cpp @@ -0,0 +1,418 @@ +#include +#include "loader/component_loader.hpp" + +#include "scheduler.hpp" +#include "updater.hpp" + +#include "version.h" + +#include "game/game.hpp" +#include "game/dvars.hpp" + +#include +#include +#include +#include +#include +#include + +#define MASTER "https://master.fed0001.xyz/" + +#define FILES_PATH "files.json" +#define FILES_PATH_DEV "files-dev.json" + +#define DATA_PATH "data/" +#define DATA_PATH_DEV "data-dev/" + +#define ERR_UPDATE_CHECK_FAIL "Failed to check for updates" +#define ERR_DOWNLOAD_FAIL "Failed to download file " +#define ERR_WRITE_FAIL "Failed to write file " + +namespace updater +{ + namespace + { + game::dvar_t* cl_auto_update; + bool has_tried_update = false; + + struct status + { + bool done; + bool success; + }; + + struct file_data + { + std::string name; + std::string data; + }; + + struct update_data_t + { + bool restart_required{}; + bool cancelled{}; + status check{}; + status download{}; + std::string error{}; + std::string current_file{}; + std::vector required_files{}; + }; + + utils::concurrency::container update_data; + + std::string select(const std::string& main, const std::string& develop) + { + if (GIT_BRANCH == "develop"s) + { + return develop; + } + + return main; + } + + std::string get_data_path() + { + if (GIT_BRANCH == "develop"s) + { + return DATA_PATH_DEV; + } + + return DATA_PATH; + } + + void set_update_check_status(bool done, bool success, const std::string& error = {}) + { + update_data.access([done, success, error](update_data_t& data_) + { + data_.check.done = done; + data_.check.success = success; + data_.error = error; + }); + } + + void set_update_download_status(bool done, bool success, const std::string& error = {}) + { + update_data.access([done, success, error](update_data_t& data_) + { + data_.download.done = done; + data_.download.success = success; + data_.error = error; + }); + } + + bool check_file(const std::string& name, const std::string& sha) + { + std::string data; + if (!utils::io::read_file(name, &data)) + { + return false; + } + + if (utils::cryptography::sha1::compute(data, true) != sha) + { + return false; + } + + return true; + } + + std::string load_binary_name() + { + utils::nt::library self; + return self.get_name(); + } + + std::string get_binary_name() + { + static const auto name = load_binary_name(); + return name; + } + + std::string get_time_str() + { + return utils::string::va("%i", uint32_t(time(nullptr))); + } + + std::optional download_file(const std::string& name) + { + return utils::http::get_data(MASTER + select(DATA_PATH, DATA_PATH_DEV) + name + "?" + get_time_str()); + } + + bool is_update_cancelled() + { + return update_data.access([](update_data_t& data_) + { + return data_.cancelled; + }); + } + + bool write_file(const std::string& name, const std::string& data) + { + if (get_binary_name() == name && + utils::io::file_exists(name) && + !utils::io::move_file(name, name + ".old")) + { + return false; + } + + return utils::io::write_file(name, data); + } + + void delete_old_file() + { + utils::io::remove_file(get_binary_name() + ".old"); + } + + void reset_data() + { + update_data.access([](update_data_t& data_) + { + data_ = {}; + }); + } + } + + void relaunch() + { + utils::nt::relaunch_self("-singleplayer"); + utils::nt::terminate(); + } + + void set_has_tried_update(bool tried) + { + has_tried_update = tried; + } + + bool get_has_tried_update() + { + return has_tried_update; + } + + bool auto_updates_enabled() + { + return cl_auto_update->current.enabled; + } + + bool is_update_check_done() + { + return update_data.access([](update_data_t& data_) + { + return data_.check.done; + }); + } + + bool is_update_download_done() + { + return update_data.access([](update_data_t& data_) + { + return data_.download.done; + }); + } + + bool get_update_check_status() + { + return update_data.access([](update_data_t& data_) + { + return data_.check.success; + }); + } + + bool get_update_download_status() + { + return update_data.access([](update_data_t& data_) + { + return data_.download.success; + }); + } + + bool is_update_available() + { + return update_data.access([](update_data_t& data_) + { + return data_.required_files.size() > 0; + }); + } + + bool is_restart_required() + { + return update_data.access([](update_data_t& data_) + { + return data_.restart_required; + }); + } + + std::string get_last_error() + { + return update_data.access([](update_data_t& data_) + { + return data_.error; + }); + } + + std::string get_current_file() + { + return update_data.access([](update_data_t& data_) + { + return data_.current_file; + }); + } + + void cancel_update() + { +#ifdef DEBUG + printf("[Updater] Cancelling update\n"); +#endif + + return update_data.access([](update_data_t& data_) + { + data_.cancelled = true; + }); + } + + void start_update_check() + { + cancel_update(); + reset_data(); + +#ifdef DEBUG + printf("[Updater] starting update check\n"); +#endif + + scheduler::once([]() + { + const auto files_data = utils::http::get_data(MASTER + select(FILES_PATH, FILES_PATH_DEV) + "?" + get_time_str()); + + if (is_update_cancelled()) + { + reset_data(); + return; + } + + if (!files_data.has_value()) + { + set_update_check_status(true, false, ERR_UPDATE_CHECK_FAIL); + return; + } + + rapidjson::Document j; + j.Parse(files_data.value().data()); + + if (!j.IsArray()) + { + set_update_check_status(true, false, ERR_UPDATE_CHECK_FAIL); + return; + } + + std::vector required_files; + + const auto files = j.GetArray(); + for (const auto& file : files) + { + if (!file.IsArray() || file.Size() != 3 || !file[0].IsString() || !file[2].IsString()) + { + continue; + } + + const auto name = file[0].GetString(); + const auto sha = file[2].GetString(); + + if (!check_file(name, sha)) + { + if (get_binary_name() == name) + { + update_data.access([](update_data_t& data_) + { + data_.restart_required = true; + }); + } + +#ifdef DEBUG + printf("[Updater] need file %s\n", name); +#endif + + required_files.push_back(name); + } + } + + update_data.access([&required_files](update_data_t& data_) + { + data_.check.done = true; + data_.check.success = true; + data_.required_files = required_files; + }); + }, scheduler::pipeline::async); + } + + void start_update_download() + { +#ifdef DEBUG + printf("[Updater] starting update download\n"); +#endif + + if (!is_update_check_done() || !get_update_check_status() || is_update_cancelled()) + { + return; + }// + + scheduler::once([]() + { + const auto required_files = update_data.access>([](update_data_t& data_) + { + return data_.required_files; + }); + + std::vector downloads; + + for (const auto& file : required_files) + { + update_data.access([file](update_data_t& data_) + { + data_.current_file = file; + }); + +#ifdef DEBUG + printf("[Updater] downloading file %s\n", file.data()); +#endif + + const auto data = download_file(file); + + if (is_update_cancelled()) + { + reset_data(); + return; + } + + if (!data.has_value()) + { + set_update_download_status(true, false, ERR_DOWNLOAD_FAIL + file); + return; + } + + downloads.push_back({file, data.value()}); + } + + for (const auto& download : downloads) + { + if (!write_file(download.name, download.data)) + { + set_update_download_status(true, false, ERR_WRITE_FAIL + download.name); + return; + } + } + + set_update_download_status(true, true); + }, scheduler::pipeline::async); + } + + class component final : public component_interface + { + public: + void post_unpack() override + { + delete_old_file(); + cl_auto_update = dvars::register_bool("cg_auto_update", true, game::DVAR_FLAG_SAVED); + } + }; +} + +REGISTER_COMPONENT(updater::component) diff --git a/src/client/component/updater.hpp b/src/client/component/updater.hpp new file mode 100644 index 00000000..9a3dd45e --- /dev/null +++ b/src/client/component/updater.hpp @@ -0,0 +1,26 @@ +#pragma once + +namespace updater +{ + void relaunch(); + + void set_has_tried_update(bool tried); + bool get_has_tried_update(); + bool auto_updates_enabled(); + + bool is_update_available(); + bool is_update_check_done(); + bool get_update_check_status(); + + bool is_update_download_done(); + bool get_update_download_status(); + + bool is_restart_required(); + + std::string get_last_error(); + std::string get_current_file(); + + void start_update_check(); + void start_update_download(); + void cancel_update(); +} \ No newline at end of file diff --git a/src/client/game/ui_scripting/lua/context.cpp b/src/client/game/ui_scripting/lua/context.cpp index 1d7bdd9b..a146787a 100644 --- a/src/client/game/ui_scripting/lua/context.cpp +++ b/src/client/game/ui_scripting/lua/context.cpp @@ -10,6 +10,8 @@ #include "../../../component/scripting.hpp" #include "../../../component/command.hpp" #include "../../../component/fastfiles.hpp" +#include "../../../component/updater.hpp" +#include "../../../component/localized_strings.hpp" #include "component/game_console.hpp" #include "component/scheduler.hpp" @@ -25,8 +27,6 @@ namespace ui_scripting::lua { namespace { - const auto json_script = utils::nt::load_resource(LUA_JSON_SCRIPT); - scripting::script_value script_convert(const sol::lua_value& value) { if (value.is()) @@ -95,13 +95,6 @@ namespace ui_scripting::lua state["io"]["readfile"] = static_cast(utils::io::read_file); } - void setup_json(sol::state& state) - { - const auto json = state.safe_script(json_script, &sol::script_pass_on_error); - handle_error(json); - state["json"] = json; - } - void setup_vector_type(sol::state& state) { auto vector_type = state.new_usertype("vector", sol::constructors()); @@ -435,121 +428,10 @@ namespace ui_scripting::lua return ::game::mod_folder; }; - static int request_id{}; - game_type["httpget"] = [](const game&, const std::string& url) + game_type["addlocalizedstring"] = [](const game&, const std::string& string, + const std::string& value) { - const auto id = request_id++; - ::scheduler::once([url, id]() - { - const auto result = utils::http::get_data(url); - ::scheduler::once([result, id] - { - event event; - event.name = "http_request_done"; - - if (result.has_value()) - { - event.arguments = {id, true, result.value()}; - } - else - { - event.arguments = {id, false}; - } - - notify(event); - }, ::scheduler::pipeline::lui); - }, ::scheduler::pipeline::async); - return id; - }; - - game_type["httpgettofile"] = [](const game&, const std::string& url, - const std::string& dest) - { - const auto id = request_id++; - ::scheduler::once([url, id, dest]() - { - auto last_report = std::chrono::high_resolution_clock::now(); - const auto result = utils::http::get_data(url, {}, [&last_report, id](size_t progress, size_t total, size_t speed) - { - const auto now = std::chrono::high_resolution_clock::now(); - if (now - last_report < 100ms && progress < total) - { - return; - } - - last_report = now; - - ::scheduler::once([id, progress, total, speed] - { - event event; - event.name = "http_request_progress"; - event.arguments = { - id, - static_cast(progress), - static_cast(total), - static_cast(speed) - }; - - notify(event); - }, ::scheduler::pipeline::lui); - }); - - if (result.has_value()) - { - const auto write = utils::io::write_file(dest, result.value(), false); - ::scheduler::once([result, id, write]() - { - event event; - event.name = "http_request_done"; - event.arguments = {id, true, write}; - - notify(event); - }, ::scheduler::pipeline::lui); - } - else - { - ::scheduler::once([result, id]() - { - event event; - event.name = "http_request_done"; - event.arguments = {id, false}; - - notify(event); - }, ::scheduler::pipeline::lui); - } - }, ::scheduler::pipeline::async); - return id; - }; - - game_type["sha"] = [](const game&, const std::string& data) - { - return utils::string::to_upper(utils::cryptography::sha1::compute(data, true)); - }; - - game_type["environment"] = [](const game&) - { - return GIT_BRANCH; - }; - - game_type["binaryname"] = [](const game&) - { - utils::nt::library self; - return self.get_name(); - }; - - game_type["relaunch"] = [](const game&) - { - utils::nt::relaunch_self("-singleplayer"); - utils::nt::terminate(); - }; - - game_type["isdebugbuild"] = [](const game&) - { -#ifdef DEBUG - return true; -#else - return false; -#endif + localized_strings::override(string, value); }; struct player @@ -677,6 +559,30 @@ namespace ui_scripting::lua state["LUI"] = state["luiglobals"]["LUI"]; state["Engine"] = state["luiglobals"]["Engine"]; state["Game"] = state["luiglobals"]["Game"]; + + auto updater_table = sol::table::create(state.lua_state()); + + updater_table["relaunch"] = updater::relaunch; + + updater_table["sethastriedupdate"] = updater::set_has_tried_update; + updater_table["gethastriedupdate"] = updater::get_has_tried_update; + updater_table["autoupdatesenabled"] = updater::auto_updates_enabled; + + updater_table["startupdatecheck"] = updater::start_update_check; + updater_table["isupdatecheckdone"] = updater::is_update_check_done; + updater_table["getupdatecheckstatus"] = updater::get_update_check_status; + updater_table["isupdateavailable"] = updater::is_update_available; + + updater_table["startupdatedownload"] = updater::start_update_download; + updater_table["isupdatedownloaddone"] = updater::is_update_download_done; + updater_table["getupdatedownloadstatus"] = updater::get_update_download_status; + updater_table["cancelupdate"] = updater::cancel_update; + updater_table["isrestartrequired"] = updater::is_restart_required; + + updater_table["getlasterror"] = updater::get_last_error; + updater_table["getcurrentfile"] = updater::get_current_file; + + state["updater"] = updater_table; } } @@ -694,7 +600,6 @@ namespace ui_scripting::lua sol::lib::table); setup_io(this->state_); - setup_json(this->state_); setup_vector_type(this->state_); setup_game_type(this->state_, this->event_handler_, this->scheduler_); setup_lui_types(this->state_, this->event_handler_, this->scheduler_); diff --git a/src/client/game/ui_scripting/lua/engine.cpp b/src/client/game/ui_scripting/lua/engine.cpp index 34fde3d1..d1bd4245 100644 --- a/src/client/game/ui_scripting/lua/engine.cpp +++ b/src/client/game/ui_scripting/lua/engine.cpp @@ -14,7 +14,8 @@ namespace ui_scripting::lua::engine { namespace { - const auto updater_script = utils::nt::load_resource(LUI_UPDATER_MENU); + const auto lui_common = utils::nt::load_resource(LUI_COMMON); + const auto lui_updater = utils::nt::load_resource(LUI_UPDATER); void handle_key_event(const int key, const int down) { @@ -72,7 +73,8 @@ namespace ui_scripting::lua::engine clear_converted_functions(); get_scripts().clear(); - load_code(updater_script); + load_code(lui_common); + // load_code(lui_updater); for (const auto& path : filesystem::get_search_paths()) { diff --git a/src/client/resource.hpp b/src/client/resource.hpp index a5a6b649..0a6f027a 100644 --- a/src/client/resource.hpp +++ b/src/client/resource.hpp @@ -2,23 +2,11 @@ #define ID_ICON 102 -#define IMAGE_SPLASH 300 -#define IMAGE_LOGO 301 +#define MENU_MAIN 300 -#define DW_ENTITLEMENT_CONFIG 302 -#define DW_SOCIAL_CONFIG 303 -#define DW_MM_CONFIG 304 -#define DW_LOOT_CONFIG 305 -#define DW_STORE_CONFIG 306 -#define DW_MOTD 307 -#define DW_FASTFILE 308 -#define DW_PLAYLISTS 309 +#define TLS_DLL 301 -#define MENU_MAIN 310 +#define ICON_IMAGE 302 -#define TLS_DLL 311 - -#define ICON_IMAGE 312 - -#define LUA_JSON_SCRIPT 313 -#define LUI_UPDATER_MENU 314 +#define LUI_COMMON 303 +#define LUI_UPDATER 304 diff --git a/src/client/resource.rc b/src/client/resource.rc index b42a1e0c..1b9843d6 100644 --- a/src/client/resource.rc +++ b/src/client/resource.rc @@ -97,8 +97,8 @@ ID_ICON ICON "resources/icon.ico" MENU_MAIN RCDATA "resources/main.html" -LUA_JSON_SCRIPT RCDATA "resources/json.lua" -LUI_UPDATER_MENU RCDATA "resources/updater.lua" +LUI_COMMON RCDATA "resources/ui_scripts/common.lua" +LUI_UPDATER RCDATA "resources/ui_scripts/updater.lua" #ifdef _DEBUG TLS_DLL RCDATA "../../build/bin/x64/Debug/tlsdll.dll" diff --git a/src/client/resources/json.lua b/src/client/resources/json.lua deleted file mode 100644 index 711ef786..00000000 --- a/src/client/resources/json.lua +++ /dev/null @@ -1,388 +0,0 @@ --- --- json.lua --- --- Copyright (c) 2020 rxi --- --- Permission is hereby granted, free of charge, to any person obtaining a copy of --- this software and associated documentation files (the "Software"), to deal in --- the Software without restriction, including without limitation the rights to --- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies --- of the Software, and to permit persons to whom the Software is furnished to do --- so, subject to the following conditions: --- --- The above copyright notice and this permission notice shall be included in all --- copies or substantial portions of the Software. --- --- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE --- SOFTWARE. --- - -local json = { _version = "0.1.2" } - -------------------------------------------------------------------------------- --- Encode -------------------------------------------------------------------------------- - -local encode - -local escape_char_map = { - [ "\\" ] = "\\", - [ "\"" ] = "\"", - [ "\b" ] = "b", - [ "\f" ] = "f", - [ "\n" ] = "n", - [ "\r" ] = "r", - [ "\t" ] = "t", -} - -local escape_char_map_inv = { [ "/" ] = "/" } -for k, v in pairs(escape_char_map) do - escape_char_map_inv[v] = k -end - - -local function escape_char(c) - return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) -end - - -local function encode_nil(val) - return "null" -end - - -local function encode_table(val, stack) - local res = {} - stack = stack or {} - - -- Circular reference? - if stack[val] then error("circular reference") end - - stack[val] = true - - if rawget(val, 1) ~= nil or next(val) == nil then - -- Treat as array -- check keys are valid and it is not sparse - local n = 0 - for k in pairs(val) do - if type(k) ~= "number" then - error("invalid table: mixed or invalid key types") - end - n = n + 1 - end - if n ~= #val then - error("invalid table: sparse array") - end - -- Encode - for i, v in ipairs(val) do - table.insert(res, encode(v, stack)) - end - stack[val] = nil - return "[" .. table.concat(res, ",") .. "]" - - else - -- Treat as an object - for k, v in pairs(val) do - if type(k) ~= "string" then - error("invalid table: mixed or invalid key types") - end - table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) - end - stack[val] = nil - return "{" .. table.concat(res, ",") .. "}" - end -end - - -local function encode_string(val) - return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' -end - - -local function encode_number(val) - -- Check for NaN, -inf and inf - if val ~= val or val <= -math.huge or val >= math.huge then - error("unexpected number value '" .. tostring(val) .. "'") - end - return string.format("%.14g", val) -end - - -local type_func_map = { - [ "nil" ] = encode_nil, - [ "table" ] = encode_table, - [ "string" ] = encode_string, - [ "number" ] = encode_number, - [ "boolean" ] = tostring, -} - - -encode = function(val, stack) - local t = type(val) - local f = type_func_map[t] - if f then - return f(val, stack) - end - error("unexpected type '" .. t .. "'") -end - - -function json.encode(val) - return ( encode(val) ) -end - - -------------------------------------------------------------------------------- --- Decode -------------------------------------------------------------------------------- - -local parse - -local function create_set(...) - local res = {} - for i = 1, select("#", ...) do - res[ select(i, ...) ] = true - end - return res -end - -local space_chars = create_set(" ", "\t", "\r", "\n") -local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") -local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") -local literals = create_set("true", "false", "null") - -local literal_map = { - [ "true" ] = true, - [ "false" ] = false, - [ "null" ] = nil, -} - - -local function next_char(str, idx, set, negate) - for i = idx, #str do - if set[str:sub(i, i)] ~= negate then - return i - end - end - return #str + 1 -end - - -local function decode_error(str, idx, msg) - local line_count = 1 - local col_count = 1 - for i = 1, idx - 1 do - col_count = col_count + 1 - if str:sub(i, i) == "\n" then - line_count = line_count + 1 - col_count = 1 - end - end - error( string.format("%s at line %d col %d", msg, line_count, col_count) ) -end - - -local function codepoint_to_utf8(n) - -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa - local f = math.floor - if n <= 0x7f then - return string.char(n) - elseif n <= 0x7ff then - return string.char(f(n / 64) + 192, n % 64 + 128) - elseif n <= 0xffff then - return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) - elseif n <= 0x10ffff then - return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, - f(n % 4096 / 64) + 128, n % 64 + 128) - end - error( string.format("invalid unicode codepoint '%x'", n) ) -end - - -local function parse_unicode_escape(s) - local n1 = tonumber( s:sub(1, 4), 16 ) - local n2 = tonumber( s:sub(7, 10), 16 ) - -- Surrogate pair? - if n2 then - return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) - else - return codepoint_to_utf8(n1) - end -end - - -local function parse_string(str, i) - local res = "" - local j = i + 1 - local k = j - - while j <= #str do - local x = str:byte(j) - - if x < 32 then - decode_error(str, j, "control character in string") - - elseif x == 92 then -- `\`: Escape - res = res .. str:sub(k, j - 1) - j = j + 1 - local c = str:sub(j, j) - if c == "u" then - local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) - or str:match("^%x%x%x%x", j + 1) - or decode_error(str, j - 1, "invalid unicode escape in string") - res = res .. parse_unicode_escape(hex) - j = j + #hex - else - if not escape_chars[c] then - decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") - end - res = res .. escape_char_map_inv[c] - end - k = j + 1 - - elseif x == 34 then -- `"`: End of string - res = res .. str:sub(k, j - 1) - return res, j + 1 - end - - j = j + 1 - end - - decode_error(str, i, "expected closing quote for string") -end - - -local function parse_number(str, i) - local x = next_char(str, i, delim_chars) - local s = str:sub(i, x - 1) - local n = tonumber(s) - if not n then - decode_error(str, i, "invalid number '" .. s .. "'") - end - return n, x -end - - -local function parse_literal(str, i) - local x = next_char(str, i, delim_chars) - local word = str:sub(i, x - 1) - if not literals[word] then - decode_error(str, i, "invalid literal '" .. word .. "'") - end - return literal_map[word], x -end - - -local function parse_array(str, i) - local res = {} - local n = 1 - i = i + 1 - while 1 do - local x - i = next_char(str, i, space_chars, true) - -- Empty / end of array? - if str:sub(i, i) == "]" then - i = i + 1 - break - end - -- Read token - x, i = parse(str, i) - res[n] = x - n = n + 1 - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "]" then break end - if chr ~= "," then decode_error(str, i, "expected ']' or ','") end - end - return res, i -end - - -local function parse_object(str, i) - local res = {} - i = i + 1 - while 1 do - local key, val - i = next_char(str, i, space_chars, true) - -- Empty / end of object? - if str:sub(i, i) == "}" then - i = i + 1 - break - end - -- Read key - if str:sub(i, i) ~= '"' then - decode_error(str, i, "expected string for key") - end - key, i = parse(str, i) - -- Read ':' delimiter - i = next_char(str, i, space_chars, true) - if str:sub(i, i) ~= ":" then - decode_error(str, i, "expected ':' after key") - end - i = next_char(str, i + 1, space_chars, true) - -- Read value - val, i = parse(str, i) - -- Set - res[key] = val - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "}" then break end - if chr ~= "," then decode_error(str, i, "expected '}' or ','") end - end - return res, i -end - - -local char_func_map = { - [ '"' ] = parse_string, - [ "0" ] = parse_number, - [ "1" ] = parse_number, - [ "2" ] = parse_number, - [ "3" ] = parse_number, - [ "4" ] = parse_number, - [ "5" ] = parse_number, - [ "6" ] = parse_number, - [ "7" ] = parse_number, - [ "8" ] = parse_number, - [ "9" ] = parse_number, - [ "-" ] = parse_number, - [ "t" ] = parse_literal, - [ "f" ] = parse_literal, - [ "n" ] = parse_literal, - [ "[" ] = parse_array, - [ "{" ] = parse_object, -} - - -parse = function(str, idx) - local chr = str:sub(idx, idx) - local f = char_func_map[chr] - if f then - return f(str, idx) - end - decode_error(str, idx, "unexpected character '" .. chr .. "'") -end - - -function json.decode(str) - if type(str) ~= "string" then - error("expected argument of type string, got " .. type(str)) - end - local res, idx = parse(str, next_char(str, 1, space_chars, true)) - idx = next_char(str, idx, space_chars, true) - if idx <= #str then - decode_error(str, idx, "trailing garbage") - end - return res -end - - -return json diff --git a/src/client/resources/ui_scripts/common.lua b/src/client/resources/ui_scripts/common.lua new file mode 100644 index 00000000..a5dd91e0 --- /dev/null +++ b/src/client/resources/ui_scripts/common.lua @@ -0,0 +1,162 @@ +menucallbacks = {} +originalmenus = {} +stack = {} + +LUI.MenuBuilder.m_types_build["generic_waiting_popup_"] = function (menu, event) + local oncancel = stack.oncancel + local popup = LUI.MenuBuilder.BuildRegisteredType("waiting_popup", { + message_text = stack.text, + isLiveWithCancel = true, + cancel_func = function(...) + local args = {...} + oncancel() + LUI.FlowManager.RequestLeaveMenu(args[1]) + end + }) + + popup.text = popup:getchildren()[7] + + stack = { + ret = popup + } + + return popup +end + +LUI.MenuBuilder.m_types_build["generic_yes_no_popup_"] = function() + local callback = stack.callback + local popup = LUI.MenuBuilder.BuildRegisteredType("generic_yesno_popup", { + popup_title = stack.title, + message_text = stack.text, + yes_action = function() + callback(true) + end, + no_action = function() + callback(false) + end + }) + + stack = { + ret = popup + } + + return popup +end + +LUI.MenuBuilder.m_types_build["generic_confirmation_popup_"] = function() + local popup = LUI.MenuBuilder.BuildRegisteredType( "generic_confirmation_popup", { + cancel_will_close = false, + popup_title = stack.title, + message_text = stack.text, + button_text = stack.buttontext, + confirmation_action = stack.callback + }) + + stack = { + ret = popup + } + + return stack.ret +end + +LUI.onmenuopen = function(name, callback) + if (not LUI.MenuBuilder.m_types_build[name]) then + return + end + + if (not menucallbacks[name]) then + menucallbacks[name] = {} + end + + table.insert(menucallbacks[name], callback) + + if (not originalmenus[name]) then + originalmenus[name] = LUI.MenuBuilder.m_types_build[name] + LUI.MenuBuilder.m_types_build[name] = function(...) + local args = {...} + local menu = originalmenus[name](table.unpack(args)) + + for k, v in luiglobals.next, menucallbacks[name] do + v(menu, table.unpack(args)) + end + + return menu + end + end +end + +local addoptionstextinfo = LUI.Options.AddOptionTextInfo +LUI.Options.AddOptionTextInfo = function(menu) + local result = addoptionstextinfo(menu) + menu.optionTextInfo = result + return result +end + +LUI.addmenubutton = function(name, data) + LUI.onmenuopen(name, function(menu) + if (not menu.list) then + return + end + + local button = menu:AddButton(data.text, data.callback, nil, true, nil, { + desc_text = data.description + }) + + local buttonlist = menu:getChildById(menu.type .. "_list") + + if (data.id) then + button.id = data.id + end + + if (data.index) then + buttonlist:removeElement(button) + buttonlist:insertElement(button, data.index) + end + + local hintbox = menu.optionTextInfo + menu:removeElement(hintbox) + + LUI.Options.InitScrollingList(menu.list, nil) + menu.optionTextInfo = LUI.Options.AddOptionTextInfo(menu) + end) +end + +LUI.openmenu = function(menu, args) + stack = args + LUI.FlowManager.RequestAddMenu(nil, menu) + return stack.ret +end + +LUI.openpopupmenu = function(menu, args) + stack = args + LUI.FlowManager.RequestPopupMenu(nil, menu) + return stack.ret +end + +LUI.yesnopopup = function(data) + for k, v in luiglobals.next, data do + stack[k] = v + end + LUI.FlowManager.RequestPopupMenu(nil, "generic_yes_no_popup_") + return stack.ret +end + +LUI.confirmationpopup = function(data) + for k, v in luiglobals.next, data do + stack[k] = v + end + LUI.FlowManager.RequestPopupMenu(nil, "generic_confirmation_popup_") + return stack.ret +end + +function userdata_:getchildren() + local children = {} + local first = self:getFirstChild() + + while (first) do + table.insert(children, first) + first = first:getNextSibling() + end + + return children +end diff --git a/src/client/resources/ui_scripts/updater.lua b/src/client/resources/ui_scripts/updater.lua new file mode 100644 index 00000000..01d77360 --- /dev/null +++ b/src/client/resources/ui_scripts/updater.lua @@ -0,0 +1,164 @@ +updatecancelled = false +taskinterval = 100 + +updater.cancelupdate() + +function startupdatecheck(popup, autoclose) + updatecancelled = false + + local callback = function() + if (not updater.getupdatecheckstatus()) then + if (autoclose) then + LUI.FlowManager.RequestLeaveMenu(popup) + return + end + + popup.text:setText("Error: " .. updater.getlasterror()) + return + end + + if (not updater.isupdateavailable()) then + if (autoclose) then + LUI.FlowManager.RequestLeaveMenu(popup) + return + end + + popup.text:setText("No updates available") + return + end + + LUI.yesnopopup({ + title = "NOTICE", + text = "An update is available, proceed with installation?", + callback = function(result) + if (result) then + startupdatedownload(popup, autoclose) + else + LUI.FlowManager.RequestLeaveMenu(popup) + end + end + }) + end + + updater.startupdatecheck() + createtask({ + done = updater.isupdatecheckdone, + cancelled = isupdatecancelled, + callback = callback, + interval = taskinterval + }) +end + +function startupdatedownload(popup, autoclose) + updater.startupdatedownload() + + local textupdate = nil + local previousfile = nil + textupdate = game:oninterval(function() + local file = updater.getcurrentfile() + if (file == previousfile) then + return + end + + file = previousfile + popup.text:setText("Downloading file " .. updater.getcurrentfile() .. "...") + end, 10) + + local callback = function() + textupdate:clear() + + if (not updater.getupdatedownloadstatus()) then + if (autoclose) then + LUI.FlowManager.RequestLeaveMenu(popup) + return + end + + popup.text:setText("Error: " .. updater.getlasterror()) + return + end + + popup.text:setText("Update successful") + + if (updater.isrestartrequired()) then + LUI.confirmationpopup({ + title = "RESTART REQUIRED", + text = "Update requires restart", + buttontext = "RESTART", + callback = function() + updater.relaunch() + end + }) + else + if (LUI.mp_menus) then + Engine.Exec("lui_restart; lui_open mp_main_menu") + else + Engine.Exec("lui_restart") + end + end + + if (autoclose) then + LUI.FlowManager.RequestLeaveMenu(popup) + end + end + + createtask({ + done = updater.isupdatedownloaddone, + cancelled = isupdatecancelled, + callback = callback, + interval = taskinterval + }) +end + +function updaterpopup(oncancel) + return LUI.openpopupmenu("generic_waiting_popup_", { + oncancel = oncancel, + withcancel = true, + text = "Checking for updates..." + }) +end + +function createtask(data) + local interval = nil + interval = game:oninterval(function() + if (data.cancelled()) then + interval:clear() + return + end + + if (data.done()) then + interval:clear() + data.callback() + end + end, data.interval) + return interval +end + +function isupdatecancelled() + return updatecancelled +end + +function tryupdate(autoclose) + updatecancelled = false + local popup = updaterpopup(function() + updater.cancelupdate() + updatecancelled = true + end) + + startupdatecheck(popup, autoclose) +end + +function tryautoupdate() + if (not updater.autoupdatesenabled()) then + return + end + + if (not updater.gethastriedupdate()) then + game:ontimeout(function() + updater.sethastriedupdate(true) + tryupdate(true) + end, 100) + end +end + +LUI.onmenuopen("mp_main_menu", tryautoupdate) +LUI.onmenuopen("main_lockout", tryautoupdate) \ No newline at end of file diff --git a/src/client/resources/updater.lua b/src/client/resources/updater.lua deleted file mode 100644 index 2e3b79f6..00000000 --- a/src/client/resources/updater.lua +++ /dev/null @@ -1,411 +0,0 @@ -menucallbacks = {} -originalmenus = {} - -LUI.onmenuopen = function(name, callback) - if (not LUI.MenuBuilder.m_types_build[name]) then - return - end - - if (not menucallbacks[name]) then - menucallbacks[name] = {} - end - - table.insert(menucallbacks[name], callback) - - if (not originalmenus[name]) then - originalmenus[name] = LUI.MenuBuilder.m_types_build[name] - LUI.MenuBuilder.m_types_build[name] = function(...) - local args = {...} - local menu = originalmenus[name](table.unpack(args)) - - for k, v in luiglobals.next, menucallbacks[name] do - v(menu, table.unpack(args)) - end - - return menu - end - end -end - -local addoptionstextinfo = LUI.Options.AddOptionTextInfo -LUI.Options.AddOptionTextInfo = function(menu) - local result = addoptionstextinfo(menu) - menu.optionTextInfo = result - return result -end - -LUI.addmenubutton = function(name, data) - LUI.onmenuopen(name, function(menu) - if (not menu.list) then - return - end - - local button = menu:AddButton(data.text, data.callback, nil, true, nil, { - desc_text = data.description - }) - - local buttonlist = menu:getChildById(menu.type .. "_list") - - if (data.id) then - button.id = data.id - end - - if (data.index) then - buttonlist:removeElement(button) - buttonlist:insertElement(button, data.index) - end - - local hintbox = menu.optionTextInfo - menu:removeElement(hintbox) - - LUI.Options.InitScrollingList(menu.list, nil) - menu.optionTextInfo = LUI.Options.AddOptionTextInfo(menu) - end) -end - -stack = {} -LUI.openmenu = function(menu, args) - stack = args - LUI.FlowManager.RequestAddMenu(nil, menu) - return stack.ret -end - -LUI.openpopupmenu = function(menu, args) - stack = args - LUI.FlowManager.RequestPopupMenu(nil, menu) - return stack.ret -end - -LUI.onmenuopen("main_lockout", function() - if (game:isdebugbuild() or not Engine.GetDvarBool("cg_autoUpdate")) then - return - end - - if (game:sharedget("has_tried_updating") == "") then - game:ontimeout(function() - game:sharedset("has_tried_updating", "1") - tryupdate(true) - end, 0) - end -end) - -stack = {} -LUI.MenuBuilder.m_types_build["generic_waiting_popup_"] = function (menu, event) - local oncancel = stack.oncancel - local popup = LUI.MenuBuilder.BuildRegisteredType("waiting_popup", { - message_text = stack.text, - isLiveWithCancel = true, - cancel_func = function(...) - local args = {...} - oncancel() - LUI.common_menus.CommonPopups.CancelCSSDownload(table.unpack(args)) - end - }) - - stack = { - ret = popup - } - - return popup -end - -LUI.MenuBuilder.m_types_build["generic_yes_no_popup_"] = function() - local callback = stack.callback - local popup = LUI.MenuBuilder.BuildRegisteredType("generic_yesno_popup", { - popup_title = stack.title, - message_text = stack.text, - yes_action = function() - callback(true) - end, - no_action = function() - callback(false) - end - }) - - stack = { - ret = popup - } - - return popup -end - -LUI.MenuBuilder.m_types_build["generic_confirmation_popup_"] = function() - local popup = LUI.MenuBuilder.BuildRegisteredType( "generic_confirmation_popup", { - cancel_will_close = false, - popup_title = stack.title, - message_text = stack.text, - button_text = stack.buttontext, - confirmation_action = stack.callback - }) - - stack = { - ret = popup - } - - return stack.ret -end - -LUI.yesnopopup = function(data) - for k, v in luiglobals.next, data do - stack[k] = v - end - LUI.FlowManager.RequestPopupMenu(nil, "generic_yes_no_popup_") - return stack.ret -end - -LUI.confirmationpopup = function(data) - for k, v in luiglobals.next, data do - stack[k] = v - end - LUI.FlowManager.RequestPopupMenu(nil, "generic_confirmation_popup_") - return stack.ret -end - -function updaterpopup(oncancel) - return LUI.openpopupmenu("generic_waiting_popup_", { - oncancel = oncancel, - withcancel = true, - text = "Checking for updates..." - }) -end - -function deleteoldfile() - io.removefile(game:binaryname() .. ".old") -end - -deleteoldfile() - -function verifyfiles(files) - local needed = {} - local updatebinary = false - - if (game:isdebugbuild()) then - return needed, updatebinary - end - - local binaryname = game:binaryname() - - for i = 1, #files do - local name = files[i][1] - - if (io.fileexists(name) and game:sha(io.readfile(name)) == files[i][3]) then - goto continue - end - - if (name == binaryname) then - updatebinary = true - end - - table.insert(needed, files[i]) - - ::continue:: - end - - return needed, updatebinary -end - -local canceled = false - -function downloadfiles(popup, files, callback) - deleteoldfile() - - local text = popup:getchildren()[7] - local folder = game:environment() == "develop" and "data-dev" or "data" - - if (#files == 0) then - callback(true) - return - end - - local total = 0 - local filedownloaded = function() - total = total + 1 - if (total == #files) then - callback(true) - end - end - - local download = nil - local stop = false - download = function(index) - if (canceled or stop) then - return - end - - local filename = files[index][1] - - local url = "https://master.fed0001.xyz/" .. folder .. "/" .. filename .. "?" .. os.time() - text:setText(string.format("Downloading file [%i/%i]\n%s", index, #files, filename)) - - if (filename == game:binaryname()) then - io.movefile(filename, filename .. ".old") - end - - httprequesttofile(url, filename, function(valid, success) - if (not valid) then - callback(false, "Invalid server response") - stop = true - return - end - - if (not success) then - callback(false, "Failed to write file " .. filename) - stop = true - return - end - - filedownloaded() - - if (files[index + 1]) then - download(index + 1) - else - callback(true) - end - end) - end - - download(1) -end - -function userdata_:getchildren() - local children = {} - local first = self:getFirstChild() - - while (first) do - table.insert(children, first) - first = first:getNextSibling() - end - - return children -end - -function tryupdate(autoclose) - canceled = false - local popup = updaterpopup(function() - canceled = true - end) - - local text = popup:getchildren()[7] - local file = game:environment() == "develop" and "files-dev.json" or "files.json" - local url = "https://master.fed0001.xyz/" .. file .. "?" .. os.time() - - httprequest(url, function(valid, data) - if (not valid) then - text:setText("Update check failed: Invalid server response") - return - end - - local valid = pcall(function() - local files = json.decode(data) - local needed, updatebinary = verifyfiles(files) - - if (#needed == 0) then - text:setText("No updates available") - if (autoclose) then - LUI.common_menus.CommonPopups.CancelCSSDownload(popup, {}) - end - return - end - - local download = function() - local gotresult = false - downloadfiles(popup, needed, function(result, error) - if (gotresult or canceled) then - return - end - - gotresult = true - - if (not result) then - text:setText("Update failed: " .. error) - return - end - - if (updatebinary) then - LUI.confirmationpopup({ - title = "RESTART REQUIRED", - text = "Update requires restart", - buttontext = "RESTART", - callback = function() - game:relaunch() - end - }) - return - end - - if (result and #needed > 0) then - game:executecommand("lui_restart") - return - end - - text:setText("Update successful!") - - if (autoclose) then - LUI.common_menus.CommonPopups.CancelCSSDownload(popup, {}) - end - end) - end - - if (autoclose) then - download() - else - LUI.yesnopopup({ - title = "NOTICE", - text = "An update is available, proceed with installation?", - callback = function(result) - if (not result) then - LUI.common_menus.CommonPopups.CancelCSSDownload(popup, {}) - else - download() - end - end - }) - end - end) - - if (not valid) then - text:setText("Update failed: unknown error") - end - end) -end - -LUI.tryupdating = function(autoclose) - tryupdate(autoclose) -end - -function httprequest(url, callback) - local request = game:httpget(url) - local listener = nil - listener = game:onnotify("http_request_done", function(id, valid, data) - if (id ~= request) then - return - end - - listener:clear() - callback(valid, data) - end) -end - -function httprequesttofile(url, dest, callback) - local request = game:httpgettofile(url, dest) - local listener = nil - listener = game:onnotify("http_request_done", function(id, valid, success) - if (id ~= request) then - return - end - - listener:clear() - callback(valid, success) - end) -end - -local localize = Engine.Localize -Engine.Localize = function(...) - local args = {...} - - if (type(args[1]) == "string" and args[1]:sub(1, 2) == "$_") then - return args[1]:sub(3, -1) - end - - return localize(table.unpack(args)) -end \ No newline at end of file