- Maps are saved in .map format, that can be loaded back via the Load in menu. There is no way to
- restore the progress if file is lost. Please keep old .map files on your machine or cloud storage as
- backups.
+ Maps are saved in .gz format, that can be loaded back via the Load in menu. There is no way to
+ restore the progress if file is lost. Please keep old save files on your machine or cloud storage as backups.
- From your Dropbox account
+ Or load from your Dropbox account
- Load
+ Load
This operation is destructive and irreversible. It will create a completely new map based on the current one.
- Don't forget to save the current project as a .map file first!
+ Don't forget to save the current project to your machine first!
-
Drop a .map file to open
+
Drop a map file to open
-
+
@@ -7930,7 +7947,7 @@
-
+
@@ -7963,15 +7980,15 @@
-
-
+
+
-
+
@@ -8001,13 +8018,12 @@
-
+
-
-
-
+
+
diff --git a/main.js b/main.js
index 4c40de43..ba7d1ee5 100644
--- a/main.js
+++ b/main.js
@@ -270,7 +270,7 @@ async function checkLoadParameters() {
const url = new URL(window.location.href);
const params = url.searchParams;
- // of there is a valid maplink, try to load .map file from URL
+ // of there is a valid maplink, try to load .gz/.map file from URL
if (params.get("maplink")) {
WARN && console.warn("Load map from URL");
const maplink = params.get("maplink");
@@ -292,17 +292,20 @@ async function checkLoadParameters() {
}
// check if there is a map saved to indexedDB
- try {
- const blob = await ldb.get("lastMap");
- if (blob) {
- WARN && console.warn("Loading last stored map");
- uploadMap(blob);
- return;
+ if (byId("onloadBehavior").value === "lastSaved") {
+ try {
+ const blob = await ldb.get("lastMap");
+ if (blob) {
+ WARN && console.warn("Loading last stored map");
+ uploadMap(blob);
+ return;
+ }
+ } catch (error) {
+ ERROR && console.error(error);
}
- } catch (error) {
- console.error(error);
}
+ // else generate random map
WARN && console.warn("Generate random map");
generateMapOnLoad();
}
@@ -574,9 +577,10 @@ void (function addDragToUpload() {
overlay.style.display = "none";
if (e.dataTransfer.items == null || e.dataTransfer.items.length !== 1) return; // no files or more than one
const file = e.dataTransfer.items[0].getAsFile();
- if (file.name.indexOf(".map") == -1) {
- // not a .map file
- alertMessage.innerHTML = "Please upload a .map file you have previously downloaded";
+
+ if (!file.name.endsWith(".map") && !file.name.endsWith(".gz")) {
+ alertMessage.innerHTML =
+ "Please upload a map file (.gz or .map formats) you have previously downloaded";
$("#alert").dialog({
resizable: false,
title: "Invalid file format",
@@ -596,7 +600,7 @@ void (function addDragToUpload() {
if (closeDialogs) closeDialogs();
uploadMap(file, () => {
overlay.style.display = "none";
- overlay.innerHTML = "Drop a .map file to open";
+ overlay.innerHTML = "Drop a map file to open";
});
});
})();
diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js
index 44f84283..b88e7d43 100644
--- a/modules/dynamic/auto-update.js
+++ b/modules/dynamic/auto-update.js
@@ -1,6 +1,6 @@
"use strict";
-// update old .map version to the current one
+// update old map file to the current version
export function resolveVersionConflicts(version) {
if (version < 1) {
// v1.0 added a new religions layer
diff --git a/modules/io/load.js b/modules/io/load.js
index 33d7eea4..00e46bc9 100644
--- a/modules/io/load.js
+++ b/modules/io/load.js
@@ -1,6 +1,5 @@
"use strict";
-// Functions to load and parse .map files
-
+// Functions to load and parse .gz/.map files
async function quickLoad() {
const blob = await ldb.get("lastMap");
if (blob) loadMapPrompt(blob);
@@ -112,11 +111,11 @@ function uploadMap(file, callback) {
const currentVersion = parseFloat(version);
const fileReader = new FileReader();
- fileReader.onload = function (fileLoadedEvent) {
+ fileReader.onloadend = async function (fileLoadedEvent) {
if (callback) callback();
document.getElementById("coas").innerHTML = ""; // remove auto-generated emblems
const result = fileLoadedEvent.target.result;
- const [mapData, mapVersion] = parseLoadedResult(result);
+ const [mapData, mapVersion] = await parseLoadedResult(result);
const isInvalid = !mapData || isNaN(mapVersion) || mapData.length < 26 || !mapData[5];
const isUpdated = mapVersion === currentVersion;
@@ -131,18 +130,40 @@ function uploadMap(file, callback) {
if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
};
- fileReader.readAsText(file, "UTF-8");
+ fileReader.readAsArrayBuffer(file);
}
-function parseLoadedResult(result) {
+async function uncompress(compressedData) {
try {
+ const uncompressedStream = new Blob([compressedData]).stream().pipeThrough(new DecompressionStream("gzip"));
+
+ let uncompressedData = [];
+ for await (const chunk of uncompressedStream) {
+ uncompressedData = uncompressedData.concat(Array.from(chunk));
+ }
+
+ return new Uint8Array(uncompressedData);
+ } catch (error) {
+ ERROR && console.error(error);
+ return null;
+ }
+}
+
+async function parseLoadedResult(result) {
+ try {
+ const resultAsString = new TextDecoder().decode(result);
// data can be in FMG internal format or base64 encoded
- const isDelimited = result.substr(0, 10).includes("|");
- const decoded = isDelimited ? result : decodeURIComponent(atob(result));
+ const isDelimited = resultAsString.substring(0, 10).includes("|");
+ const decoded = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString));
+
const mapData = decoded.split("\r\n");
const mapVersion = parseFloat(mapData[0].split("|")[0] || mapData[0]);
return [mapData, mapVersion];
} catch (error) {
+ // map file can be compressed with gzip
+ const uncompressedData = await uncompress(result);
+ if (uncompressedData) return parseLoadedResult(uncompressedData);
+
ERROR && console.error(error);
return [null, null];
}
@@ -153,7 +174,7 @@ function showUploadMessage(type, mapData, mapVersion) {
let message, title, canBeLoaded;
if (type === "invalid") {
- message = `The file does not look like a valid .map file. Please check the data format`;
+ message = `The file does not look like a valid save file. Please check the data format`;
title = "Invalid file";
canBeLoaded = false;
} else if (type === "ancient") {
@@ -165,7 +186,7 @@ function showUploadMessage(type, mapData, mapVersion) {
title = "Newer file";
canBeLoaded = false;
} else if (type === "outdated") {
- message = `The map version (${mapVersion}) does not match the Generator version (${version}). Click OK to get map auto-updated. In case of issues please keep using an ${archive} of the Generator`;
+ message = `The map version (${mapVersion}) does not match the Generator version (${version}). That is fine, click OK to the get map auto-updated. In case of issues please keep using an ${archive} of the Generator`;
title = "Outdated file";
canBeLoaded = true;
}
@@ -435,7 +456,7 @@ async function parseLoadedData(data) {
{
// dynamically import and run auto-udpdate script
const versionNumber = parseFloat(params[0]);
- const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.92.05");
+ const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.93.00");
resolveVersionConflicts(versionNumber);
}
diff --git a/modules/io/save.js b/modules/io/save.js
index 97cf7c09..42fcb355 100644
--- a/modules/io/save.js
+++ b/modules/io/save.js
@@ -1,8 +1,43 @@
"use strict";
-// functions to save project as .map file
-// prepare map data for saving
-function getMapData() {
+// functions to save the project to a file
+async function saveMap(method) {
+ if (customization) return tip("Map cannot be saved in EDIT mode, please complete the edit and retry", false, "error");
+ closeDialogs("#alert");
+
+ try {
+ const compressedMapData = await compressData(prepareMapData());
+ const filename = getFileName() + ".gz";
+
+ saveToStorage(compressedMapData, method === "storage"); // any method saves to indexedDB
+ if (method === "machine") saveToMachine(compressedMapData, filename);
+ if (method === "dropbox") saveToDropbox(compressedMapData, filename);
+ } catch (error) {
+ ERROR && console.error(error);
+ alertMessage.innerHTML = /* html */ `An error is occured on map saving. If the issue persists, please copy the message below and report it on ${link(
+ "https://github.com/Azgaar/Fantasy-Map-Generator/issues",
+ "GitHub"
+ )}.
${parseError(error)}
`;
+
+ $("#alert").dialog({
+ resizable: false,
+ title: "Saving error",
+ width: "28em",
+ buttons: {
+ Retry: function () {
+ $(this).dialog("close");
+ saveMap(method);
+ },
+ Close: function () {
+ $(this).dialog("close");
+ }
+ },
+ position: {my: "center", at: "center", of: "svg"}
+ });
+ }
+}
+
+function prepareMapData() {
const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
@@ -117,36 +152,30 @@ function getMapData() {
return mapData;
}
-// Download .map file
-function dowloadMap() {
- if (customization)
- return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
- closeDialogs("#alert");
+// save map file to indexedDB
+async function saveToStorage(compressedMapData, showTip = false) {
+ const blob = new Blob([compressedMapData], {type: "text/plain"});
+ await ldb.set("lastMap", blob);
+ showTip && tip("Map is saved to the browser storage", false, "success");
+}
- const mapData = getMapData();
- const blob = new Blob([mapData], {type: "text/plain"});
+// download .gz file
+function saveToMachine(compressedMapData, filename) {
+ const blob = new Blob([compressedMapData], {type: "text/plain"});
const URL = window.URL.createObjectURL(blob);
+
const link = document.createElement("a");
- link.download = getFileName() + ".map";
+ link.download = filename;
link.href = URL;
link.click();
- tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000);
+
+ tip('Map is saved to the "Downloads" folder (CTRL + J to open)', true, "success", 8000);
window.URL.revokeObjectURL(URL);
}
-async function saveToDropbox() {
- if (customization)
- return tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
- closeDialogs("#alert");
- const mapData = getMapData();
- const filename = getFileName() + ".map";
- try {
- await Cloud.providers.dropbox.save(filename, mapData);
- tip("Map is saved to your Dropbox", true, "success", 8000);
- } catch (msg) {
- ERROR && console.error(msg);
- tip("Cannot save .map to your Dropbox", true, "error", 8000);
- }
+async function saveToDropbox(compressedMapData, filename) {
+ await Cloud.providers.dropbox.save(filename, compressedMapData);
+ tip("Map is saved to your Dropbox", true, "success", 8000);
}
async function initiateAutosave() {
@@ -161,38 +190,43 @@ async function initiateAutosave() {
if (diffInMinutes < timeoutMinutes) return;
if (customization) return tip("Autosave: map cannot be saved in edit mode", false, "warning", 2000);
- tip("Autosave: saving map...", false, "warning", 3000);
- const mapData = getMapData();
- const blob = new Blob([mapData], {type: "text/plain"});
- await ldb.set("lastMap", blob);
- INFO && console.log("Autosaved at", new Date().toLocaleTimeString());
- lastSavedAt = Date.now();
+ try {
+ tip("Autosave: saving map...", false, "warning", 3000);
+ const compressedMapData = await compressData(prepareMapData());
+ await saveToStorage(compressedMapData);
+ tip("Autosave: map is saved", false, "success", 2000);
+
+ lastSavedAt = Date.now();
+ } catch (error) {
+ ERROR && console.error(error);
+ }
}
setInterval(autosave, MINUTE / 2);
}
-async function quickSave() {
- if (customization)
- return tip("Map cannot be saved when edit mode is active, please exit the mode first", false, "error");
+async function compressData(uncompressedData) {
+ const compressedStream = new Blob([uncompressedData]).stream().pipeThrough(new CompressionStream("gzip"));
- const mapData = getMapData();
- const blob = new Blob([mapData], {type: "text/plain"});
- await ldb.set("lastMap", blob); // auto-save map
- tip("Map is saved to browser memory. Please also save as .map file to secure progress", true, "success", 2000);
+ let compressedData = [];
+ for await (const chunk of compressedStream) {
+ compressedData = compressedData.concat(Array.from(chunk));
+ }
+
+ return new Uint8Array(compressedData);
}
const saveReminder = function () {
if (localStorage.getItem("noReminder")) return;
const message = [
- "Please don't forget to save your work as a .map file",
- "Please remember to save work as a .map file",
- "Saving in .map format will ensure your data won't be lost in case of issues",
+ "Please don't forget to save the project to desktop from time to time",
+ "Please remember to save the map to your desktop",
+ "Saving will ensure your data won't be lost in case of issues",
"Safety is number one priority. Please save the map",
"Don't forget to save your map on a regular basis!",
"Just a gentle reminder for you to save the map",
- "Please don't forget to save your progress (saving as .map is the best option)",
- "Don't want to be reminded about need to save? Press CTRL+Q"
+ "Please don't forget to save your progress (saving to desktop is the best option)",
+ "Don't want to get reminded about need to save? Press CTRL+Q"
];
const interval = 15 * 60 * 1000; // remind every 15 minutes
diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js
index 41031786..40bc55fb 100644
--- a/modules/ui/heightmap-editor.js
+++ b/modules/ui/heightmap-editor.js
@@ -28,7 +28,7 @@ function editHeightmap(options) {
Erase mode also allows you Convert an Image into a heightmap or use Template Editor.
You can keep the data, but you won't be able to change the coastline.
Try risk mode to change the coastline and keep the data. The data will be restored as much as possible, but it can cause unpredictable errors.
-
Please save the map before editing the heightmap!
+
Please save the map before editing the heightmap!
Check out ${link(
"https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization",
"wiki"
diff --git a/modules/ui/hotkeys.js b/modules/ui/hotkeys.js
index f7443469..4543d187 100644
--- a/modules/ui/hotkeys.js
+++ b/modules/ui/hotkeys.js
@@ -25,15 +25,15 @@ function handleKeyup(event) {
if (code === "F1") showInfo();
else if (code === "F2") regeneratePrompt();
- else if (code === "F6") quickSave();
+ else if (code === "F6") saveMap("storage");
else if (code === "F9") quickLoad();
else if (code === "Tab") toggleOptions(event);
else if (code === "Escape") closeAllDialogs();
else if (code === "Delete") removeElementOnKey();
else if (code === "KeyO" && document.getElementById("canvas3d")) toggle3dOptions();
else if (ctrl && code === "KeyQ") toggleSaveReminder();
- else if (ctrl && code === "KeyS") dowloadMap();
- else if (ctrl && code === "KeyC") saveToDropbox();
+ else if (ctrl && code === "KeyS") saveMap("machine");
+ else if (ctrl && code === "KeyC") saveMap("dropbox");
else if (ctrl && code === "KeyZ" && undo?.offsetParent) undo.click();
else if (ctrl && code === "KeyY" && redo?.offsetParent) redo.click();
else if (shift && code === "KeyH") editHeightmap();
diff --git a/modules/ui/options.js b/modules/ui/options.js
index ce3b3561..8a6c4c36 100644
--- a/modules/ui/options.js
+++ b/modules/ui/options.js
@@ -793,7 +793,7 @@ async function showLoadPane() {
$("#loadMapData").dialog({
title: "Load map",
resizable: false,
- width: "24em",
+ width: "auto",
position: {my: "center", at: "center", of: "svg"},
buttons: {
Close: function () {
@@ -844,8 +844,8 @@ async function connectToDropbox() {
function loadURL() {
const pattern = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
- const inner = `Provide URL to a .map file:
-
+ const inner = `Provide URL to map file:
+
Please note server should allow CORS for file to be loaded. If CORS is not allowed, save file to Dropbox and provide a direct link`;
alertMessage.innerHTML = inner;
$("#alert").dialog({
diff --git a/utils/polyfills.js b/utils/polyfills.js
index 369e647f..667d81ab 100644
--- a/utils/polyfills.js
+++ b/utils/polyfills.js
@@ -14,3 +14,19 @@ if (Array.prototype.flat === undefined) {
return this.reduce((acc, val) => (Array.isArray(val) ? acc.concat(val.flat()) : acc.concat(val)), []);
};
}
+
+// readable stream iterator: https://bugs.chromium.org/p/chromium/issues/detail?id=929585#c10
+if (ReadableStream.prototype[Symbol.asyncIterator] === undefined) {
+ ReadableStream.prototype[Symbol.asyncIterator] = async function* () {
+ const reader = this.getReader();
+ try {
+ while (true) {
+ const {done, value} = await reader.read();
+ if (done) return;
+ yield value;
+ }
+ } finally {
+ reader.releaseLock();
+ }
+ };
+}
diff --git a/versioning.js b/versioning.js
index 67c0b12c..0772ba26 100644
--- a/versioning.js
+++ b/versioning.js
@@ -1,7 +1,7 @@
"use strict";
// version and caching control
-const version = "1.92.05"; // generator version, update each time
+const version = "1.93.00"; // generator version, update each time
{
document.title += " v" + version;
@@ -23,11 +23,13 @@ const version = "1.92.05"; // generator version, update each time
const discord = "https://discordapp.com/invite/X7E84HU";
const patreon = "https://www.patreon.com/azgaar";
- alertMessage.innerHTML = /* html */ `The Fantasy Map Generator is updated up to version ${version}. This version is compatible with previous versions, loaded .map files will be auto-updated.
+ alertMessage.innerHTML = /* html */ `The Fantasy Map Generator is updated up to version ${version}. This version is compatible with previous versions, loaded save files will be auto-updated.
${storedVersion ? "Reload the page to fetch fresh code." : ""}
Latest changes:
+
Auto-load of the last saved map is now optional (see Onload behavior in Options)
+
Save files compression (file extension is changed to .gz). Old .map files are still supported
New label placement algorithm for states
North and South Poles temperature can be set independently