awacke1 commited on
Commit
7136196
·
verified ·
1 Parent(s): e32b405

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -132
app.py CHANGED
@@ -6,25 +6,25 @@ import json
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 ---
13
- DB_FILE = "world_state.db" # SQLite database file
14
  PLOT_WIDTH = 50.0
15
  PLOT_DEPTH = 50.0
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,
@@ -32,6 +32,7 @@ def init_db():
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,
@@ -41,71 +42,77 @@ def init_db():
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
@@ -113,37 +120,33 @@ def load_world_state_from_db():
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', {})
@@ -151,101 +154,83 @@ def save_plot_data_to_db(target_grid_x, target_grid_z, objects_data_list):
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}")
156
- continue
157
 
158
  rel_x = pos.get('x', 0.0) - plot_x_offset
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),
166
- rot.get('_order', 'XYZ')
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)
217
- col_idx = 0
218
  sorted_plots_for_nav = sorted(plots_metadata, key=lambda p: (p['grid_x'], p['grid_z']))
219
  for plot in sorted_plots_for_nav:
220
  button_label = f"➡️ {plot.get('name', plot['id'])} ({plot['grid_x']},{plot['grid_z']})"
221
  if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
222
- target_x = plot['x_offset'] + PLOT_WIDTH / 2
223
- target_z = plot['z_offset'] + PLOT_DEPTH / 2
224
- try:
225
- js_code = f"teleportPlayer({target_x}, {target_z});"
226
- streamlit_js_eval(js_code=js_code, key=f"teleport_{plot['id']}")
227
  except Exception as e: st.error(f"Teleport command failed: {e}")
228
  col_idx = (col_idx + 1) % max_cols
229
-
230
  st.markdown("---")
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(
238
- "Select Object:", options=object_types, index=current_object_index, key="selected_object_widget"
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")
@@ -256,48 +241,35 @@ with st.sidebar:
256
  save_data_from_js = st.session_state.get("js_save_processor", None)
257
 
258
  if save_data_from_js is not None:
259
- st.info("Processing save request...")
260
  save_processed_successfully = False
261
  try:
262
  payload = json.loads(save_data_from_js) if isinstance(save_data_from_js, str) else save_data_from_js
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:
295
  st.rerun()
296
 
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,15 +279,15 @@ try:
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)
@@ -323,8 +295,5 @@ try:
323
  # Embed HTML Component
324
  components.html( html_content_with_state, height=750, scrolling=False )
325
 
326
- except FileNotFoundError:
327
- st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
328
- except Exception as e:
329
- st.error(f"An critical error occurred during HTML prep/render: {e}")
330
- st.exception(e)
 
6
  import sqlite3 # Use SQLite for robust state management
7
  import uuid
8
  import math
9
+ import time # For potential delays if needed
10
  from streamlit_js_eval import streamlit_js_eval # For JS communication
11
 
12
  # --- Constants ---
13
+ DB_FILE = "world_state_v3.db" # Use a new DB file name to ensure fresh start
14
  PLOT_WIDTH = 50.0
15
  PLOT_DEPTH = 50.0
16
 
17
  # --- Database Setup ---
18
  def init_db():
19
  """Initializes the SQLite database and tables."""
20
+ st.write(f"DEBUG: Initializing Database '{os.path.abspath(DB_FILE)}'...")
21
  try:
22
+ # Ensure connection closes even if errors occur during table creation
23
+ with sqlite3.connect(DB_FILE, timeout=10) as conn: # Added timeout
 
 
 
 
24
  cursor = conn.cursor()
25
+ # Enable WAL mode for potentially better concurrency - might help?
26
+ # cursor.execute('PRAGMA journal_mode=WAL;')
27
+ # Plots table
28
  cursor.execute('''
29
  CREATE TABLE IF NOT EXISTS plots (
30
  grid_x INTEGER NOT NULL, grid_z INTEGER NOT NULL, name TEXT,
 
32
  PRIMARY KEY (grid_x, grid_z)
33
  )
34
  ''')
35
+ # Objects table
36
  cursor.execute('''
37
  CREATE TABLE IF NOT EXISTS objects (
38
  obj_id TEXT PRIMARY KEY, plot_grid_x INTEGER NOT NULL, plot_grid_z INTEGER NOT NULL,
 
42
  )
43
  ''')
44
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_objects_plot ON objects (plot_grid_x, plot_grid_z)')
45
+ print(f"DEBUG: Database {DB_FILE} initialized/checked.") # To console
 
46
  except sqlite3.Error as e:
 
47
  st.exception(f"CRITICAL DATABASE INIT ERROR: {e}")
48
  st.stop()
49
 
50
+ init_db() # Run initialization at the start
51
 
52
+ # --- Helper Functions (Database Operations - Robust Connections) ---
53
 
54
+ # *** REMOVED @st.cache_data - Load fresh every time ***
55
  def load_world_state_from_db():
56
  """Loads all plot metadata and object data fresh from the SQLite DB."""
57
+ session_id = st.runtime.scriptrunner.get_script_run_ctx().session_id[:5] # Short ID for logging
58
+ st.write(f"DEBUG [{session_id}]: Executing load_world_state_from_db...")
59
  plots_metadata = []
 
60
  all_initial_objects_world = []
61
 
62
  try:
63
+ # Ensure connection is opened and closed for each load operation
64
+ with sqlite3.connect(DB_FILE, timeout=10) as conn:
65
  conn.row_factory = sqlite3.Row
66
  cursor = conn.cursor()
67
 
68
+ # 1. Load Plot Metadata
69
+ cursor.execute("SELECT grid_x, grid_z, name FROM plots ORDER BY grid_x, grid_z")
70
  plot_rows = cursor.fetchall()
71
+ st.write(f"DEBUG [{session_id}]: DB Read Found {len(plot_rows)} plot rows.")
72
+ if not plot_rows:
73
+ st.write(f"DEBUG [{session_id}]: No plots found in DB.")
74
 
75
+ plot_keys = set() # To efficiently map objects later
76
  for row in plot_rows:
77
  gx, gz = row['grid_x'], row['grid_z']
78
+ plots_metadata.append({
79
  'id': f"plot_X{gx}_Z{gz}", 'grid_x': gx, 'grid_z': gz,
80
  'name': row['name'] or f"Plot ({gx},{gz})",
81
+ 'x_offset': gx * PLOT_WIDTH, 'z_offset': gz * PLOT_DEPTH
82
+ })
83
+ plot_keys.add((gx, gz))
84
+
85
+ # 2. Load All Objects
86
+ cursor.execute("SELECT obj_id, plot_grid_x, plot_grid_z, type, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, rot_order FROM objects")
 
 
87
  object_rows = cursor.fetchall()
88
+ st.write(f"DEBUG [{session_id}]: DB Read Found {len(object_rows)} object rows.")
89
 
90
+ # 3. Combine and Calculate World Coordinates
91
+ objects_loaded_count = 0
92
  for row in object_rows:
93
  plot_key = (row['plot_grid_x'], row['plot_grid_z'])
94
+ # Ensure the object belongs to a known plot (data integrity check)
95
+ if plot_key in plot_keys:
96
+ obj_data = dict(row) # Convert row object to dict
97
+ # Find corresponding plot metadata for offset calculation
98
+ plot_meta = next((p for p in plots_metadata if p['grid_x'] == row['plot_grid_x'] and p['grid_z'] == row['plot_grid_z']), None)
99
+ if plot_meta:
100
+ world_obj_data = obj_data.copy()
101
+ world_obj_data['pos_x'] = obj_data['pos_x'] + plot_meta['x_offset']
102
+ world_obj_data['pos_z'] = obj_data['pos_z'] + plot_meta['z_offset']
103
+ world_obj_data['pos_y'] = obj_data['pos_y'] # Y is already world Y
104
+ all_initial_objects_world.append(world_obj_data)
105
+ objects_loaded_count += 1
106
+ else:
107
+ print(f"WARN: Object {obj_data['obj_id']} references non-existent plot {plot_key}") # To console
108
+ else:
109
+ print(f"WARN: Object {obj_data['obj_id']} references unknown plot {plot_key}") # To console
110
 
 
 
 
 
 
 
 
 
 
111
 
112
+ st.write(f"DEBUG [{session_id}]: DB Load Complete. Processed {len(plots_metadata)} plots, {objects_loaded_count} objects.")
113
 
114
  except sqlite3.Error as e:
115
+ st.exception(f"Database load error for session {session_id}: {e}")
116
  return [], [] # Return empty on error
117
 
118
  return plots_metadata, all_initial_objects_world
 
120
 
121
  def save_plot_data_to_db(target_grid_x, target_grid_z, objects_data_list):
122
  """Saves object data list to DB for a specific plot. Overwrites existing objects."""
123
+ st.write(f"DEBUG: Executing save_plot_data_to_db for plot ({target_grid_x},{target_grid_z})...")
124
  plot_x_offset = target_grid_x * PLOT_WIDTH
125
  plot_z_offset = target_grid_z * PLOT_DEPTH
126
+ # Use a generic name or allow passing one if needed later
127
+ plot_name = f"Plot ({target_grid_x},{target_grid_z})"
128
 
129
  if not isinstance(objects_data_list, list):
130
  st.error("Save Error: Invalid object data format (expected list).")
131
  return False
132
 
133
  try:
134
+ # Use 'with' for automatic transaction handling and closing
135
+ with sqlite3.connect(DB_FILE, timeout=10) as conn:
136
  cursor = conn.cursor()
 
 
137
 
138
+ # 1. Upsert Plot (ensures plot exists, updates timestamp)
 
139
  cursor.execute('''
140
  INSERT INTO plots (grid_x, grid_z, name) VALUES (?, ?, ?)
141
+ ON CONFLICT(grid_x, grid_z) DO UPDATE SET last_updated = CURRENT_TIMESTAMP
142
  ''', (target_grid_x, target_grid_z, plot_name))
143
+ # st.write(f"DEBUG: Upserted plot metadata for ({target_grid_x},{target_grid_z})")
 
144
 
145
  # 2. Delete ALL existing objects for this specific plot
 
146
  cursor.execute("DELETE FROM objects WHERE plot_grid_x = ? AND plot_grid_z = ?", (target_grid_x, target_grid_z))
147
+ # st.write(f"DEBUG: Deleted {cursor.rowcount} old objects for plot.")
148
 
149
  # 3. Insert the new objects
 
150
  objects_to_insert = []
151
  for obj in objects_data_list:
152
  pos = obj.get('position', {})
 
154
  obj_type = obj.get('type', 'Unknown')
155
  obj_id = obj.get('obj_id', str(uuid.uuid4()))
156
 
157
+ if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown': continue
 
 
158
 
159
  rel_x = pos.get('x', 0.0) - plot_x_offset
160
  rel_z = pos.get('z', 0.0) - plot_z_offset
161
+ rel_y = pos.get('y', 0.0)
162
 
163
  objects_to_insert.append((
164
  obj_id, target_grid_x, target_grid_z, obj_type,
165
  rel_x, rel_y, rel_z,
166
+ rot.get('_x', 0.0), rot.get('_y', 0.0), rot.get('_z', 0.0), rot.get('_order', 'XYZ')
 
167
  ))
 
168
 
169
  if objects_to_insert:
 
170
  cursor.executemany('''
171
+ INSERT OR REPLACE INTO objects
172
+ (obj_id, plot_grid_x, plot_grid_z, type, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z, rot_order)
173
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
174
+ ''', objects_to_insert) # Use INSERT OR REPLACE based on obj_id primary key
175
+ st.write(f"DEBUG: Inserted/Replaced {len(objects_to_insert)} objects.")
176
  else:
177
+ st.write(f"DEBUG: No objects provided to insert for plot ({target_grid_x},{target_grid_z}). Plot cleared.")
178
 
179
+ # Commit happens automatically when 'with' block exits without error
180
 
181
+ st.success(f"Plot ({target_grid_x},{target_grid_z}) saved successfully to DB.")
 
 
182
  return True
183
 
184
  except sqlite3.Error as e:
185
+ st.exception(f"DATABASE SAVE ERROR for plot ({target_grid_x},{target_grid_z}): {e}")
 
186
  return False
187
 
 
188
  # --- Page Config ---
189
+ st.set_page_config( page_title="DB Synced World Builder v3", layout="wide")
190
 
191
  # --- Initialize Session State ---
192
  if 'selected_object' not in st.session_state: st.session_state.selected_object = 'None'
193
  if 'js_save_data_result' not in st.session_state: st.session_state.js_save_data_result = None
194
+ # No refresh counter needed
195
 
196
+ # --- Load World State From DB (Fresh on each run/rerun) ---
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 (Just reruns the script)
204
  if st.button("🔄 Refresh World View", key="refresh_button"):
205
  st.info("Reloading world state from database...")
206
+ st.rerun() # Rerun forces call to load_world_state_from_db
207
 
208
  st.header("Navigation (Plots)")
209
+ # ... (Navigation button code unchanged - uses latest plots_metadata) ...
210
  st.caption("Click to teleport player to a plot.")
211
+ max_cols = 2; cols = st.columns(max_cols); col_idx = 0
 
 
212
  sorted_plots_for_nav = sorted(plots_metadata, key=lambda p: (p['grid_x'], p['grid_z']))
213
  for plot in sorted_plots_for_nav:
214
  button_label = f"➡️ {plot.get('name', plot['id'])} ({plot['grid_x']},{plot['grid_z']})"
215
  if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
216
+ target_x = plot['x_offset'] + PLOT_WIDTH/2; target_z = plot['z_offset'] + PLOT_DEPTH/2
217
+ try: streamlit_js_eval(js_code=f"teleportPlayer({target_x}, {target_z});", key=f"teleport_{plot['id']}")
 
 
 
218
  except Exception as e: st.error(f"Teleport command failed: {e}")
219
  col_idx = (col_idx + 1) % max_cols
 
220
  st.markdown("---")
221
 
222
  # --- Object Placement ---
223
  st.header("Place Objects")
224
+ # ... (Object selection unchanged) ...
225
  object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
226
  current_object_index = object_types.index(st.session_state.selected_object) if st.session_state.selected_object in object_types else 0
227
+ selected_object_type_widget = st.selectbox( "Select Object:", options=object_types, index=current_object_index, key="selected_object_widget")
228
+ if selected_object_type_widget != st.session_state.selected_object: st.session_state.selected_object = selected_object_type_widget
 
 
 
 
 
229
  st.markdown("---")
230
 
231
  # --- Saving ---
232
  st.header("Save Work")
233
+ st.caption("Saves ALL objects in the player's current plot to the central database.")
234
  if st.button("💾 Save Current Plot", key="save_button"):
235
  js_get_data_code = "getSaveDataAndPosition();"
236
  streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor")
 
241
  save_data_from_js = st.session_state.get("js_save_processor", None)
242
 
243
  if save_data_from_js is not None:
244
+ st.write("DEBUG: Processing save request from JS...") # Debug
245
  save_processed_successfully = False
246
  try:
247
  payload = json.loads(save_data_from_js) if isinstance(save_data_from_js, str) else save_data_from_js
 
248
  if isinstance(payload, dict) and 'playerPosition' in payload and 'objectsToSave' in payload:
249
  player_pos = payload['playerPosition']
250
  objects_to_save = payload['objectsToSave'] # World coords from JS
251
+ if isinstance(objects_to_save, list):
 
252
  target_grid_x = math.floor(player_pos.get('x', 0.0) / PLOT_WIDTH)
253
  target_grid_z = math.floor(player_pos.get('z', 0.0) / PLOT_DEPTH)
 
 
254
  # --- Save the data to SQLite DB ---
255
  save_ok = save_plot_data_to_db(target_grid_x, target_grid_z, objects_to_save)
256
+ if save_ok: save_processed_successfully = True # Flag success
257
+ else: st.error(f"Failed DB save for plot ({target_grid_x},{target_grid_z}).")
258
+ else: st.error("Save Error: 'objectsToSave' format invalid (expected list).")
259
+ else: st.error("Save Error: Invalid payload structure received.")
260
+ except json.JSONDecodeError: st.error("Save Error: Failed to decode JSON data from client.")
261
+ except Exception as e: st.exception(f"Save Error: An unexpected error occurred: {e}")
262
+
263
+ # Clear the trigger data from session state ALWAYS after processing attempt
 
 
 
 
 
 
 
 
 
264
  st.session_state.js_save_processor = None
265
+ # Rerun ONLY if save was flagged successful to reload the state
266
  if save_processed_successfully:
267
  st.rerun()
268
 
269
 
270
  # --- Main Area ---
271
  st.header("Database Synced 3D World")
272
+ st.caption(f"DB: '{os.path.abspath(DB_FILE)}'. Plots loaded: {len(plots_metadata)}. Use 'Refresh' button to sync.")
273
 
274
  # --- Load and Prepare HTML ---
275
  html_file_path = 'index.html'
 
279
  with open(html_file_path, 'r', encoding='utf-8') as f:
280
  html_template = f.read()
281
 
282
+ # Inject data loaded fresh from DB
283
  js_injection_script = f"""
284
  <script>
 
285
  window.ALL_INITIAL_OBJECTS = {json.dumps(all_initial_objects)};
286
+ window.PLOTS_METADATA = {json.dumps(plots_metadata)};
287
  window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
288
  window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
289
  window.PLOT_DEPTH = {json.dumps(PLOT_DEPTH)};
290
+ console.log("Streamlit State Injected:", {{ /* Basic logging */ }});
291
  </script>
292
  """
293
  html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
 
295
  # Embed HTML Component
296
  components.html( html_content_with_state, height=750, scrolling=False )
297
 
298
+ except FileNotFoundError: st.error(f"CRITICAL ERROR: Could not find file '{html_file_path}'.")
299
+ except Exception as e: st.exception(f"HTML prep/render error: {e}")