Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mod download UI integration #595

Merged
merged 106 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
54a4ddc
feat: add ModDownloader API declaration
Alystrasz Aug 21, 2023
0207d0d
docs: add documentation to header
Alystrasz Aug 21, 2023
7a87357
refactor: declare header as .hpp file
Alystrasz Aug 21, 2023
8a40845
feat: add basic FetchModsListFromAPI implementation
Alystrasz Aug 21, 2023
e5f84ee
feat: add global ModDownloader instance
Alystrasz Aug 21, 2023
8a16ec5
feat: add command to call FetchModsListFromAPI from in-game console
Alystrasz Aug 21, 2023
f4e0a99
fix: adjust header file extension
Alystrasz Aug 21, 2023
053fa83
feat: complete FetchModsListFromAPI implementation
Alystrasz Aug 22, 2023
35afd31
style: format
Alystrasz Aug 22, 2023
180b432
style: format
Alystrasz Aug 22, 2023
5dfda30
feat: add IsModAuthorized implementation
Alystrasz Aug 23, 2023
b4584e7
feat: add a ConCommand to invoke IsModAuthorized
Alystrasz Aug 23, 2023
61f47dd
fix: use std::string instead of char* in VerifiedModVersion struct
Alystrasz Aug 23, 2023
f7e9dd8
feat: add DownloadMod skeleton
Alystrasz Aug 23, 2023
d30fa9f
feat: add a ConCommand to invoke DownloadMod
Alystrasz Aug 23, 2023
2dde906
fix: use std::string instead of char* in VerifiedModDetails struct
Alystrasz Aug 23, 2023
9b041d7
feat: add FetchModFromDistantStore implementation
Alystrasz Aug 23, 2023
5a1a54c
fix: DownloadMod uses a thread to avoid UI freezing
Alystrasz Aug 23, 2023
c4af53d
refactor: use a map to store mod version information to ease their re…
Alystrasz Aug 23, 2023
7ed2f43
feat: bypass checksum verification for now
Alystrasz Aug 23, 2023
29a7351
style: apply clang formatting
Alystrasz Aug 23, 2023
840771d
docs: add archive download destination log
Alystrasz Aug 24, 2023
edc656b
refactor: replace all char pointers with std::string
Alystrasz Aug 24, 2023
5b957f6
build: add minizip dependency
Alystrasz Aug 25, 2023
16bedc6
feat: list files in downloaded mod archive
Alystrasz Aug 25, 2023
af92411
feat: compute mod files extraction destination
Alystrasz Aug 25, 2023
8a2e3ec
feat: create directories while extracting mod archive
Alystrasz Aug 25, 2023
85f416a
fix: do not stop mod archive extraction when trying to create an alre…
Alystrasz Aug 25, 2023
07e4c40
build: disable LZMA compression in minizip
Alystrasz Aug 26, 2023
b22b06f
feat: ensure files are present in zip archive
Alystrasz Aug 26, 2023
b631df8
feat: add file creation skeleton
Alystrasz Aug 29, 2023
1e5878a
feat: open zip file to prepare its extraction
Alystrasz Aug 29, 2023
b3b6ca4
style: apply clang format
Alystrasz Aug 29, 2023
53e33ef
refactor: reorganize debug prints
Alystrasz Aug 30, 2023
8e0a766
feat: enforce Thunderstore format for remote mods
Alystrasz Aug 30, 2023
9b68db0
style: apply clang format
Alystrasz Aug 31, 2023
0c75bfc
style: use camelCase everywhere
Alystrasz Aug 31, 2023
741c9f5
style: apply clang format
Alystrasz Aug 31, 2023
79d0812
refactor: set IsModAuthorized function as private and remove associat…
Alystrasz Aug 31, 2023
ff40df5
feat: add a flag to bypass mod verification process
Alystrasz Sep 4, 2023
efb3d89
fix: rename flag in header
Alystrasz Sep 4, 2023
3b0c502
feat: remove mod archive after download
Alystrasz Sep 4, 2023
a3d87f2
fix: remove mod archive when archive checksum verification fails
Alystrasz Sep 4, 2023
e737d38
feat: add a clean subroutine to the ExtractMod function
Alystrasz Sep 4, 2023
c64aaab
fix: disable curl verbose mode
Alystrasz Sep 4, 2023
6f5c2f2
style: apply clang format
Alystrasz Sep 4, 2023
24bb06e
fix: fail mod fetching HTTP request if status >= 400
Alystrasz Sep 5, 2023
ea3a10a
fix: mod archive fetching failure is now tolerated
Alystrasz Sep 7, 2023
82427b8
feat: add IsModLegit function skeleton
Alystrasz Sep 7, 2023
c4c9c41
style: apply clang format
Alystrasz Sep 7, 2023
3452aa3
Merge branch 'feat/auto-dl' of github.com:Alystrasz/NorthstarLauncher…
Alystrasz Sep 8, 2023
61efbc2
fix: do not try to always read bufferSize bytes
Alystrasz Sep 10, 2023
1e6c04e
fix: convert hash to string using bytes raw values
Alystrasz Sep 10, 2023
15043b6
style: move files in CMakeLists.txt
Alystrasz Sep 10, 2023
5576aa9
refactor: pass string views to FetchModFromDistantStore
Alystrasz Sep 11, 2023
471baf7
refactor: pass string views to IsModAuthorized
Alystrasz Sep 11, 2023
d6d2630
refactor: pass string view to IsModLegit
Alystrasz Sep 11, 2023
e8bbfe8
style: update NorthstarDLL/CMakeLists.txt
Alystrasz Sep 14, 2023
6d094c1
style: replace tabs with spaces in NorthstarDLL/CMakeLists.txt
Alystrasz Sep 14, 2023
1196fe1
Merge branch 'main' into feat/auto-dl
Alystrasz Sep 14, 2023
1f35a65
Merge branch 'main' into feat/auto-dl
Alystrasz Sep 14, 2023
b915456
Merge branch 'main' into feat/auto-dl
Alystrasz Sep 24, 2023
1f3d954
style: remove typo
Alystrasz Sep 24, 2023
377ca2c
style: rewrite else case
Alystrasz Sep 24, 2023
7b38a4b
style: use PascalCase for function names
Alystrasz Sep 24, 2023
99127c1
refactor: initialize bufferSize on declaration
Alystrasz Sep 24, 2023
915583d
refactor: bufferSize is const
Alystrasz Sep 24, 2023
4f2394e
refactor: move modState initialization to header
Alystrasz Sep 25, 2023
9cebe1d
Merge branch 'R2Northstar:main' into feat/auto-dl
Alystrasz Oct 6, 2023
2626848
Merge branch 'R2Northstar:main' into feat/auto-dl
Alystrasz Oct 11, 2023
6a2af4b
Merge branch 'main' into feat/auto-dl
Alystrasz Oct 12, 2023
8480576
fix: check archive information status
Alystrasz Oct 13, 2023
059bb6e
fix: abort mod extraction if information cannot be retrieved from
Alystrasz Oct 13, 2023
fbb6af4
Merge branch 'main' into feat/auto-dl
Alystrasz Oct 15, 2023
4dbaa47
refactor: use GetArray method to read mod versions
Alystrasz Oct 15, 2023
16edd06
Merge branch 'main' into feat/auto-dl
Alystrasz Oct 15, 2023
7548c47
fix: use mod name as dependency prefix with bypass flag
Alystrasz Oct 15, 2023
8a1792c
feat: add a command line arg to specify verified mods list URL
Alystrasz Oct 16, 2023
b281073
Merge branch 'main' into feat/auto-dl
Alystrasz Oct 16, 2023
bd350b5
refactor: remove useless GetModInstallProgress method signature
Alystrasz Oct 16, 2023
ee3d3ff
refactor: CancelDownload method takes no input argument
Alystrasz Oct 16, 2023
606d752
Merge branch 'R2Northstar:main' into feat/auto-dl
Alystrasz Oct 23, 2023
1990e7f
Merge branch 'main' into feat/auto-dl
Alystrasz Oct 23, 2023
c664eed
refactor: set IsModAuthorized as public member
Alystrasz Oct 29, 2023
c3d23fc
feat: expose IsModAuthorized function to squirrel VM
Alystrasz Oct 29, 2023
0224cd4
feat: expose DownloadMod function to squirrel VM
Alystrasz Oct 29, 2023
edcdc87
feat: expose mod install state to squirrel VM
Alystrasz Oct 29, 2023
884022f
style: clang formatting
Alystrasz Oct 29, 2023
cbd3442
feat: update mod install status
Alystrasz Oct 29, 2023
249dfd3
feat: GetModArchiveSize function computes archive uncompressed size
Alystrasz Oct 31, 2023
b079efb
feat: update stats during archive extraction
Alystrasz Oct 31, 2023
5400928
feat: add curl download callback to update UI
Alystrasz Nov 1, 2023
dfff7dc
fix: reset progression stats between mod downloading and extraction
Alystrasz Nov 1, 2023
970ee94
style: clang formatting
Alystrasz Nov 1, 2023
f2bd1e5
Merge branch 'R2Northstar:main' into feat/auto-dl-integration
Alystrasz Nov 3, 2023
ea095ff
Merge branch 'main' into feat/auto-dl-integration
Alystrasz Nov 5, 2023
a4acfb5
feat: invoke FetchModsListFromAPI function on app start
Alystrasz Nov 5, 2023
1a993f5
refactor: remove test console commands
Alystrasz Nov 5, 2023
4ac1b50
Merge branch 'R2Northstar:main' into feat/auto-dl-integration
Alystrasz Nov 12, 2023
1b075aa
Merge branch 'main' into feat/auto-dl-integration
Alystrasz Nov 23, 2023
3bc7207
Merge branch 'main' into feat/auto-dl-integration
Alystrasz Nov 27, 2023
c656859
fix: remove mod fetching request timeout
Alystrasz Dec 2, 2023
bab9261
Merge branch 'fix/mad-timeout' into feat/auto-dl-integration
Alystrasz Dec 2, 2023
f0a64e7
Merge branch 'main' into feat/auto-dl-integration
Alystrasz Dec 4, 2023
eda53a2
Merge branch 'main' into feat/auto-dl-integration
Alystrasz Dec 11, 2023
d4ac223
Merge branch 'main' into feat/auto-dl-integration
Alystrasz Dec 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 128 additions & 42 deletions NorthstarDLL/mods/autodownload/moddownloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,51 @@ size_t WriteData(void* ptr, size_t size, size_t nmemb, FILE* stream)
return written;
}

void FetchModSync(std::promise<std::optional<fs::path>>&& p, std::string_view url, fs::path downloadPath)
int ModDownloader::ModFetchingProgressCallback(
void* ptr, curl_off_t totalDownloadSize, curl_off_t finishedDownloadSize, curl_off_t totalToUpload, curl_off_t nowUploaded)
{
if (totalDownloadSize != 0 && finishedDownloadSize != 0)
{
ModDownloader* instance = static_cast<ModDownloader*>(ptr);
auto currentDownloadProgress = roundf(static_cast<float>(finishedDownloadSize) / totalDownloadSize * 100);
instance->modState.progress = finishedDownloadSize;
instance->modState.total = totalDownloadSize;
instance->modState.ratio = currentDownloadProgress;
}

return 0;
}

std::optional<fs::path> ModDownloader::FetchModFromDistantStore(std::string_view modName, std::string_view modVersion)
{
// Retrieve mod prefix from local mods list, or use mod name as mod prefix if bypass flag is set
std::string modPrefix = strstr(GetCommandLineA(), VERIFICATION_FLAG) ? modName.data() : verifiedMods[modName.data()].dependencyPrefix;
// Build archive distant URI
std::string archiveName = std::format("{}-{}.zip", modPrefix, modVersion.data());
std::string url = STORE_URL + archiveName;
spdlog::info(std::format("Fetching mod archive from {}", url));

// Download destination
std::filesystem::path downloadPath = std::filesystem::temp_directory_path() / archiveName;
spdlog::info(std::format("Downloading archive to {}", downloadPath.generic_string()));

// Update state
modState.state = DOWNLOADING;

// Download the actual archive
bool failed = false;
FILE* fp = fopen(downloadPath.generic_string().c_str(), "wb");
CURLcode result;
CURL* easyhandle;
easyhandle = curl_easy_init();

curl_easy_setopt(easyhandle, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(easyhandle, CURLOPT_URL, url.data());
curl_easy_setopt(easyhandle, CURLOPT_FAILONERROR, 1L);
curl_easy_setopt(easyhandle, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(easyhandle, CURLOPT_WRITEFUNCTION, WriteData);
curl_easy_setopt(easyhandle, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(easyhandle, CURLOPT_XFERINFOFUNCTION, ModDownloader::ModFetchingProgressCallback);
curl_easy_setopt(easyhandle, CURLOPT_XFERINFODATA, this);
result = curl_easy_perform(easyhandle);

if (result == CURLcode::CURLE_OK)
Expand All @@ -154,28 +186,7 @@ void FetchModSync(std::promise<std::optional<fs::path>>&& p, std::string_view ur
REQUEST_END_CLEANUP:
curl_easy_cleanup(easyhandle);
fclose(fp);
p.set_value(failed ? std::optional<fs::path>() : std::optional<fs::path>(downloadPath));
}

std::optional<fs::path> ModDownloader::FetchModFromDistantStore(std::string_view modName, std::string_view modVersion)
{
// Retrieve mod prefix from local mods list, or use mod name as mod prefix if bypass flag is set
std::string modPrefix = strstr(GetCommandLineA(), VERIFICATION_FLAG) ? modName.data() : verifiedMods[modName.data()].dependencyPrefix;
// Build archive distant URI
std::string archiveName = std::format("{}-{}.zip", modPrefix, modVersion.data());
std::string url = STORE_URL + archiveName;
spdlog::info(std::format("Fetching mod archive from {}", url));

// Download destination
std::filesystem::path downloadPath = std::filesystem::temp_directory_path() / archiveName;
spdlog::info(std::format("Downloading archive to {}", downloadPath.generic_string()));

// Download the actual archive
std::promise<std::optional<fs::path>> promise;
auto f = promise.get_future();
std::thread t(&FetchModSync, std::move(promise), std::string_view(url), downloadPath);
t.join();
return f.get();
return failed ? std::optional<fs::path>() : std::optional<fs::path>(downloadPath);
}

bool ModDownloader::IsModLegit(fs::path modPath, std::string_view expectedChecksum)
Expand All @@ -186,6 +197,9 @@ bool ModDownloader::IsModLegit(fs::path modPath, std::string_view expectedChecks
return true;
}

// Update state
modState.state = CHECKSUMING;

NTSTATUS status;
BCRYPT_ALG_HANDLE algorithmHandle = NULL;
BCRYPT_HASH_HANDLE hashHandle = NULL;
Expand All @@ -207,6 +221,7 @@ bool ModDownloader::IsModLegit(fs::path modPath, std::string_view expectedChecks
BCRYPT_HASH_REUSABLE_FLAG); // Flags; Loads a provider which supports reusable hash
if (!NT_SUCCESS(status))
{
modState.state = MOD_CORRUPTED;
goto cleanup;
}

Expand All @@ -221,6 +236,7 @@ bool ModDownloader::IsModLegit(fs::path modPath, std::string_view expectedChecks
if (!NT_SUCCESS(status))
{
// goto cleanup;
modState.state = MOD_CORRUPTED;
return false;
}

Expand All @@ -235,13 +251,15 @@ bool ModDownloader::IsModLegit(fs::path modPath, std::string_view expectedChecks
0); // Flags
if (!NT_SUCCESS(status))
{
modState.state = MOD_CORRUPTED;
goto cleanup;
}

// Hash archive content
if (!fp.is_open())
{
spdlog::error("Unable to open archive.");
modState.state = FAILED_READING_ARCHIVE;
return false;
}
fp.seekg(0, fp.beg);
Expand All @@ -254,6 +272,7 @@ bool ModDownloader::IsModLegit(fs::path modPath, std::string_view expectedChecks
status = BCryptHashData(hashHandle, (PBYTE)buffer.data(), bytesRead, 0);
if (!NT_SUCCESS(status))
{
modState.state = MOD_CORRUPTED;
goto cleanup;
}
}
Expand All @@ -269,6 +288,7 @@ bool ModDownloader::IsModLegit(fs::path modPath, std::string_view expectedChecks
0); // Flags
if (!NT_SUCCESS(status))
{
modState.state = MOD_CORRUPTED;
goto cleanup;
}

Expand Down Expand Up @@ -316,6 +336,30 @@ bool ModDownloader::IsModAuthorized(std::string_view modName, std::string_view m
return versions.count(modVersion.data()) != 0;
}

int GetModArchiveSize(unzFile file, unz_global_info64 info)
{
int totalSize = 0;

for (int i = 0; i < info.number_entry; i++)
{
char zipFilename[256];
unz_file_info64 fileInfo;
unzGetCurrentFileInfo64(file, &fileInfo, zipFilename, sizeof(zipFilename), NULL, 0, NULL, 0);

totalSize += fileInfo.uncompressed_size;

if ((i + 1) < info.number_entry)
{
unzGoToNextFile(file);
}
}

// Reset file pointer for archive extraction
unzGoToFirstFile(file);

return totalSize;
}

void ModDownloader::ExtractMod(fs::path modPath)
{
unzFile file;
Expand All @@ -326,6 +370,7 @@ void ModDownloader::ExtractMod(fs::path modPath)
if (file == NULL)
{
spdlog::error("Cannot open archive located at {}.", modPath.generic_string());
modState.state = FAILED_READING_ARCHIVE;
goto EXTRACTION_CLEANUP;
}

Expand All @@ -335,9 +380,15 @@ void ModDownloader::ExtractMod(fs::path modPath)
if (status != UNZ_OK)
{
spdlog::error("Failed getting information from archive (error code: {})", status);
modState.state = FAILED_READING_ARCHIVE;
goto EXTRACTION_CLEANUP;
}

// Update state
modState.state = EXTRACTING;
modState.total = GetModArchiveSize(file, gi);
modState.progress = 0;

// Mod directory name (removing the ".zip" fom the archive name)
name = modPath.filename().string();
name = name.substr(0, name.length() - 4);
Expand All @@ -362,6 +413,7 @@ void ModDownloader::ExtractMod(fs::path modPath)
if (!std::filesystem::create_directories(fileDestination.parent_path(), ec) && ec.value() != 0)
{
spdlog::error("Parent directory ({}) creation failed.", fileDestination.parent_path().generic_string());
modState.state = FAILED_WRITING_TO_DISK;
goto EXTRACTION_CLEANUP;
}
}
Expand All @@ -373,6 +425,7 @@ void ModDownloader::ExtractMod(fs::path modPath)
if (!std::filesystem::create_directory(fileDestination, ec) && ec.value() != 0)
{
spdlog::error("Directory creation failed: {}", ec.message());
modState.state = FAILED_WRITING_TO_DISK;
goto EXTRACTION_CLEANUP;
}
}
Expand All @@ -383,6 +436,7 @@ void ModDownloader::ExtractMod(fs::path modPath)
if (unzLocateFile(file, zipFilename, 0) != UNZ_OK)
{
spdlog::error("File \"{}\" was not found in archive.", zipFilename);
modState.state = FAILED_READING_ARCHIVE;
goto EXTRACTION_CLEANUP;
}

Expand All @@ -397,6 +451,7 @@ void ModDownloader::ExtractMod(fs::path modPath)
if (status != UNZ_OK)
{
spdlog::error("Could not open file {} from archive.", zipFilename);
modState.state = FAILED_READING_ARCHIVE;
goto EXTRACTION_CLEANUP;
}

Expand All @@ -405,6 +460,7 @@ void ModDownloader::ExtractMod(fs::path modPath)
if (fout == NULL)
{
spdlog::error("Failed creating destination file.");
modState.state = FAILED_WRITING_TO_DISK;
goto EXTRACTION_CLEANUP;
}

Expand All @@ -413,6 +469,7 @@ void ModDownloader::ExtractMod(fs::path modPath)
if (buffer == NULL)
{
spdlog::error("Error while allocating memory.");
modState.state = FAILED_WRITING_TO_DISK;
goto EXTRACTION_CLEANUP;
}

Expand All @@ -434,11 +491,16 @@ void ModDownloader::ExtractMod(fs::path modPath)
break;
}
}

// Update extraction stats
modState.progress += bufferSize;
modState.ratio = roundf(static_cast<float>(modState.progress) / modState.total * 100);
} while (err > 0);

if (err != UNZ_OK)
{
spdlog::error("An error occurred during file extraction (code: {})", err);
modState.state = FAILED_WRITING_TO_DISK;
goto EXTRACTION_CLEANUP;
}
err = unzCloseCurrentFile(file);
Expand Down Expand Up @@ -492,12 +554,14 @@ void ModDownloader::DownloadMod(std::string modName, std::string modVersion)
if (!fetchingResult.has_value())
{
spdlog::error("Something went wrong while fetching archive, aborting.");
modState.state = MOD_FETCHING_FAILED;
goto REQUEST_END_CLEANUP;
}
archiveLocation = fetchingResult.value();
if (!IsModLegit(archiveLocation, std::string_view(expectedHash)))
{
spdlog::warn("Archive hash does not match expected checksum, aborting.");
modState.state = MOD_CORRUPTED;
goto REQUEST_END_CLEANUP;
}

Expand All @@ -514,39 +578,61 @@ void ModDownloader::DownloadMod(std::string modName, std::string modVersion)
spdlog::error("Error while removing downloaded archive: {}", a.what());
}

modState.state = DONE;
spdlog::info("Done downloading {}.", modName);
});

requestThread.detach();
}

void ConCommandFetchVerifiedMods(const CCommand& args)
ON_DLL_LOAD_RELIESON("engine.dll", ModDownloader, (ConCommand), (CModule module))
{
g_pModDownloader = new ModDownloader();
g_pModDownloader->FetchModsListFromAPI();
}

void ConCommandDownloadMod(const CCommand& args)
ADD_SQFUNC(
"bool", NSIsModDownloadable, "string name, string version", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI)
{
if (args.ArgC() < 3)
{
return;
}
g_pSquirrel<context>->newarray(sqvm, 0);

// Split arguments string by whitespaces (https://stackoverflow.com/a/5208977)
std::string buffer;
std::stringstream ss(args.ArgS());
std::vector<std::string> tokens;
while (ss >> buffer)
tokens.push_back(buffer);
const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1);
const SQChar* modVersion = g_pSquirrel<context>->getstring(sqvm, 2);

std::string modName = tokens[0];
std::string modVersion = tokens[1];
bool result = g_pModDownloader->IsModAuthorized(modName, modVersion);
g_pSquirrel<context>->pushbool(sqvm, result);

return SQRESULT_NOTNULL;
}

ADD_SQFUNC("void", NSDownloadMod, "string name, string version", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI)
{
const SQChar* modName = g_pSquirrel<context>->getstring(sqvm, 1);
const SQChar* modVersion = g_pSquirrel<context>->getstring(sqvm, 2);
g_pModDownloader->DownloadMod(modName, modVersion);

return SQRESULT_NOTNULL;
}

ON_DLL_LOAD_RELIESON("engine.dll", ModDownloader, (ConCommand), (CModule module))
ADD_SQFUNC("ModInstallState", NSGetModInstallState, "", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI)
{
g_pModDownloader = new ModDownloader();
RegisterConCommand("fetch_verified_mods", ConCommandFetchVerifiedMods, "fetches verified mods list from GitHub repository", FCVAR_NONE);
RegisterConCommand("download_mod", ConCommandDownloadMod, "downloads a mod from remote store", FCVAR_NONE);
g_pSquirrel<context>->pushnewstructinstance(sqvm, 4);

// state
g_pSquirrel<context>->pushinteger(sqvm, g_pModDownloader->modState.state);
g_pSquirrel<context>->sealstructslot(sqvm, 0);

// progress
g_pSquirrel<context>->pushinteger(sqvm, g_pModDownloader->modState.progress);
g_pSquirrel<context>->sealstructslot(sqvm, 1);

// total
g_pSquirrel<context>->pushinteger(sqvm, g_pModDownloader->modState.total);
g_pSquirrel<context>->sealstructslot(sqvm, 2);

// ratio
g_pSquirrel<context>->pushfloat(sqvm, g_pModDownloader->modState.ratio);
g_pSquirrel<context>->sealstructslot(sqvm, 3);

return SQRESULT_NOTNULL;
}
Loading