awacke1 commited on
Commit
c6b8a39
·
verified ·
1 Parent(s): ad3f401

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +232 -94
app.py CHANGED
@@ -1,133 +1,271 @@
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}")
 
1
  # app.py
2
  import streamlit as st
3
+ # st.set_page_config MUST be the very 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
8
+ import json
9
+ import pandas as pd
10
+ import uuid
11
+ import math
12
+ import time
13
+
14
+ from streamlit_js_eval import streamlit_js_eval # For JS communication
15
+
16
+ # Import the GameState class for global state sharing.
17
+ from gamestate import GameState
18
+
19
+ # --- Constants ---
20
+ SAVE_DIR = "saved_worlds"
21
+ PLOT_WIDTH = 50.0 # Width of each plot in 3D space
22
+ PLOT_DEPTH = 50.0 # Depth of each plot (can be same as width)
23
+ CSV_COLUMNS = ['obj_id', 'type', 'pos_x', 'pos_y', 'pos_z', 'rot_x', 'rot_y', 'rot_z', 'rot_order']
24
+
25
+ # --- Ensure Save Directory Exists ---
26
+ os.makedirs(SAVE_DIR, exist_ok=True)
27
+
28
+ # --- Helper Functions (unchanged from your original code) ---
29
+
30
+ @st.cache_data(ttl=3600)
31
+ def load_plot_metadata():
32
+ """Scans SAVE_DIR for plot_X*.csv files, extracts grid coordinates and metadata."""
33
+ plots = []
34
  try:
35
+ plot_files = [f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")]
36
+ except FileNotFoundError:
37
+ st.error(f"Save directory '{SAVE_DIR}' not found.")
38
+ return []
39
  except Exception as e:
40
+ st.error(f"Error listing save directory '{SAVE_DIR}': {e}")
41
+ return []
42
+
43
+ parsed_plots = []
44
+ for filename in plot_files:
45
+ try:
46
+ parts = filename[:-4].split('_') # Remove .csv
47
+ grid_x = int(parts[1][1:]) # Extract after "X"
48
+ grid_z = int(parts[2][1:]) # Extract after "Z"
49
+ plot_name = " ".join(parts[3:]) if len(parts) > 3 else f"Plot ({grid_x},{grid_z})"
50
+ parsed_plots.append({
51
+ 'id': filename[:-4],
52
+ 'filename': filename,
53
+ 'grid_x': grid_x,
54
+ 'grid_z': grid_z,
55
+ 'name': plot_name,
56
+ 'x_offset': grid_x * PLOT_WIDTH,
57
+ 'z_offset': grid_z * PLOT_DEPTH
58
+ })
59
+ except (IndexError, ValueError):
60
+ st.warning(f"Could not parse grid coordinates from filename: {filename}. Skipping.")
61
+ continue
62
+
63
+ parsed_plots.sort(key=lambda p: (p['grid_x'], p['grid_z']))
64
+ return parsed_plots
65
 
66
+ def load_plot_objects(filename, x_offset, z_offset):
67
+ """Loads objects from a CSV file and applies 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
+ st.warning(f"CSV '{filename}' missing essential columns. Skipping.")
74
+ return []
75
+ # Ensure an obj_id exists for each row.
76
+ df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in range(len(df))]))
77
+ for col, default in [('rot_x', 0.0), ('rot_y', 0.0), ('rot_z', 0.0), ('rot_order', 'XYZ')]:
78
+ if col not in df.columns:
79
+ df[col] = default
80
+ for _, row in df.iterrows():
81
+ obj_data = row.to_dict()
82
+ # Apply plot offsets to positions.
83
+ obj_data['pos_x'] += x_offset
84
+ obj_data['pos_z'] += z_offset
85
+ objects.append(obj_data)
86
+ return objects
87
+ except FileNotFoundError:
88
+ st.error(f"File not found during object load: {filename}")
89
+ return []
90
+ except pd.errors.EmptyDataError:
91
+ return []
92
  except Exception as e:
93
+ st.error(f"Error loading objects from {filename}: {e}")
94
+ return []
95
+
96
+ def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
97
+ """Saves object data list to a CSV file, making positions relative to the plot origin."""
98
+ file_path = os.path.join(SAVE_DIR, filename)
99
+ relative_objects = []
100
+ if not isinstance(objects_data_list, list):
101
+ st.error("Invalid data format received for saving (expected a list).")
102
+ return False
103
+
104
+ for obj in objects_data_list:
105
+ pos = obj.get('position', {})
106
+ rot = obj.get('rotation', {})
107
+ obj_type = obj.get('type', 'Unknown')
108
+ obj_id = obj.get('obj_id', str(uuid.uuid4()))
109
+ if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown':
110
+ print(f"Skipping malformed object during save prep: {obj}")
111
+ continue
112
+ relative_obj = {
113
+ 'obj_id': obj_id, 'type': obj_type,
114
+ 'pos_x': pos.get('x', 0.0) - plot_x_offset,
115
+ 'pos_y': pos.get('y', 0.0),
116
+ 'pos_z': pos.get('z', 0.0) - plot_z_offset,
117
+ 'rot_x': rot.get('_x', 0.0), 'rot_y': rot.get('_y', 0.0),
118
+ 'rot_z': rot.get('_z', 0.0), 'rot_order': rot.get('_order', 'XYZ')
119
+ }
120
+ relative_objects.append(relative_obj)
121
+
122
+ try:
123
+ df = pd.DataFrame(relative_objects, columns=CSV_COLUMNS)
124
+ df.to_csv(file_path, index=False)
125
+ st.success(f"Saved {len(relative_objects)} objects to {filename}")
126
+ return True
127
+ except Exception as e:
128
+ st.error(f"Failed to save plot data to {filename}: {e}")
129
+ return False
130
+
131
+ # --- Global State Management ---
132
+
133
+ # Create a singleton GameState instance using st.cache_resource.
134
+ @st.cache_resource
135
+ def get_game_state():
136
+ return GameState(state_file="global_state.json")
137
+
138
+ game_state = get_game_state()
139
+
140
+ # --- Page Config and Session State Initialization ---
141
+ # (st.set_page_config already called at top.)
142
+ if 'selected_object' not in st.session_state:
143
+ st.session_state.selected_object = 'None'
144
+ if 'js_save_data' not in st.session_state:
145
+ st.session_state.js_save_data = None
146
+
147
+ # --- Load Plot Metadata and Initial Objects ---
148
+ plots_metadata = load_plot_metadata()
149
+ all_initial_objects = []
150
+ for plot in plots_metadata:
151
+ all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset'], plot['z_offset']))
152
+
153
+ # Optionally, merge the objects from your CSVs into the global state if not already present.
154
+ if not game_state.get_state().get("objects"):
155
+ game_state.update_state(all_initial_objects)
156
 
157
  # --- Sidebar Controls ---
158
  with st.sidebar:
159
  st.title("🏗️ World Controls")
160
+ st.header("Navigation (Plots)")
161
+ st.caption("Click to teleport player to a plot.")
162
+ max_cols = 2
163
+ cols = st.columns(max_cols)
164
+ col_idx = 0
165
+ sorted_plots_for_nav = sorted(plots_metadata, key=lambda p: (p['grid_x'], p['grid_z']))
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']
170
+ target_z = plot['z_offset']
171
+ try:
172
+ js_code = f"teleportPlayer({target_x + PLOT_WIDTH/2}, {target_z + PLOT_DEPTH/2});"
173
+ streamlit_js_eval(js_code=js_code, key=f"teleport_{plot['id']}")
174
+ except Exception as e:
175
+ st.error(f"Failed to send teleport command: {e}")
176
+ col_idx = (col_idx + 1) % max_cols
177
+
178
+ st.markdown("---")
179
+ st.header("Place Objects")
180
+ object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
181
+ current_object_index = object_types.index(st.session_state.selected_object) if st.session_state.selected_object in object_types else 0
182
+ selected_object_type_widget = st.selectbox("Select Object:", options=object_types, index=current_object_index, key="selected_object_widget")
183
+ if selected_object_type_widget != st.session_state.selected_object:
184
+ st.session_state.selected_object = selected_object_type_widget
185
+
186
+ st.markdown("---")
187
+ st.header("Save Work")
188
+ st.caption("Saves newly placed objects to the current plot and updates the global state.")
189
+ if st.button("💾 Save Current Work", key="save_button"):
190
+ try:
191
+ # Trigger JS to get data (player position and new objects).
192
+ js_get_data_code = "getSaveDataAndPosition();"
193
+ streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor")
194
+ except Exception as e:
195
+ st.error(f"Error triggering JS save: {e}")
196
+ st.experimental_rerun()
197
 
198
  st.markdown("---")
199
+ st.header("Download Global State as Markdown")
200
+ global_state = game_state.get_state()
201
+ default_md_name = global_state.get("last_updated", "save").replace(":", "").replace(" ", "_") + ".md"
202
  download_name = st.text_input("Override File Name", value=default_md_name)
203
  md_outline = f"""# Global Save: {download_name}
204
+ - ⏰ **Timestamp:** {global_state.get("last_updated", "N/A")}
205
+ - 🎮 **Number of Game Objects:** {len(global_state.get("objects", []))}
206
+
 
207
  ## Game Objects:
208
  """
209
+ for i, obj in enumerate(global_state.get("objects", []), start=1):
210
+ obj_type = obj.get("type", "Unknown")
211
+ pos = (obj.get("x", 0), obj.get("y", 0), obj.get("z", 0))
212
+ md_outline += f"- {i}. ✨ **{obj_type}** at {pos}\n"
213
  st.download_button("Download Markdown Save", data=md_outline, file_name=download_name, mime="text/markdown")
214
 
215
+ # --- Process Save Data from JS ---
 
216
  save_data_from_js = st.session_state.get("js_save_data", None)
217
  if save_data_from_js:
218
  try:
219
+ payload = json.loads(save_data_from_js) if isinstance(save_data_from_js, str) else save_data_from_js
220
+ if isinstance(payload, dict) and "playerPosition" in payload and "objectsToSave" in payload:
221
+ player_pos = payload["playerPosition"]
222
+ new_objects = payload["objectsToSave"]
223
+ # Optionally, add additional info (like a timestamp) to each new object.
224
+ for obj in new_objects:
225
+ obj["timestamp"] = time.strftime("%Y-%m-%d %H:%M:%S")
226
+ game_state.update_state(new_objects)
227
+ st.success("Global state updated with new objects.")
228
+ st.session_state["js_save_data"] = None
229
+ st.experimental_rerun()
230
+ else:
231
+ st.error("Invalid payload received from client.")
232
  except Exception as e:
233
+ st.error(f"Error processing save data: {e}")
234
+ st.session_state["js_save_data"] = None
235
+
236
+ # --- Main Area ---
237
+ st.header("Infinite Shared 3D World")
238
+ st.caption("Move to empty areas to expand the world. Use the sidebar 'Save' controls to store your work.")
239
 
240
+ # --- Inject State into HTML ---
241
+ # We inject both the original objects (for initial 3D scene rendering) and the global state.
242
  injected_state = {
243
+ "ALL_INITIAL_OBJECTS": all_initial_objects,
244
+ "PLOTS_METADATA": plots_metadata,
245
+ "SELECTED_OBJECT_TYPE": st.session_state.selected_object,
246
+ "PLOT_WIDTH": PLOT_WIDTH,
247
+ "PLOT_DEPTH": PLOT_DEPTH,
248
+ "GLOBAL_STATE": game_state.get_state()
249
  }
250
+
251
  html_file_path = "index.html"
252
  try:
253
  with open(html_file_path, "r", encoding="utf-8") as f:
254
+ html_template = f.read()
255
  js_injection_script = f"""
256
  <script>
257
+ window.ALL_INITIAL_OBJECTS = {json.dumps(injected_state["ALL_INITIAL_OBJECTS"])};
258
+ window.PLOTS_METADATA = {json.dumps(injected_state["PLOTS_METADATA"])};
259
+ window.SELECTED_OBJECT_TYPE = {json.dumps(injected_state["SELECTED_OBJECT_TYPE"])};
260
+ window.PLOT_WIDTH = {json.dumps(injected_state["PLOT_WIDTH"])};
261
+ window.PLOT_DEPTH = {json.dumps(injected_state["PLOT_DEPTH"])};
262
  window.GLOBAL_STATE = {json.dumps(injected_state["GLOBAL_STATE"])};
263
  console.log("Injected Global State:", window.GLOBAL_STATE);
264
  </script>
265
  """
266
+ html_content_with_state = html_template.replace("</head>", js_injection_script + "\n</head>", 1)
267
+ components.html(html_content_with_state, height=750, scrolling=False)
268
+ except FileNotFoundError:
269
+ st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
270
  except Exception as e:
271
+ st.error(f"An error occurred during HTML component rendering: {e}")