2
0
mirror of https://github.com/Laupetin/OpenAssetTools.git synced 2025-10-17 12:09:03 +00:00

Merge pull request #540 from Laupetin/feature/modman-unlink

feat: modman unlinking
This commit is contained in:
Jan
2025-10-11 22:51:24 +02:00
committed by GitHub
44 changed files with 1091 additions and 171 deletions

View File

@@ -364,14 +364,16 @@ class LinkerImpl final : public Linker
zoneDirectory = fs::current_path();
auto absoluteZoneDirectory = absolute(zoneDirectory).string();
auto zone = ZoneLoading::LoadZone(zonePath);
if (!zone)
auto maybeZone = ZoneLoading::LoadZone(zonePath);
if (!maybeZone)
{
con::error("Failed to load zone \"{}\".", zonePath);
con::error("Failed to load zone \"{}\": {}", zonePath, maybeZone.error());
return false;
}
con::debug("Load zone \"{}\"", zone->m_name);
auto zone = std::move(*maybeZone);
con::debug("Loaded zone \"{}\"", zone->m_name);
m_loaded_zones.emplace_back(std::move(zone));
}

View File

@@ -47,10 +47,16 @@ function ModMan:project()
self:include(includes)
Utils:include(includes)
ZoneLoading:include(includes)
ObjLoading:include(includes)
ObjWriting:include(includes)
json:include(includes)
webview:include(includes)
links:linkto(Utils)
links:linkto(ZoneLoading)
links:linkto(ObjLoading)
links:linkto(ObjWriting)
links:linkto(webview)
links:linkall()
end

View File

@@ -0,0 +1,42 @@
#include "FastFileContext.h"
#include "Web/Binds/ZoneBinds.h"
#include "Web/UiCommunication.h"
#include "ZoneLoading.h"
void FastFileContext::Destroy()
{
// Unload all zones
m_loaded_zones.clear();
}
result::Expected<Zone*, std::string> FastFileContext::LoadFastFile(const std::string& path)
{
auto zone = ZoneLoading::LoadZone(path);
if (!zone)
return result::Unexpected(std::move(zone.error()));
auto* result = m_loaded_zones.emplace_back(std::move(*zone)).get();
ui::NotifyZoneLoaded(result->m_name, path);
return result;
}
result::Expected<NoResult, std::string> FastFileContext::UnloadZone(const std::string& zoneName)
{
const auto existingZone = std::ranges::find_if(m_loaded_zones,
[&zoneName](const std::unique_ptr<Zone>& zone)
{
return zone->m_name == zoneName;
});
if (existingZone != m_loaded_zones.end())
{
m_loaded_zones.erase(existingZone);
ui::NotifyZoneUnloaded(zoneName);
return NoResult();
}
return result::Unexpected(std::format("No zone with name {} loaded", zoneName));
}

View File

@@ -0,0 +1,18 @@
#pragma once
#include "Utils/Result.h"
#include "Zone/Zone.h"
#include <memory>
#include <vector>
class FastFileContext
{
public:
void Destroy();
result::Expected<Zone*, std::string> LoadFastFile(const std::string& path);
result::Expected<NoResult, std::string> UnloadZone(const std::string& zoneName);
std::vector<std::unique_ptr<Zone>> m_loaded_zones;
};

View File

@@ -0,0 +1,18 @@
#include "ModManContext.h"
ModManContext& ModManContext::Get()
{
static ModManContext context;
return context;
}
void ModManContext::Startup()
{
m_db_thread.Start();
}
void ModManContext::Destroy()
{
m_fast_file.Destroy();
m_db_thread.Terminate();
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include "FastFileContext.h"
#include "Utils/DispatchableThread.h"
#include "Web/WebViewLib.h"
#include <memory>
class ModManContext
{
public:
static ModManContext& Get();
void Startup();
void Destroy();
std::unique_ptr<webview::webview> m_main_webview;
std::unique_ptr<webview::webview> m_dev_tools_webview;
FastFileContext m_fast_file;
DispatchableThread m_db_thread;
};

105
src/ModMan/ModManArgs.cpp Normal file
View File

@@ -0,0 +1,105 @@
#include "ModManArgs.h"
#include "GitVersion.h"
#include "Utils/Arguments/UsageInformation.h"
#include "Utils/Logging/Log.h"
#include <format>
#include <type_traits>
namespace
{
// clang-format off
const CommandLineOption* const OPTION_HELP =
CommandLineOption::Builder::Create()
.WithShortName("?")
.WithLongName("help")
.WithDescription("Displays usage information.")
.Build();
const CommandLineOption* const OPTION_VERSION =
CommandLineOption::Builder::Create()
.WithLongName("version")
.WithDescription("Prints the application version.")
.Build();
const CommandLineOption* const OPTION_VERBOSE =
CommandLineOption::Builder::Create()
.WithShortName("v")
.WithLongName("verbose")
.WithDescription("Outputs a lot more and more detailed messages.")
.Build();
const CommandLineOption* const OPTION_NO_COLOR =
CommandLineOption::Builder::Create()
.WithLongName("no-color")
.WithDescription("Disables colored terminal output.")
.Build();
// clang-format on
const CommandLineOption* const COMMAND_LINE_OPTIONS[]{
OPTION_HELP,
OPTION_VERSION,
OPTION_VERBOSE,
OPTION_NO_COLOR,
};
} // namespace
ModManArgs::ModManArgs()
: m_argument_parser(COMMAND_LINE_OPTIONS, std::extent_v<decltype(COMMAND_LINE_OPTIONS)>)
{
}
void ModManArgs::PrintUsage() const
{
UsageInformation usage(m_argument_parser.GetExecutableName());
for (const auto* commandLineOption : COMMAND_LINE_OPTIONS)
{
usage.AddCommandLineOption(commandLineOption);
}
usage.Print();
}
void ModManArgs::PrintVersion()
{
con::info("OpenAssetTools ModMan {}", GIT_VERSION);
}
bool ModManArgs::ParseArgs(const int argc, const char** argv, bool& shouldContinue)
{
shouldContinue = true;
if (!m_argument_parser.ParseArguments(argc, argv))
{
PrintUsage();
return false;
}
// Check if the user requested help
if (m_argument_parser.IsOptionSpecified(OPTION_HELP))
{
PrintUsage();
shouldContinue = false;
return true;
}
// Check if the user wants to see the version
if (m_argument_parser.IsOptionSpecified(OPTION_VERSION))
{
PrintVersion();
shouldContinue = false;
return true;
}
// -v; --verbose
if (m_argument_parser.IsOptionSpecified(OPTION_VERBOSE))
con::globalLogLevel = con::LogLevel::DEBUG;
else
con::globalLogLevel = con::LogLevel::INFO;
// --no-color
con::globalUseColor = !m_argument_parser.IsOptionSpecified(OPTION_NO_COLOR);
return true;
}

19
src/ModMan/ModManArgs.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "Utils/Arguments/ArgumentParser.h"
class ModManArgs
{
public:
ModManArgs();
bool ParseArgs(int argc, const char** argv, bool& shouldContinue);
private:
/**
* \brief Prints a command line usage help text for the ModMan tool to stdout.
*/
void PrintUsage() const;
static void PrintVersion();
ArgumentParser m_argument_parser;
};

View File

@@ -0,0 +1,77 @@
#include "DispatchableThread.h"
DispatchableThread::DispatchableThread()
: m_terminate(false)
{
}
DispatchableThread::~DispatchableThread()
{
Terminate();
}
void DispatchableThread::Start()
{
m_terminate = false;
m_thread = std::thread(
[&]
{
ThreadLoop();
});
}
void DispatchableThread::Terminate()
{
std::unique_lock lock(m_cb_mutex);
if (!m_terminate)
{
m_terminate = true;
m_cv.notify_all();
lock.unlock();
m_thread.join();
}
else
{
lock.unlock();
}
}
void DispatchableThread::Dispatch(cb_t cb)
{
std::lock_guard lock(m_cb_mutex);
m_cb_list.emplace_back(std::move(cb));
m_cv.notify_one();
}
std::optional<DispatchableThread::cb_t> DispatchableThread::NextCallback()
{
if (m_terminate || m_cb_list.empty())
return std::nullopt;
auto cb = std::move(m_cb_list.front());
m_cb_list.pop_front();
return cb;
}
void DispatchableThread::ThreadLoop()
{
while (!m_terminate)
{
std::unique_lock lock(m_cb_mutex);
m_cv.wait(lock,
[&]
{
return !m_cb_list.empty() || m_terminate;
});
auto maybeCb = NextCallback();
lock.unlock();
if (maybeCb)
(*maybeCb)();
}
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include <condition_variable>
#include <deque>
#include <functional>
#include <mutex>
#include <optional>
#include <thread>
class DispatchableThread
{
public:
using cb_t = std::function<void()>;
DispatchableThread();
~DispatchableThread();
DispatchableThread(const DispatchableThread& other) = delete;
DispatchableThread(DispatchableThread&& other) noexcept = default;
DispatchableThread& operator=(const DispatchableThread& other) = delete;
DispatchableThread& operator=(DispatchableThread&& other) noexcept = default;
void Start();
void Terminate();
void Dispatch(cb_t cb);
private:
std::optional<cb_t> NextCallback();
void ThreadLoop();
std::mutex m_cb_mutex;
std::deque<cb_t> m_cb_list;
std::condition_variable m_cv;
std::thread m_thread;
bool m_terminate;
};

View File

@@ -0,0 +1,15 @@
#include "Binds.h"
#include "UnlinkingBinds.h"
#include "Web/Binds/DialogBinds.h"
#include "ZoneBinds.h"
namespace ui
{
void RegisterAllBinds(webview::webview& wv)
{
RegisterDialogHandlerBinds(wv);
RegisterUnlinkingBinds(wv);
RegisterZoneBinds(wv);
}
} // namespace ui

View File

@@ -0,0 +1,8 @@
#pragma once
#include "Web/WebViewLib.h"
namespace ui
{
void RegisterAllBinds(webview::webview& wv);
}

View File

@@ -0,0 +1,93 @@
#include "UnlinkingBinds.h"
#include "Context/ModManContext.h"
#include "IObjWriter.h"
#include "SearchPath/OutputPathFilesystem.h"
#include "SearchPath/SearchPaths.h"
#include "Utils/PathUtils.h"
#include "Web/UiCommunication.h"
#include "Json/JsonExtension.h"
#include <filesystem>
namespace fs = std::filesystem;
namespace
{
class ZoneLoadedDto
{
public:
std::string zoneName;
std::string filePath;
};
NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneLoadedDto, zoneName, filePath);
class ZoneUnloadedDto
{
public:
std::string zoneName;
};
NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneUnloadedDto, zoneName);
result::Expected<NoResult, std::string> UnlinkZoneInDbThread(const std::string& zoneName)
{
const auto& context = ModManContext::Get().m_fast_file;
const auto existingZone = std::ranges::find_if(context.m_loaded_zones,
[&zoneName](const std::unique_ptr<Zone>& zone)
{
return zone->m_name == zoneName;
});
if (existingZone == context.m_loaded_zones.end())
return result::Unexpected(std::format("No zone with name {} loaded", zoneName));
const auto& zone = *existingZone->get();
const auto* objWriter = IObjWriter::GetObjWriterForGame(zone.m_game_id);
const auto outputFolderPath = fs::path(utils::GetExecutablePath()).parent_path() / "zone_dump" / zoneName;
const auto outputFolderPathStr = outputFolderPath.string();
OutputPathFilesystem outputFolderOutputPath(outputFolderPath);
SearchPaths searchPaths;
AssetDumpingContext dumpingContext(zone, outputFolderPathStr, outputFolderOutputPath, searchPaths);
objWriter->DumpZone(dumpingContext);
return NoResult();
}
void UnlinkZone(webview::webview& wv, std::string id, std::string zoneName) // NOLINT(performance-unnecessary-value-param) Copy is made for thread safety
{
ModManContext::Get().m_db_thread.Dispatch(
[&wv, id, zoneName]
{
auto result = UnlinkZoneInDbThread(zoneName);
if (result)
{
con::debug("Unlinked zone \"{}\"", zoneName);
ui::PromiseResolve(wv, id, true);
}
else
{
con::warn("Failed to unlink zone \"{}\": {}", zoneName, result.error());
ui::PromiseReject(wv, id, std::move(result).error());
}
});
}
} // namespace
namespace ui
{
void RegisterUnlinkingBinds(webview::webview& wv)
{
BindAsync<std::string>(wv,
"unlinkZone",
[&wv](const std::string& id, std::string zoneName)
{
UnlinkZone(wv, id, std::move(zoneName));
});
}
} // namespace ui

View File

@@ -0,0 +1,8 @@
#pragma once
#include "Web/WebViewLib.h"
namespace ui
{
void RegisterUnlinkingBinds(webview::webview& wv);
} // namespace ui

View File

@@ -0,0 +1,107 @@
#include "ZoneBinds.h"
#include "Context/ModManContext.h"
#include "Web/UiCommunication.h"
#include "Json/JsonExtension.h"
namespace
{
class ZoneLoadedDto
{
public:
std::string zoneName;
std::string filePath;
};
NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneLoadedDto, zoneName, filePath);
class ZoneUnloadedDto
{
public:
std::string zoneName;
};
NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneUnloadedDto, zoneName);
void LoadFastFile(webview::webview& wv, std::string id, std::string path) // NOLINT(performance-unnecessary-value-param) Copy is made for thread safety
{
ModManContext::Get().m_db_thread.Dispatch(
[&wv, id, path]
{
auto maybeZone = ModManContext::Get().m_fast_file.LoadFastFile(path);
if (maybeZone)
{
ui::PromiseResolve(wv,
id,
ZoneLoadedDto{
.zoneName = maybeZone.value()->m_name,
.filePath = path,
});
con::debug("Loaded zone \"{}\"", maybeZone.value()->m_name);
}
else
{
con::warn("Failed to load zone \"{}\": {}", path, maybeZone.error());
ui::PromiseReject(wv, id, std::move(maybeZone).error());
}
});
}
void UnloadZone(webview::webview& wv, std::string id, std::string zoneName) // NOLINT(performance-unnecessary-value-param) Copy is made for thread safety
{
ModManContext::Get().m_db_thread.Dispatch(
[&wv, id, zoneName]
{
auto result = ModManContext::Get().m_fast_file.UnloadZone(zoneName);
if (result)
{
con::debug("Unloaded zone \"{}\"", zoneName);
ui::PromiseResolve(wv, id, true);
}
else
{
con::warn("Failed unloading zone {}: {}", zoneName, result.error());
ui::PromiseReject(wv, id, std::move(result).error());
}
});
}
} // namespace
namespace ui
{
void NotifyZoneLoaded(std::string zoneName, std::string fastFilePath)
{
const ZoneLoadedDto dto{
.zoneName = std::move(zoneName),
.filePath = std::move(fastFilePath),
};
Notify(*ModManContext::Get().m_main_webview, "zoneLoaded", dto);
}
void NotifyZoneUnloaded(std::string zoneName)
{
const ZoneUnloadedDto dto{
.zoneName = std::move(zoneName),
};
Notify(*ModManContext::Get().m_main_webview, "zoneUnloaded", dto);
}
void RegisterZoneBinds(webview::webview& wv)
{
BindAsync<std::string>(wv,
"loadFastFile",
[&wv](const std::string& id, std::string path)
{
LoadFastFile(wv, id, std::move(path));
});
BindAsync<std::string>(wv,
"unloadZone",
[&wv](const std::string& id, std::string zoneName)
{
UnloadZone(wv, id, std::move(zoneName));
});
}
} // namespace ui

View File

@@ -0,0 +1,11 @@
#pragma once
#include "Web/WebViewLib.h"
namespace ui
{
void NotifyZoneLoaded(std::string zoneName, std::string fastFilePath);
void NotifyZoneUnloaded(std::string zoneName);
void RegisterZoneBinds(webview::webview& wv);
} // namespace ui

View File

@@ -1,5 +1,7 @@
#pragma once
#define NOMINMAX
#ifdef _MSC_VER
#pragma warning(push, 0)
#else

View File

@@ -1,32 +1,38 @@
#include "GitVersion.h"
#include "Web/Binds/DialogBinds.h"
#include "Context/ModManContext.h"
#include "GitVersion.h"
#include "ModManArgs.h"
#include "Web/Binds/Binds.h"
#include "Web/Platform/AssetHandler.h"
#include "Web/UiCommunication.h"
#include "Web/ViteAssets.h"
#include "Web/WebViewLib.h"
#include <chrono>
#include <format>
#include <iostream>
#include <optional>
#include <string>
#include <thread>
#ifdef _WIN32
#include <Windows.h>
#endif
using namespace std::string_literals;
using namespace PLATFORM_NAMESPACE;
namespace
{
#ifdef _DEBUG
std::optional<webview::webview> devToolWindow;
void RunDevToolsWindow()
void SpawnDevToolsWindow()
{
con::debug("Creating dev tools window");
auto& context = ModManContext::Get();
try
{
auto& newWindow = devToolWindow.emplace(false, nullptr);
context.m_dev_tools_webview = std::make_unique<webview::webview>(false, nullptr);
auto& newWindow = *context.m_dev_tools_webview;
newWindow.set_title("Devtools");
newWindow.set_size(640, 480, WEBVIEW_HINT_NONE);
newWindow.set_size(480, 320, WEBVIEW_HINT_MIN);
@@ -39,59 +45,44 @@ namespace
}
#endif
int RunMainWindow()
int SpawnMainWindow()
{
con::debug("Creating main window");
auto& context = ModManContext::Get();
try
{
webview::webview w(
context.m_main_webview = std::make_unique<webview::webview>(
#ifdef _DEBUG
true,
#else
false,
#endif
nullptr);
w.set_title("OpenAssetTools ModMan");
w.set_size(1280, 640, WEBVIEW_HINT_NONE);
w.set_size(480, 320, WEBVIEW_HINT_MIN);
auto& newWindow = *context.m_main_webview;
ui::RegisterDialogHandlerBinds(w);
newWindow.set_title("OpenAssetTools ModMan");
newWindow.set_size(1280, 640, WEBVIEW_HINT_NONE);
newWindow.set_size(480, 320, WEBVIEW_HINT_MIN);
// A binding that counts up or down and immediately returns the new value.
ui::Bind<std::string, std::string>(w,
"greet",
[&w](std::string name) -> std::string
{
ui::Notify(w, "greeting", name);
return std::format("Hello from C++ {}!", name);
});
#if defined(WEBVIEW_PLATFORM_WINDOWS) && defined(WEBVIEW_EDGE)
InstallAssetHandler(w);
constexpr auto urlPrefix = URL_PREFIX;
#elif defined(WEBVIEW_PLATFORM_LINUX) && defined(WEBVIEW_GTK)
InstallAssetHandler(w);
constexpr auto urlPrefix = URL_PREFIX;
#else
#error Unsupported platform
#endif
InstallAssetHandler(newWindow);
ui::RegisterAllBinds(newWindow);
#ifdef _DEBUG
w.navigate(VITE_DEV_SERVER ? std::format("http://localhost:{}", VITE_DEV_SERVER_PORT) : std::format("{}index.html", urlPrefix));
newWindow.navigate(VITE_DEV_SERVER ? std::format("http://localhost:{}", VITE_DEV_SERVER_PORT) : std::format("{}index.html", URL_PREFIX));
if (VITE_DEV_SERVER)
{
w.dispatch(
newWindow.dispatch(
[]
{
RunDevToolsWindow();
SpawnDevToolsWindow();
});
}
#else
w.navigate(std::format("{}index.html", urlPrefix));
newWindow.navigate(std::format("{}index.html", URL_PREFIX));
#endif
w.run();
newWindow.run();
}
catch (const webview::exception& e)
{
@@ -104,16 +95,50 @@ namespace
} // namespace
#ifdef _WIN32
#define MODMAN_ARGC __argc
#define MODMAN_ARGV const_cast<const char**>(__argv)
int WINAPI WinMain(HINSTANCE /*hInst*/, HINSTANCE /*hPrevInst*/, LPSTR /*lpCmdLine*/, int /*nCmdShow*/)
{
#else
int main()
{
#define MODMAN_ARGC argc
#define MODMAN_ARGV argv
int main(int argc, const char** argv)
#endif
{
#ifdef _WIN32
// Attach console if possible on Windows for stdout/stderr in console
if (AttachConsole(-1))
{
FILE* fDummy;
(void)freopen_s(&fDummy, "CONOUT$", "w", stdout);
(void)freopen_s(&fDummy, "CONOUT$", "w", stderr);
(void)freopen_s(&fDummy, "CONIN$", "r", stdin);
std::cout.clear();
std::clog.clear();
std::cerr.clear();
std::cin.clear();
}
#endif
#ifdef __linux__
g_set_prgname("OpenAssetTools-ModMan");
g_set_application_name("OpenAssetTools ModMan");
#endif
ModManArgs args;
auto shouldContinue = true;
if (!args.ParseArgs(MODMAN_ARGC, MODMAN_ARGV, shouldContinue))
return false;
if (!shouldContinue)
return true;
con::info("Starting ModMan " GIT_VERSION);
const auto result = RunMainWindow();
ModManContext::Get().Startup();
const auto result = SpawnMainWindow();
ModManContext::Get().Destroy();
return result;
}

View File

@@ -1,28 +1,72 @@
<script setup lang="ts">
import { onUnmounted, ref } from "vue";
import { webviewBinds, webviewAddEventListener, webviewRemoveEventListener } from "./native";
import { computed, ref } from "vue";
import { webviewBinds } from "@/native";
import { useZoneStore } from "@/stores/ZoneStore";
import SpinningLoader from "@/components/SpinningLoader.vue";
const greetMsg = ref("");
const lastPersonGreeted = ref("");
const lastPath = ref("");
const name = ref("");
const zoneStore = useZoneStore();
const loadingFastFile = ref(false);
const unlinkingFastFile = ref(false);
async function greet() {
greetMsg.value = await webviewBinds.greet(name.value);
const performingAction = computed<boolean>(() => loadingFastFile.value || unlinkingFastFile.value);
async function openFastFileSelect() {
return await webviewBinds.openFileDialog({ filters: [{ name: "Fastfiles", filter: "*.ff" }] });
}
function onPersonGreeted(person: string) {
lastPersonGreeted.value = person;
async function onOpenFastFileClick() {
if (performingAction.value) return;
const fastFilePath = await openFastFileSelect();
if (!fastFilePath) return;
loadingFastFile.value = true;
webviewBinds
.loadFastFile(fastFilePath)
.catch((e: string) => {
console.error("Failed to load fastfile:", e);
})
.finally(() => {
loadingFastFile.value = false;
});
}
async function onOpenFastfileClick() {
lastPath.value =
(await webviewBinds.openFileDialog({ filters: [{ name: "Fastfiles", filter: "*.ff" }] })) ?? "";
async function onUnlinkFastFileClick() {
if (performingAction.value) return;
const fastFilePath = await openFastFileSelect();
if (!fastFilePath) return;
try {
unlinkingFastFile.value = true;
let loadedZoneName: string;
try {
loadedZoneName = (await webviewBinds.loadFastFile(fastFilePath)).zoneName;
} catch (e: unknown) {
console.error("Failed to load fastfile:", e as string);
return;
}
try {
await webviewBinds.unlinkZone(loadedZoneName);
} catch (e: unknown) {
console.error("Failed to unlink fastfile:", e as string);
return;
} finally {
webviewBinds.unloadZone(loadedZoneName);
}
} finally {
unlinkingFastFile.value = false;
}
}
webviewAddEventListener("greeting", onPersonGreeted);
onUnmounted(() => webviewRemoveEventListener("greeting", onPersonGreeted));
function onUnloadClicked(zoneName: string) {
webviewBinds.unloadZone(zoneName).catch((e: string) => {
console.error("Failed to unload zone:", e);
});
}
</script>
<template>
@@ -30,25 +74,47 @@ onUnmounted(() => webviewRemoveEventListener("greeting", onPersonGreeted));
<h1>Welcome to ModMan</h1>
<small>Nothing to see here yet, this is mainly for testing</small>
<form class="row" @submit.prevent="greet">
<input id="greet-input" v-model="name" placeholder="Enter a name..." autocomplete="off" />
<button type="submit">Greet</button>
</form>
<p>{{ greetMsg }}</p>
<p>The last person greeted is: {{ lastPersonGreeted }}</p>
<p>
<button @click="onOpenFastfileClick">Open fastfile</button>
<span>The last path: {{ lastPath }}</span>
</p>
<div class="actions">
<button :disabled="performingAction" @click="onOpenFastFileClick">
<SpinningLoader v-if="loadingFastFile" class="loading" />
<span>Load fastfile</span>
</button>
<button :disabled="performingAction" @click="onUnlinkFastFileClick">
<SpinningLoader v-if="unlinkingFastFile" class="loading" />
<span>Unlink fastfile</span>
</button>
</div>
<div>
<h3>Loaded zones:</h3>
<div class="zone-list">
<div v-for="zone in zoneStore.loadedZones" :key="zone" class="zone">
<span>{{ zone }}</span>
<button :disabled="performingAction" @click="onUnloadClicked(zone)">Unload</button>
</div>
</div>
</div>
</main>
</template>
<style scoped>
.logo.vite:hover {
filter: drop-shadow(0 0 2em #747bff);
.actions {
display: flex;
justify-content: center;
column-gap: 0.5em;
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #249b73);
.loading {
margin-right: 0.2em;
}
.zone-list {
display: flex;
flex-direction: column;
row-gap: 0.5em;
}
.zone > button {
margin-left: 0.5em;
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<svg viewBox="0 0 100 100" class="loading-spinner">
<circle class="loading-spinner-box" cx="50" cy="50" r="40" fill="transparent" />
</svg>
</template>
<style lang="scss" scoped>
.loading-spinner {
width: 0.8em;
height: 0.8em;
display: inline-block;
animation: rotation 1s infinite linear;
.loading-spinner-box {
stroke: currentColor;
stroke-width: 10;
stroke-dasharray: 100% 100%;
}
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -71,13 +71,17 @@ button {
cursor: pointer;
}
button:hover {
button:not(:disabled):hover {
border-color: #396cd8;
}
button:active {
button:not(:disabled):active {
border-color: #396cd8;
background-color: #e8e8e8;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
input,
button {
@@ -103,7 +107,7 @@ button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
button:not(:disabled):active {
background-color: #0f0f0f69;
}
}

View File

@@ -12,7 +12,7 @@ export interface SaveFileDialogDto {
}
export interface DialogBinds {
openFileDialog(options?: OpenFileDialogDto): string | null;
saveFileDialog(options?: SaveFileDialogDto): string | null;
folderSelectDialog(): string | null;
openFileDialog(options?: OpenFileDialogDto): Promise<string | null>;
saveFileDialog(options?: SaveFileDialogDto): Promise<string | null>;
folderSelectDialog(): Promise<string | null>;
}

View File

@@ -0,0 +1,3 @@
export interface UnlinkingBinds {
unlinkZone(zoneName: string): Promise<void>;
}

View File

@@ -0,0 +1,18 @@
export interface ZoneLoadedDto {
zoneName: string;
filePath: string;
}
export interface ZoneUnloadedDto {
zoneName: string;
}
export interface ZoneBinds {
loadFastFile(path: string): Promise<ZoneLoadedDto>;
unloadZone(zoneName: string): Promise<void>;
}
export interface ZoneEventMap {
zoneLoaded: ZoneLoadedDto;
zoneUnloaded: ZoneUnloadedDto;
}

View File

@@ -1,13 +1,10 @@
import type { DialogBinds } from "./DialogBinds";
import type { UnlinkingBinds } from "./UnlinkingBinds";
import type { ZoneBinds, ZoneEventMap } from "./ZoneBinds";
export type NativeMethods = DialogBinds & UnlinkingBinds & ZoneBinds;
export type NativeMethods = {
greet(name: string): Promise<string>;
} & DialogBinds;
interface NativeEventMap {
greeting: string;
}
type NativeEventMap = ZoneEventMap;
type WebViewExtensions = {
webviewBinds: NativeMethods;

View File

@@ -0,0 +1,20 @@
import { readonly, ref } from "vue";
import { defineStore } from "pinia";
import { webviewAddEventListener } from "@/native";
export const useZoneStore = defineStore("zone", () => {
const loadedZones = ref<string[]>([]);
webviewAddEventListener("zoneLoaded", (dto) => {
loadedZones.value.push(dto.zoneName);
});
webviewAddEventListener("zoneUnloaded", (dto) => {
const index = loadedZones.value.indexOf(dto.zoneName);
if (index >= 0) {
loadedZones.value.splice(index, 1);
}
});
return { loadedZones: readonly(loadedZones) };
});

View File

@@ -1,12 +0,0 @@
import { ref, computed } from "vue";
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubleCount, increment };
});

View File

@@ -1,19 +1,17 @@
#include "Unlinker.h"
#include "ContentLister/ContentPrinter.h"
#include "ContentLister/ZoneDefWriter.h"
#include "IObjLoader.h"
#include "IObjWriter.h"
#include "ObjWriting.h"
#include "SearchPath/IWD.h"
#include "SearchPath/OutputPathFilesystem.h"
#include "SearchPath/SearchPathFilesystem.h"
#include "SearchPath/SearchPaths.h"
#include "UnlinkerArgs.h"
#include "UnlinkerPaths.h"
#include "Utils/ClassUtils.h"
#include "Utils/Logging/Log.h"
#include "Utils/ObjFileStream.h"
#include "Zone/Definition/ZoneDefWriter.h"
#include "ZoneLoading.h"
#include <cassert>
@@ -21,7 +19,6 @@
#include <format>
#include <fstream>
#include <regex>
#include <set>
namespace fs = std::filesystem;
@@ -54,12 +51,12 @@ public:
}
private:
_NODISCARD bool ShouldLoadObj() const
[[nodiscard]] bool ShouldLoadObj() const
{
return m_args.m_task != UnlinkerArgs::ProcessingTask::LIST && !m_args.m_skip_obj;
}
bool WriteZoneDefinitionFile(const Zone& zone, const fs::path& zoneDefinitionFileFolder) const
[[nodiscard]] bool WriteZoneDefinitionFile(const Zone& zone, const fs::path& zoneDefinitionFileFolder) const
{
auto zoneDefinitionFilePath(zoneDefinitionFileFolder);
zoneDefinitionFilePath.append(zone.m_name);
@@ -73,7 +70,7 @@ private:
}
const auto* zoneDefWriter = IZoneDefWriter::GetZoneDefWriterForGame(zone.m_game_id);
zoneDefWriter->WriteZoneDef(zoneDefinitionFile, m_args, zone);
zoneDefWriter->WriteZoneDef(zoneDefinitionFile, zone, m_args.m_use_gdt);
zoneDefinitionFile.close();
@@ -230,13 +227,15 @@ private:
auto absoluteZoneDirectory = absolute(std::filesystem::path(zonePath).remove_filename()).string();
auto searchPathsForZone = paths.GetSearchPathsForZone(absoluteZoneDirectory);
auto zone = ZoneLoading::LoadZone(zonePath);
if (zone == nullptr)
auto maybeZone = ZoneLoading::LoadZone(zonePath);
if (!maybeZone)
{
con::error("Failed to load zone \"{}\".", zonePath);
con::error("Failed to load zone \"{}\": {}", zonePath, maybeZone.error());
return false;
}
auto zone = std::move(*maybeZone);
con::debug("Loaded zone \"{}\"", zone->m_name);
if (ShouldLoadObj())
@@ -290,16 +289,16 @@ private:
auto searchPathsForZone = paths.GetSearchPathsForZone(absoluteZoneDirectory);
std::string zoneName;
auto zone = ZoneLoading::LoadZone(zonePath);
if (zone == nullptr)
auto maybeZone = ZoneLoading::LoadZone(zonePath);
if (!maybeZone)
{
con::error("Failed to load zone \"{}\".", zonePath);
con::error("Failed to load zone \"{}\": {}", zonePath, maybeZone.error());
return false;
}
zoneName = zone->m_name;
con::debug("Loaded zone \"{}\"", zoneName);
auto zone = std::move(*maybeZone);
con::debug("Loaded zone \"{}\"", zone->m_name);
const auto* objLoader = IObjLoader::GetObjLoaderForGame(zone->m_game_id);
if (ShouldLoadObj())
@@ -311,6 +310,8 @@ private:
if (ShouldLoadObj())
objLoader->UnloadContainersOfZone(*zone);
// Copy zone name for using it after freeing the zone
std::string zoneName = zone->m_name;
zone.reset();
con::debug("Unloaded zone \"{}\"", zoneName);
}

185
src/Utils/Utils/Result.h Normal file
View File

@@ -0,0 +1,185 @@
#pragma once
#include <type_traits>
#include <variant>
using NoResult = std::monostate;
// Can be replaced by std::expected with c++23
namespace result
{
template<typename TError> class Unexpected
{
public:
Unexpected(TError result)
: m_data(std::move(result))
{
}
constexpr std::add_lvalue_reference_t<TError> value() &
{
return m_data;
}
constexpr std::add_const_t<std::add_lvalue_reference_t<TError>> value() const&
{
return m_data;
}
constexpr std::add_rvalue_reference_t<TError> value() &&
{
return std::move(m_data);
}
constexpr std::add_const_t<std::add_rvalue_reference_t<TError>> value() const&&
{
return std::move(m_data);
}
constexpr std::add_lvalue_reference_t<TError> operator*() &
{
return m_data;
}
constexpr std::add_const_t<std::add_lvalue_reference_t<TError>> operator*() const&
{
return m_data;
}
constexpr std::add_rvalue_reference_t<TError> operator*() &&
{
return std::move(m_data);
}
constexpr std::add_const_t<std::add_rvalue_reference_t<TError>> operator*() const&&
{
return std::move(m_data);
}
constexpr std::add_pointer_t<TError> operator->()
{
return m_data;
}
constexpr std::add_const_t<std::add_pointer_t<TError>> operator->() const
{
return m_data;
}
private:
TError m_data;
};
template<typename TResult, typename TError> class Expected
{
public:
Expected(TResult result)
: m_data(std::variant<TResult, TError>(std::in_place_index<0>, std::move(result)))
{
}
Expected(Unexpected<TError> unexpected)
: m_data(std::variant<TResult, TError>(std::in_place_index<1>, std::move(*unexpected)))
{
}
constexpr operator bool() const noexcept
{
return m_data.index() == 0;
}
constexpr bool has_value() const noexcept
{
return m_data.index() == 0;
}
constexpr std::add_lvalue_reference_t<TResult> value() &
{
return std::get<0>(m_data);
}
constexpr std::add_const_t<std::add_lvalue_reference_t<TResult>> value() const&
{
return std::get<0>(m_data);
}
constexpr std::add_rvalue_reference_t<TResult> value() &&
{
return std::move(std::get<0>(m_data));
}
constexpr std::add_const_t<std::add_rvalue_reference_t<TResult>> value() const&&
{
return std::move(std::get<0>(m_data));
}
constexpr std::add_lvalue_reference_t<TResult> operator*() &
{
return std::get<0>(m_data);
}
constexpr std::add_const_t<std::add_lvalue_reference_t<TResult>> operator*() const&
{
return std::get<0>(m_data);
}
constexpr std::add_rvalue_reference_t<TResult> operator*() &&
{
return std::move(std::get<0>(m_data));
}
constexpr std::add_const_t<std::add_rvalue_reference_t<TResult>> operator*() const&&
{
return std::move(std::get<0>(m_data));
}
constexpr std::add_pointer_t<TResult> operator->()
{
return std::get<0>(m_data);
}
constexpr std::add_const_t<std::add_pointer_t<TResult>> operator->() const
{
return std::get<0>(m_data);
}
constexpr std::add_lvalue_reference_t<TError> error() &
{
return std::get<1>(m_data);
}
constexpr std::add_const_t<std::add_lvalue_reference_t<TError>> error() const&
{
return std::get<1>(m_data);
}
constexpr std::add_rvalue_reference_t<TError> error() &&
{
return std::move(std::get<1>(m_data));
}
constexpr std::add_const_t<std::add_rvalue_reference_t<TError>> error() const&&
{
return std::move(std::get<1>(m_data));
}
private:
explicit Expected(std::variant<TResult, TError> data)
: m_data(std::move(data))
{
}
std::variant<TResult, TError> m_data;
};
#define ENSURE_RESULT_VAR(var) \
if (!(var)) \
return (var);
#define ENSURE_RESULT(expr) \
{ \
const auto result = (expr); \
if (!result) \
return result; \
}
} // namespace result

View File

@@ -6,9 +6,9 @@
using namespace IW3;
void ZoneDefWriter::WriteMetaData(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const {}
void ZoneDefWriter::WriteMetaData(ZoneDefinitionOutputStream& stream, const Zone& zone) const {}
void ZoneDefWriter::WriteContent(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const
void ZoneDefWriter::WriteContent(ZoneDefinitionOutputStream& stream, const Zone& zone) const
{
const auto* pools = dynamic_cast<GameAssetPoolIW3*>(zone.m_pools.get());

View File

@@ -1,13 +1,13 @@
#pragma once
#include "ContentLister/ZoneDefWriter.h"
#include "Zone/Definition/ZoneDefWriter.h"
namespace IW3
{
class ZoneDefWriter final : public AbstractZoneDefWriter
{
protected:
void WriteMetaData(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const override;
void WriteContent(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const override;
void WriteMetaData(ZoneDefinitionOutputStream& stream, const Zone& zone) const override;
void WriteContent(ZoneDefinitionOutputStream& stream, const Zone& zone) const override;
};
} // namespace IW3

View File

@@ -6,9 +6,9 @@
using namespace IW4;
void ZoneDefWriter::WriteMetaData(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const {}
void ZoneDefWriter::WriteMetaData(ZoneDefinitionOutputStream& stream, const Zone& zone) const {}
void ZoneDefWriter::WriteContent(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const
void ZoneDefWriter::WriteContent(ZoneDefinitionOutputStream& stream, const Zone& zone) const
{
const auto* pools = dynamic_cast<GameAssetPoolIW4*>(zone.m_pools.get());

View File

@@ -1,13 +1,13 @@
#pragma once
#include "ContentLister/ZoneDefWriter.h"
#include "Zone/Definition/ZoneDefWriter.h"
namespace IW4
{
class ZoneDefWriter final : public AbstractZoneDefWriter
{
protected:
void WriteMetaData(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const override;
void WriteContent(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const override;
void WriteMetaData(ZoneDefinitionOutputStream& stream, const Zone& zone) const override;
void WriteContent(ZoneDefinitionOutputStream& stream, const Zone& zone) const override;
};
} // namespace IW4

View File

@@ -6,9 +6,9 @@
using namespace IW5;
void ZoneDefWriter::WriteMetaData(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const {}
void ZoneDefWriter::WriteMetaData(ZoneDefinitionOutputStream& stream, const Zone& zone) const {}
void ZoneDefWriter::WriteContent(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const
void ZoneDefWriter::WriteContent(ZoneDefinitionOutputStream& stream, const Zone& zone) const
{
const auto* pools = dynamic_cast<GameAssetPoolIW5*>(zone.m_pools.get());

View File

@@ -1,13 +1,13 @@
#pragma once
#include "ContentLister/ZoneDefWriter.h"
#include "Zone/Definition/ZoneDefWriter.h"
namespace IW5
{
class ZoneDefWriter final : public AbstractZoneDefWriter
{
protected:
void WriteMetaData(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const override;
void WriteContent(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const override;
void WriteMetaData(ZoneDefinitionOutputStream& stream, const Zone& zone) const override;
void WriteContent(ZoneDefinitionOutputStream& stream, const Zone& zone) const override;
};
} // namespace IW5

View File

@@ -6,9 +6,9 @@
using namespace T5;
void ZoneDefWriter::WriteMetaData(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const {}
void ZoneDefWriter::WriteMetaData(ZoneDefinitionOutputStream& stream, const Zone& zone) const {}
void ZoneDefWriter::WriteContent(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const
void ZoneDefWriter::WriteContent(ZoneDefinitionOutputStream& stream, const Zone& zone) const
{
const auto* pools = dynamic_cast<GameAssetPoolT5*>(zone.m_pools.get());

View File

@@ -1,13 +1,13 @@
#pragma once
#include "ContentLister/ZoneDefWriter.h"
#include "Zone/Definition/ZoneDefWriter.h"
namespace T5
{
class ZoneDefWriter final : public AbstractZoneDefWriter
{
protected:
void WriteMetaData(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const override;
void WriteContent(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const override;
void WriteMetaData(ZoneDefinitionOutputStream& stream, const Zone& zone) const override;
void WriteContent(ZoneDefinitionOutputStream& stream, const Zone& zone) const override;
};
} // namespace T5

View File

@@ -48,7 +48,7 @@ namespace
}
} // namespace
void ZoneDefWriter::WriteMetaData(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const
void ZoneDefWriter::WriteMetaData(ZoneDefinitionOutputStream& stream, const Zone& zone) const
{
const auto* assetPoolT6 = dynamic_cast<GameAssetPoolT6*>(zone.m_pools.get());
if (assetPoolT6 && !assetPoolT6->m_key_value_pairs->m_asset_lookup.empty())
@@ -64,7 +64,7 @@ void ZoneDefWriter::WriteMetaData(ZoneDefinitionOutputStream& stream, const Unli
}
}
void ZoneDefWriter::WriteContent(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const
void ZoneDefWriter::WriteContent(ZoneDefinitionOutputStream& stream, const Zone& zone) const
{
const auto* pools = dynamic_cast<GameAssetPoolT6*>(zone.m_pools.get());

View File

@@ -1,13 +1,13 @@
#pragma once
#include "ContentLister/ZoneDefWriter.h"
#include "Zone/Definition/ZoneDefWriter.h"
namespace T6
{
class ZoneDefWriter final : public AbstractZoneDefWriter
{
protected:
void WriteMetaData(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const override;
void WriteContent(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const override;
void WriteMetaData(ZoneDefinitionOutputStream& stream, const Zone& zone) const override;
void WriteContent(ZoneDefinitionOutputStream& stream, const Zone& zone) const override;
};
} // namespace T6

View File

@@ -1,10 +1,10 @@
#include "ZoneDefWriter.h"
#include "Game/IW3/ZoneDefWriterIW3.h"
#include "Game/IW4/ZoneDefWriterIW4.h"
#include "Game/IW5/ZoneDefWriterIW5.h"
#include "Game/T5/ZoneDefWriterT5.h"
#include "Game/T6/ZoneDefWriterT6.h"
#include "Game/IW3/Zone/Definition/ZoneDefWriterIW3.h"
#include "Game/IW4/Zone/Definition/ZoneDefWriterIW4.h"
#include "Game/IW5/Zone/Definition/ZoneDefWriterIW5.h"
#include "Game/T5/Zone/Definition/ZoneDefWriterT5.h"
#include "Game/T6/Zone/Definition/ZoneDefWriterT6.h"
#include <cassert>
@@ -25,7 +25,7 @@ const IZoneDefWriter* IZoneDefWriter::GetZoneDefWriterForGame(GameId game)
return result;
}
void AbstractZoneDefWriter::WriteZoneDef(std::ostream& stream, const UnlinkerArgs& args, const Zone& zone) const
void AbstractZoneDefWriter::WriteZoneDef(std::ostream& stream, const Zone& zone, const bool useGdt) const
{
ZoneDefinitionOutputStream out(stream);
const auto* game = IGame::GetGameById(zone.m_game_id);
@@ -34,13 +34,13 @@ void AbstractZoneDefWriter::WriteZoneDef(std::ostream& stream, const UnlinkerArg
out.WriteMetaData(META_DATA_KEY_GAME, game->GetShortName());
out.EmptyLine();
if (args.m_use_gdt)
if (useGdt)
{
out.WriteComment("Load asset gdt files");
out.WriteMetaData(META_DATA_KEY_GDT, zone.m_name);
out.EmptyLine();
}
WriteMetaData(out, args, zone);
WriteContent(out, args, zone);
WriteMetaData(out, zone);
WriteContent(out, zone);
}

View File

@@ -1,7 +1,7 @@
#pragma once
#include "UnlinkerArgs.h"
#include "Zone/Definition/ZoneDefinitionStream.h"
#include "Zone/Zone.h"
class IZoneDefWriter
{
@@ -13,7 +13,7 @@ public:
IZoneDefWriter& operator=(const IZoneDefWriter& other) = default;
IZoneDefWriter& operator=(IZoneDefWriter&& other) noexcept = default;
virtual void WriteZoneDef(std::ostream& stream, const UnlinkerArgs& args, const Zone& zone) const = 0;
virtual void WriteZoneDef(std::ostream& stream, const Zone& zone, bool useGdt) const = 0;
static const IZoneDefWriter* GetZoneDefWriterForGame(GameId game);
};
@@ -24,9 +24,9 @@ protected:
static constexpr auto META_DATA_KEY_GAME = "game";
static constexpr auto META_DATA_KEY_GDT = "gdt";
virtual void WriteMetaData(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const = 0;
virtual void WriteContent(ZoneDefinitionOutputStream& stream, const UnlinkerArgs& args, const Zone& zone) const = 0;
virtual void WriteMetaData(ZoneDefinitionOutputStream& stream, const Zone& zone) const = 0;
virtual void WriteContent(ZoneDefinitionOutputStream& stream, const Zone& zone) const = 0;
public:
void WriteZoneDef(std::ostream& stream, const UnlinkerArgs& args, const Zone& zone) const override;
void WriteZoneDef(std::ostream& stream, const Zone& zone, bool useGdt) const override;
};

View File

@@ -8,7 +8,8 @@ Zone::Zone(std::string name, const zone_priority_t priority, GameId gameId)
m_language(GameLanguage::LANGUAGE_NONE),
m_game_id(gameId),
m_pools(ZoneAssetPools::CreateForGame(gameId, this, priority)),
m_memory(std::make_unique<ZoneMemory>())
m_memory(std::make_unique<ZoneMemory>()),
m_registered(false)
{
}

View File

@@ -2,7 +2,6 @@
#include "Loading/IZoneLoaderFactory.h"
#include "Loading/ZoneLoader.h"
#include "Utils/Logging/Log.h"
#include "Utils/ObjFileStream.h"
#include <filesystem>
@@ -12,24 +11,18 @@
namespace fs = std::filesystem;
std::unique_ptr<Zone> ZoneLoading::LoadZone(const std::string& path)
result::Expected<std::unique_ptr<Zone>, std::string> ZoneLoading::LoadZone(const std::string& path)
{
auto zoneName = fs::path(path).filename().replace_extension().string();
std::ifstream file(path, std::fstream::in | std::fstream::binary);
if (!file.is_open())
{
con::error("Could not open file '{}'.", path);
return nullptr;
}
return result::Unexpected(std::format("Could not open file '{}'.", path));
ZoneHeader header{};
file.read(reinterpret_cast<char*>(&header), sizeof(header));
if (file.gcount() != sizeof(header))
{
con::error("Failed to read zone header from file '{}'.", path);
return nullptr;
}
return result::Unexpected(std::format("Failed to read zone header from file '{}'.", path));
std::unique_ptr<ZoneLoader> zoneLoader;
for (auto game = 0u; game < static_cast<unsigned>(GameId::COUNT); game++)
@@ -42,10 +35,7 @@ std::unique_ptr<Zone> ZoneLoading::LoadZone(const std::string& path)
}
if (!zoneLoader)
{
con::error("Could not create factory for zone '{}'.", zoneName);
return nullptr;
}
return result::Unexpected(std::format("Could not create factory for zone '{}'.", zoneName));
auto loadedZone = zoneLoader->LoadZone(file);

View File

@@ -1,4 +1,6 @@
#pragma once
#include "Utils/Result.h"
#include "Zone/Zone.h"
#include <string>
@@ -6,5 +8,5 @@
class ZoneLoading
{
public:
static std::unique_ptr<Zone> LoadZone(const std::string& path);
static result::Expected<std::unique_ptr<Zone>, std::string> LoadZone(const std::string& path);
};