awacke1 commited on
Commit
55e0bb5
·
verified ·
1 Parent(s): d9c5a9e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +93 -400
app.py CHANGED
@@ -1,440 +1,133 @@
1
  # app.py
2
  import streamlit as st
3
-
4
- # --- Must be the first Streamlit command ---
5
  st.set_page_config(page_title="Infinite World Builder", layout="wide")
6
 
7
  import streamlit.components.v1 as components
8
- import os
9
- import json
10
- import pandas as pd
11
- import uuid
12
- import math
13
- import time
14
  from datetime import datetime
15
 
16
- # Import our GameState class from gamestate.py
17
- from gamestate import GameState
18
-
19
- # --- Constants ---
20
- SAVE_DIR = "saved_worlds"
21
- GLOBAL_SAVES_DIR = "global_saves" # Folder for global game saves
22
- PLOT_WIDTH = 50.0 # Width of each plot in 3D space
23
- PLOT_DEPTH = 50.0 # Depth of each plot
24
- CSV_COLUMNS = ['obj_id', 'type', 'pos_x', 'pos_y', 'pos_z', 'rot_x', 'rot_y', 'rot_z', 'rot_order']
25
-
26
- # --- Ensure Required Directories Exist ---
27
- os.makedirs(SAVE_DIR, exist_ok=True)
28
- os.makedirs(GLOBAL_SAVES_DIR, exist_ok=True)
29
-
30
- # --- Helper Functions for Global Save Management ---
31
-
32
- def load_latest_global_save():
33
- """
34
- Load the JSON file with the most recent timestamp from GLOBAL_SAVES_DIR.
35
- Returns None if no global save exists.
36
- """
37
- json_files = [f for f in os.listdir(GLOBAL_SAVES_DIR) if f.endswith(".json")]
38
- if not json_files:
39
- return None
40
- json_files.sort(reverse=True)
41
- latest_file = json_files[0]
42
- with open(os.path.join(GLOBAL_SAVES_DIR, latest_file), "r", encoding="utf-8") as f:
43
- return json.load(f)
44
-
45
- def consolidate_global_saves():
46
- """
47
- Merge all JSON files in GLOBAL_SAVES_DIR into one consolidated file and delete older ones.
48
- The union of game objects (using unique 'obj_id's) is computed.
49
- Returns the consolidated state.
50
- """
51
- json_files = [f for f in os.listdir(GLOBAL_SAVES_DIR) if f.endswith(".json")]
52
- if not json_files:
53
- return None
54
- merged_state = {"timestamp": "", "game_state": [], "player_position": {"x": 0, "y": 0, "z": 0}}
55
- obj_dict = {}
56
- latest_timestamp = None
57
- latest_player_position = {"x": 0, "y": 0, "z": 0}
58
- for f in json_files:
59
- with open(os.path.join(GLOBAL_SAVES_DIR, f), "r", encoding="utf-8") as file:
60
- data = json.load(file)
61
- for obj in data.get("game_state", []):
62
- obj_id = obj.get("obj_id")
63
- if obj_id:
64
- obj_dict[obj_id] = obj
65
- file_timestamp = data.get("timestamp")
66
- if not latest_timestamp or file_timestamp > latest_timestamp:
67
- latest_timestamp = file_timestamp
68
- latest_player_position = data.get("player_position", {"x": 0, "y": 0, "z": 0})
69
- merged_state["timestamp"] = latest_timestamp if latest_timestamp else time.strftime("%Y-%m-%d %H:%M:%S")
70
- merged_state["game_state"] = list(obj_dict.values())
71
- merged_state["player_position"] = latest_player_position
72
- consolidated_filename = f"consolidated_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
73
- consolidated_path = os.path.join(GLOBAL_SAVES_DIR, consolidated_filename)
74
- with open(consolidated_path, "w", encoding="utf-8") as f:
75
- json.dump(merged_state, f, indent=2)
76
- # Delete all old JSON files except the newly consolidated one.
77
- for f in json_files:
78
- try:
79
- os.remove(os.path.join(GLOBAL_SAVES_DIR, f))
80
- except Exception as e:
81
- st.error(f"Error deleting old global save {f}: {e}")
82
- return merged_state
83
 
84
- def perform_global_save():
85
- """
86
- Immediately save the current game state (with player position) to a new JSON file,
87
- then consolidate all global saves and update session state.
88
- """
89
- global_save_data = {
90
- "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
91
- "game_state": game_state.get_state(),
92
- "player_position": st.session_state.get("player_position", {"x": 0, "y": 0, "z": 0})
93
  }
94
- default_save_name = f"save_{time.strftime('%Y%m%d_%H%M%S')}.json"
95
- save_file_path = os.path.join(GLOBAL_SAVES_DIR, default_save_name)
96
- with open(save_file_path, "w", encoding="utf-8") as f:
97
- json.dump(global_save_data, f, indent=2)
98
- # Consolidate all global saves into one file and update session state.
99
- consolidated_state = consolidate_global_saves()
100
- if consolidated_state is not None:
101
- st.session_state.loaded_global_state = consolidated_state
102
 
103
- def reload_global_state():
104
- """
105
- Reload all global save files, consolidate them, and update the game state.
106
- """
107
- consolidated_state = consolidate_global_saves()
108
- if consolidated_state is not None:
109
- with game_state.lock:
110
- game_state.world_state = consolidated_state.get("game_state", [])
111
- st.session_state.player_position = consolidated_state.get("player_position", {"x": 0, "y": 0, "z": 0})
112
- st.session_state.loaded_global_state = consolidated_state
113
- st.success("Global state reloaded!")
114
- else:
115
- st.warning("No global save found to reload.")
116
-
117
- @st.cache_data(ttl=3600)
118
- def load_plot_metadata():
119
- """Scans SAVE_DIR for plot files and returns metadata."""
120
- plots = []
121
  try:
122
- plot_files = [f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")]
123
- except FileNotFoundError:
124
- st.error(f"Save directory '{SAVE_DIR}' not found.")
125
- return []
126
  except Exception as e:
127
- st.error(f"Error listing save directory '{SAVE_DIR}': {e}")
128
- return []
129
 
130
- parsed_plots = []
131
- for filename in plot_files:
132
- try:
133
- parts = filename[:-4].split('_')
134
- grid_x = int(parts[1][1:])
135
- grid_z = int(parts[2][1:])
136
- plot_name = " ".join(parts[3:]) if len(parts) > 3 else f"Plot ({grid_x},{grid_z})"
137
- parsed_plots.append({
138
- 'id': filename[:-4],
139
- 'filename': filename,
140
- 'grid_x': grid_x,
141
- 'grid_z': grid_z,
142
- 'name': plot_name,
143
- 'x_offset': grid_x * PLOT_WIDTH,
144
- 'z_offset': grid_z * PLOT_DEPTH
145
- })
146
- except (IndexError, ValueError):
147
- st.warning(f"Could not parse grid coordinates from filename: {filename}. Skipping.")
148
- continue
149
-
150
- parsed_plots.sort(key=lambda p: (p['grid_x'], p['grid_z']))
151
- return parsed_plots
152
-
153
- def load_plot_objects(filename, x_offset, z_offset):
154
- """Loads objects from a CSV file and applies world offsets."""
155
- file_path = os.path.join(SAVE_DIR, filename)
156
- objects = []
157
  try:
158
- df = pd.read_csv(file_path)
159
- if not all(col in df.columns for col in ['type', 'pos_x', 'pos_y', 'pos_z']):
160
- st.warning(f"CSV '{filename}' missing essential columns. Skipping.")
161
- return []
162
- df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in range(len(df))]))
163
- for col, default in [('rot_x', 0.0), ('rot_y', 0.0), ('rot_z', 0.0), ('rot_order', 'XYZ')]:
164
- if col not in df.columns:
165
- df[col] = default
166
-
167
- for _, row in df.iterrows():
168
- obj_data = row.to_dict()
169
- obj_data['pos_x'] += x_offset
170
- obj_data['pos_z'] += z_offset
171
- objects.append(obj_data)
172
- return objects
173
- except FileNotFoundError:
174
- st.error(f"File not found during object load: {filename}")
175
- return []
176
- except pd.errors.EmptyDataError:
177
- return []
178
  except Exception as e:
179
- st.error(f"Error loading objects from {filename}: {e}")
180
- return []
181
 
182
- def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
183
- """Saves object data list to a CSV file, making positions relative to the plot origin."""
184
- file_path = os.path.join(SAVE_DIR, filename)
185
- relative_objects = []
186
- if not isinstance(objects_data_list, list):
187
- st.error("Invalid data format received for saving (expected a list).")
188
- return False
189
 
190
- for obj in objects_data_list:
191
- pos = obj.get('position', {})
192
- rot = obj.get('rotation', {})
193
- obj_type = obj.get('type', 'Unknown')
194
- obj_id = obj.get('obj_id', str(uuid.uuid4()))
195
-
196
- if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown':
197
- print(f"Skipping malformed object during save prep: {obj}")
198
- continue
199
-
200
- relative_obj = {
201
- 'obj_id': obj_id,
202
- 'type': obj_type,
203
- 'pos_x': pos.get('x', 0.0) - plot_x_offset,
204
- 'pos_y': pos.get('y', 0.0),
205
- 'pos_z': pos.get('z', 0.0) - plot_z_offset,
206
- 'rot_x': rot.get('_x', 0.0),
207
- 'rot_y': rot.get('_y', 0.0),
208
- 'rot_z': rot.get('_z', 0.0),
209
- 'rot_order': rot.get('_order', 'XYZ')
210
- }
211
- relative_objects.append(relative_obj)
212
-
213
- try:
214
- df = pd.DataFrame(relative_objects, columns=CSV_COLUMNS)
215
- df.to_csv(file_path, index=False)
216
- st.success(f"Saved {len(relative_objects)} objects to {filename}")
217
- return True
218
- except Exception as e:
219
- st.error(f"Failed to save plot data to {filename}: {e}")
220
- return False
221
-
222
- # --- Initialize GameState Singleton ---
223
- @st.cache_resource
224
- def get_game_state():
225
- return GameState(save_dir=SAVE_DIR, csv_filename="world_state.csv")
226
-
227
- game_state = get_game_state()
228
-
229
- # --- Session State Initialization ---
230
- if 'selected_object' not in st.session_state:
231
- st.session_state.selected_object = 'None'
232
- if 'new_plot_name' not in st.session_state:
233
- st.session_state.new_plot_name = ""
234
- if 'js_save_data_result' not in st.session_state:
235
- st.session_state.js_save_data_result = None
236
- if 'player_position' not in st.session_state:
237
- st.session_state.player_position = {"x": 0, "y": 0, "z": 0}
238
- if 'loaded_global_state' not in st.session_state:
239
- st.session_state.loaded_global_state = None
240
-
241
- # --- On Client Start: Load Latest Global Save if Available ---
242
- latest_global_save = load_latest_global_save()
243
- if latest_global_save is not None:
244
- with game_state.lock:
245
- game_state.world_state = latest_global_save.get("game_state", [])
246
- st.session_state.player_position = latest_global_save.get("player_position", {"x": 0, "y": 0, "z": 0})
247
- st.session_state.loaded_global_state = latest_global_save
248
-
249
- plots_metadata = load_plot_metadata()
250
- all_initial_objects = []
251
- for plot in plots_metadata:
252
- all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset'], plot['z_offset']))
253
-
254
- # If GameState is empty, update it with initial objects.
255
- if not game_state.get_state():
256
- game_state.update_state(all_initial_objects)
257
-
258
- # --- Sidebar ---
259
  with st.sidebar:
260
  st.title("🏗️ World Controls")
261
- st.header("Navigation (Plots)")
262
- st.caption("Click to teleport player to a plot.")
263
- max_cols = 2
264
- cols = st.columns(max_cols)
265
- col_idx = 0
266
- sorted_plots_for_nav = sorted(plots_metadata, key=lambda p: (p['grid_x'], p['grid_z']))
267
- for plot in sorted_plots_for_nav:
268
- button_label = f"➡️ {plot.get('name', plot['id'])} ({plot['grid_x']},{plot['grid_z']})"
269
- if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
270
- target_x = plot['x_offset']
271
- target_z = plot['z_offset']
272
- try:
273
- from streamlit_js_eval import streamlit_js_eval
274
- js_code = f"teleportPlayer({target_x + PLOT_WIDTH/2}, {target_z + PLOT_DEPTH/2});"
275
- streamlit_js_eval(js_code=js_code, key=f"teleport_{plot['id']}")
276
- except Exception as e:
277
- st.error(f"Failed to send teleport command: {e}")
278
- col_idx = (col_idx + 1) % max_cols
279
-
280
- st.markdown("---")
281
- st.header("Place Objects")
282
- object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
283
- current_object_index = object_types.index(st.session_state.selected_object) if st.session_state.selected_object in object_types else 0
284
- selected_object_type_widget = st.selectbox("Select Object:", options=object_types, index=current_object_index, key="selected_object_widget")
285
- if selected_object_type_widget != st.session_state.selected_object:
286
- st.session_state.selected_object = selected_object_type_widget
287
-
288
- st.markdown("---")
289
- st.header("Save Work (Per Plot)")
290
- st.caption("Saves newly placed objects to the current plot.")
291
- if st.button("💾 Save Current Work", key="save_button"):
292
- from streamlit_js_eval import streamlit_js_eval
293
- js_get_data_code = "getSaveDataAndPosition();"
294
- streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor")
295
- st.rerun()
296
-
297
- st.markdown("---")
298
- st.header("Global Save & Load")
299
- if st.button("💾 Global Save"):
300
- global_save_data = {
301
- "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
302
- "game_state": game_state.get_state(),
303
- "player_position": st.session_state.get("player_position", {"x": 0, "y": 0, "z": 0})
304
- }
305
- default_save_name = f"save_{time.strftime('%Y%m%d_%H%M%S')}.json"
306
- save_file_path = os.path.join(GLOBAL_SAVES_DIR, default_save_name)
307
- with open(save_file_path, "w", encoding="utf-8") as f:
308
- json.dump(global_save_data, f, indent=2)
309
- st.success(f"Global state saved to {default_save_name}")
310
- st.session_state.loaded_global_state = global_save_data
311
- perform_global_save()
312
-
313
  if st.button("🔄 Reload Global State"):
314
- reload_global_state()
315
-
316
- st.subheader("📂 Global Saves")
317
- save_files = sorted([f for f in os.listdir(GLOBAL_SAVES_DIR) if f.endswith(".json")])
318
- for file in save_files:
319
- if st.button(f"Load {file}", key=file):
320
- file_path = os.path.join(GLOBAL_SAVES_DIR, file)
321
- with open(file_path, "r", encoding="utf-8") as f:
322
- loaded_save = json.load(f)
323
- with game_state.lock:
324
- game_state.world_state = loaded_save.get("game_state", [])
325
- st.session_state.player_position = loaded_save.get("player_position", {"x": 0, "y": 0, "z": 0})
326
- st.session_state.loaded_global_state = loaded_save
327
- st.success(f"Global state loaded from {file}")
328
 
329
  st.markdown("---")
330
  st.header("Download Global Save as Markdown")
331
- current_save = st.session_state.get("loaded_global_state")
332
- if current_save is None:
333
- current_save = {"timestamp": "N/A", "game_state": [], "player_position": {"x": 0, "y": 0, "z": 0}}
334
- default_md_name = current_save.get("timestamp", "save").replace(":", "").replace(" ", "_") + ".md"
335
  download_name = st.text_input("Override File Name", value=default_md_name)
336
- if st.button("Generate Markdown & Download"):
337
- md_outline = f"""# Global Save: {download_name}
338
- - **Timestamp:** {current_save.get("timestamp", "N/A")}
339
- - 🎮 **Number of Game Objects:** {len(current_save.get("game_state", []))}
340
- - 🧭 **Player Position:** {current_save.get("player_position", {"x": 0, "y": 0, "z": 0})}
341
 
342
  ## Game Objects:
343
  """
344
- for i, obj in enumerate(current_save.get("game_state", []), start=1):
345
- obj_type = obj.get("type", "Unknown")
346
- pos = (obj.get("pos_x", 0), obj.get("pos_y", 0), obj.get("pos_z", 0))
347
- md_outline += f"- {i}. ✨ **{obj_type}** at {pos}\n"
348
- st.download_button("Download Markdown Save", data=md_outline, file_name=download_name, mime="text/markdown")
349
-
350
- # --- Process Save Data from JS (Per Plot Save) ---
351
- save_data_from_js = st.session_state.get("js_save_processor", None)
352
- if save_data_from_js is not None:
353
- st.info("Received save data from client...")
354
- save_processed_successfully = False
355
  try:
356
  payload = json.loads(save_data_from_js) if isinstance(save_data_from_js, str) else save_data_from_js
357
- if isinstance(payload, dict) and 'playerPosition' in payload and 'objectsToSave' in payload:
358
- player_pos = payload['playerPosition']
359
- objects_to_save = payload['objectsToSave']
360
- if isinstance(objects_to_save, list):
361
- target_grid_x = math.floor(player_pos.get('x', 0.0) / PLOT_WIDTH)
362
- target_grid_z = math.floor(player_pos.get('z', 0.0) / PLOT_DEPTH)
363
- target_filename = f"plot_X{target_grid_x}_Z{target_grid_z}.csv"
364
- target_plot_x_offset = target_grid_x * PLOT_WIDTH
365
- target_plot_z_offset = target_grid_z * PLOT_DEPTH
366
- st.write(f"Attempting to save plot: {target_filename} (Player at: x={player_pos.get('x', 0):.1f}, z={player_pos.get('z', 0):.1f})")
367
- is_new_plot_file = not os.path.exists(os.path.join(SAVE_DIR, target_filename))
368
- save_ok = save_plot_data(target_filename, objects_to_save, target_plot_x_offset, target_plot_z_offset)
369
- if save_ok:
370
- load_plot_metadata.clear() # Clear cache so metadata reloads
371
- try:
372
- from streamlit_js_eval import streamlit_js_eval
373
- streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js_state")
374
- except Exception as js_e:
375
- st.warning(f"Could not reset JS state after save: {js_e}")
376
- if is_new_plot_file:
377
- st.success(f"New plot created and saved: {target_filename}")
378
- else:
379
- st.success(f"Updated existing plot: {target_filename}")
380
- game_state.update_state(objects_to_save)
381
- save_processed_successfully = True
382
- perform_global_save()
383
- else:
384
- st.error(f"Failed to save plot data to file: {target_filename}")
385
- else:
386
- st.error("Invalid 'objectsToSave' format received (expected list).")
387
- else:
388
- st.error("Invalid save payload structure received from client.")
389
- except json.JSONDecodeError:
390
- st.error("Failed to decode save data from client.")
391
  except Exception as e:
392
- st.error(f"Error processing save: {e}")
393
- st.session_state.js_save_processor = None
394
- if save_processed_successfully:
395
- st.rerun()
396
-
397
- # --- Main Area ---
398
- st.header("Infinite Shared 3D World")
399
- st.caption("Move to empty areas to expand the world. Use the sidebar 'Save' controls to store your work.")
400
 
401
- # Inject state into JS—including the shared GAME_STATE from our GameState singleton.
402
  injected_state = {
403
- "ALL_INITIAL_OBJECTS": all_initial_objects,
404
- "PLOTS_METADATA": plots_metadata,
405
- "SELECTED_OBJECT_TYPE": st.session_state.selected_object,
406
- "PLOT_WIDTH": PLOT_WIDTH,
407
- "PLOT_DEPTH": PLOT_DEPTH,
408
- "GAME_STATE": game_state.get_state()
409
  }
410
-
411
- html_file_path = 'index.html'
412
- html_content_with_state = None
413
-
414
  try:
415
- with open(html_file_path, 'r', encoding='utf-8') as f:
416
  html_template = f.read()
417
  js_injection_script = f"""
418
  <script>
419
- window.ALL_INITIAL_OBJECTS = {json.dumps(injected_state["ALL_INITIAL_OBJECTS"])};
420
- window.PLOTS_METADATA = {json.dumps(injected_state["PLOTS_METADATA"])};
421
- window.SELECTED_OBJECT_TYPE = {json.dumps(injected_state["SELECTED_OBJECT_TYPE"])};
422
- window.PLOT_WIDTH = {json.dumps(injected_state["PLOT_WIDTH"])};
423
- window.PLOT_DEPTH = {json.dumps(injected_state["PLOT_DEPTH"])};
424
- window.GAME_STATE = {json.dumps(injected_state["GAME_STATE"])};
425
- console.log("Streamlit State Injected:", {{
426
- selectedObject: window.SELECTED_OBJECT_TYPE,
427
- initialObjectsCount: window.ALL_INITIAL_OBJECTS ? window.ALL_INITIAL_OBJECTS.length : 0,
428
- plotCount: window.PLOTS_METADATA ? window.PLOTS_METADATA.length : 0,
429
- gameStateObjects: window.GAME_STATE ? window.GAME_STATE.length : 0
430
- }});
431
  </script>
432
  """
433
- html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
434
- components.html(html_content_with_state, height=750, scrolling=False)
435
- except FileNotFoundError:
436
- st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
437
- st.warning(f"Make sure `{html_file_path}` is in the same directory as `app.py` and `{SAVE_DIR}` exists.")
438
  except Exception as e:
439
- st.error(f"An critical error occurred during HTML preparation or component rendering: {e}")
440
- st.exception(e)
 
1
  # app.py
2
  import streamlit as st
3
+ # st.set_page_config MUST be the first Streamlit command
 
4
  st.set_page_config(page_title="Infinite World Builder", layout="wide")
5
 
6
  import streamlit.components.v1 as components
7
+ import os, json, time, uuid, math
 
 
 
 
 
8
  from datetime import datetime
9
 
10
+ # --- Global State File ---
11
+ GLOBAL_STATE_FILE = "global_state.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ # If the global state file does not exist, create it with an empty state.
14
+ if not os.path.exists(GLOBAL_STATE_FILE):
15
+ empty_state = {
16
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
17
+ "game_state": [], # List of object records
18
+ "player_position": {"x": 0, "y": 0, "z": 0}
 
 
 
19
  }
20
+ with open(GLOBAL_STATE_FILE, "w", encoding="utf-8") as f:
21
+ json.dump(empty_state, f, indent=2)
 
 
 
 
 
 
22
 
23
+ def load_global_state():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  try:
25
+ with open(GLOBAL_STATE_FILE, "r", encoding="utf-8") as f:
26
+ return json.load(f)
 
 
27
  except Exception as e:
28
+ st.error(f"Error loading global state: {e}")
29
+ return {"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), "game_state": [], "player_position": {"x":0, "y":0, "z":0}}
30
 
31
+ def save_global_state(state):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  try:
33
+ with open(GLOBAL_STATE_FILE, "w", encoding="utf-8") as f:
34
+ json.dump(state, f, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  except Exception as e:
36
+ st.error(f"Error saving global state: {e}")
 
37
 
38
+ # --- Initialize global state in session_state ---
39
+ if "global_state" not in st.session_state:
40
+ st.session_state["global_state"] = load_global_state()
 
 
 
 
41
 
42
+ # --- Utility: Update Global State with New Objects ---
43
+ def add_objects_to_global(new_objects, player_position):
44
+ """
45
+ Merge new object records into the global state.
46
+ Each object is expected to have a unique 'obj_id'. If not, one is generated.
47
+ """
48
+ state = st.session_state["global_state"]
49
+ # Create dictionary of existing objects by obj_id
50
+ existing = {obj["obj_id"]: obj for obj in state.get("game_state", []) if "obj_id" in obj}
51
+ for obj in new_objects:
52
+ obj_id = obj.get("obj_id", str(uuid.uuid4()))
53
+ obj["obj_id"] = obj_id
54
+ existing[obj_id] = obj
55
+ state["game_state"] = list(existing.values())
56
+ # Update player position and timestamp (for simplicity, overwrite with latest)
57
+ state["player_position"] = player_position
58
+ state["timestamp"] = time.strftime("%Y-%m-%d %H:%M:%S")
59
+ st.session_state["global_state"] = state
60
+ save_global_state(state)
61
+
62
+ # --- Sidebar Controls ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  with st.sidebar:
64
  st.title("🏗️ World Controls")
65
+ # Button to force reload global state from file.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  if st.button("🔄 Reload Global State"):
67
+ st.session_state["global_state"] = load_global_state()
68
+ st.success("Global state reloaded.")
69
+ # Button to clear the global state.
70
+ if st.button("🗑️ Clear Global State"):
71
+ empty = {
72
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
73
+ "game_state": [],
74
+ "player_position": {"x": 0, "y": 0, "z": 0}
75
+ }
76
+ st.session_state["global_state"] = empty
77
+ save_global_state(empty)
78
+ st.success("Global state cleared.")
 
 
79
 
80
  st.markdown("---")
81
  st.header("Download Global Save as Markdown")
82
+ state = st.session_state["global_state"]
83
+ default_md_name = state.get("timestamp", "save").replace(":", "").replace(" ", "_") + ".md"
 
 
84
  download_name = st.text_input("Override File Name", value=default_md_name)
85
+ md_outline = f"""# Global Save: {download_name}
86
+ - **Timestamp:** {state.get("timestamp", "N/A")}
87
+ - 🎮 **Number of Game Objects:** {len(state.get("game_state", []))}
88
+ - 🧭 **Player Position:** {state.get("player_position", {"x":0, "y":0, "z":0})}
 
89
 
90
  ## Game Objects:
91
  """
92
+ for i, obj in enumerate(state.get("game_state", []), start=1):
93
+ obj_type = obj.get("type", "Unknown")
94
+ pos = (obj.get("x", 0), obj.get("y", 0), obj.get("z", 0))
95
+ md_outline += f"- {i}. ✨ **{obj_type}** at {pos}\n"
96
+ st.download_button("Download Markdown Save", data=md_outline, file_name=download_name, mime="text/markdown")
97
+
98
+ # --- Process Save Data from JavaScript ---
99
+ # The JS component will set st.session_state["js_save_data"] to a JSON string when an object is placed.
100
+ save_data_from_js = st.session_state.get("js_save_data", None)
101
+ if save_data_from_js:
 
102
  try:
103
  payload = json.loads(save_data_from_js) if isinstance(save_data_from_js, str) else save_data_from_js
104
+ if isinstance(payload, dict) and "playerPosition" in payload and "objectsToSave" in payload:
105
+ player_pos = payload["playerPosition"]
106
+ objects = payload["objectsToSave"]
107
+ add_objects_to_global(objects, player_pos)
108
+ st.success("Global state updated with new objects.")
109
+ st.session_state["js_save_data"] = None
110
+ st.experimental_rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  except Exception as e:
112
+ st.error(f"Error processing JS save data: {e}")
113
+ st.session_state["js_save_data"] = None
 
 
 
 
 
 
114
 
115
+ # --- Inject Global State into JavaScript ---
116
  injected_state = {
117
+ "GLOBAL_STATE": st.session_state["global_state"]
 
 
 
 
 
118
  }
119
+ html_file_path = "index.html"
 
 
 
120
  try:
121
+ with open(html_file_path, "r", encoding="utf-8") as f:
122
  html_template = f.read()
123
  js_injection_script = f"""
124
  <script>
125
+ // Inject the global state from Streamlit into the web app.
126
+ window.GLOBAL_STATE = {json.dumps(injected_state["GLOBAL_STATE"])};
127
+ console.log("Injected Global State:", window.GLOBAL_STATE);
 
 
 
 
 
 
 
 
 
128
  </script>
129
  """
130
+ html_content = html_template.replace("</head>", js_injection_script + "\n</head>", 1)
131
+ components.html(html_content, height=750, scrolling=True)
 
 
 
132
  except Exception as e:
133
+ st.error(f"Error loading index.html: {e}")