forked from oplancelot/shelllab
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathembedded_data.go
More file actions
203 lines (179 loc) · 7.9 KB
/
Copy pathembedded_data.go
File metadata and controls
203 lines (179 loc) · 7.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package main
import (
"database/sql"
_ "embed" // for //go:embed on embeddedDB
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
_ "modernc.org/sqlite"
)
// localMergeTables are the spawn tables that carry an `origin` column. On an
// official DB upgrade their user-scraped ('local') rows are grafted into the
// new baseline. `id` (AUTOINCREMENT) is intentionally excluded so it re-assigns;
// INSERT OR IGNORE drops a local row when the new official already has the same
// spawn (unique key), so a newer official always wins.
var localMergeTables = []struct{ name, cols string }{
{"creature_spawn", "creature_entry,map_id,zone_id,zone_name,position_x,position_y,position_z,origin"},
{"gameobject_spawn", "gameobject_entry,map_id,zone_id,zone_name,position_x,position_y,position_z,origin"},
}
// refreshDBPreservingLocal replaces the extracted DB with the (newer) embedded
// official baseline while carrying over the user's local scrapes. It writes the
// embedded DB to a temp file, grafts local rows from the existing DB into it,
// then atomically swaps it into place. If the graft fails (e.g. an old DB with
// no `origin` column — nothing was tagged local anyway), it still ships the
// fresh official baseline rather than failing the launch.
func refreshDBPreservingLocal(dbPath string) error {
tmpPath := dbPath + ".new"
_ = os.Remove(tmpPath)
if err := os.WriteFile(tmpPath, embeddedDB, 0644); err != nil {
return err
}
if err := graftLocalRows(tmpPath, dbPath); err != nil {
log.Printf(" ⚠ could not preserve local scrapes (%v); shipping fresh official data", err)
}
// Swap the new baseline in via a backup, so a failed rename never leaves the
// user without a database (Windows can't rename over an existing file).
bak := dbPath + ".bak"
_ = os.Remove(bak)
if err := os.Rename(dbPath, bak); err != nil {
return err
}
if err := os.Rename(tmpPath, dbPath); err != nil {
_ = os.Rename(bak, dbPath) // restore the original on failure
return err
}
_ = os.Remove(bak)
return nil
}
// graftLocalRows copies origin='local' rows from oldPath into the new baseline
// at newPath (INSERT OR IGNORE, so newer official rows win on key collision).
func graftLocalRows(newPath, oldPath string) error {
db, err := sql.Open("sqlite", newPath)
if err != nil {
return err
}
defer db.Close()
if _, err := db.Exec("ATTACH DATABASE ? AS old", oldPath); err != nil {
return err
}
defer db.Exec("DETACH DATABASE old")
for _, t := range localMergeTables {
q := fmt.Sprintf(
"INSERT OR IGNORE INTO main.%s (%s) SELECT %s FROM old.%s WHERE origin='local'",
t.name, t.cols, t.cols, t.name)
if res, err := db.Exec(q); err != nil {
// Old DB predates the origin column (pre-Stage-1) — nothing tagged local.
log.Printf(" (skip %s local graft: %v)", t.name, err)
} else if n, _ := res.RowsAffected(); n > 0 {
log.Printf(" ✓ preserved %d local %s row(s)", n, t.name)
}
}
return nil
}
//go:embed data/inklab.db
var embeddedDB []byte
// embeddedDBVersion identifies the embedded database's data revision. Bump it
// whenever data/inklab.db is regenerated with fixes so production builds
// overwrite a previously-extracted (stale) copy instead of keeping it forever.
const embeddedDBVersion = 6
// dbVersionFile is the marker written next to the extracted database recording
// which embeddedDBVersion produced it.
const dbVersionFile = ".dbversion"
func readExtractedDBVersion(dataDir string) int {
b, err := os.ReadFile(filepath.Join(dataDir, dbVersionFile))
if err != nil {
return 0
}
v, _ := strconv.Atoi(strings.TrimSpace(string(b)))
return v
}
func writeExtractedDBVersion(dataDir string, v int) {
_ = os.WriteFile(filepath.Join(dataDir, dbVersionFile), []byte(strconv.Itoa(v)), 0644)
}
// Icons are not embedded — they're extracted locally from the client art via
// the Tools tab, or downloaded on demand by the icon service.
//
// NPC model/map images are not embedded either — users build their own cache by
// syncing NPCs (scraped from octowow.st), or share a data/npc_images folder.
// InitializeData ensures data directory exists and extracts embedded database on first run
// Icons are NOT embedded - they remain external and can be updated independently
// Returns the absolute path to the data directory and whether we're in dev mode
func InitializeData() (string, bool, error) {
var baseDir string
// Detect if running in dev mode (wails dev)
// In dev mode, the executable is in build/bin/ directory or a temp directory
// We want to use the current working directory (project root) instead
exePath, err := os.Executable()
if err != nil {
return "", false, fmt.Errorf("failed to get executable path: %w", err)
}
// Check if we're running from dev mode locations:
// - build/bin (wails dev on Windows/Linux)
// - Temp/tmp (some dev environments)
isDevMode := strings.Contains(exePath, "Temp") ||
strings.Contains(exePath, "tmp") ||
strings.Contains(exePath, "build"+string(os.PathSeparator)+"bin") ||
strings.Contains(exePath, "build/bin")
if isDevMode {
// Dev mode: use current working directory (project root)
cwd, err := os.Getwd()
if err != nil {
return "", false, fmt.Errorf("failed to get working directory: %w", err)
}
baseDir = cwd
log.Println("🔧 Development mode detected, using project root:", baseDir)
} else {
// Production mode: use executable directory
baseDir = filepath.Dir(exePath)
log.Println("📦 Production mode, using executable directory:", baseDir)
}
dataDir := filepath.Join(baseDir, "data")
iconsDir := filepath.Join(dataDir, "icons")
dbPath := filepath.Join(dataDir, "inklab.db")
// Create directories
if err := os.MkdirAll(iconsDir, 0755); err != nil {
return "", false, fmt.Errorf("failed to create data directory: %w", err)
}
// Extract on first run, and in production also refresh when the embedded DB
// is a newer revision than the previously-extracted copy — otherwise data
// fixes never reach users who already have a data/inklab.db. Dev mode always
// uses the on-disk db as-is (it's managed via git / rebuilddb).
_, statErr := os.Stat(dbPath)
missing := os.IsNotExist(statErr)
stale := !isDevMode && !missing && readExtractedDBVersion(dataDir) < embeddedDBVersion
switch {
case missing:
// First run: nothing to preserve, write the embedded baseline as-is.
log.Println("Extracting embedded database...")
if err := os.WriteFile(dbPath, embeddedDB, 0644); err != nil {
return "", false, fmt.Errorf("failed to write database: %w", err)
}
writeExtractedDBVersion(dataDir, embeddedDBVersion)
log.Println("✓ Database ready at", dbPath)
case stale:
// Newer official data shipped: refresh to it, but graft the user's own
// scraped ('local') rows into the new baseline so an update never wipes
// their additions. A newer official row for the same spawn wins (the
// graft is INSERT OR IGNORE against the spawn's unique key).
log.Printf("Embedded database is newer (v%d); merging (preserving local scrapes)...", embeddedDBVersion)
if err := refreshDBPreservingLocal(dbPath); err != nil {
return "", false, fmt.Errorf("failed to refresh database: %w", err)
}
writeExtractedDBVersion(dataDir, embeddedDBVersion)
log.Println("✓ Database refreshed at", dbPath)
default:
log.Println("✓ Using existing database:", dbPath)
}
// Icons live in data/icons (extracted from client art via the Tools tab, or
// downloaded on demand). Nothing to extract from the binary.
// NPC images are built locally (synced/scraped from octowow.st) — just make
// sure the directory exists for the sync to write into.
npcImagesDir := filepath.Join(dataDir, "npc_images")
if err := os.MkdirAll(npcImagesDir, 0755); err != nil {
log.Printf("Warning: Failed to create npc_images directory: %v", err)
}
return dataDir, isDevMode, nil
}