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