awacke1 commited on
Commit
50498a4
·
verified ·
1 Parent(s): 14b289d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +205 -163
app.py CHANGED
@@ -3,159 +3,208 @@ import streamlit as st
3
  import streamlit.components.v1 as components
4
  import os
5
  import json
6
- import pandas as pd
7
  import uuid
8
- import math # For floor function
9
  from streamlit_js_eval import streamlit_js_eval # For JS communication
10
 
11
  # --- Constants ---
12
- SAVE_DIR = "saved_worlds"
13
- PLOT_WIDTH = 50.0 # Width of each plot in 3D space
14
- PLOT_DEPTH = 50.0 # Depth of each plot
15
- CSV_COLUMNS = ['obj_id', 'type', 'pos_x', 'pos_y', 'pos_z', 'rot_x', 'rot_y', 'rot_z', 'rot_order']
16
-
17
- # --- Ensure Save Directory Exists ---
18
- os.makedirs(SAVE_DIR, exist_ok=True)
19
-
20
- # --- Helper Functions ---
21
-
22
- # Cache key now depends on refresh_trigger to force reload when counter changes
23
- @st.cache_data(ttl=3600)
24
- def load_plot_metadata(refresh_trigger):
25
- """Scans save dir for plot_X*_Z*.csv, sorts, calculates metadata."""
26
- # This function depends on refresh_trigger only to invalidate cache.
27
- # The value of refresh_trigger itself isn't used inside.
28
- st.write(f"Cache Trigger: {refresh_trigger}. Reloading plot metadata...") # Debug log
29
-
30
- plots = []
31
- plot_files = []
32
- try:
33
- plot_files = sorted([f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")])
34
- except FileNotFoundError:
35
- st.error(f"Save directory '{SAVE_DIR}' not found.")
36
- return [] # Return empty list on error
37
- except Exception as e:
38
- st.error(f"Error listing save directory '{SAVE_DIR}': {e}")
39
- return []
40
-
41
- parsed_plots = []
42
- for filename in plot_files:
43
- try:
44
- parts = filename[:-4].split('_')
45
- grid_x = int(parts[1][1:])
46
- grid_z = int(parts[2][1:])
47
- # Default name is coordinates, override if name parts exist
48
- plot_name = " ".join(parts[3:]) if len(parts) > 3 else f"Plot ({grid_x},{grid_z})"
49
-
50
- parsed_plots.append({
51
- 'id': filename[:-4],
52
- 'filename': filename,
53
- 'grid_x': grid_x, 'grid_z': grid_z, 'name': plot_name,
54
- 'x_offset': grid_x * PLOT_WIDTH,
55
- 'z_offset': grid_z * PLOT_DEPTH
56
- })
57
- except (IndexError, ValueError):
58
- st.warning(f"Could not parse grid coordinates from filename: {filename}. Skipping.")
59
- continue
60
-
61
- parsed_plots.sort(key=lambda p: (p['grid_x'], p['grid_z']))
62
- st.write(f"Metadata loaded for {len(parsed_plots)} plots.") # Debug log
63
- return parsed_plots
64
-
65
- # No caching needed for loading objects, should always load fresh based on metadata
66
- def load_plot_objects(filename, x_offset, z_offset):
67
- """Loads objects from a CSV, applying the plot's world offsets."""
68
- file_path = os.path.join(SAVE_DIR, filename)
69
- objects = []
70
- try:
71
- df = pd.read_csv(file_path)
72
- if not all(col in df.columns for col in ['type', 'pos_x', 'pos_y', 'pos_z']):
73
- return [] # Skip files missing essential columns silently now
74
- df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in range(len(df))]))
75
- for col, default in [('rot_x', 0.0), ('rot_y', 0.0), ('rot_z', 0.0), ('rot_order', 'XYZ')]:
76
- if col not in df.columns: df[col] = default
77
-
78
- for _, row in df.iterrows():
79
- obj_data = row.to_dict()
80
- obj_data['pos_x'] += x_offset
81
- obj_data['pos_z'] += z_offset
82
- objects.append(obj_data)
83
- return objects
84
- except FileNotFoundError: return [] # File might have been deleted between metadata load and object load
85
- except pd.errors.EmptyDataError: return [] # Empty file is okay
86
- except Exception as e:
87
- st.error(f"Error loading objects from {filename}: {e}")
88
- return []
89
-
90
- # Overwrites the target file with the provided objects
91
- def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
92
- """Saves object data list to a CSV, making positions relative. Overwrites file."""
93
- file_path = os.path.join(SAVE_DIR, filename)
94
- relative_objects = []
95
- if not isinstance(objects_data_list, list):
96
- st.error("Invalid data format received for saving (expected a list).")
97
- return False
98
 
99
- for obj in objects_data_list:
100
- pos = obj.get('position', {})
101
- rot = obj.get('rotation', {})
102
- obj_type = obj.get('type', 'Unknown')
103
- # Use ID provided by JS if available (important for consistency if JS tracks it)
104
- obj_id = obj.get('obj_id', str(uuid.uuid4()))
105
-
106
- if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown':
107
- print(f"Skipping malformed object during save prep: {obj}")
108
- continue
109
-
110
- relative_obj = {
111
- 'obj_id': obj_id, 'type': obj_type,
112
- 'pos_x': pos.get('x', 0.0) - plot_x_offset, # Make relative X
113
- 'pos_y': pos.get('y', 0.0),
114
- 'pos_z': pos.get('z', 0.0) - plot_z_offset, # Make relative Z
115
- 'rot_x': rot.get('_x', 0.0), 'rot_y': rot.get('_y', 0.0), 'rot_z': rot.get('_z', 0.0),
116
- 'rot_order': rot.get('_order', 'XYZ')
117
- }
118
- relative_objects.append(relative_obj)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
  try:
121
- # Overwrite the file with the new data (or empty if list is empty)
122
- df = pd.DataFrame(relative_objects, columns=CSV_COLUMNS)
123
- df.to_csv(file_path, index=False)
124
- # st.success(f"Saved/Updated {len(relative_objects)} objects to {filename}") # Success message in calling block
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  return True
126
- except Exception as e:
127
- st.error(f"Failed to save plot data to {filename}: {e}")
 
 
 
128
  return False
 
 
 
129
 
130
  # --- Page Config ---
131
- st.set_page_config( page_title="Synced World Builder", layout="wide")
132
 
133
- # --- Initialize Session State ---
134
  if 'selected_object' not in st.session_state: st.session_state.selected_object = 'None'
135
  if 'js_save_data_result' not in st.session_state: st.session_state.js_save_data_result = None
136
- # Counter for forcing cache refresh
137
- if 'refresh_counter' not in st.session_state: st.session_state.refresh_counter = 0
138
 
139
- # --- Load Plot Metadata (using refresh counter) ---
140
- plots_metadata = load_plot_metadata(st.session_state.refresh_counter)
141
-
142
- # --- Load ALL Objects for Rendering ---
143
- all_initial_objects = []
144
- # st.write(f"Loading objects for {len(plots_metadata)} plots...") # Less verbose now
145
- for plot in plots_metadata:
146
- all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset'], plot['z_offset']))
147
- # st.write(f"Total objects loaded: {len(all_initial_objects)}")
148
 
149
  # --- Sidebar ---
150
  with st.sidebar:
151
  st.title("🏗️ World Controls")
152
 
153
- # *** Refresh Button Logic ***
154
  if st.button("🔄 Refresh World View", key="refresh_button"):
155
- st.session_state.refresh_counter += 1 # Increment counter
156
- load_plot_metadata.clear() # Clear cache explicitly
157
- st.info(f"Refresh triggered (Counter: {st.session_state.refresh_counter}). Reloading...")
158
- st.rerun() # Force rerun with new counter value
159
 
160
  st.header("Navigation (Plots)")
161
  st.caption("Click to teleport player to a plot.")
@@ -166,8 +215,8 @@ with st.sidebar:
166
  for plot in sorted_plots_for_nav:
167
  button_label = f"➡️ {plot.get('name', plot['id'])} ({plot['grid_x']},{plot['grid_z']})"
168
  if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
169
- target_x = plot['x_offset'] + PLOT_WIDTH / 2 # Center X
170
- target_z = plot['z_offset'] + PLOT_DEPTH / 2 # Center Z
171
  try:
172
  js_code = f"teleportPlayer({target_x}, {target_z});"
173
  streamlit_js_eval(js_code=js_code, key=f"teleport_{plot['id']}")
@@ -185,16 +234,16 @@ with st.sidebar:
185
  )
186
  if selected_object_type_widget != st.session_state.selected_object:
187
  st.session_state.selected_object = selected_object_type_widget
188
- # Rerun will update JS via injection, sessionStorage handles unsaved items
189
 
190
  st.markdown("---")
191
 
192
  # --- Saving ---
193
  st.header("Save Work")
194
- st.caption("Saves ALL objects currently within the player's plot, overwriting previous save for that plot.")
195
- if st.button("💾 Save Current Plot", key="save_button"): # Renamed button slightly
196
- # JS func now sends ALL objects in current plot + player position
197
  js_get_data_code = "getSaveDataAndPosition();"
 
198
  streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor")
199
  st.rerun()
200
 
@@ -210,35 +259,30 @@ if save_data_from_js is not None:
210
 
211
  if isinstance(payload, dict) and 'playerPosition' in payload and 'objectsToSave' in payload:
212
  player_pos = payload['playerPosition']
213
- # These are ALL objects JS found within the target plot boundaries
214
  objects_to_save = payload['objectsToSave']
215
 
216
  if isinstance(objects_to_save, list):
217
- # Determine target plot based on player position
218
  target_grid_x = math.floor(player_pos.get('x', 0.0) / PLOT_WIDTH)
219
  target_grid_z = math.floor(player_pos.get('z', 0.0) / PLOT_DEPTH)
220
 
221
- target_filename = f"plot_X{target_grid_x}_Z{target_grid_z}.csv"
222
- target_plot_x_offset = target_grid_x * PLOT_WIDTH
223
- target_plot_z_offset = target_grid_z * PLOT_DEPTH
224
-
225
- st.write(f"Saving plot: {target_filename} (Player at: x={player_pos.get('x', 0):.1f}, z={player_pos.get('z', 0):.1f})")
226
- is_new_plot_file = not os.path.exists(os.path.join(SAVE_DIR, target_filename))
227
 
228
- # --- Save/Overwrite the data ---
229
- save_ok = save_plot_data(target_filename, objects_to_save, target_plot_x_offset, target_plot_z_offset)
230
 
231
  if save_ok:
232
- # Clear cache so next reload/refresh sees the change
233
- load_plot_metadata.clear()
234
- # No need to call JS reset function anymore
235
- if is_new_plot_file: st.success(f"New plot created/saved: {target_filename}")
236
- else: st.success(f"Updated existing plot: {target_filename}")
 
237
  save_processed_successfully = True
238
  else:
239
- st.error(f"Failed to save plot data to file: {target_filename}")
240
  else: st.error("Invalid 'objectsToSave' format (expected list).")
241
- else: st.error("Invalid save payload structure received (missing keys).")
242
 
243
  except json.JSONDecodeError:
244
  st.error("Failed to decode save data from client.")
@@ -249,15 +293,14 @@ if save_data_from_js is not None:
249
 
250
  # Clear the trigger data from session state
251
  st.session_state.js_save_processor = None
252
- # Rerun after processing save to reload the world state
253
  if save_processed_successfully:
254
- st.session_state.refresh_counter += 1 # Increment counter to ensure cache miss on THIS rerun
255
  st.rerun()
256
 
257
 
258
  # --- Main Area ---
259
- st.header("Synced Infinite 3D World")
260
- st.caption(f"Saving to '{SAVE_DIR}'. Plots loaded: {len(plots_metadata)}. Use 'Refresh' button to see others' saves.")
261
 
262
  # --- Load and Prepare HTML ---
263
  html_file_path = 'index.html'
@@ -267,11 +310,11 @@ try:
267
  with open(html_file_path, 'r', encoding='utf-8') as f:
268
  html_template = f.read()
269
 
 
270
  js_injection_script = f"""
271
  <script>
272
- // Inject necessary data for JS initialization
273
  window.ALL_INITIAL_OBJECTS = {json.dumps(all_initial_objects)};
274
- window.PLOTS_METADATA = {json.dumps(plots_metadata)}; // Needed for ground generation logic
275
  window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
276
  window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
277
  window.PLOT_DEPTH = {json.dumps(PLOT_DEPTH)};
@@ -284,12 +327,11 @@ try:
284
  """
285
  html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
286
 
287
- # --- Embed HTML Component ---
288
  components.html( html_content_with_state, height=750, scrolling=False )
289
 
290
  except FileNotFoundError:
291
  st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
292
- st.warning(f"Make sure `{html_file_path}` is nearby and `{SAVE_DIR}` exists.")
293
  except Exception as e:
294
  st.error(f"An critical error occurred during HTML prep/render: {e}")
295
  st.exception(e)
 
3
  import streamlit.components.v1 as components
4
  import os
5
  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 ---
12
+ DB_FILE = "world_state.db" # SQLite database file
13
+ PLOT_WIDTH = 50.0
14
+ 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}")
157
+ 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) # 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),
170
+ rot.get('_order', 'XYZ')
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.")
 
215
  for plot in sorted_plots_for_nav:
216
  button_label = f"➡️ {plot.get('name', plot['id'])} ({plot['grid_x']},{plot['grid_z']})"
217
  if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
218
+ target_x = plot['x_offset'] + PLOT_WIDTH / 2
219
+ target_z = plot['z_offset'] + PLOT_DEPTH / 2
220
  try:
221
  js_code = f"teleportPlayer({target_x}, {target_z});"
222
  streamlit_js_eval(js_code=js_code, key=f"teleport_{plot['id']}")
 
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
 
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.")
 
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:
 
298
  st.rerun()
299
 
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
  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)};
 
327
  """
328
  html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
329
 
330
+ # Embed HTML Component
331
  components.html( html_content_with_state, height=750, scrolling=False )
332
 
333
  except FileNotFoundError:
334
  st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
 
335
  except Exception as e:
336
  st.error(f"An critical error occurred during HTML prep/render: {e}")
337
  st.exception(e)