Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -6,6 +6,7 @@ import json
|
|
6 |
import sqlite3 # Use SQLite for robust state management
|
7 |
import uuid
|
8 |
import math
|
|
|
9 |
from streamlit_js_eval import streamlit_js_eval # For JS communication
|
10 |
|
11 |
# --- Constants ---
|
@@ -15,142 +16,140 @@ PLOT_DEPTH = 50.0
|
|
15 |
|
16 |
# --- Database Setup ---
|
17 |
def init_db():
|
18 |
-
"""Initializes the SQLite database and
|
19 |
try:
|
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 |
-
rot_order TEXT DEFAULT 'XYZ',
|
46 |
-
FOREIGN KEY (plot_grid_x, plot_grid_z) REFERENCES plots(grid_x, grid_z) ON DELETE CASCADE
|
47 |
-
)
|
48 |
-
''')
|
49 |
-
# Index for faster object lookups by plot
|
50 |
-
cursor.execute('''
|
51 |
-
CREATE INDEX IF NOT EXISTS idx_objects_plot ON objects (plot_grid_x, plot_grid_z)
|
52 |
-
''')
|
53 |
-
conn.commit()
|
54 |
-
conn.close()
|
55 |
-
# st.success("Database initialized successfully.") # Optional success message
|
56 |
except sqlite3.Error as e:
|
57 |
-
st.
|
58 |
-
st.
|
|
|
59 |
|
60 |
-
#
|
61 |
-
init_db()
|
62 |
|
63 |
-
# --- Helper Functions (Database Operations) ---
|
64 |
|
|
|
65 |
def load_world_state_from_db():
|
66 |
"""Loads all plot metadata and object data fresh from the SQLite DB."""
|
|
|
67 |
plots_metadata = []
|
68 |
-
all_objects_by_plot = {}
|
69 |
-
all_initial_objects_world = []
|
70 |
|
71 |
try:
|
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 |
for plot_meta in plots_metadata:
|
102 |
plot_key = (plot_meta['grid_x'], plot_meta['grid_z'])
|
103 |
for obj_data in all_objects_by_plot[plot_key]:
|
104 |
-
# Create a copy to modify coordinates for injection
|
105 |
world_obj_data = obj_data.copy()
|
106 |
-
# Apply offset to relative DB coords to get world coords for JS
|
107 |
world_obj_data['pos_x'] = obj_data['pos_x'] + plot_meta['x_offset']
|
108 |
world_obj_data['pos_z'] = obj_data['pos_z'] + plot_meta['z_offset']
|
109 |
-
# Keep original relative pos_y as world pos_y
|
110 |
world_obj_data['pos_y'] = obj_data['pos_y']
|
111 |
all_initial_objects_world.append(world_obj_data)
|
112 |
|
113 |
-
st.write(f"DB Load:
|
114 |
|
115 |
except sqlite3.Error as e:
|
116 |
st.error(f"Database load error: {e}")
|
117 |
-
# Return empty
|
118 |
-
return [], []
|
119 |
|
120 |
return plots_metadata, all_initial_objects_world
|
121 |
|
122 |
|
123 |
def save_plot_data_to_db(target_grid_x, target_grid_z, objects_data_list):
|
124 |
-
"""Saves object data list to DB for a specific plot. Overwrites existing objects
|
|
|
125 |
plot_x_offset = target_grid_x * PLOT_WIDTH
|
126 |
plot_z_offset = target_grid_z * PLOT_DEPTH
|
127 |
-
plot_name = f"Plot ({target_grid_x},{target_grid_z})" # Simple default name
|
|
|
|
|
|
|
|
|
128 |
|
129 |
-
conn = None # Ensure conn is defined for finally block
|
130 |
try:
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
|
|
|
|
|
|
|
|
149 |
for obj in objects_data_list:
|
150 |
pos = obj.get('position', {})
|
151 |
rot = obj.get('rotation', {})
|
152 |
obj_type = obj.get('type', 'Unknown')
|
153 |
-
obj_id = obj.get('obj_id', str(uuid.uuid4()))
|
154 |
|
155 |
if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown':
|
156 |
print(f"Skipping malformed object during DB save prep: {obj}")
|
@@ -160,10 +159,7 @@ def save_plot_data_to_db(target_grid_x, target_grid_z, objects_data_list):
|
|
160 |
rel_z = pos.get('z', 0.0) - plot_z_offset
|
161 |
rel_y = pos.get('y', 0.0) # Y is absolute
|
162 |
|
163 |
-
|
164 |
-
INSERT INTO objects (obj_id, plot_grid_x, plot_grid_z, type, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, rot_order)
|
165 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
166 |
-
''', (
|
167 |
obj_id, target_grid_x, target_grid_z, obj_type,
|
168 |
rel_x, rel_y, rel_z,
|
169 |
rot.get('_x', 0.0), rot.get('_y', 0.0), rot.get('_z', 0.0),
|
@@ -171,42 +167,50 @@ def save_plot_data_to_db(target_grid_x, target_grid_z, objects_data_list):
|
|
171 |
))
|
172 |
insert_count += 1
|
173 |
|
174 |
-
|
175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
176 |
return True
|
177 |
|
178 |
except sqlite3.Error as e:
|
179 |
st.error(f"Database save error for plot ({target_grid_x},{target_grid_z}): {e}")
|
180 |
-
|
181 |
-
conn.rollback() # Rollback on error
|
182 |
return False
|
183 |
-
|
184 |
-
if conn:
|
185 |
-
conn.close()
|
186 |
|
187 |
# --- Page Config ---
|
188 |
-
st.set_page_config( page_title="DB Synced World Builder", layout="wide")
|
189 |
|
190 |
-
# --- Initialize Session State
|
191 |
if 'selected_object' not in st.session_state: st.session_state.selected_object = 'None'
|
192 |
if 'js_save_data_result' not in st.session_state: st.session_state.js_save_data_result = None
|
193 |
-
# Remove refresh_counter - not needed as we load fresh every time
|
194 |
|
195 |
# --- Load World State From DB (Runs on every script execution/rerun) ---
|
196 |
-
# No caching - always reads the current DB state
|
197 |
plots_metadata, all_initial_objects = load_world_state_from_db()
|
198 |
|
199 |
# --- Sidebar ---
|
200 |
with st.sidebar:
|
201 |
st.title("🏗️ World Controls")
|
202 |
|
203 |
-
#
|
204 |
if st.button("🔄 Refresh World View", key="refresh_button"):
|
205 |
-
# Simply rerun the script. The data loading above will fetch fresh data from DB.
|
206 |
st.info("Reloading world state from database...")
|
207 |
st.rerun()
|
208 |
|
209 |
st.header("Navigation (Plots)")
|
|
|
210 |
st.caption("Click to teleport player to a plot.")
|
211 |
max_cols = 2
|
212 |
cols = st.columns(max_cols)
|
@@ -227,6 +231,7 @@ with st.sidebar:
|
|
227 |
|
228 |
# --- Object Placement ---
|
229 |
st.header("Place Objects")
|
|
|
230 |
object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
|
231 |
current_object_index = object_types.index(st.session_state.selected_object) if st.session_state.selected_object in object_types else 0
|
232 |
selected_object_type_widget = st.selectbox(
|
@@ -234,16 +239,15 @@ with st.sidebar:
|
|
234 |
)
|
235 |
if selected_object_type_widget != st.session_state.selected_object:
|
236 |
st.session_state.selected_object = selected_object_type_widget
|
237 |
-
# Rerun
|
238 |
|
239 |
st.markdown("---")
|
240 |
|
241 |
# --- Saving ---
|
242 |
st.header("Save Work")
|
243 |
-
st.caption("Saves ALL objects
|
244 |
if st.button("💾 Save Current Plot", key="save_button"):
|
245 |
js_get_data_code = "getSaveDataAndPosition();"
|
246 |
-
# Use key to store result in session state
|
247 |
streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor")
|
248 |
st.rerun()
|
249 |
|
@@ -259,39 +263,32 @@ if save_data_from_js is not None:
|
|
259 |
|
260 |
if isinstance(payload, dict) and 'playerPosition' in payload and 'objectsToSave' in payload:
|
261 |
player_pos = payload['playerPosition']
|
262 |
-
|
263 |
-
objects_to_save = payload['objectsToSave']
|
264 |
|
265 |
-
if isinstance(objects_to_save, list):
|
266 |
target_grid_x = math.floor(player_pos.get('x', 0.0) / PLOT_WIDTH)
|
267 |
target_grid_z = math.floor(player_pos.get('z', 0.0) / PLOT_DEPTH)
|
268 |
|
269 |
st.write(f"Attempting DB save for plot ({target_grid_x},{target_grid_z})")
|
270 |
-
|
271 |
# --- Save the data to SQLite DB ---
|
272 |
save_ok = save_plot_data_to_db(target_grid_x, target_grid_z, objects_to_save)
|
273 |
|
274 |
if save_ok:
|
275 |
-
# No cache clear needed
|
276 |
-
# Optional: Could tell JS to clear sessionStorage here if desired, but maybe not necessary
|
277 |
-
# try:
|
278 |
-
# streamlit_js_eval(js_code="clearUnsavedSessionState();", key="clear_session_js")
|
279 |
-
# except Exception as js_e: st.warning(f"Could not clear JS session state: {js_e}")
|
280 |
-
st.success(f"Plot ({target_grid_x},{target_grid_z}) state saved to database.")
|
281 |
save_processed_successfully = True
|
282 |
else:
|
283 |
-
st.error(f"Failed
|
284 |
else: st.error("Invalid 'objectsToSave' format (expected list).")
|
285 |
else: st.error("Invalid save payload structure received.")
|
286 |
|
287 |
except json.JSONDecodeError:
|
288 |
st.error("Failed to decode save data from client.")
|
289 |
-
print("Received raw data:", save_data_from_js)
|
290 |
except Exception as e:
|
291 |
st.error(f"Error processing save: {e}")
|
292 |
st.exception(e)
|
293 |
|
294 |
-
# Clear the trigger data from session state
|
295 |
st.session_state.js_save_processor = None
|
296 |
# Rerun after processing save to reload world state from DB
|
297 |
if save_processed_successfully:
|
@@ -300,7 +297,7 @@ if save_data_from_js is not None:
|
|
300 |
|
301 |
# --- Main Area ---
|
302 |
st.header("Database Synced 3D World")
|
303 |
-
st.caption(f"
|
304 |
|
305 |
# --- Load and Prepare HTML ---
|
306 |
html_file_path = 'index.html'
|
@@ -310,19 +307,15 @@ try:
|
|
310 |
with open(html_file_path, 'r', encoding='utf-8') as f:
|
311 |
html_template = f.read()
|
312 |
|
313 |
-
# Inject data loaded fresh from DB
|
314 |
js_injection_script = f"""
|
315 |
<script>
|
|
|
316 |
window.ALL_INITIAL_OBJECTS = {json.dumps(all_initial_objects)};
|
317 |
-
window.PLOTS_METADATA = {json.dumps(plots_metadata)};
|
318 |
window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
|
319 |
window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
|
320 |
window.PLOT_DEPTH = {json.dumps(PLOT_DEPTH)};
|
321 |
-
console.log("Streamlit State Injected:", {{
|
322 |
-
selectedObject: window.SELECTED_OBJECT_TYPE,
|
323 |
-
initialObjectsCount: window.ALL_INITIAL_OBJECTS ? window.ALL_INITIAL_OBJECTS.length : 0,
|
324 |
-
plotCount: window.PLOTS_METADATA ? window.PLOTS_METADATA.length : 0
|
325 |
-
}});
|
326 |
</script>
|
327 |
"""
|
328 |
html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
|
|
|
6 |
import sqlite3 # Use SQLite for robust state management
|
7 |
import uuid
|
8 |
import math
|
9 |
+
import time # For adding slight delays if needed for debugging FS issues
|
10 |
from streamlit_js_eval import streamlit_js_eval # For JS communication
|
11 |
|
12 |
# --- Constants ---
|
|
|
16 |
|
17 |
# --- Database Setup ---
|
18 |
def init_db():
|
19 |
+
"""Initializes the SQLite database and tables."""
|
20 |
try:
|
21 |
+
# Use WAL mode for potentially better concurrency, though might have visibility delays on some systems
|
22 |
+
# conn = sqlite3.connect(DB_FILE, isolation_level=None) # Auto-commit mode
|
23 |
+
# cursor.execute('PRAGMA journal_mode=WAL;')
|
24 |
+
# conn.close()
|
25 |
+
|
26 |
+
with sqlite3.connect(DB_FILE) as conn: # Use context manager
|
27 |
+
cursor = conn.cursor()
|
28 |
+
cursor.execute('''
|
29 |
+
CREATE TABLE IF NOT EXISTS plots (
|
30 |
+
grid_x INTEGER NOT NULL, grid_z INTEGER NOT NULL, name TEXT,
|
31 |
+
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
32 |
+
PRIMARY KEY (grid_x, grid_z)
|
33 |
+
)
|
34 |
+
''')
|
35 |
+
cursor.execute('''
|
36 |
+
CREATE TABLE IF NOT EXISTS objects (
|
37 |
+
obj_id TEXT PRIMARY KEY, plot_grid_x INTEGER NOT NULL, plot_grid_z INTEGER NOT NULL,
|
38 |
+
type TEXT NOT NULL, pos_x REAL NOT NULL, pos_y REAL NOT NULL, pos_z REAL NOT NULL,
|
39 |
+
rot_x REAL DEFAULT 0.0, rot_y REAL DEFAULT 0.0, rot_z REAL DEFAULT 0.0, rot_order TEXT DEFAULT 'XYZ',
|
40 |
+
FOREIGN KEY (plot_grid_x, plot_grid_z) REFERENCES plots(grid_x, grid_z) ON DELETE CASCADE
|
41 |
+
)
|
42 |
+
''')
|
43 |
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_objects_plot ON objects (plot_grid_x, plot_grid_z)')
|
44 |
+
# No explicit commit needed with 'with' statement unless changes made
|
45 |
+
print(f"Database {os.path.abspath(DB_FILE)} initialized/checked.") # Print to console
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
except sqlite3.Error as e:
|
47 |
+
# Use st.exception for full traceback in the app UI
|
48 |
+
st.exception(f"CRITICAL DATABASE INIT ERROR: {e}")
|
49 |
+
st.stop()
|
50 |
|
51 |
+
init_db() # Ensure DB exists and has tables
|
|
|
52 |
|
53 |
+
# --- Helper Functions (Database Operations - Using 'with') ---
|
54 |
|
55 |
+
# No Caching - always load fresh from DB
|
56 |
def load_world_state_from_db():
|
57 |
"""Loads all plot metadata and object data fresh from the SQLite DB."""
|
58 |
+
st.write(f"Executing load_world_state_from_db for session {st.runtime.scriptrunner.get_script_run_ctx().session_id[:5]}...") # Debug session ID
|
59 |
plots_metadata = []
|
60 |
+
all_objects_by_plot = {}
|
61 |
+
all_initial_objects_world = []
|
62 |
|
63 |
try:
|
64 |
+
with sqlite3.connect(DB_FILE) as conn: # Auto-closes connection
|
65 |
+
conn.row_factory = sqlite3.Row
|
66 |
+
cursor = conn.cursor()
|
67 |
+
|
68 |
+
st.write("DB Read: Loading plot metadata...")
|
69 |
+
cursor.execute("SELECT grid_x, grid_z, name, strftime('%Y-%m-%d %H:%M:%S', last_updated) as updated_ts FROM plots ORDER BY grid_x, grid_z")
|
70 |
+
plot_rows = cursor.fetchall()
|
71 |
+
st.write(f"DB Read: Found {len(plot_rows)} plot rows.")
|
72 |
+
|
73 |
+
for row in plot_rows:
|
74 |
+
gx, gz = row['grid_x'], row['grid_z']
|
75 |
+
plot_meta = {
|
76 |
+
'id': f"plot_X{gx}_Z{gz}", 'grid_x': gx, 'grid_z': gz,
|
77 |
+
'name': row['name'] or f"Plot ({gx},{gz})",
|
78 |
+
'x_offset': gx * PLOT_WIDTH, 'z_offset': gz * PLOT_DEPTH,
|
79 |
+
'last_updated': row['updated_ts'] # Add timestamp for debugging
|
80 |
+
}
|
81 |
+
plots_metadata.append(plot_meta)
|
82 |
+
all_objects_by_plot[(gx, gz)] = []
|
83 |
+
|
84 |
+
st.write("DB Read: Loading all objects...")
|
85 |
+
cursor.execute("SELECT * FROM objects")
|
86 |
+
object_rows = cursor.fetchall()
|
87 |
+
st.write(f"DB Read: Found {len(object_rows)} object rows.")
|
88 |
+
|
89 |
+
# Group objects by plot
|
90 |
+
for row in object_rows:
|
91 |
+
plot_key = (row['plot_grid_x'], row['plot_grid_z'])
|
92 |
+
if plot_key in all_objects_by_plot:
|
93 |
+
all_objects_by_plot[plot_key].append(dict(row))
|
94 |
+
|
95 |
+
# Combine and calculate world coordinates (outside DB connection)
|
96 |
for plot_meta in plots_metadata:
|
97 |
plot_key = (plot_meta['grid_x'], plot_meta['grid_z'])
|
98 |
for obj_data in all_objects_by_plot[plot_key]:
|
|
|
99 |
world_obj_data = obj_data.copy()
|
|
|
100 |
world_obj_data['pos_x'] = obj_data['pos_x'] + plot_meta['x_offset']
|
101 |
world_obj_data['pos_z'] = obj_data['pos_z'] + plot_meta['z_offset']
|
|
|
102 |
world_obj_data['pos_y'] = obj_data['pos_y']
|
103 |
all_initial_objects_world.append(world_obj_data)
|
104 |
|
105 |
+
st.write(f"DB Load Complete: {len(plots_metadata)} plots, {len(all_initial_objects_world)} total objects processed.")
|
106 |
|
107 |
except sqlite3.Error as e:
|
108 |
st.error(f"Database load error: {e}")
|
109 |
+
return [], [] # Return empty on error
|
|
|
110 |
|
111 |
return plots_metadata, all_initial_objects_world
|
112 |
|
113 |
|
114 |
def save_plot_data_to_db(target_grid_x, target_grid_z, objects_data_list):
|
115 |
+
"""Saves object data list to DB for a specific plot. Overwrites existing objects."""
|
116 |
+
st.write(f"Executing save_plot_data_to_db for plot ({target_grid_x},{target_grid_z})...")
|
117 |
plot_x_offset = target_grid_x * PLOT_WIDTH
|
118 |
plot_z_offset = target_grid_z * PLOT_DEPTH
|
119 |
+
plot_name = f"Plot ({target_grid_x},{target_grid_z})" # Simple default name for upsert
|
120 |
+
|
121 |
+
if not isinstance(objects_data_list, list):
|
122 |
+
st.error("Save Error: Invalid object data format (expected list).")
|
123 |
+
return False
|
124 |
|
|
|
125 |
try:
|
126 |
+
with sqlite3.connect(DB_FILE) as conn: # Auto commit/rollback/close
|
127 |
+
cursor = conn.cursor()
|
128 |
+
# Use savepoint for finer transaction control within 'with' if needed, but default is fine
|
129 |
+
# cursor.execute('BEGIN') # Implicitly handled by 'with' unless isolation_level=None
|
130 |
+
|
131 |
+
# 1. Upsert Plot - ensure plot exists and update timestamp
|
132 |
+
st.write(f"DB Save: Upserting plot ({target_grid_x},{target_grid_z})...")
|
133 |
+
cursor.execute('''
|
134 |
+
INSERT INTO plots (grid_x, grid_z, name) VALUES (?, ?, ?)
|
135 |
+
ON CONFLICT(grid_x, grid_z) DO UPDATE SET name = excluded.name, last_updated = CURRENT_TIMESTAMP
|
136 |
+
''', (target_grid_x, target_grid_z, plot_name))
|
137 |
+
st.write(f"DB Save: Plot upserted.")
|
138 |
+
|
139 |
+
|
140 |
+
# 2. Delete ALL existing objects for this specific plot
|
141 |
+
st.write(f"DB Save: Deleting old objects for plot ({target_grid_x},{target_grid_z})...")
|
142 |
+
cursor.execute("DELETE FROM objects WHERE plot_grid_x = ? AND plot_grid_z = ?", (target_grid_x, target_grid_z))
|
143 |
+
st.write(f"DB Save: Deleted {cursor.rowcount} old objects.")
|
144 |
+
|
145 |
+
# 3. Insert the new objects
|
146 |
+
insert_count = 0
|
147 |
+
objects_to_insert = []
|
148 |
for obj in objects_data_list:
|
149 |
pos = obj.get('position', {})
|
150 |
rot = obj.get('rotation', {})
|
151 |
obj_type = obj.get('type', 'Unknown')
|
152 |
+
obj_id = obj.get('obj_id', str(uuid.uuid4()))
|
153 |
|
154 |
if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown':
|
155 |
print(f"Skipping malformed object during DB save prep: {obj}")
|
|
|
159 |
rel_z = pos.get('z', 0.0) - plot_z_offset
|
160 |
rel_y = pos.get('y', 0.0) # Y is absolute
|
161 |
|
162 |
+
objects_to_insert.append((
|
|
|
|
|
|
|
163 |
obj_id, target_grid_x, target_grid_z, obj_type,
|
164 |
rel_x, rel_y, rel_z,
|
165 |
rot.get('_x', 0.0), rot.get('_y', 0.0), rot.get('_z', 0.0),
|
|
|
167 |
))
|
168 |
insert_count += 1
|
169 |
|
170 |
+
if objects_to_insert:
|
171 |
+
st.write(f"DB Save: Inserting {insert_count} new objects...")
|
172 |
+
cursor.executemany('''
|
173 |
+
INSERT OR REPLACE INTO objects (obj_id, plot_grid_x, plot_grid_z, type, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, rot_order)
|
174 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
175 |
+
''', objects_to_insert) # Use INSERT OR REPLACE to handle potential obj_id conflicts cleanly
|
176 |
+
st.write(f"DB Save: Objects inserted.")
|
177 |
+
else:
|
178 |
+
st.write(f"DB Save: No new objects to insert.")
|
179 |
+
|
180 |
+
# 'with' statement handles commit on success, rollback on error
|
181 |
+
|
182 |
+
st.success(f"DB Save Commit: Plot ({target_grid_x},{target_grid_z}) saved with {insert_count} objects.")
|
183 |
+
# Add a tiny delay AFTER commit, maybe helps filesystem cache? (Experimental)
|
184 |
+
# time.sleep(0.1)
|
185 |
return True
|
186 |
|
187 |
except sqlite3.Error as e:
|
188 |
st.error(f"Database save error for plot ({target_grid_x},{target_grid_z}): {e}")
|
189 |
+
st.exception(e) # Show full traceback
|
|
|
190 |
return False
|
191 |
+
|
|
|
|
|
192 |
|
193 |
# --- Page Config ---
|
194 |
+
st.set_page_config( page_title="DB Synced World Builder v2", layout="wide")
|
195 |
|
196 |
+
# --- Initialize Session State ---
|
197 |
if 'selected_object' not in st.session_state: st.session_state.selected_object = 'None'
|
198 |
if 'js_save_data_result' not in st.session_state: st.session_state.js_save_data_result = None
|
|
|
199 |
|
200 |
# --- Load World State From DB (Runs on every script execution/rerun) ---
|
|
|
201 |
plots_metadata, all_initial_objects = load_world_state_from_db()
|
202 |
|
203 |
# --- Sidebar ---
|
204 |
with st.sidebar:
|
205 |
st.title("🏗️ World Controls")
|
206 |
|
207 |
+
# Refresh Button (Just triggers rerun, load function always hits DB)
|
208 |
if st.button("🔄 Refresh World View", key="refresh_button"):
|
|
|
209 |
st.info("Reloading world state from database...")
|
210 |
st.rerun()
|
211 |
|
212 |
st.header("Navigation (Plots)")
|
213 |
+
# ... (Navigation button code remains the same as previous version) ...
|
214 |
st.caption("Click to teleport player to a plot.")
|
215 |
max_cols = 2
|
216 |
cols = st.columns(max_cols)
|
|
|
231 |
|
232 |
# --- Object Placement ---
|
233 |
st.header("Place Objects")
|
234 |
+
# ... (Object selection code remains the same) ...
|
235 |
object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
|
236 |
current_object_index = object_types.index(st.session_state.selected_object) if st.session_state.selected_object in object_types else 0
|
237 |
selected_object_type_widget = st.selectbox(
|
|
|
239 |
)
|
240 |
if selected_object_type_widget != st.session_state.selected_object:
|
241 |
st.session_state.selected_object = selected_object_type_widget
|
242 |
+
# Rerun updates injection, sessionStorage persists JS side
|
243 |
|
244 |
st.markdown("---")
|
245 |
|
246 |
# --- Saving ---
|
247 |
st.header("Save Work")
|
248 |
+
st.caption("Saves ALL objects currently within the player's plot to the central database.")
|
249 |
if st.button("💾 Save Current Plot", key="save_button"):
|
250 |
js_get_data_code = "getSaveDataAndPosition();"
|
|
|
251 |
streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor")
|
252 |
st.rerun()
|
253 |
|
|
|
263 |
|
264 |
if isinstance(payload, dict) and 'playerPosition' in payload and 'objectsToSave' in payload:
|
265 |
player_pos = payload['playerPosition']
|
266 |
+
objects_to_save = payload['objectsToSave'] # World coords from JS
|
|
|
267 |
|
268 |
+
if isinstance(objects_to_save, list): # Allow saving empty list (clears plot)
|
269 |
target_grid_x = math.floor(player_pos.get('x', 0.0) / PLOT_WIDTH)
|
270 |
target_grid_z = math.floor(player_pos.get('z', 0.0) / PLOT_DEPTH)
|
271 |
|
272 |
st.write(f"Attempting DB save for plot ({target_grid_x},{target_grid_z})")
|
|
|
273 |
# --- Save the data to SQLite DB ---
|
274 |
save_ok = save_plot_data_to_db(target_grid_x, target_grid_z, objects_to_save)
|
275 |
|
276 |
if save_ok:
|
277 |
+
# No cache clear needed. Success message is inside save_plot_data_to_db
|
|
|
|
|
|
|
|
|
|
|
278 |
save_processed_successfully = True
|
279 |
else:
|
280 |
+
st.error(f"Failed DB save for plot ({target_grid_x},{target_grid_z}).")
|
281 |
else: st.error("Invalid 'objectsToSave' format (expected list).")
|
282 |
else: st.error("Invalid save payload structure received.")
|
283 |
|
284 |
except json.JSONDecodeError:
|
285 |
st.error("Failed to decode save data from client.")
|
286 |
+
print("Received raw data:", save_data_from_js) # Log raw data
|
287 |
except Exception as e:
|
288 |
st.error(f"Error processing save: {e}")
|
289 |
st.exception(e)
|
290 |
|
291 |
+
# Clear the trigger data from session state ALWAYS after attempting processing
|
292 |
st.session_state.js_save_processor = None
|
293 |
# Rerun after processing save to reload world state from DB
|
294 |
if save_processed_successfully:
|
|
|
297 |
|
298 |
# --- Main Area ---
|
299 |
st.header("Database Synced 3D World")
|
300 |
+
st.caption(f"DB: '{os.path.abspath(DB_FILE)}'. Plots loaded: {len(plots_metadata)}. Use 'Refresh' to see updates.")
|
301 |
|
302 |
# --- Load and Prepare HTML ---
|
303 |
html_file_path = 'index.html'
|
|
|
307 |
with open(html_file_path, 'r', encoding='utf-8') as f:
|
308 |
html_template = f.read()
|
309 |
|
|
|
310 |
js_injection_script = f"""
|
311 |
<script>
|
312 |
+
// Inject data loaded fresh from DB
|
313 |
window.ALL_INITIAL_OBJECTS = {json.dumps(all_initial_objects)};
|
314 |
+
window.PLOTS_METADATA = {json.dumps(plots_metadata)}; // Still useful for JS ground generation
|
315 |
window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
|
316 |
window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
|
317 |
window.PLOT_DEPTH = {json.dumps(PLOT_DEPTH)};
|
318 |
+
console.log("Streamlit State Injected:", {{ /* ... logging ... */ }});
|
|
|
|
|
|
|
|
|
319 |
</script>
|
320 |
"""
|
321 |
html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
|