New updater

This commit is contained in:
Federico Cecchetto 2022-03-20 00:13:14 +01:00
parent 2245fd20c1
commit 0b50ac734d
10 changed files with 810 additions and 944 deletions

View File

@ -0,0 +1,418 @@
#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "scheduler.hpp"
#include "updater.hpp"
#include "version.h"
#include "game/game.hpp"
#include "game/dvars.hpp"
#include <utils/nt.hpp>
#include <utils/concurrency.hpp>
#include <utils/http.hpp>
#include <utils/cryptography.hpp>
#include <utils/io.hpp>
#include <utils/string.hpp>
#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<std::string> required_files{};
};
utils::concurrency::container<update_data_t> 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<std::string> 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<bool>([](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<bool>([](update_data_t& data_)
{
return data_.check.done;
});
}
bool is_update_download_done()
{
return update_data.access<bool>([](update_data_t& data_)
{
return data_.download.done;
});
}
bool get_update_check_status()
{
return update_data.access<bool>([](update_data_t& data_)
{
return data_.check.success;
});
}
bool get_update_download_status()
{
return update_data.access<bool>([](update_data_t& data_)
{
return data_.download.success;
});
}
bool is_update_available()
{
return update_data.access<bool>([](update_data_t& data_)
{
return data_.required_files.size() > 0;
});
}
bool is_restart_required()
{
return update_data.access<bool>([](update_data_t& data_)
{
return data_.restart_required;
});
}
std::string get_last_error()
{
return update_data.access<std::string>([](update_data_t& data_)
{
return data_.error;
});
}
std::string get_current_file()
{
return update_data.access<std::string>([](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<std::string> 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<std::vector<std::string>>([](update_data_t& data_)
{
return data_.required_files;
});
std::vector<file_data> 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)

View File

@ -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();
}

View File

@ -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<int>())
@ -95,13 +95,6 @@ namespace ui_scripting::lua
state["io"]["readfile"] = static_cast<std::string(*)(const std::string&)>(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<scripting::vector>("vector", sol::constructors<scripting::vector(float, float, float)>());
@ -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<int>(progress),
static_cast<int>(total),
static_cast<int>(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_);

View File

@ -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())
{

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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