New updater
This commit is contained in:
parent
2245fd20c1
commit
0b50ac734d
418
src/client/component/updater.cpp
Normal file
418
src/client/component/updater.cpp
Normal 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)
|
26
src/client/component/updater.hpp
Normal file
26
src/client/component/updater.hpp
Normal 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();
|
||||
}
|
@ -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_);
|
||||
|
@ -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())
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
162
src/client/resources/ui_scripts/common.lua
Normal file
162
src/client/resources/ui_scripts/common.lua
Normal 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
|
164
src/client/resources/ui_scripts/updater.lua
Normal file
164
src/client/resources/ui_scripts/updater.lua
Normal 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)
|
@ -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
|
Loading…
Reference in New Issue
Block a user