Skip to content

Zipped Save Files #166

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

Draft
wants to merge 42 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4d948f8
First implementation of saving/loading menu
Causeless Aug 14, 2023
ece8b62
Added activity/scene/date info
Causeless Aug 17, 2023
8dcf097
Always show save/load - we should disable the "Create Save" button in…
Causeless Aug 18, 2023
65fef08
Added order-by to save list
Causeless Aug 18, 2023
dd02bc4
Lots of fixes and improvements. Added save menu to main menu. Lots of…
Causeless Aug 18, 2023
79e0a79
Cleanup and consistency with listbox selection. Show "Overwrite" whe…
Causeless Aug 19, 2023
1f10ab7
Added confirmation dialog for overwrite/delete
Causeless Aug 19, 2023
bc69c68
Fixed spacing
Causeless Aug 19, 2023
2f29d79
Hopefully fix build
Causeless Aug 20, 2023
7656ea9
Added read/write with arbitrary streams
Causeless Aug 20, 2023
44a0d78
Prototyping for zip saves...
Causeless Aug 20, 2023
d5619dd
Merge branch 'development' into zip-save-files
Causeless Nov 13, 2023
d621aa6
Fix compile error
Causeless Nov 13, 2023
e16149a
Merge branch 'development' into zip-save-files
Causeless Nov 13, 2023
43f664e
Merge branch 'development' into zip-save-files
Causeless Dec 31, 2023
bfbb06a
Merge remote-tracking branch 'source/zip-save-files' into zip-save-files
traunts Jan 2, 2024
7bb4d19
Merge branch 'development' into zip-save-files
traunts Jan 2, 2024
2c6d289
Merge branch 'development' into zip-save-files
Causeless Jan 5, 2024
59ffb04
Merge branch 'development' into zip-save-files
Causeless Jan 20, 2024
b22c5ab
format
HeliumAnt Jan 20, 2024
882b41d
Merge branch 'development' into zip-save-files
HeliumAnt Jan 20, 2024
c0357ba
fix eof newlines
HeliumAnt Jan 20, 2024
5224634
apply doxygen
HeliumAnt Jan 20, 2024
6507b46
Merge branch 'development' into zip-save-files
HeliumAnt Jan 20, 2024
3542ee6
Merge branch 'development' into zip-save-files
Causeless Jan 21, 2024
6294d3d
Merge branch 'development' into zip-save-files
Causeless Jan 21, 2024
43dff35
Merge branch 'development' into zip-save-files
Causeless Jan 21, 2024
4dfdad6
Merge branch 'development' into zip-save-files
Causeless Jan 21, 2024
56bd587
re-run clang format
Causeless Jan 21, 2024
920f01a
Merge branch 'development' into zip-save-files
Causeless Sep 11, 2024
fdef918
Merge branch 'development' into zip-save-files
Causeless Dec 7, 2024
bb24006
Merge branch 'development' into zip-save-files
Causeless Dec 7, 2024
0853cbe
Merge branch 'development' into zip-save-files
Causeless Feb 28, 2025
1d3c878
Merge branch 'development' into zip-save-files
Causeless Jul 6, 2025
2bc4bdd
added comments so I kinda vaguely remember what I need to do
Causeless Jul 6, 2025
8093ff5
A little progress
Causeless Jul 6, 2025
5863694
a vague probably non-working way to save to memory buffer
Causeless Jul 6, 2025
4de5aa1
saving zips! need to do loading next
Causeless Jul 7, 2025
18313b2
Save/load ini- need to do pngs next
Causeless Jul 8, 2025
51eeba3
Progress towards loading, need to fix up unseen layers
Causeless Jul 8, 2025
b5e109a
Added extra free that migh've caused memory leaks
Causeless Jul 8, 2025
5d9326d
Might as well null these too
Causeless Jul 8, 2025
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
52 changes: 26 additions & 26 deletions RTEA.vcxproj

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions RTEA.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -484,9 +484,6 @@
<ClInclude Include="Source\Entities\Round.h">
<Filter>Entities</Filter>
</ClInclude>
<ClInclude Include="Source\System\NetworkMessages.h">
<Filter>System</Filter>
</ClInclude>
<ClInclude Include="Source\System\InputScheme.h">
<Filter>System</Filter>
</ClInclude>
Expand Down Expand Up @@ -657,6 +654,12 @@
<ClInclude Include="Source\Renderer\raylib\rlutils.h">
<Filter>Renderer\raylib</Filter>
</ClInclude>
<ClInclude Include="Source\Renderer\raylib\rlutil.h" />
<ClInclude Include="Source\GUI\imgui\imgui_internal.h" />
<ClInclude Include="Source\GUI\imgui\imstb_rectpack.h" />
<ClInclude Include="Source\GUI\imgui\imstb_textedit.h" />
<ClInclude Include="Source\GUI\imgui\imstd_truetype.h" />
<ClInclude Include="Source\GUI\imgui\backends\imgui_impl_opengl3_loader.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="Source\System\Box.cpp">
Expand Down
6 changes: 6 additions & 0 deletions Source/Entities/SLTerrain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,12 @@ int SLTerrain::SaveData(const std::string& pathBase, bool doAsyncSaves) {
return 0;
}

void SLTerrain::CopyBitmapData(std::vector<SceneLayerInfo>& layerInfos) const {
layerInfos.emplace_back(std::string("Mat"), SceneLayer::CopyBitmap());
layerInfos.emplace_back(std::string("FG"), m_FGColorLayer->CopyBitmap());
layerInfos.emplace_back(std::string("BG"), m_BGColorLayer->CopyBitmap());
}

int SLTerrain::ClearData() {
RTEAssert(SceneLayer::ClearData() == 0, "Failed to clear material bitmap data of an SLTerrain!");
RTEAssert(m_FGColorLayer && m_FGColorLayer->ClearData() == 0, "Failed to clear the foreground color bitmap data of an SLTerrain!");
Expand Down
12 changes: 12 additions & 0 deletions Source/Entities/SLTerrain.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ namespace RTE {
/// @return An error return value signaling success or any particular failure. Anything below 0 is an error signal.
int SaveData(const std::string& pathBase, bool doAsyncSaves = true) override;

/// Copies bitmap data into layerInfos.
/// @param layerInfos List of SceneLayerInfo to emplace our copied data into.
void CopyBitmapData(std::vector<SceneLayerInfo>& layerInfos) const;

/// Clears out any previously loaded bitmap data from memory.
/// @return An error return value signaling success or any particular failure. Anything below 0 is an error signal.
int ClearData() override;
Expand All @@ -86,6 +90,14 @@ namespace RTE {
/// @param layerToDraw The layer that should be drawn. See LayerType enumeration.
void SetLayerToDraw(LayerType layerToDraw) { m_LayerToDraw = layerToDraw; }

/// Gets the foreground scenelayer of this SLTerrain.
/// @return A pointer to the foreground scenelayer.
SceneLayer* GetFGSceneLayer() { return m_FGColorLayer.get(); }

/// Gets the background scenelayer of this SLTerrain.
/// @return A pointer to the background scenelayer.
SceneLayer* GetBGSceneLayer() { return m_BGColorLayer.get(); }

/// Gets the foreground color bitmap of this SLTerrain.
/// @return A pointer to the foreground color bitmap.
BITMAP* GetFGColorBitmap() { return m_FGColorLayer->GetBitmap(); m_FGColorLayer->SetUpdated(); }
Expand Down
38 changes: 38 additions & 0 deletions Source/Entities/Scene.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,44 @@ int Scene::SaveData(std::string pathBase, bool doAsyncSaves) {
return 0;
}

void Scene::ConstructSceneLayersFromBitmaps(std::vector<SceneLayerInfo>&& layerInfos) {
for (SceneLayerInfo& sceneLayerInfo : layerInfos) {
if (sceneLayerInfo.name == "Mat") {
m_pTerrain->LoadDataFromBitmap(sceneLayerInfo.bitmap.release());
} else if (sceneLayerInfo.name == "FG") {
m_pTerrain->GetFGSceneLayer()->LoadDataFromBitmap(sceneLayerInfo.bitmap.release());
} else if (sceneLayerInfo.name == "BG") {
m_pTerrain->GetBGSceneLayer()->LoadDataFromBitmap(sceneLayerInfo.bitmap.release());
} else {
char endCh = sceneLayerInfo.name.back();
int team = endCh - '0';
m_apUnseenLayer[team]->LoadDataFromBitmap(sceneLayerInfo.bitmap.release());
}
}
}

std::vector<SceneLayerInfo> Scene::GetCopiedSceneLayerBitmaps() const {
std::vector<SceneLayerInfo> layerInfos;

// Save Terrain's data
m_pTerrain->CopyBitmapData(layerInfos);

// Don't bother saving background layers to disk, as they are never altered

// Save unseen layers' data
char str[64];
for (int team = Activity::TeamOne; team < Activity::MaxTeamCount; ++team)
{
if (m_apUnseenLayer[team])
{
std::snprintf(str, sizeof(str), "US T%d", team);
layerInfos.emplace_back(std::string(str), m_apUnseenLayer[team]->CopyBitmap());
}
}

return layerInfos;
}

int Scene::SavePreview(const std::string& bitmapPath) {
// Do not save preview for MetaScenes!
if (!m_MetasceneParent.empty()) {
Expand Down
9 changes: 9 additions & 0 deletions Source/Entities/Scene.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ namespace RTE {
class SceneObject;
class Deployment;

struct SceneLayerInfo {
std::string name;
std::unique_ptr<BITMAP> bitmap;
};

/// Contains everything that defines a complete scene.
class Scene : public Entity {

Expand Down Expand Up @@ -246,6 +251,10 @@ namespace RTE {
/// Anything below 0 is an error signal.
int SaveData(std::string pathBase, bool doAsyncSaves = true);

void ConstructSceneLayersFromBitmaps(std::vector<SceneLayerInfo>&& layerInfos);

std::vector<SceneLayerInfo> GetCopiedSceneLayerBitmaps() const;

/// Saves preview bitmap for this scene.
/// @param bitmapPath The full filepath the where to save the Bitmap data.
int SavePreview(const std::string& bitmapPath);
Expand Down
35 changes: 35 additions & 0 deletions Source/Entities/SceneLayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,33 @@ void SceneLayerImpl<TRACK_DRAWINGS, STATIC_TEXTURE>::InitScrollRatios(bool initF
m_ScaledDimensions.SetXY(mainBitmapWidth * m_ScaleFactor.GetX(), mainBitmapHeight * m_ScaleFactor.GetY());
}

template <bool TRACK_DRAWINGS, bool STATIC_TEXTURE>
int SceneLayerImpl<TRACK_DRAWINGS, STATIC_TEXTURE>::LoadDataFromBitmap(BITMAP* bitmap) {
if (m_MainBitmapOwned) {
destroy_bitmap(m_MainBitmap);
m_MainBitmap = nullptr;
}

m_MainBitmap = bitmap;
m_MainBitmapOwned = true;

m_BackBitmap = create_bitmap_ex(bitmap_color_depth(m_MainBitmap), m_MainBitmap->w, m_MainBitmap->h);
if constexpr (!STATIC_TEXTURE) {
m_MainTexture = std::make_unique<BigTexture>(m_MainBitmap);
}
m_LastClearColor = ColorKeys::g_InvalidColor;

InitScrollRatios();
return 0;
}

template <bool TRACK_DRAWINGS, bool STATIC_TEXTURE>
int SceneLayerImpl<TRACK_DRAWINGS, STATIC_TEXTURE>::LoadData() {
if (m_MainBitmapOwned) {
destroy_bitmap(m_MainBitmap);
m_MainBitmap = nullptr;
}

// Load from disk and take ownership. Don't cache because the bitmap will be modified.
m_MainBitmap = m_BitmapFile.GetAsBitmap(COLORCONV_NONE, false);
m_MainBitmapOwned = true;
Expand Down Expand Up @@ -245,6 +270,16 @@ int SceneLayerImpl<TRACK_DRAWINGS, STATIC_TEXTURE>::SaveData(const std::string&
return 0;
}

template <bool TRACK_DRAWINGS, bool STATIC_TEXTURE>
std::unique_ptr<BITMAP> SceneLayerImpl<TRACK_DRAWINGS, STATIC_TEXTURE>::CopyBitmap() const {
BITMAP* outputBitmap = create_bitmap_ex(bitmap_color_depth(m_MainBitmap), m_MainBitmap->w, m_MainBitmap->h);
if (m_MainBitmap) {
outputBitmap = create_bitmap_ex(bitmap_color_depth(m_MainBitmap), m_MainBitmap->w, m_MainBitmap->h);
blit(m_MainBitmap, outputBitmap, 0, 0, 0, 0, m_MainBitmap->w, m_MainBitmap->h);
}
return std::unique_ptr<BITMAP>(outputBitmap);
}

template <bool TRACK_DRAWINGS, bool STATIC_TEXTURE>
int SceneLayerImpl<TRACK_DRAWINGS, STATIC_TEXTURE>::ClearData() {
if (m_MainBitmap && m_MainBitmapOwned) {
Expand Down
9 changes: 9 additions & 0 deletions Source/Entities/SceneLayer.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ namespace RTE {
/// @return Whether this SceneLayer's bitmap data was loaded from a file or not.
virtual bool IsLoadedFromDisk() const { return !m_BitmapFile.GetDataPath().empty(); }

/// Loads previously specified/created data into memory from an existing BITMAP. Has to be done before using this SceneLayer if the bitmap was not generated at runtime.
/// @param bitmap Pointer to the bitmap to take. Takes ownership!
/// @return An error return value signaling success or any particular failure. Anything below 0 is an error signal.
virtual int LoadDataFromBitmap(BITMAP* bitmap);

/// Loads previously specified/created data into memory. Has to be done before using this SceneLayer if the bitmap was not generated at runtime.
/// @return An error return value signaling success or any particular failure. Anything below 0 is an error signal.
virtual int LoadData();
Expand All @@ -89,6 +94,10 @@ namespace RTE {
/// Clears out any previously loaded bitmap data from memory.
/// @return An error return value signaling success or any particular failure. Anything below 0 is an error signal.
virtual int ClearData();

/// Copies the bitmap.
/// @return The copied bitmap.
std::unique_ptr<BITMAP> CopyBitmap() const;
#pragma endregion

#pragma region Getters and Setters
Expand Down
135 changes: 120 additions & 15 deletions Source/Managers/ActivityMan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@

#include "MusicMan.h"

#include "zip.h"
#include "unzip.h"

#include "fmem.h"

using namespace RTE;

ActivityMan::ActivityMan() {
Expand Down Expand Up @@ -79,15 +84,6 @@ bool ActivityMan::SaveCurrentGame(const std::string& fileName) {
return false;
}

// TODO, save to a zip instead of a directory
std::filesystem::create_directory(g_PresetMan.GetFullModulePath(c_UserScriptedSavesModuleName) + "/" + fileName);

if (scene->SaveData(c_UserScriptedSavesModuleName + "/" + fileName + "/Save") < 0) {
// This print is actually pointless because game will abort if it fails to save layer bitmaps. It stays here for now because in reality the game doesn't properly abort if the layer bitmaps fail to save. It is what it is.
g_ConsoleMan.PrintString("ERROR: Failed to save scene bitmaps while saving!");
return false;
}

// We need a copy of our scene, because we have to do some fixup to remove PLACEONLOAD items and only keep the current MovableMan state.
std::unique_ptr<Scene> modifiableScene(dynamic_cast<Scene*>(scene->Clone()));

Expand All @@ -103,8 +99,10 @@ bool ActivityMan::SaveCurrentGame(const std::string& fileName) {
modifiableScene->GetTerrain()->SetPresetName(fileName);
modifiableScene->GetTerrain()->MigrateToModule(g_PresetMan.GetModuleID(c_UserScriptedSavesModuleName));

std::unique_ptr<std::stringstream> iniStream = std::make_unique<std::stringstream>();

// Block the main thread for a bit to let the Writer access the relevant data.
std::unique_ptr<Writer> writer(std::make_unique<Writer>(g_PresetMan.GetFullModulePath(c_UserScriptedSavesModuleName) + "/" + fileName + "/Save.ini"));
std::unique_ptr<Writer> writer(std::make_unique<Writer>(std::move(iniStream)));
writer->NewPropertyWithValue("Activity", activity);

// Pull all stuff from MovableMan into the Scene for saving, so existing Actors/ADoors are saved, without transferring ownership, so the game can continue.
Expand All @@ -121,9 +119,63 @@ bool ActivityMan::SaveCurrentGame(const std::string& fileName) {
writer->NewPropertyWithValue("PlaceUnitsIfSceneIsRestarted", g_SceneMan.GetPlaceUnitsOnLoad());
writer->NewPropertyWithValue("Scene", modifiableScene.get());

auto saveWriterData = [](Writer* writerToSave) {
writerToSave->EndWrite();
// Get BITMAPS so save into our zip
// I tired std::moving this into the function directly but threadpool really doesn't like that
std::vector<SceneLayerInfo>* sceneLayerInfos = new std::vector<SceneLayerInfo>();
*sceneLayerInfos = std::move(scene->GetCopiedSceneLayerBitmaps());

auto saveWriterData = [fileName, sceneLayerInfos](Writer* writerToSave) {
std::stringstream* stream = static_cast<std::stringstream*>(writerToSave->GetStream());
stream->flush();

// Ugly copies, but eh. todo - use a string stream that just gives us a raw buffer to grab at
std::string streamAsString = stream->str();

zip_fileinfo zfi = {0};

// Create zip sav file
zipFile zippedSaveFile = zipOpen((g_PresetMan.GetFullModulePath(c_UserScriptedSavesModuleName) + "/" + fileName + ".ccsave").c_str(), APPEND_STATUS_CREATE);
if (!zippedSaveFile) {
g_ConsoleMan.PrintString("ERROR: Couldn't create zip save file!");
return;
}

const int defaultCompression = 6;
zipOpenNewFileInZip(zippedSaveFile, (fileName + ".ini").c_str(), &zfi, nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, defaultCompression);
zipWriteInFileInZip(zippedSaveFile, streamAsString.data(), streamAsString.size());
zipCloseFileInZip(zippedSaveFile);

PALETTE palette;
get_palette(palette);

for (const SceneLayerInfo& layerInfo : *sceneLayerInfos)
{
// A bit of a finicky workaround, but to save a png to memory we create a memory stream and send that into allegro to save into
fmem memStructure;
fmem_init(&memStructure);

// Save the png to our memory stream
FILE* stream = fmem_open(&memStructure, "w");
save_stream_png(stream, layerInfo.bitmap.get(), palette);
fflush(stream);

// Actually get the memory
void* buffer;
size_t size;
fmem_mem(&memStructure, &buffer, &size);

zipOpenNewFileInZip(zippedSaveFile, (fileName + " " + layerInfo.name + ".png").c_str(), &zfi, nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, defaultCompression);
zipWriteInFileInZip(zippedSaveFile, static_cast<const char*>(buffer), size);
zipCloseFileInZip(zippedSaveFile);

fclose(stream);
fmem_term(&memStructure);
}

zipClose(zippedSaveFile, fileName.c_str());

delete writerToSave;
delete sceneLayerInfos;
};

// For some reason I can't std::move a unique ptr in, so just releasing and deleting manually...
Expand All @@ -139,14 +191,36 @@ bool ActivityMan::SaveCurrentGame(const std::string& fileName) {
bool ActivityMan::LoadAndLaunchGame(const std::string& fileName) {
m_SaveGameTask.wait();

std::string saveFilePath = g_PresetMan.GetFullModulePath(c_UserScriptedSavesModuleName) + "/" + fileName + "/Save.ini";
std::string saveFilePath = g_PresetMan.GetFullModulePath(c_UserScriptedSavesModuleName) + "/" + fileName + ".ccsave";

if (!std::filesystem::exists(saveFilePath)) {
// load zip sav file
unzFile zippedSaveFile = unzOpen(saveFilePath.c_str());
if (!zippedSaveFile) {
RTEError::ShowMessageBox("Game loading failed! Make sure you have a saved game called \"" + fileName + "\"");
return false;
}

Reader reader(saveFilePath, true, nullptr, false);
unz_file_info info;
char* buffer = nullptr;

auto unzipFileIntoBuffer = [&](std::string fullFileName) {
unzLocateFile(zippedSaveFile, fullFileName.c_str(), nullptr);
unzOpenCurrentFile(zippedSaveFile);
unzGetCurrentFileInfo(zippedSaveFile, &info, nullptr, 0, nullptr, 0, nullptr, 0);

buffer = (char*)malloc(info.uncompressed_size);
if (!buffer) {
// If this ever hits I've lost all faith in modern OSes, but alas when one is writing C, one must dance along
RTEError::ShowMessageBox("Catastrophic failure! Failed to allocate memory for savegame");
}

unzReadCurrentFile(zippedSaveFile, buffer, info.uncompressed_size);
unzCloseCurrentFile(zippedSaveFile);
};

unzipFileIntoBuffer(fileName + ".ini");

Reader reader(std::make_unique<std::istringstream>(buffer), saveFilePath, true, nullptr, false);

std::unique_ptr<Scene> scene(std::make_unique<Scene>());
std::unique_ptr<GAScripted> activity(std::make_unique<GAScripted>());
Expand All @@ -169,6 +243,10 @@ bool ActivityMan::LoadAndLaunchGame(const std::string& fileName) {
}
}

free(buffer);

int numberOfTeams = activity->m_TeamCount;

// SetSceneToLoad() doesn't Clone(), but when the Activity starts, it will eventually call LoadScene(), which does a Clone() of scene internally.
g_SceneMan.SetSceneToLoad(scene.get(), true, true);
// Saved Scenes get their presetname set to their filename to ensure they're separate from the preset Scene they're based off of.
Expand All @@ -179,7 +257,34 @@ bool ActivityMan::LoadAndLaunchGame(const std::string& fileName) {
// When this method exits, our Scene object will be destroyed, which will cause problems if you try to restart it. To avoid this, set the Scene to load to the preset object with the same name.
g_SceneMan.SetSceneToLoad(originalScenePresetName, placeObjectsIfSceneIsRestarted, placeUnitsIfSceneIsRestarted);

// Replace our scene images with the ones from the zip
std::vector<SceneLayerInfo> layerInfos;

PALETTE palette;
get_palette(palette);

unzipFileIntoBuffer(fileName + " Mat.png");
layerInfos.emplace_back("Mat", std::unique_ptr<BITMAP>(load_memory_png(buffer, info.uncompressed_size, palette)));
free(buffer);

unzipFileIntoBuffer(fileName + " FG.png");
layerInfos.emplace_back("FG", std::unique_ptr<BITMAP>(load_memory_png(buffer, info.uncompressed_size, palette)));
free(buffer);

unzipFileIntoBuffer(fileName + " BG.png");
layerInfos.emplace_back("BG", std::unique_ptr<BITMAP>(load_memory_png(buffer, info.uncompressed_size, palette)));
free(buffer);

for (int i = 0; i < numberOfTeams; ++i) {
unzipFileIntoBuffer(fileName + std::format(" T%i.png", i));
layerInfos.emplace_back(std::format("T%i", i), std::unique_ptr<BITMAP>(load_memory_png(buffer, info.uncompressed_size, palette)));
free(buffer);
}

g_SceneMan.GetScene()->ConstructSceneLayersFromBitmaps(std::move(layerInfos));

g_ConsoleMan.PrintString("SYSTEM: Game \"" + fileName + "\" loaded!");

return true;
}

Expand Down
Loading
Loading