Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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
|
10 |
from streamlit_js_eval import streamlit_js_eval # For JS communication
|
11 |
|
12 |
# --- Constants ---
|
13 |
-
DB_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 |
-
#
|
22 |
-
|
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 |
-
|
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() #
|
52 |
|
53 |
-
# --- Helper Functions (Database Operations -
|
54 |
|
55 |
-
#
|
56 |
def load_world_state_from_db():
|
57 |
"""Loads all plot metadata and object data fresh from the SQLite DB."""
|
58 |
-
|
|
|
59 |
plots_metadata = []
|
60 |
-
all_objects_by_plot = {}
|
61 |
all_initial_objects_world = []
|
62 |
|
63 |
try:
|
64 |
-
|
|
|
65 |
conn.row_factory = sqlite3.Row
|
66 |
cursor = conn.cursor()
|
67 |
|
68 |
-
|
69 |
-
cursor.execute("SELECT grid_x, grid_z, name
|
70 |
plot_rows = cursor.fetchall()
|
71 |
-
st.write(f"DB Read
|
|
|
|
|
72 |
|
|
|
73 |
for row in plot_rows:
|
74 |
gx, gz = row['grid_x'], row['grid_z']
|
75 |
-
|
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 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
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
|
88 |
|
89 |
-
#
|
|
|
90 |
for row in object_rows:
|
91 |
plot_key = (row['plot_grid_x'], row['plot_grid_z'])
|
92 |
-
|
93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
106 |
|
107 |
except sqlite3.Error as e:
|
108 |
-
st.
|
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 |
-
|
|
|
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
|
|
|
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
|
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
|
136 |
''', (target_grid_x, target_grid_z, plot_name))
|
137 |
-
st.write(f"
|
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"
|
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)
|
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
|
|
|
174 |
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
175 |
-
''', objects_to_insert) # Use INSERT OR REPLACE
|
176 |
-
st.write(f"
|
177 |
else:
|
178 |
-
st.write(f"
|
179 |
|
180 |
-
#
|
181 |
|
182 |
-
st.success(f"
|
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.
|
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
|
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 (
|
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
|
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
|
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 /
|
223 |
-
target_z =
|
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
|
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 |
-
|
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
|
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.
|
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 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
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
|
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
|
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)};
|
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:", {{ /*
|
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 |
-
|
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}")
|
|
|
|
|
|