awacke1 commited on
Commit
6e7b462
·
verified ·
1 Parent(s): 820a1ee

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +271 -0
app.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}")