changeset 1464:3e3192241ea7 default tip

add: API for world/level storage
author sam <sam@basx.dev>
date Mon, 24 Mar 2025 22:57:47 +0700
parents f9d86889e018
children
files semicongine/core/types.nim semicongine/storage.nim tests/test_storage.nim
diffstat 3 files changed, 95 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/core/types.nim	Mon Mar 24 00:15:08 2025 +0700
+++ b/semicongine/core/types.nim	Mon Mar 24 22:57:47 2025 +0700
@@ -475,8 +475,8 @@
 
   # === storage ===
   StorageType* = enum
-    SystemStorage
-    UserStorage # ? level storage type ?
+    SystemStorage # usually device-specific settings
+    UserStorage # usually user specific settings (should be synced)
 
   # === steam ===
   SteamUserStatsRef* = ptr object
--- a/semicongine/storage.nim	Mon Mar 24 00:15:08 2025 +0700
+++ b/semicongine/storage.nim	Mon Mar 24 22:57:47 2025 +0700
@@ -1,8 +1,11 @@
 import std/marshal
 import std/os
+import std/dirs
 import std/paths
 import std/strformat
+import std/strutils
 import std/tables
+import std/times
 
 import ./core
 
@@ -11,6 +14,12 @@
 const STORAGE_NAME = Path("storage.db")
 const DEFAULT_KEY_VALUE_TABLE_NAME = "shelf"
 
+# ==============================================================
+#
+# API to store key/value pairs
+#
+# ==============================================================
+
 proc path(storageType: StorageType): Path =
   case storageType
   of SystemStorage:
@@ -75,3 +84,62 @@
 
 proc purge*(storageType: StorageType) =
   storageType.path().string.removeFile()
+
+# ==============================================================
+#
+# API to store "worlds", which is one database per "world"
+#
+# ==============================================================
+
+const DEFAULT_WORLD_TABLE_NAME = "world"
+const WORLD_DIR = "worlds"
+
+proc path(worldName: string): Path =
+  let dir = Path(getDataDir()) / Path(AppName()) / Path(WORLD_DIR)
+  string(dir).createDir()
+  dir / Path(worldName & ".db")
+
+proc ensureExists(worldName: string): DbConn =
+  open(string(worldName.path), "", "", "")
+
+proc ensureExists(worldName: string, table: string): DbConn =
+  result = worldName.ensureExists()
+  result.exec(
+    sql(
+      &"""CREATE TABLE IF NOT EXISTS {table} (
+    key INT NOT NULL UNIQUE,
+    value TEXT NOT NULL
+  )"""
+    )
+  )
+
+proc storeWorld*[T](
+    worldName: string, world: T, table = DEFAULT_WORLD_TABLE_NAME, deleteOld = false
+) =
+  let db = worldName.ensureExists(table)
+  defer:
+    db.close()
+  let key = $(int(now().toTime().toUnixFloat() * 1000))
+  db.exec(sql(&"""INSERT INTO {table} VALUES(?, ?)"""), key, $$world)
+  db.exec(sql(&"""DELETE FROM {table} WHERE key <> ?"""), key)
+
+proc loadWorld*[T](worldName: string, table = DEFAULT_WORLD_TABLE_NAME): T =
+  let db = worldName.ensureExists(table)
+  defer:
+    db.close()
+  let dbResult =
+    db.getValue(sql(&"""SELECT value FROM {table} ORDER BY key DESC LIMIT 1"""))
+  return to[T](dbResult)
+
+proc listWorlds*(): seq[string] =
+  let dir = Path(getDataDir()) / Path(AppName()) / Path(WORLD_DIR)
+
+  if dir.dirExists():
+    for (kind, path) in walkDir(
+      dir = string(dir), relative = true, checkDir = true, skipSpecial = true
+    ):
+      if kind in [pcFile, pcLinkToFile] and path.endsWith(".db"):
+        result.add path[0 .. ^4]
+
+proc purgeWorld*(worldName: string) =
+  worldName.path().string.removeFile()
--- a/tests/test_storage.nim	Mon Mar 24 00:15:08 2025 +0700
+++ b/tests/test_storage.nim	Mon Mar 24 22:57:47 2025 +0700
@@ -14,6 +14,28 @@
   store(storage, KEY, TEST_VALUE)
   assert storage.load(KEY, 0) == TEST_VALUE
 
+proc testWorldAPI() =
+  assert listWorlds().len == 0
+
+  "testWorld".storeWorld(42)
+  assert listWorlds() == @["testWorld"]
+  assert loadWorld[int]("testWorld") == 42
+
+  "testWorld".storeWorld("hello")
+  assert listWorlds() == @["testWorld"]
+  assert loadWorld[string]("testWorld") == "hello"
+
+  "earth".storeWorld("hello")
+  assert "earth" in listWorlds()
+  assert "testWorld" in listWorlds()
+  assert loadWorld[string]("earth") == "hello"
+
+  "earth".purgeWorld()
+  assert listWorlds() == @["testWorld"]
+
+  "testWorld".purgeWorld()
+  assert listWorlds().len == 0
+
 proc stressTest(storage: StorageType) =
   for i in 1 .. 10000:
     let key = &"key-{i}"
@@ -30,6 +52,9 @@
   echo "UserStorage: Testing simple store/load"
   UserStorage.testSimple()
 
+  echo "Testing world-storage API"
+  testWorldAPI()
+
   echo "Stress test with 10'000 saves/loads"
   SystemStorage.stressTest()