mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-06-09 18:14:06 +09:00
new savegame scheme suggestion
This commit is contained in:
@@ -1,74 +1,31 @@
|
|||||||
## Format ##
|
## Introduction ##
|
||||||
|
|
||||||
Contain everything on [TEVD](github.com/minjaesong/TerranVirtualDisk)
|
On the main game, any player can access any generated worlds, and thus players data and worlds are saved separately.
|
||||||
|
|
||||||
* Save meta
|
The main game directory is composed of following directories:
|
||||||
- binary
|
|
||||||
- Filename : world (with no extension)
|
|
||||||
|
|
||||||
|Type |Mnemonic |Description |
|
```
|
||||||
|----------|-----------|-----------------------------|
|
.Terrarum
|
||||||
|Byte[4] |TESV |Magic |
|
+ Players
|
||||||
|Byte[n] |name |Savegame name, UTF-8 |
|
- <Player Name Here>, JSON.gz
|
||||||
|Byte |NULL |String terminator |
|
+ Shared
|
||||||
|Byte[8] |terraseed |Terrain seed |
|
- <e.g. Disk GUID>, TEVD { * }
|
||||||
|Byte[8] |rogueseed |Randomiser seed |
|
- <this directory can have anything>
|
||||||
|Byte[4] |crc1 |CRC-32 of worldinfo1 entry |
|
+ Worlds
|
||||||
|Byte[4] |crc2 |CRC-32 of worldinfo2 entry |
|
- <World Name Here>, TVDA { WriteWorld, actors JSON, chunk data, screenshot.tga.gz taken by the last player }
|
||||||
|Byte[4] |crc3 |CRC-32 of worldinfo3 entry |
|
```
|
||||||
|Byte[4] |crc4 |CRC-32 of worldinfo4 entry |
|
|
||||||
|Byte[32] |hash4321 |SHA-256 of crc4..crc3..crc2..crc1|
|
|
||||||
|Int |refid |Reference ID of the player |
|
|
||||||
|Long |time_t |Current world's time_t |
|
|
||||||
|Byte[6] |t_create |Creation time of the savefile in time_t|
|
|
||||||
|Byte[6] |t_lastacc |Last play time in time_t |
|
|
||||||
|Int |t_wasted |Total playtime in time_t |
|
|
||||||
|
|
||||||
Endianness: Big
|
(TEVD stands for Terrarum Virtual Disk spec version 3, TVDA stands for spec version 254; both have MAGIC header of `TEVd`)
|
||||||
|
|
||||||
each entry on the disk contains CRC of its data, we can compare CRC saved in meta && CRC of entry header && CRC of actual content
|
## Prerequisites ##
|
||||||
|
|
||||||
* Actor/Faction data
|
1. Player ID must not be strictly 9545698 (0x91A7E2)
|
||||||
- GSON
|
1. Use classname `net.torvald.terrarum.modulebasegame.gameactors.IngamePlayer` to check
|
||||||
- Filename : (refid) (with no extension)
|
2. Each world has to be uniquely identifiable
|
||||||
|
1. Use GUID, World Name, etc.
|
||||||
|
3. `ActorNowPlaying` must be drawn on top of other actors of same RenderOrder
|
||||||
|
|
||||||
|
## Goals ##
|
||||||
|
|
||||||
* Prop data
|
1. Allows multiple players share same world
|
||||||
- CSV
|
2. Makes multiplayer possible
|
||||||
- Filename : (with no extension)
|
|
||||||
worldinfo2 -- tileprop
|
|
||||||
worldinfo3 -- itemprop
|
|
||||||
worldinfo4 -- materialprop
|
|
||||||
worldinfo5 -- modules loadorder
|
|
||||||
|
|
||||||
|
|
||||||
* Human-readable
|
|
||||||
- Tiles_list.txt -- list of tiles in csv
|
|
||||||
- Items_list.txt -- list of items in csv
|
|
||||||
- Materials_list.txt -- list of materials in csv
|
|
||||||
- load_order.txt -- module load order
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## How it works ##
|
|
||||||
* If hash discrepancy has detected, (hash of csv in save dir != stored hash || hash of TEMD != stored hash), printout "Save file corrupted. Continue?" with prompt "Yes/No"
|
|
||||||
|
|
||||||
Directory:
|
|
||||||
|
|
||||||
+--- <save1.tevd>
|
|
||||||
--- 2a93bc5f (item ID) Actor/DynamicItem/Faction/etc. data (JSON)
|
|
||||||
--- 423bdc83 (item ID) Actor/DynamicItem/Faction/etc. data (JSON)
|
|
||||||
--- Items_list.txt Human-readable
|
|
||||||
--- Materials_list.txt Human-readable
|
|
||||||
--- Tiles_list.txt Human-readable
|
|
||||||
--- world save meta (binary)
|
|
||||||
--- worldinfo1 TEMD (binary)
|
|
||||||
--- worldinfo2 tileprop (CSV)
|
|
||||||
--- worldinfo3 itemprop (CSV)
|
|
||||||
--- worldinfo4 materialprop (CSV)
|
|
||||||
+--- computers
|
|
||||||
--- (UUID) virtual disk
|
|
||||||
+--- tapestries
|
|
||||||
--- (random Int) tapestry
|
|
||||||
|
|
||||||
Alongside with save1.tevd (extension should not exist in real game), keep save1.backup.tevd as a last-working save.
|
|
||||||
|
|||||||
@@ -929,8 +929,14 @@ public class App implements ApplicationListener {
|
|||||||
public static String operationSystem;
|
public static String operationSystem;
|
||||||
/** %appdata%/Terrarum, without trailing slash */
|
/** %appdata%/Terrarum, without trailing slash */
|
||||||
public static String defaultDir;
|
public static String defaultDir;
|
||||||
/** defaultDir + "/Saves", without trailing slash */
|
/** For Demo version only. defaultDir + "/Saves", without trailing slash */
|
||||||
public static String defaultSaveDir;
|
public static String saveDir;
|
||||||
|
/** For shared materials (e.g. image of a computer disk). defaultDir + "/Shared", without trailing slash */
|
||||||
|
public static String saveSharedDir;
|
||||||
|
/** For the main game where any players can access any world (unless flagged as private). defaultDir + "/Players", without trailing slash */
|
||||||
|
public static String playersDir;
|
||||||
|
/** For the main game. defaultDir + "/Worlds", without trailing slash */
|
||||||
|
public static String worldsDir;
|
||||||
/** defaultDir + "/config.json" */
|
/** defaultDir + "/config.json" */
|
||||||
public static String configDir;
|
public static String configDir;
|
||||||
public static RunningEnvironment environment;
|
public static RunningEnvironment environment;
|
||||||
@@ -958,8 +964,10 @@ public class App implements ApplicationListener {
|
|||||||
defaultDir = System.getProperty("user.home") + "/.Terrarum";
|
defaultDir = System.getProperty("user.home") + "/.Terrarum";
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultSaveDir = defaultDir + "/Players"; // as per the save format in the game's planning
|
saveDir = defaultDir + "/Saves"; // for the demo release
|
||||||
defaultSaveDir = defaultDir + "/Saves"; // for the demo release
|
saveSharedDir = defaultDir + "/Shared";
|
||||||
|
playersDir = defaultDir + "/Players";
|
||||||
|
worldsDir = defaultDir + "/Worlds";
|
||||||
configDir = defaultDir + "/config.json";
|
configDir = defaultDir + "/config.json";
|
||||||
|
|
||||||
System.out.println(String.format("os.name = %s (with identifier %s)", OSName, operationSystem));
|
System.out.println(String.format("os.name = %s (with identifier %s)", OSName, operationSystem));
|
||||||
@@ -969,7 +977,7 @@ public class App implements ApplicationListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void createDirs() {
|
private static void createDirs() {
|
||||||
File[] dirs = {new File(defaultSaveDir)};
|
File[] dirs = {new File(saveDir)};
|
||||||
|
|
||||||
for (File it : dirs) {
|
for (File it : dirs) {
|
||||||
if (!it.exists())
|
if (!it.exists())
|
||||||
|
|||||||
@@ -368,23 +368,23 @@ open class IngameInstance(val batch: SpriteBatch) : Screen {
|
|||||||
fun makeSavegameBackupCopy() {
|
fun makeSavegameBackupCopy() {
|
||||||
try {
|
try {
|
||||||
// do not overwrite clean .2 with dirty .1
|
// do not overwrite clean .2 with dirty .1
|
||||||
val file2 = File(App.defaultSaveDir, INGAME.savegameNickname+".3")
|
val file2 = File(App.saveDir, INGAME.savegameNickname + ".3")
|
||||||
val file1 = File(App.defaultSaveDir, INGAME.savegameNickname+".2")
|
val file1 = File(App.saveDir, INGAME.savegameNickname + ".2")
|
||||||
val flags2 = FileInputStream(file2).let { it.skip(49L); val r = it.read(); it.close(); r }
|
val flags2 = FileInputStream(file2).let { it.skip(49L); val r = it.read(); it.close(); r }
|
||||||
val flags1 = FileInputStream(file1).let { it.skip(49L); val r = it.read(); it.close(); r }
|
val flags1 = FileInputStream(file1).let { it.skip(49L); val r = it.read(); it.close(); r }
|
||||||
if (!(flags2 == 0 && flags1 != 0) || !file2.exists()) file1.copyTo(file2, true)
|
if (!(flags2 == 0 && flags1 != 0) || !file2.exists()) file1.copyTo(file2, true)
|
||||||
} catch (e: NoSuchFileException) {} catch (e: FileNotFoundException) {}
|
} catch (e: NoSuchFileException) {} catch (e: FileNotFoundException) {}
|
||||||
try {
|
try {
|
||||||
// do not overwrite clean .2 with dirty .1
|
// do not overwrite clean .2 with dirty .1
|
||||||
val file2 = File(App.defaultSaveDir, INGAME.savegameNickname+".2")
|
val file2 = File(App.saveDir, INGAME.savegameNickname + ".2")
|
||||||
val file1 = File(App.defaultSaveDir, INGAME.savegameNickname+".1")
|
val file1 = File(App.saveDir, INGAME.savegameNickname + ".1")
|
||||||
val flags2 = FileInputStream(file2).let { it.skip(49L); val r = it.read(); it.close(); r }
|
val flags2 = FileInputStream(file2).let { it.skip(49L); val r = it.read(); it.close(); r }
|
||||||
val flags1 = FileInputStream(file1).let { it.skip(49L); val r = it.read(); it.close(); r }
|
val flags1 = FileInputStream(file1).let { it.skip(49L); val r = it.read(); it.close(); r }
|
||||||
if (!(flags2 == 0 && flags1 != 0) || !file2.exists()) file1.copyTo(file2, true)
|
if (!(flags2 == 0 && flags1 != 0) || !file2.exists()) file1.copyTo(file2, true)
|
||||||
} catch (e: NoSuchFileException) {} catch (e: FileNotFoundException) {}
|
} catch (e: NoSuchFileException) {} catch (e: FileNotFoundException) {}
|
||||||
try {
|
try {
|
||||||
File(App.defaultSaveDir, INGAME.savegameNickname).copyTo(
|
File(App.saveDir, INGAME.savegameNickname).copyTo(
|
||||||
File(App.defaultSaveDir, INGAME.savegameNickname+".1"), // don't use .bak as it's used by the savecracker
|
File(App.saveDir, INGAME.savegameNickname + ".1"), // don't use .bak as it's used by the savecracker
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
} catch (e: NoSuchFileException) {}
|
} catch (e: NoSuchFileException) {}
|
||||||
@@ -392,7 +392,7 @@ open class IngameInstance(val batch: SpriteBatch) : Screen {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun getSaveFileMain() = File(App.defaultSaveDir, savegameNickname)
|
fun getSaveFileMain() = File(App.saveDir, savegameNickname)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack
|
|||||||
import net.torvald.util.CircularArray
|
import net.torvald.util.CircularArray
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.PrintStream
|
import java.io.PrintStream
|
||||||
import java.util.logging.Level
|
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.round
|
import kotlin.math.round
|
||||||
|
|
||||||
@@ -244,7 +243,7 @@ object Terrarum : Disposable {
|
|||||||
|
|
||||||
val currentSaveDir: File
|
val currentSaveDir: File
|
||||||
get() {
|
get() {
|
||||||
val file = File(defaultSaveDir + "/test")
|
val file = File(saveDir + "/test")
|
||||||
|
|
||||||
// failsafe?
|
// failsafe?
|
||||||
if (!file.exists()) file.mkdir()
|
if (!file.exists()) file.mkdir()
|
||||||
@@ -683,7 +682,7 @@ class Codex : KVHashMap() {
|
|||||||
|
|
||||||
fun AppUpdateListOfSavegames() {
|
fun AppUpdateListOfSavegames() {
|
||||||
App.savegames.clear()
|
App.savegames.clear()
|
||||||
File(App.defaultSaveDir).listFiles().filter { !it.isDirectory && !it.name.contains('.') }.map { file ->
|
File(App.saveDir).listFiles().filter { !it.isDirectory && !it.name.contains('.') }.map { file ->
|
||||||
try {
|
try {
|
||||||
DiskSkimmer(file, Common.CHARSET, true)
|
DiskSkimmer(file, Common.CHARSET, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ object Save : ConsoleCommand {
|
|||||||
val ingame = Terrarum.ingame!! as TerrarumIngame
|
val ingame = Terrarum.ingame!! as TerrarumIngame
|
||||||
val savename = args[1].trim()
|
val savename = args[1].trim()
|
||||||
val disk = VDUtil.createNewDisk(1L shl 60, savename, Common.CHARSET)
|
val disk = VDUtil.createNewDisk(1L shl 60, savename, Common.CHARSET)
|
||||||
val file = File(App.defaultSaveDir + "/${args[1]}")
|
val file = File(App.saveDir + "/${args[1]}")
|
||||||
|
|
||||||
WriteSavegame(disk, file, ingame, false)
|
WriteSavegame(disk, file, ingame, false)
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class UIInventoryEscMenu(val full: UIInventoryFull) : UICanvas() {
|
|||||||
INGAME.makeSavegameBackupCopy()
|
INGAME.makeSavegameBackupCopy()
|
||||||
|
|
||||||
// save the game
|
// save the game
|
||||||
WriteSavegame(INGAME.savegameArchive, File(App.defaultSaveDir, INGAME.savegameNickname), Terrarum.ingame!! as TerrarumIngame, false) {
|
WriteSavegame(INGAME.savegameArchive, File(App.saveDir, INGAME.savegameNickname), Terrarum.ingame!! as TerrarumIngame, false) {
|
||||||
// callback:
|
// callback:
|
||||||
System.gc()
|
System.gc()
|
||||||
screen = 0
|
screen = 0
|
||||||
|
|||||||
Reference in New Issue
Block a user