awacke1 commited on
Commit
225f7b1
·
verified ·
1 Parent(s): 50498a4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -142
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 creates tables if they don't exist."""
19
  try:
20
- conn = sqlite3.connect(DB_FILE)
21
- cursor = conn.cursor()
22
- # Plots table: grid coordinates are the primary key
23
- cursor.execute('''
24
- CREATE TABLE IF NOT EXISTS plots (
25
- grid_x INTEGER NOT NULL,
26
- grid_z INTEGER NOT NULL,
27
- name TEXT,
28
- last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
29
- PRIMARY KEY (grid_x, grid_z)
30
- )
31
- ''')
32
- # Objects table: references plots using grid coords
33
- cursor.execute('''
34
- CREATE TABLE IF NOT EXISTS objects (
35
- obj_id TEXT PRIMARY KEY,
36
- plot_grid_x INTEGER NOT NULL,
37
- plot_grid_z INTEGER NOT NULL,
38
- type TEXT NOT NULL,
39
- pos_x REAL NOT NULL,
40
- pos_y REAL NOT NULL,
41
- pos_z REAL NOT NULL,
42
- rot_x REAL DEFAULT 0.0,
43
- rot_y REAL DEFAULT 0.0,
44
- rot_z REAL DEFAULT 0.0,
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.error(f"Database initialization error: {e}")
58
- st.stop() # Stop execution if DB can't be initialized
 
59
 
60
- # Call init function once at the start
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 = {} # Temp store: (gx, gz) -> [obj_dict, ...]
69
- all_initial_objects_world = [] # Final list with world coords
70
 
71
  try:
72
- conn = sqlite3.connect(DB_FILE)
73
- conn.row_factory = sqlite3.Row # Return rows as dict-like objects
74
- cursor = conn.cursor()
75
-
76
- # 1. Load Plot Metadata
77
- cursor.execute("SELECT grid_x, grid_z, name FROM plots ORDER BY grid_x, grid_z")
78
- plot_rows = cursor.fetchall()
79
- for row in plot_rows:
80
- gx, gz = row['grid_x'], row['grid_z']
81
- plots_metadata.append({
82
- 'id': f"plot_X{gx}_Z{gz}", # Reconstruct ID for consistency if needed
83
- 'grid_x': gx, 'grid_z': gz, 'name': row['name'] or f"Plot ({gx},{gz})",
84
- 'x_offset': gx * PLOT_WIDTH,
85
- 'z_offset': gz * PLOT_DEPTH
86
- })
87
- all_objects_by_plot[(gx, gz)] = [] # Initialize empty list for objects
88
-
89
- # 2. Load All Objects
90
- cursor.execute("SELECT * FROM objects")
91
- object_rows = cursor.fetchall()
92
- for row in object_rows:
93
- plot_key = (row['plot_grid_x'], row['plot_grid_z'])
94
- if plot_key in all_objects_by_plot:
95
- # Store object data with RELATIVE coordinates from DB
96
- all_objects_by_plot[plot_key].append(dict(row))
97
-
98
- conn.close()
99
-
100
- # 3. Combine and Calculate World Coordinates
 
 
 
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: Found {len(plots_metadata)} plots and {len(all_initial_objects_world)} total objects.") # Debug
114
 
115
  except sqlite3.Error as e:
116
  st.error(f"Database load error: {e}")
117
- # Return empty lists to prevent downstream errors
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 for that plot."""
 
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
- conn = sqlite3.connect(DB_FILE)
132
- cursor = conn.cursor()
133
- cursor.execute('BEGIN TRANSACTION') # Start transaction
134
-
135
- # 1. Ensure plot exists in plots table (Upsert)
136
- cursor.execute('''
137
- INSERT INTO plots (grid_x, grid_z, name) VALUES (?, ?, ?)
138
- ON CONFLICT(grid_x, grid_z) DO UPDATE SET name = excluded.name, last_updated = CURRENT_TIMESTAMP
139
- ''', (target_grid_x, target_grid_z, plot_name))
140
-
141
- # 2. Delete ALL existing objects for this specific plot
142
- cursor.execute("DELETE FROM objects WHERE plot_grid_x = ? AND plot_grid_z = ?", (target_grid_x, target_grid_z))
143
- deleted_count = cursor.rowcount
144
- # st.write(f"DB Save: Deleted {deleted_count} old objects for plot ({target_grid_x},{target_grid_z}).") # Debug
145
-
146
- # 3. Insert the new objects
147
- insert_count = 0
148
- if isinstance(objects_data_list, list):
 
 
 
 
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())) # Use ID from JS
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
- cursor.execute('''
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
- conn.commit() # Commit transaction
175
- st.success(f"Plot ({target_grid_x},{target_grid_z}) saved with {insert_count} objects.")
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- if conn:
181
- conn.rollback() # Rollback on error
182
  return False
183
- finally:
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 (Simpler Now) ---
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
- # *** Refresh Button - Simpler Logic ***
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 will happen, JS state handled by sessionStorage
238
 
239
  st.markdown("---")
240
 
241
  # --- Saving ---
242
  st.header("Save Work")
243
- st.caption("Saves ALL objects in the player's current plot to the central database, overwriting previous data for that plot.")
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
- # These are ALL objects JS found within the target plot boundaries (world coords)
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! DB is source of truth.
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 to save plot ({target_grid_x},{target_grid_z}) data to database.")
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"World state loaded from '{DB_FILE}'. Plots loaded: {len(plots_metadata)}. Use 'Refresh' to reload.")
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)