2
0
mirror of https://github.com/Laupetin/OpenAssetTools.git synced 2025-11-17 18:52:06 +00:00

feat: combine loading bar and zone list

This commit is contained in:
Jan Laupetin
2025-10-16 23:04:03 +01:00
parent 78538d68f5
commit 23f5ad67e9
11 changed files with 276 additions and 129 deletions

View File

@@ -10,7 +10,7 @@ namespace fs = std::filesystem;
namespace namespace
{ {
constexpr double MIN_PROGRESS_TO_REPORT = 0.005; constexpr double MIN_PROGRESS_TO_REPORT = 0.5;
class LoadingEventProgressReporter : public ProgressCallback class LoadingEventProgressReporter : public ProgressCallback
{ {
@@ -23,7 +23,7 @@ namespace
void OnProgress(const size_t current, const size_t total) override void OnProgress(const size_t current, const size_t total) override
{ {
const double percentage = static_cast<double>(current) / static_cast<double>(total); const double percentage = static_cast<double>(current) / static_cast<double>(total) * 100.0;
if (percentage - m_last_progress >= MIN_PROGRESS_TO_REPORT) if (percentage - m_last_progress >= MIN_PROGRESS_TO_REPORT)
{ {
@@ -38,38 +38,53 @@ namespace
}; };
} // namespace } // namespace
LoadedZone::LoadedZone(std::unique_ptr<Zone> zone, std::string filePath)
: m_zone(std::move(zone)),
m_file_path(std::move(filePath))
{
}
void FastFileContext::Destroy() void FastFileContext::Destroy()
{ {
// Unload all zones // Unload all zones
m_loaded_zones.clear(); m_loaded_zones.clear();
} }
result::Expected<Zone*, std::string> FastFileContext::LoadFastFile(const std::string& path) result::Expected<LoadedZone*, std::string> FastFileContext::LoadFastFile(const std::string& path)
{ {
auto zone = ZoneLoading::LoadZone(path, std::make_unique<LoadingEventProgressReporter>(fs::path(path).filename().replace_extension().string())); auto zone = ZoneLoading::LoadZone(path, std::make_unique<LoadingEventProgressReporter>(fs::path(path).filename().replace_extension().string()));
if (!zone) if (!zone)
return result::Unexpected(std::move(zone.error())); return result::Unexpected(std::move(zone.error()));
auto* result = m_loaded_zones.emplace_back(std::move(*zone)).get(); auto loadedZone = std::make_unique<LoadedZone>(std::move(*zone), path);
ui::NotifyZoneLoaded(result->m_name, path); LoadedZone* result;
{
std::lock_guard lock(m_zone_lock);
result = m_loaded_zones.emplace_back(std::move(loadedZone)).get();
}
ui::NotifyZoneLoaded(*result);
return result; return result;
} }
result::Expected<NoResult, std::string> FastFileContext::UnloadZone(const std::string& zoneName) 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); std::lock_guard lock(m_zone_lock);
ui::NotifyZoneUnloaded(zoneName); const auto existingZone = std::ranges::find_if(m_loaded_zones,
return NoResult(); [&zoneName](const std::unique_ptr<LoadedZone>& loadedZone)
{
return loadedZone->m_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)); return result::Unexpected(std::format("No zone with name {} loaded", zoneName));

View File

@@ -4,15 +4,26 @@
#include "Zone/Zone.h" #include "Zone/Zone.h"
#include <memory> #include <memory>
#include <shared_mutex>
#include <vector> #include <vector>
class LoadedZone
{
public:
std::unique_ptr<Zone> m_zone;
std::string m_file_path;
LoadedZone(std::unique_ptr<Zone> zone, std::string filePath);
};
class FastFileContext class FastFileContext
{ {
public: public:
void Destroy(); void Destroy();
result::Expected<Zone*, std::string> LoadFastFile(const std::string& path); result::Expected<LoadedZone*, std::string> LoadFastFile(const std::string& path);
result::Expected<NoResult, std::string> UnloadZone(const std::string& zoneName); result::Expected<NoResult, std::string> UnloadZone(const std::string& zoneName);
std::vector<std::unique_ptr<Zone>> m_loaded_zones; std::vector<std::unique_ptr<LoadedZone>> m_loaded_zones;
std::shared_mutex m_zone_lock;
}; };

View File

@@ -23,7 +23,7 @@ namespace
NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneUnlinkProgressDto, zoneName, percentage); NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneUnlinkProgressDto, zoneName, percentage);
constexpr double MIN_PROGRESS_TO_REPORT = 0.005; constexpr double MIN_PROGRESS_TO_REPORT = 0.5;
class UnlinkingEventProgressReporter : public ProgressCallback class UnlinkingEventProgressReporter : public ProgressCallback
{ {
@@ -36,7 +36,7 @@ namespace
void OnProgress(const size_t current, const size_t total) override void OnProgress(const size_t current, const size_t total) override
{ {
const double percentage = static_cast<double>(current) / static_cast<double>(total); const double percentage = static_cast<double>(current) / static_cast<double>(total) * 100.0;
if (percentage - m_last_progress >= MIN_PROGRESS_TO_REPORT) if (percentage - m_last_progress >= MIN_PROGRESS_TO_REPORT)
{ {
@@ -54,17 +54,17 @@ namespace
{ {
const auto& context = ModManContext::Get().m_fast_file; const auto& context = ModManContext::Get().m_fast_file;
const auto existingZone = std::ranges::find_if(context.m_loaded_zones, const auto existingZone = std::ranges::find_if(context.m_loaded_zones,
[&zoneName](const std::unique_ptr<Zone>& zone) [&zoneName](const std::unique_ptr<LoadedZone>& loadedZone)
{ {
return zone->m_name == zoneName; return loadedZone->m_zone->m_name == zoneName;
}); });
if (existingZone == context.m_loaded_zones.end()) if (existingZone == context.m_loaded_zones.end())
return result::Unexpected(std::format("No zone with name {} loaded", zoneName)); return result::Unexpected(std::format("No zone with name {} loaded", zoneName));
const auto& zone = *existingZone->get(); const auto& loadedZone = *existingZone->get();
const auto* objWriter = IObjWriter::GetObjWriterForGame(zone.m_game_id); const auto* objWriter = IObjWriter::GetObjWriterForGame(loadedZone.m_zone->m_game_id);
const auto outputFolderPath = fs::path(utils::GetExecutablePath()).parent_path() / "zone_dump" / zoneName; const auto outputFolderPath = fs::path(utils::GetExecutablePath()).parent_path() / "zone_dump" / zoneName;
const auto outputFolderPathStr = outputFolderPath.string(); const auto outputFolderPathStr = outputFolderPath.string();
@@ -72,7 +72,7 @@ namespace
OutputPathFilesystem outputFolderOutputPath(outputFolderPath); OutputPathFilesystem outputFolderOutputPath(outputFolderPath);
SearchPaths searchPaths; SearchPaths searchPaths;
AssetDumpingContext dumpingContext( AssetDumpingContext dumpingContext(
zone, outputFolderPathStr, outputFolderOutputPath, searchPaths, std::make_unique<UnlinkingEventProgressReporter>(zoneName)); *loadedZone.m_zone, outputFolderPathStr, outputFolderOutputPath, searchPaths, std::make_unique<UnlinkingEventProgressReporter>(zoneName));
objWriter->DumpZone(dumpingContext); objWriter->DumpZone(dumpingContext);
return NoResult(); return NoResult();

View File

@@ -7,6 +7,15 @@
namespace namespace
{ {
class ZoneDto
{
public:
std::string name;
std::string filePath;
};
NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneDto, name, filePath);
class ZoneLoadProgressDto class ZoneLoadProgressDto
{ {
public: public:
@@ -19,11 +28,10 @@ namespace
class ZoneLoadedDto class ZoneLoadedDto
{ {
public: public:
std::string zoneName; ZoneDto zone;
std::string filePath;
}; };
NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneLoadedDto, zoneName, filePath); NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneLoadedDto, zone);
class ZoneUnloadedDto class ZoneUnloadedDto
{ {
@@ -33,6 +41,33 @@ namespace
NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneUnloadedDto, zoneName); NLOHMANN_DEFINE_TYPE_EXTENSION(ZoneUnloadedDto, zoneName);
ZoneDto CreateZoneDto(const LoadedZone& loadedZone)
{
return ZoneDto{
.name = loadedZone.m_zone->m_name,
.filePath = loadedZone.m_file_path,
};
}
std::vector<ZoneDto> GetLoadedZones()
{
auto& context = ModManContext::Get().m_fast_file;
std::vector<ZoneDto> result;
{
std::shared_lock lock(context.m_zone_lock);
result.reserve(context.m_loaded_zones.size());
for (const auto& loadedZone : context.m_loaded_zones)
{
result.emplace_back(CreateZoneDto(*loadedZone));
}
}
return result;
}
void LoadFastFile(webview::webview& wv, std::string id, std::string path) // NOLINT(performance-unnecessary-value-param) Copy is made for thread safety 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( ModManContext::Get().m_db_thread.Dispatch(
@@ -45,10 +80,9 @@ namespace
ui::PromiseResolve(wv, ui::PromiseResolve(wv,
id, id,
ZoneLoadedDto{ ZoneLoadedDto{
.zoneName = maybeZone.value()->m_name, .zone = CreateZoneDto(*maybeZone.value()),
.filePath = path,
}); });
con::debug("Loaded zone \"{}\"", maybeZone.value()->m_name); con::debug("Loaded zone \"{}\"", maybeZone.value()->m_zone->m_name);
} }
else else
{ {
@@ -89,11 +123,10 @@ namespace ui
Notify(*ModManContext::Get().m_main_webview, "zoneLoadProgress", dto); Notify(*ModManContext::Get().m_main_webview, "zoneLoadProgress", dto);
} }
void NotifyZoneLoaded(std::string zoneName, std::string fastFilePath) void NotifyZoneLoaded(const LoadedZone& loadedZone)
{ {
const ZoneLoadedDto dto{ const ZoneLoadedDto dto{
.zoneName = std::move(zoneName), .zone = CreateZoneDto(loadedZone),
.filePath = std::move(fastFilePath),
}; };
Notify(*ModManContext::Get().m_main_webview, "zoneLoaded", dto); Notify(*ModManContext::Get().m_main_webview, "zoneLoaded", dto);
} }
@@ -108,6 +141,13 @@ namespace ui
void RegisterZoneBinds(webview::webview& wv) void RegisterZoneBinds(webview::webview& wv)
{ {
BindRetOnly<std::vector<ZoneDto>>(wv,
"getZones",
[]
{
return GetLoadedZones();
});
BindAsync<std::string>(wv, BindAsync<std::string>(wv,
"loadFastFile", "loadFastFile",
[&wv](const std::string& id, std::string path) [&wv](const std::string& id, std::string path)

View File

@@ -1,11 +1,12 @@
#pragma once #pragma once
#include "Context/FastFileContext.h"
#include "Web/WebViewLib.h" #include "Web/WebViewLib.h"
namespace ui namespace ui
{ {
void NotifyZoneLoadProgress(std::string zoneName, double percentage); void NotifyZoneLoadProgress(std::string zoneName, double percentage);
void NotifyZoneLoaded(std::string zoneName, std::string fastFilePath); void NotifyZoneLoaded(const LoadedZone& loadedZone);
void NotifyZoneUnloaded(std::string zoneName); void NotifyZoneUnloaded(std::string zoneName);
void RegisterZoneBinds(webview::webview& wv); void RegisterZoneBinds(webview::webview& wv);

View File

@@ -1,80 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import Button from "primevue/button"; import Button from "primevue/button";
import { computed, ref } from "vue"; import { webviewBinds } from "@/native";
import { webviewAddEventListener, webviewBinds } from "@/native";
import ProgressBar from "primevue/progressbar";
import ZoneSelector from "./components/ZoneSelector.vue"; import ZoneSelector from "./components/ZoneSelector.vue";
import { useZoneStore } from "./stores/ZoneStore";
const loadingFastFile = ref(false); const zoneStore = useZoneStore();
const unlinkingFastFile = ref(false);
const lastPercentage = ref<number>(0);
const performingAction = computed<boolean>(() => loadingFastFile.value || unlinkingFastFile.value);
async function openFastFileSelect() { async function openFastFileSelect() {
return await webviewBinds.openFileDialog({ filters: [{ name: "Fastfiles", filter: "*.ff" }] }); return await webviewBinds.openFileDialog({ filters: [{ name: "Fastfiles", filter: "*.ff" }] });
} }
async function onOpenFastFileClick() { async function onOpenFastFileClick() {
if (performingAction.value) return;
const fastFilePath = await openFastFileSelect(); const fastFilePath = await openFastFileSelect();
if (!fastFilePath) return; if (!fastFilePath) return;
loadingFastFile.value = true; zoneStore.loadFastFile(fastFilePath);
lastPercentage.value = 0;
webviewBinds
.loadFastFile(fastFilePath)
.catch((e: string) => {
console.error("Failed to load fastfile:", e);
})
.finally(() => {
loadingFastFile.value = false;
lastPercentage.value = 100;
});
} }
async function onUnlinkFastFileClick() {
if (performingAction.value) return;
const fastFilePath = await openFastFileSelect();
if (!fastFilePath) return;
try {
unlinkingFastFile.value = true;
let loadedZoneName: string;
try {
lastPercentage.value = 0;
loadedZoneName = (await webviewBinds.loadFastFile(fastFilePath)).zoneName;
} catch (e: unknown) {
console.error("Failed to load fastfile:", e as string);
return;
}
try {
lastPercentage.value = 0;
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;
lastPercentage.value = 100;
}
}
webviewAddEventListener("zoneLoadProgress", (dto) => {
lastPercentage.value = Math.floor(dto.percentage * 1000) / 10;
});
webviewAddEventListener("zoneUnlinkProgress", (dto) => {
lastPercentage.value = Math.floor(dto.percentage * 1000) / 10;
});
</script> </script>
<template> <template>
@@ -83,28 +24,10 @@ webviewAddEventListener("zoneUnlinkProgress", (dto) => {
<small>Nothing to see here yet, this is mainly for testing</small> <small>Nothing to see here yet, this is mainly for testing</small>
<div class="actions"> <div class="actions">
<Button <Button label="Load fastfile" @click="onOpenFastFileClick" />
label="Load fastfile"
:disabled="performingAction"
:loading="loadingFastFile"
@click="onOpenFastFileClick"
/>
<Button
label="Unlink fastfile"
:disabled="performingAction"
:loading="unlinkingFastFile"
@click="onUnlinkFastFileClick"
/>
</div> </div>
<ZoneSelector /> <ZoneSelector />
<ProgressBar
v-if="performingAction"
class="progressbar"
:show-value="false"
:value="lastPercentage"
/>
</main> </main>
</template> </template>
@@ -113,6 +36,7 @@ webviewAddEventListener("zoneUnlinkProgress", (dto) => {
display: flex; display: flex;
justify-content: center; justify-content: center;
column-gap: 0.5em; column-gap: 0.5em;
margin-top: 1em;
} }
.zone-list { .zone-list {

View File

@@ -1,17 +1,46 @@
<script setup lang="ts"> <script setup lang="ts">
import Button from "primevue/button"; import Button from "primevue/button";
import ProgressBar from "primevue/progressbar";
import Listbox from "primevue/listbox"; import Listbox from "primevue/listbox";
import { ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { useZoneStore } from "@/stores/ZoneStore"; import { useZoneStore } from "@/stores/ZoneStore";
import { webviewBinds } from "@/native"; import { webviewBinds } from "@/native";
interface SelectableZone {
isLoading: boolean;
zoneName: string;
}
const zoneStore = useZoneStore(); const zoneStore = useZoneStore();
const selectedZone = ref<string | null>(null); const selectedZone = ref<SelectableZone | null>(null);
const availableZones = computed<SelectableZone[]>(() => {
const result = [
...zoneStore.zonesCurrentlyBeingLoaded.map(
(zoneBeingLoaded) =>
({
isLoading: true,
zoneName: zoneBeingLoaded,
}) satisfies SelectableZone,
),
...zoneStore.loadedZones.map(
(loadedZone) =>
({
isLoading: false,
zoneName: loadedZone.name,
}) satisfies SelectableZone,
),
];
result.sort((a, b) => a.zoneName.localeCompare(b.zoneName));
return result;
});
function onUnloadClicked() { function onUnloadClicked() {
if (!selectedZone.value) return; if (!selectedZone.value) return;
webviewBinds.unloadZone(selectedZone.value).catch((e: string) => { webviewBinds.unloadZone(selectedZone.value.zoneName).catch((e: string) => {
console.error("Failed to unload zone:", e); console.error("Failed to unload zone:", e);
}); });
} }
@@ -21,7 +50,8 @@ watch(
(newValue) => { (newValue) => {
// Reset selection if unloaded // Reset selection if unloaded
if (!selectedZone.value) return; if (!selectedZone.value) return;
if (newValue.indexOf(selectedZone.value) >= 0) return; if (newValue.findIndex((loadedZone) => loadedZone.name === selectedZone.value?.zoneName) >= 0)
return;
selectedZone.value = null; selectedZone.value = null;
}, },
{ deep: true }, { deep: true },
@@ -31,7 +61,25 @@ watch(
<template> <template>
<div class="zone-selector"> <div class="zone-selector">
<div class="zone-list"> <div class="zone-list">
<Listbox v-model="selectedZone" :options="zoneStore.loadedZones" class="zone" /> <Listbox
v-model="selectedZone"
:options="availableZones"
data-key="zoneName"
emptyMessage="No zones loaded"
class="zone"
>
<template #option="{ option }: { option: SelectableZone }">
<div class="selectable-zone">
<span>{{ option.zoneName }}</span>
<ProgressBar
v-if="option.isLoading"
class="zone-progressbar"
:value="zoneStore.getPercentageForZoneBeingLoaded(option.zoneName)"
:show-value="false"
/>
</div>
</template>
</Listbox>
</div> </div>
<div class="zone-actions"> <div class="zone-actions">
@@ -51,4 +99,18 @@ watch(
padding: 1rem 2rem; padding: 1rem 2rem;
} }
} }
.selectable-zone {
position: relative;
text-align: left;
width: 100%;
}
.zone-progressbar {
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 0.2em;
}
</style> </style>

View File

@@ -1,5 +1,8 @@
export interface ZoneUnlinkProgressDto { export interface ZoneUnlinkProgressDto {
zoneName: string; zoneName: string;
/**
* Between 0-100
*/
percentage: number; percentage: number;
} }

View File

@@ -1,11 +1,18 @@
export interface ZoneDto {
name: string;
filePath: string;
}
export interface ZoneLoadProgressDto { export interface ZoneLoadProgressDto {
zoneName: string; zoneName: string;
/**
* Between 0-100
*/
percentage: number; percentage: number;
} }
export interface ZoneLoadedDto { export interface ZoneLoadedDto {
zoneName: string; zone: ZoneDto;
filePath: string;
} }
export interface ZoneUnloadedDto { export interface ZoneUnloadedDto {
@@ -13,6 +20,7 @@ export interface ZoneUnloadedDto {
} }
export interface ZoneBinds { export interface ZoneBinds {
getZones(): Promise<ZoneDto[]>;
loadFastFile(path: string): Promise<ZoneLoadedDto>; loadFastFile(path: string): Promise<ZoneLoadedDto>;
unloadZone(zoneName: string): Promise<void>; unloadZone(zoneName: string): Promise<void>;
} }

View File

@@ -0,0 +1,31 @@
import { ref } from "vue";
import { defineStore } from "pinia";
import { webviewAddEventListener, webviewBinds } from "@/native";
export const useUnlinkingStore = defineStore("unlinking", () => {
const isUnlinking = ref(false);
const lastPercentage = ref<number>(0);
const failureMessage = ref<string | null>(null);
function unlinkZone(zoneName: string) {
isUnlinking.value = true;
lastPercentage.value = 0;
failureMessage.value = null;
return webviewBinds
.unlinkZone(zoneName)
.catch((e: string) => {
console.error("Failed to unlink fastfile:", e);
failureMessage.value = e;
})
.finally(() => {
isUnlinking.value = false;
lastPercentage.value = 100;
});
}
webviewAddEventListener("zoneUnlinkProgress", (dto) => {
lastPercentage.value = dto.percentage;
});
return { isUnlinking, lastPercentage, unlinkZone };
});

View File

@@ -1,20 +1,72 @@
import { ref } from "vue"; import { computed, ref } from "vue";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { webviewAddEventListener } from "@/native"; import { webviewAddEventListener, webviewBinds } from "@/native";
import type { ZoneDto, ZoneLoadedDto } from "@/native/ZoneBinds";
export const useZoneStore = defineStore("zone", () => { export const useZoneStore = defineStore("zone", () => {
const loadedZones = ref<string[]>([]); const loadedZones = ref<ZoneDto[]>([]);
const zonesCurrentlyBeingLoaded = ref<string[]>([]);
const lastPercentageByZoneName = ref<Record<string, number>>({});
const isLoadingZone = computed(() => zonesCurrentlyBeingLoaded.value.length > 0);
function loadFastFile(fastFilePath: string): Promise<ZoneLoadedDto> {
const lastDirectorySeparator = fastFilePath.replace(/\\/g, "/").lastIndexOf("/");
const lastDot = fastFilePath.lastIndexOf(".");
const expectedZoneName = fastFilePath.substring(
lastDirectorySeparator >= 0 ? lastDirectorySeparator + 1 : 0,
lastDot > lastDirectorySeparator ? lastDot : fastFilePath.length,
);
zonesCurrentlyBeingLoaded.value.push(expectedZoneName);
lastPercentageByZoneName.value[expectedZoneName] = 0;
return webviewBinds
.loadFastFile(fastFilePath)
.catch((e: string) => {
console.error("Failed to load fastfile:", e);
})
.finally(() => {
zonesCurrentlyBeingLoaded.value.splice(
zonesCurrentlyBeingLoaded.value.indexOf(expectedZoneName),
1,
);
delete lastPercentageByZoneName.value[expectedZoneName];
}) as Promise<ZoneLoadedDto>;
}
function getPercentageForZoneBeingLoaded(zoneName: string) {
return lastPercentageByZoneName.value[zoneName] ?? 100;
}
// Initially get all loaded zones
webviewBinds.getZones().then((allZones) => {
loadedZones.value = allZones;
});
webviewAddEventListener("zoneLoadProgress", (dto) => {
if (lastPercentageByZoneName.value[dto.zoneName] !== undefined) {
lastPercentageByZoneName.value[dto.zoneName] = dto.percentage;
}
});
webviewAddEventListener("zoneLoaded", (dto) => { webviewAddEventListener("zoneLoaded", (dto) => {
loadedZones.value.push(dto.zoneName); loadedZones.value.push(dto.zone);
}); });
webviewAddEventListener("zoneUnloaded", (dto) => { webviewAddEventListener("zoneUnloaded", (dto) => {
const index = loadedZones.value.indexOf(dto.zoneName); const index = loadedZones.value.findIndex((zone) => zone.name === dto.zoneName);
if (index >= 0) { if (index >= 0) {
loadedZones.value.splice(index, 1); loadedZones.value.splice(index, 1);
} }
}); });
return { loadedZones }; return {
loadedZones,
zonesCurrentlyBeingLoaded,
isLoadingZone,
lastPercentageByZoneName,
loadFastFile,
getPercentageForZoneBeingLoaded,
};
}); });