mirror of
https://github.com/louis-e/arnis.git
synced 2026-02-19 15:34:37 -05:00
Compare commits
1 Commits
v2.5.0
...
parallel-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56fe1b9515 |
3129
Cargo.lock
generated
3129
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "arnis"
|
||||
version = "2.5.0"
|
||||
version = "2.4.1"
|
||||
edition = "2021"
|
||||
description = "Arnis - Generate real life cities in Minecraft"
|
||||
homepage = "https://github.com/louis-e/arnis"
|
||||
@@ -14,8 +14,8 @@ overflow-checks = true
|
||||
|
||||
[features]
|
||||
default = ["gui"]
|
||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "tauri-build", "bedrock"]
|
||||
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek", "rusty-leveldb"]
|
||||
gui = ["tauri", "tauri-plugin-log", "tauri-plugin-shell", "tokio", "rfd", "dirs", "tauri-build", "bedrock"]
|
||||
bedrock = ["bedrockrs_level", "bedrockrs_shared", "nbtx", "zip", "byteorder", "vek"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = {version = "2", optional = true}
|
||||
@@ -23,9 +23,9 @@ tauri-build = {version = "2", optional = true}
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
byteorder = { version = "1.5", optional = true }
|
||||
clap = { version = "4.5.53", features = ["derive", "env"] }
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
colored = "3.0.0"
|
||||
dirs = "6.0.0"
|
||||
dirs = {version = "6.0.0", optional = true }
|
||||
fastanvil = "0.32.0"
|
||||
fastnbt = "2.6.0"
|
||||
flate2 = "1.1"
|
||||
@@ -35,13 +35,12 @@ geo = "0.31.0"
|
||||
image = "0.25"
|
||||
indicatif = "0.17.11"
|
||||
itertools = "0.14.0"
|
||||
jsonwebtoken = "10.3.0"
|
||||
log = "0.4.27"
|
||||
once_cell = "1.21.3"
|
||||
rand = { version = "0.9.1", features = ["std", "std_rng"] }
|
||||
rand_chacha = "0.9"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3"
|
||||
rayon = "1.10.0"
|
||||
reqwest = { version = "0.13.1", features = ["blocking", "json", "query"] }
|
||||
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
|
||||
rfd = { version = "0.16.0", optional = true }
|
||||
semver = "1.0.27"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -55,10 +54,9 @@ bedrockrs_shared = { git = "https://github.com/bedrock-crustaceans/bedrock-rs",
|
||||
nbtx = { git = "https://github.com/bedrock-crustaceans/nbtx", optional = true }
|
||||
vek = { version = "0.17", optional = true }
|
||||
zip = { version = "0.6", default-features = false, features = ["deflate"], optional = true }
|
||||
rusty-leveldb = { version = "3", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.62.0", features = ["Win32_System_Console"] }
|
||||
windows = { version = "0.61.1", features = ["Win32_System_Console"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.23.0"
|
||||
|
||||
@@ -39,8 +39,6 @@ GUI Build: ```cargo run```<br>
|
||||
|
||||
After your pull request was merged, I will take care of regularly creating update releases which will include your changes.
|
||||
|
||||
If you are using Nix, you can run the program directly with `nix run github:louis-e/arnis -- --terrain --path=YOUR_PATH/.minecraft/saves/worldname --bbox="min_lat,min_lng,max_lat,max_lng"`
|
||||
|
||||
## :star: Star History
|
||||
|
||||
<a href="https://star-history.com/#louis-e/arnis&Date">
|
||||
|
||||
183
analyze_performance.py
Normal file
183
analyze_performance.py
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Analyze performance data from Windows Performance Monitor CSV exports."""
|
||||
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
def parse_pdh_csv(filepath):
|
||||
"""Parse a Windows Performance Monitor CSV file."""
|
||||
data = []
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8-sig', errors='replace') as f:
|
||||
reader = csv.reader(f)
|
||||
header = next(reader)
|
||||
|
||||
# Clean up column names - extract the metric name
|
||||
clean_cols = []
|
||||
for col in header:
|
||||
if 'Verfügbare MB' in col or 'Verf' in col:
|
||||
clean_cols.append('available_mb')
|
||||
elif 'Zugesicherte' in col:
|
||||
clean_cols.append('committed_pct')
|
||||
elif 'Bytes geschrieben' in col:
|
||||
clean_cols.append('disk_write_bytes_sec')
|
||||
elif 'Arbeitsseiten' in col and 'arnis-windows' not in col:
|
||||
clean_cols.append('working_set')
|
||||
elif 'Arbeitsseiten' in col and 'arnis-windows' in col:
|
||||
clean_cols.append('gui_working_set')
|
||||
elif 'Private Bytes' in col and 'arnis-windows' not in col:
|
||||
clean_cols.append('private_bytes')
|
||||
elif 'Private Bytes' in col and 'arnis-windows' in col:
|
||||
clean_cols.append('gui_private_bytes')
|
||||
elif 'Prozessorzeit' in col and 'arnis-windows' not in col and 'Prozessorinformationen' not in col:
|
||||
clean_cols.append('cpu_pct')
|
||||
elif 'Prozessorzeit' in col and 'arnis-windows' in col:
|
||||
clean_cols.append('gui_cpu_pct')
|
||||
elif 'Threadanzahl' in col and 'arnis-windows' not in col:
|
||||
clean_cols.append('thread_count')
|
||||
elif 'Threadanzahl' in col and 'arnis-windows' in col:
|
||||
clean_cols.append('gui_thread_count')
|
||||
elif 'PDH-CSV' in col:
|
||||
clean_cols.append('timestamp')
|
||||
else:
|
||||
clean_cols.append(col[:30]) # truncate long names
|
||||
|
||||
for row in reader:
|
||||
if not row or not row[0].strip():
|
||||
continue
|
||||
entry = {}
|
||||
for i, val in enumerate(row):
|
||||
if i >= len(clean_cols):
|
||||
break
|
||||
col_name = clean_cols[i]
|
||||
if col_name == 'timestamp':
|
||||
try:
|
||||
entry[col_name] = datetime.strptime(val.strip(), '%m/%d/%Y %H:%M:%S.%f')
|
||||
except:
|
||||
entry[col_name] = val
|
||||
elif val.strip() == '' or val.strip() == ' ':
|
||||
entry[col_name] = None
|
||||
else:
|
||||
try:
|
||||
entry[col_name] = float(val)
|
||||
except:
|
||||
entry[col_name] = val
|
||||
data.append(entry)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def analyze_run(data, name):
|
||||
"""Analyze a single run's data."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {name}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Time range
|
||||
timestamps = [d.get('timestamp') for d in data if isinstance(d.get('timestamp'), datetime)]
|
||||
if timestamps:
|
||||
duration = (timestamps[-1] - timestamps[0]).total_seconds()
|
||||
print(f"Duration: {duration:.1f}s ({duration/60:.1f} min)")
|
||||
|
||||
# Memory usage (working set) - prefer 'working_set' (arnis backend) over gui_working_set
|
||||
working_sets = [d.get('working_set') for d in data if d.get('working_set') is not None]
|
||||
gui_ws = [d.get('gui_working_set') for d in data if d.get('gui_working_set') is not None]
|
||||
|
||||
# Use GUI working set if backend working set not available (before scenario)
|
||||
if working_sets:
|
||||
max_ws = max(working_sets) / (1024**3) # GB
|
||||
avg_ws = sum(working_sets) / len(working_sets) / (1024**3)
|
||||
print(f"Backend Working Set: max={max_ws:.2f} GB, avg={avg_ws:.2f} GB")
|
||||
|
||||
if gui_ws:
|
||||
max_gui_ws = max(gui_ws) / (1024**3)
|
||||
print(f"GUI Working Set: max={max_gui_ws:.2f} GB")
|
||||
# For before, we only have GUI data, so use that as the main metric
|
||||
if not working_sets:
|
||||
working_sets = gui_ws
|
||||
max_ws = max_gui_ws
|
||||
|
||||
# Private bytes
|
||||
private = [d.get('private_bytes') for d in data if d.get('private_bytes') is not None]
|
||||
if private:
|
||||
max_private = max(private) / (1024**3)
|
||||
avg_private = sum(private) / len(private) / (1024**3)
|
||||
print(f"Private Bytes: max={max_private:.2f} GB, avg={avg_private:.2f} GB")
|
||||
|
||||
# Available system memory
|
||||
avail = [d.get('available_mb') for d in data if d.get('available_mb') is not None]
|
||||
if avail:
|
||||
min_avail = min(avail) / 1024 # GB
|
||||
max_avail = max(avail) / 1024
|
||||
print(f"System Available Memory: min={min_avail:.2f} GB, max={max_avail:.2f} GB")
|
||||
|
||||
# CPU usage
|
||||
cpu = [d.get('cpu_pct') for d in data if d.get('cpu_pct') is not None]
|
||||
if cpu:
|
||||
max_cpu = max(cpu)
|
||||
avg_cpu = sum(cpu) / len(cpu)
|
||||
print(f"CPU %: max={max_cpu:.1f}%, avg={avg_cpu:.1f}%")
|
||||
|
||||
# Thread count
|
||||
threads = [d.get('thread_count') for d in data if d.get('thread_count') is not None]
|
||||
if threads:
|
||||
max_threads = max(threads)
|
||||
print(f"Thread count: max={int(max_threads)}")
|
||||
|
||||
# Disk writes
|
||||
disk = [d.get('disk_write_bytes_sec') for d in data if d.get('disk_write_bytes_sec') is not None]
|
||||
if disk:
|
||||
max_disk = max(disk) / (1024**2) # MB/s
|
||||
avg_disk = sum(disk) / len(disk) / (1024**2)
|
||||
print(f"Disk Write: max={max_disk:.1f} MB/s, avg={avg_disk:.1f} MB/s")
|
||||
|
||||
return {
|
||||
'duration': duration if timestamps else 0,
|
||||
'max_working_set_gb': max(working_sets) / (1024**3) if working_sets else 0,
|
||||
'max_private_bytes_gb': max(private) / (1024**3) if private else 0,
|
||||
'avg_cpu': sum(cpu) / len(cpu) if cpu else 0,
|
||||
'max_cpu': max(cpu) if cpu else 0,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
print("Performance Analysis: BEFORE vs AFTER Parallel Processing")
|
||||
print("=" * 60)
|
||||
|
||||
before_path = Path("arnis_before.csv")
|
||||
after_path = Path("arnis_after.csv")
|
||||
|
||||
if before_path.exists():
|
||||
before_data = parse_pdh_csv(before_path)
|
||||
before_stats = analyze_run(before_data, "BEFORE (Sequential)")
|
||||
else:
|
||||
print("arnis_before.csv not found")
|
||||
before_stats = None
|
||||
|
||||
if after_path.exists():
|
||||
after_data = parse_pdh_csv(after_path)
|
||||
after_stats = analyze_run(after_data, "AFTER (Parallel)")
|
||||
else:
|
||||
print("arnis_after.csv not found")
|
||||
after_stats = None
|
||||
|
||||
# Comparison
|
||||
if before_stats and after_stats:
|
||||
print(f"\n{'='*60}")
|
||||
print(" COMPARISON")
|
||||
print(f"{'='*60}")
|
||||
|
||||
time_diff = after_stats['duration'] - before_stats['duration']
|
||||
time_ratio = after_stats['duration'] / before_stats['duration'] if before_stats['duration'] > 0 else 0
|
||||
print(f"Duration: {before_stats['duration']:.1f}s -> {after_stats['duration']:.1f}s ({time_ratio:.2f}x, {time_diff:+.1f}s)")
|
||||
|
||||
mem_ratio = after_stats['max_working_set_gb'] / before_stats['max_working_set_gb'] if before_stats['max_working_set_gb'] > 0 else 0
|
||||
print(f"Peak Memory: {before_stats['max_working_set_gb']:.2f} GB -> {after_stats['max_working_set_gb']:.2f} GB ({mem_ratio:.2f}x)")
|
||||
|
||||
cpu_diff = after_stats['avg_cpu'] - before_stats['avg_cpu']
|
||||
print(f"Avg CPU: {before_stats['avg_cpu']:.1f}% -> {after_stats['avg_cpu']:.1f}% ({cpu_diff:+.1f}%)")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
212
arnis_after.csv
Normal file
212
arnis_after.csv
Normal file
@@ -0,0 +1,212 @@
|
||||
"(PDH-CSV 4.0) (Mitteleurop<6F>ische Zeit)(-60)","\\ROADRUNNER\Arbeitsspeicher\Verf<72>gbare MB","\\ROADRUNNER\Arbeitsspeicher\Zugesicherte verwendete Bytes (%)","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Aktuelle Warteschlangenl<6E>nge","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Bytes geschrieben/s","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Mittlere Sek./Schreibvorg<72>nge","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Schreibvorg<72>nge/s","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Zeit (%)","\\ROADRUNNER\Prozess(arnis-windows)\Arbeitsseiten","\\ROADRUNNER\Prozess(arnis)\Arbeitsseiten","\\ROADRUNNER\Prozess(arnis-windows)\E/A-Bytes gelesen/s","\\ROADRUNNER\Prozess(arnis)\E/A-Bytes gelesen/s","\\ROADRUNNER\Prozess(arnis-windows)\E/A-Datenbytes/s","\\ROADRUNNER\Prozess(arnis)\E/A-Datenbytes/s","\\ROADRUNNER\Prozess(arnis-windows)\E/A-Schreibvorg<72>nge/s","\\ROADRUNNER\Prozess(arnis)\E/A-Schreibvorg<72>nge/s","\\ROADRUNNER\Prozess(arnis-windows)\Private Bytes","\\ROADRUNNER\Prozess(arnis)\Private Bytes","\\ROADRUNNER\Prozess(arnis-windows)\Prozessorzeit (%)","\\ROADRUNNER\Prozess(arnis)\Prozessorzeit (%)","\\ROADRUNNER\Prozess(arnis-windows)\Threadanzahl","\\ROADRUNNER\Prozess(arnis)\Threadanzahl","\\ROADRUNNER\Prozessorinformationen(0,0)\Prozessorzeit (%)"
|
||||
"01/27/2026 21:49:44.151","12746","64.201036203699430871","0"," "," "," "," "," ","29315072"," "," "," "," "," "," "," ","8900608"," "," "," ","40"," "
|
||||
"01/27/2026 21:49:45.152","12743","64.19952420149918737","0","116877.24273461557459","8.3499999999999996751e-005","23.231614550748048487","0.19385484677997560921"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","6.2790441242259076304"
|
||||
"01/27/2026 21:49:46.171","12742","64.208226502921490919","0","142183.78738217015052","0.00030800000000000000554","4.9064082991307609305","0.15111770184909190107"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","3.4048780794156852103"
|
||||
"01/27/2026 21:49:47.170","12742","64.206333752769580769","0","0","0","0","0"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","0"
|
||||
"01/27/2026 21:49:48.167","12765","64.218516965447577149","0","92406.716692898364272","0.00028894000000000000846","5.013385237244920134","0.1788449123872541402"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","4.4122934340259050146"
|
||||
"01/27/2026 21:49:49.168","12764","64.214459518951940709","0","150899.54270564959734","0.00011216363636363636347","10.989770721412510213","0.12326769080690583302"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","3.2131824694520805252"
|
||||
"01/27/2026 21:49:50.169","12749","64.293889786185204116","0","169198.42306973855011","0.00023646999999999999075","9.9838571014526618086","0.2360349639579291392"," ","29315072"," ","223.63839907253964157"," ","223.63839907253964157"," ","0"," ","8900608"," ","0"," ","40","3.3032218318417294611"
|
||||
"01/27/2026 21:49:51.167","12750","64.293280608089006023","0","57488.566498172956926","0.00017981249999999998645","8.0201683172674318456","0.14424488479661923268"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","1.2922777395397599953"
|
||||
"01/27/2026 21:49:52.156","12749","64.287896073490358617","0","10873.380635370705932","0.00022130000000000001233","3.0338673647797729238","0.067124930729073398195"," ","29315072"," ","0"," ","0"," ","0"," ","8900608"," ","0"," ","40","0.47280661781649024888"
|
||||
"01/27/2026 21:49:53.166","12750","64.292388633892954886","0","198668.69878159218933","0.0001286857142857142811","13.858028653849903122","0.17838443271057205508"," ","29372416"," ","221.72845846159844996"," ","605.79382401115287848"," ","2.9695775686821219708"," ","8900608"," ","0"," ","40","0.98555022725796970207"
|
||||
"01/27/2026 21:49:54.165","12733","64.284621822714015593","0","1296829.4491823313292","0.00012115833333333332839","36.026598437626496718","0.43636684508381734515"," ","33755136"," ","448.33100277935199074"," ","4211895.6425528964028"," ","5.0036942274481246429"," ","8019968"," ","1.5632051618485096611"," ","40","3.0812799653923916843"
|
||||
"01/27/2026 21:49:55.162","12723","64.314361932760661489","0","0","0","0","0.073700618954755645063"," ","35278848"," ","503170.75050394039135"," ","508278.39024856238393"," ","6.0207934121281256878"," ","9277440"," ","3.1356628214242530106"," ","45","9.0622377543083878493"
|
||||
"01/27/2026 21:49:56.153","12724","64.318321497253677421","0","1247689.9431102154776","0.00041058888888888885223","9.0778351743519287709","0.37273695420660279964"," ","35278848"," ","0"," ","0"," ","0"," ","9277440"," ","0"," ","45","0.71209912904023342506"
|
||||
"01/27/2026 21:49:57.169","12725","64.313622253088652769","0","1039800.1256299206289","0.0015873499999999999589","1.9678872290416073998","0.31244015098534727581"," ","35278848"," ","0"," ","0"," ","0"," ","9277440"," ","0"," ","45","0.046609649890272386585"
|
||||
"01/27/2026 21:49:58.170","12775","64.089679383693649584","0","0","0","0","0"," ","35278848"," ","0"," ","0"," ","0"," ","9277440"," ","0"," ","45","1.5790544030494069183"
|
||||
"01/27/2026 21:49:59.168","12790","64.034974543385899892","0","1050344.1493410007097","0.0025869999999999998899","1.0016862386140830132","0.25918488307926468295"," ","35278848"," ","0"," ","0"," ","0"," ","9277440"," ","0"," ","45","0"
|
||||
"01/27/2026 21:50:00.169","12762","64.097174248680744313","0","114080.13845965237124","6.3770000000000007776e-005","19.983208110224978782","0.31184487053797710354"," ","35278848"," ","0"," ","0"," ","0"," ","9277440"," ","0"," ","45","6.3476419485044903723"
|
||||
"01/27/2026 21:50:01.170","12773","64.067890998923189727","0","24040.855868055819883","0.00022316666666666665209","5.9942293553995567024","0.13379846034844156133"," ","35278848"," ","0"," ","0"," ","0"," ","9277440"," ","0"," ","45","1.6371475687101066931"
|
||||
"01/27/2026 21:50:02.158","12772","64.128752719640900182","0","0","0","0","0"," ","35655680"," ","226.61720206666794297"," ","339.92580310000192867"," ","1.0116839377976247771"," ","9707520"," ","0"," ","45","2.0117555548587517933"
|
||||
"01/27/2026 21:50:03.170","12763","64.150530231220301403","0","76922.788000345550245","0.00013399166666666666647","11.861038445776966199","0.15892774821748678615"," ","43597824"," ","0"," ","0"," ","0"," ","18137088"," ","0"," ","45","1.160689787685253993"
|
||||
"01/27/2026 21:50:04.169","12757","64.162452394180576221","0","0","0","0","0"," ","50364416"," ","0"," ","0"," ","0"," ","25014272"," ","3.1283388760824424324"," ","45","1.4574832398887460627"
|
||||
"01/27/2026 21:50:05.170","12748","64.166335776486988607","0","0","0","0","0"," ","58380288"," ","0"," ","0"," ","0"," ","33206272"," ","4.6817582916311435426"," ","44","3.2442901147175629006"
|
||||
"01/27/2026 21:50:06.170","12743","64.188428750305533299","0","151560.24487732132548","0.00031206666666666664457","3.0001632088785630259","0.093621591567056641758"," ","66879488"," ","0"," ","0"," ","0"," ","42012672"," ","1.5625265629515703303"," ","44","6.2484062229057890647"
|
||||
"01/27/2026 21:50:07.170","12739","64.129046412214890438","0","0","0","0","0"," ","77299712"," ","0"," ","0"," ","0"," ","52727808"," ","3.1265858043199514782"," ","44","1.5125471639215404274"
|
||||
"01/27/2026 21:50:08.168","12737","64.136737087773326493","0","221512.63614698767196","0.00034925714285714284327","7.0104006303752246509","0.24484697664860091693"," ","90259456"," ","0"," ","0"," ","0"," ","66203648"," ","6.2593815610837513219"," ","44","1.4147404129309038012"
|
||||
"01/27/2026 21:50:09.169","12725","64.169577384407077147","0","0","0","0","0"," ","102637568"," ","0"," ","0"," ","0"," ","78856192"," ","3.1220552774622976067"," ","44","4.7759060488242743858"
|
||||
"01/27/2026 21:50:10.168","12714","64.20401676190179785","0","98440.33987072094169","0.00028076666666666669691","3.0041607626562787381","0.084346626079880651639"," ","115486720"," ","0"," ","0"," ","0"," ","92315648"," ","9.3879807609982321992"," ","44","0"
|
||||
"01/27/2026 21:50:11.168","12709","64.234637972953407825","0","4094.1564013724619144","0.00063150000000000000838","0.99954990267882370958","0.063123582780606313225"," ","127291392"," ","0"," ","0"," ","0"," ","104419328"," ","1.5618463672952869192"," ","44","3.1655252276922118959"
|
||||
"01/27/2026 21:50:12.171","12693","64.269240494880193637","0","0","0","0","0"," ","139763712"," ","0"," ","0"," ","0"," ","117067776"," ","1.5584051346681460082"," ","44","3.3788816505749497132"
|
||||
"01/27/2026 21:50:13.169","12678","64.303070694278702035","0","86179.525649920717115","8.6116666666666668293e-005","12.022813288214386773","0.103538348414056805"," ","151339008"," ","0"," ","0"," ","0"," ","129167360"," ","3.1309980529700749408"," ","44","7.6355574373827899137"
|
||||
"01/27/2026 21:50:14.170","12675","64.328601086588719227","0","36319.403335506380245","0.0003498500000000000199","1.9982066095679127393","0.069903319223792420578"," ","161058816"," ","0"," ","0"," ","0"," ","139071488"," ","7.8050547582660900758"," ","44","9.4569052834805660268"
|
||||
"01/27/2026 21:50:15.171","12663","64.339283146974466376","0","118633.95177779145888","0.00012265714285714286475","13.982315167775798415","0.21097823966660581019"," ","169115648"," ","0"," ","0"," ","0"," ","147320832"," ","0"," ","44","3.2452274803329350661"
|
||||
"01/27/2026 21:50:16.169","12680","64.299111153068750468","0","176383.79177483185777","0.00023925714285714285291","7.0101661429404922288","0.16771888200363777033"," ","170295296"," ","0"," ","0"," ","0"," ","157147136"," ","17.212015073068570814"," ","43","4.5515527766197383386"
|
||||
"01/27/2026 21:50:17.162","12513","64.808707070725205313","0","0","0","0","0"," ","316452864"," ","677.35714996334513671"," ","1016.0357249450175914"," ","3.0239158480506476145"," ","304222208"," ","100.79634149598905424"," ","43","5.5002579464959122646"
|
||||
"01/27/2026 21:50:18.153","12047","66.17082400856791935","0","12386.270190436887788","0.00065830000000000000928","1.0079972485707102692","0.066365275768236281495"," ","801759232"," ","2192698.4308103630319"," ","2192924.2221940429881"," ","2.0159944971414205384"," ","816644096"," ","99.237913313622357236"," ","52","11.791482726705716289"
|
||||
"01/27/2026 21:50:19.171","12056","66.073673676250891162","0","1027943.840953755076","3.7708670520231215542e-005","170.09431287206899697","0.64131760114815083984"," ","801427456"," ","0"," ","0"," ","0"," ","785248256"," ","1238.0577502390588052"," ","52","87.711305333657236361"
|
||||
"01/27/2026 21:50:20.169","11683","67.189482824688198548","0","0","0","0","0"," ","1193062400"," ","0"," ","0"," ","0"," ","1207631872"," ","1199.2725857534383067"," ","52","84.344063870207534706"
|
||||
"01/27/2026 21:50:21.167","12197","65.758976681567489209","0","98431.981262036904809","0.00028293333333333334396","3.0039056781627473391","0.084988666508998403359"," ","654458880"," ","1121.458119847425678"," ","1682.1871797711382897"," ","5.0065094636045781584"," ","660942848"," ","730.62167287093279811"," ","43","48.371487784280986943"
|
||||
"01/27/2026 21:50:22.170","10914","69.570106284127135154","0","44927.0100614126859","0.00010571250000000000134","7.9770969569269674082","0.08433404178449116495"," ","1999941632"," ","9424.9400546092128934"," ","8602926.1387629974633"," ","944.28885227622981802"," ","2108280832"," ","1338.4426712501094698"," ","58","92.209297606227536903"
|
||||
"01/27/2026 21:50:23.167","10679","70.1412165933617473","0","4277087.253197716549","0.0014359999999999999935","9.0255911611784060966","1.2959332323647190233"," ","2215051264"," ","60423.324293702367868"," ","41892262.32058378309"," ","17601.908451222596341"," ","2323849216"," ","501.36692678919808941"," ","58","76.497374223974631491"
|
||||
"01/27/2026 21:50:24.169","10607","70.305211462617194229","0","9362936.4392447564751","0.0034966624999999999443","15.978143497509757154","5.5869539144657771601"," ","2273783808"," ","30410.40161163544326"," ","19318356.420252736658"," ","10537.585636607684137"," ","2381434880"," ","240.29356308843210854"," ","58","54.750532508216053884"
|
||||
"01/27/2026 21:50:25.171","10556","70.377277413005302265","0","4086.1291378417158739","0.00061390000000000001425","0.99759012154338766454","0.061243817132885514098"," ","2304122880"," ","38135.875166360623552"," ","27797729.025991909206"," ","10224.301155698180082"," ","2411061248"," ","235.37568178514703732"," ","57","50.11922005215334508"
|
||||
"01/27/2026 21:50:26.169","10646","70.013956299520557991","0","12642454.189482485875","0.0076883823529411768338","17.036079008123401479","13.097755286100811745"," ","2325094400"," ","9536.1957553707234183"," ","1859914.9278341671452"," ","7154.1510611172334393"," ","2431336448"," ","156.57881551257642627"," ","57","31.107005701033063616"
|
||||
"01/27/2026 21:50:27.169","10639","70.032013573225597725","0","9942254.8753778282553","0.00032751532258064514937","248.13374408806345173","8.1984747810162996728"," ","2341912576"," ","14751.951301751643769"," ","9674004.2883113995194"," ","4919.6516922621294725"," ","2447740928"," ","129.76228200311840055"," ","57","17.140021232166034792"
|
||||
"01/27/2026 21:50:28.169","10624","70.059763051117712962","1","0","0","0","0"," ","2350510080"," ","13887.055680213745291"," ","9623900.5747609175742"," ","4279.7089797893740979"," ","2456276992"," ","162.48695229773048254"," ","57","34.378845399659574866"
|
||||
"01/27/2026 21:50:29.169","10544","70.400751864165243887","0","13819735.229708110914","0.001033360666666666627","149.95372428068696991","15.595066527935175671"," ","2350444544"," ","8909.250605263214311"," ","5178735.842119121924"," ","3445.9365839701863479"," ","2456023040"," ","148.39274474818395788"," ","57","35.958204712416474536"
|
||||
"01/27/2026 21:50:30.168","10457","70.639075285438238438","0","8541511.430721545592","0.0019598948717948717554","39.043623440470035746","7.6521811037251517007"," ","2350706688"," ","3752.1923244841459564"," ","789298.88364269398153"," ","2816.1464804626207297"," ","2456023040"," ","115.75495809144926795"," ","57","60.893595239375251538"
|
||||
"01/27/2026 21:50:31.169","10423","70.760374486157758156","0","8897102.315781397745","0.00084457578947368431264","94.918901290737196064","8.0169842597338760726"," ","2350759936"," ","3313.169228211416339"," ","735583.51744269696064"," ","2486.8752138173144886"," ","2456023040"," ","110.84782411029476634"," ","57","11.009493319904194664"
|
||||
"01/27/2026 21:50:32.169","10472","70.549963710492008317","0","143323.28057551657548","0.00015364545454545454947","10.997182521837906677","0.16895169477013483039"," ","2350858240"," ","7158.1660778508548901"," ","4886054.1929157748818"," ","2300.410634795365695"," ","2456023040"," ","118.70903351253483038"," ","57","34.39580309083989107"
|
||||
"01/27/2026 21:50:33.170","10485","70.514849915754723497","0","13130170.966100767255","0.0013322203252032521932","122.86999126224540646","16.369284307391883004"," ","2350968832"," ","3164.6514822666135842"," ","660591.02863260381855"," ","2373.4886116999600745"," ","2456023040"," ","120.18749298854261554"," ","57","11.032526158809774941"
|
||||
"01/27/2026 21:50:34.170","10495","70.501317845308534515","0","10946851.342131813988","0.0013394725274725274335","91.019450856648063564","12.191989527203823229"," ","2351104000"," ","15475.307073121526628"," ","13320481.586915124208"," ","2395.5119208974956564"," ","2456023040"," ","123.46574896336281313"," ","57","14.042833000190446668"
|
||||
"01/27/2026 21:50:35.170","10501","70.54300186469755829","0","11477459.974901933223","0.0006450450261780104038","190.93964397853838477","12.316214362245668568"," ","2351513600"," ","11372.405182721740857"," ","9044571.0111033897847"," ","2225.2965837498763904"," ","2456023040"," ","131.20583611556349979"," ","57","9.4054941106823441999"
|
||||
"01/27/2026 21:50:36.170","10483","70.540445570494142657","0","221178.80229814600898","0.00033769999999999996852","4.9998825027611850658","0.16884581262384693034"," ","2351644672"," ","11019.741036085652013"," ","9037578.6169025041163"," ","2124.9500636735037915"," ","2456023040"," ","124.99690007687809157"," ","57","14.0646311971463156"
|
||||
"01/27/2026 21:50:37.169","10492","70.53700815200269858","0","10640702.010342037305","0.000276337762237762255","429.63302129357396097","11.872376823823810099"," ","2351783936"," ","2687.9604409136418326"," ","536102.89400402549654"," ","2015.9703306852316018"," ","2456023040"," ","114.23075047237074386"," ","57","9.2409943281254740555"
|
||||
"01/27/2026 21:50:38.171","10499","70.529273983680013771","0","8528471.2360408864915","0.0035371807692307693713","25.948857396956334753","9.1788181166449369641"," ","2351972352"," ","2495.0824420150324841"," ","544185.46486729301978"," ","1873.3078974648863095"," ","2456023040"," ","112.28162347051220138"," ","57","7.9917780205270076976"
|
||||
"01/27/2026 21:50:39.170","10509","70.476189947332301244","0","9009261.7181815039366","0.00068547040816326532033","98.023476622651116941","6.7189840322345917301"," ","2352095232"," ","2660.6372226148159825"," ","558448.74847525975201"," ","1995.4779169611119869"," ","2456023040"," ","118.7742893421704764"," ","57","4.6672036476565814667"
|
||||
"01/27/2026 21:50:40.169","10511","70.482466479410049942","0","0","0","0","0"," ","2352148480"," ","2757.217206397946029"," ","579702.91385359375272"," ","2068.9148005856682175"," ","2456023040"," ","118.97510089088557095"," ","57","9.2036309330229890691"
|
||||
"01/27/2026 21:50:41.169","10518","70.45824145210400502","0","4077146.8798495628871","0.0015635999999999998424","10.993342431823286631","1.718978635097222929"," ","2352271360"," ","2842.2787160095845138"," ","583799.45105244254228"," ","2132.7084317737176207"," ","2456461312"," ","123.36706973988549407"," ","57","17.235065008139827114"
|
||||
"01/27/2026 21:50:42.169","10523","70.45927484763303994","0","4434737.803733243607","0.00041770862068965519676","57.983915261906339822","2.4220562870081363549"," ","2352390144"," ","2931.1868887570585684"," ","605981.90062076773029"," ","2198.39016656779404"," ","2456461312"," ","118.7179580231295688"," ","57","7.8373746925704761424"
|
||||
"01/27/2026 21:50:43.169","10519","70.462483812696873997","4","0","0","0","0"," ","2352590848"," ","7213.9232319336333603"," ","4890832.8960500871763"," ","2341.6242770322569413"," ","2456461312"," ","110.96830480141287012"," ","57","15.601570996108515388"
|
||||
"01/27/2026 21:50:44.170","10523","70.457273319004400491","0","6311018.5268822452053","0.00017956057007125888407","420.66582307015306696","7.5532376106969705276"," ","2352734208"," ","7342.1673822315551661"," ","4809942.9812956592068"," ","2439.0624088224317347"," ","2456461312"," ","109.28439231033550527"," ","57","14.133691756164967757"
|
||||
"01/27/2026 21:50:45.169","10527","70.461896404265871752","0","6365989.812155360356","0.00019072807486631015322","374.28820191547492868","7.1387539678202998061"," ","2352906240"," ","7601.853427138897132"," ","4920483.7725048288703"," ","2630.0251193418935145"," ","2456461312"," ","125.09679990376554315"," ","57","13.995950066161199743"
|
||||
"01/27/2026 21:50:46.171","10519","70.459535920633825867","0","8449449.7441951278597","0.0034042045454545453304","21.966498892538986354","7.4777701273005678928"," ","2352979968"," ","7796.1101524065643389"," ","4952449.0199995981529"," ","2782.7560187957346898"," ","2456461312"," ","115.44776459179865924"," ","57","12.631664082228077461"
|
||||
"01/27/2026 21:50:47.170","10515","70.461504806406210832","0","4098.3930517028893519","0.00060210000000000005269","1.0005842411384007207","0.060245388140994025894"," ","2353098752"," ","8144.7557228665818911"," ","5044815.6678684679791"," ","3037.7737560961845702"," ","2456461312"," ","126.6368865071342924"," ","57","17.141165339657938205"
|
||||
"01/27/2026 21:50:48.170","10544","70.46187465788375448","0","5179045.8344612037763","0.00023416082949308757426","217.07132963891933741","5.0828458591174765502"," ","2353197056"," ","4557.4975937092931417"," ","921951.95341189112514"," ","3420.1238526979964263"," ","2456461312"," ","134.41614478191775106"," ","57","15.597197716927958311"
|
||||
"01/27/2026 21:50:49.169","10533","70.457501749148946146","0","7572093.0642005652189","0.00013412629107981220017","426.15111318473532265","5.7159760780091710686"," ","2353258496"," ","13784.888121327821864"," ","9542735.8541338760406"," ","4195.4877199454922447"," ","2456461312"," ","132.86354617444021642"," ","57","23.409858617975231709"
|
||||
"01/27/2026 21:50:50.169","10524","70.445514323759255149","0","10276717.642242111266","0.00040804894366197185396","284.22251780919282282","11.597713921542077031"," ","2353594368"," ","32421.382700516234763"," ","26776161.0564911291"," ","5874.5992237322598157"," ","2456461312"," ","153.24555828069944141"," ","57","28.068411419263540552"
|
||||
"01/27/2026 21:50:51.169","10521","70.447124254528219467","0","0","0","0","0"," ","2353774592"," ","16620.970991367750685"," ","6341991.9715952128172"," ","9401.0243068206455064"," ","2456465408"," ","156.18357512549911803"," ","57","28.155555442270397748"
|
||||
"01/27/2026 21:50:52.158","10514","70.521159090688726678","0","9258209.5270443074405","0.00012994450474898235808","745.01110440567424575","9.6810353437431633239"," ","2354065408"," ","14831.482936010925187"," ","5982249.1248393980786"," ","7851.4263879496220397"," ","2456465408"," ","151.63088110481089643"," ","57","30.5025128269616701"
|
||||
"01/27/2026 21:50:53.170","10509","70.521322258403827732","0","11016937.085175897926","0.00033535093833780160342","368.84240837282158054","12.447882736800552905"," ","2354348032"," ","3540.0960374656870044"," ","596844.37025844678283"," ","2656.0608817410152369"," ","2456465408"," ","118.97084202036791112"," ","57","8.8405236467310803761"
|
||||
"01/27/2026 21:50:54.169","10516","70.515502586615156133","0","11693554.042049037293","0.00023336273408239700767","534.16307998832041903","12.464908102389820499"," ","2354810880"," ","11659.559663565285518"," ","9007700.050825515762"," ","2600.7940224150434005"," ","2456465408"," ","115.65597266948088873"," ","57","9.3470789003012981766"
|
||||
"01/27/2026 21:50:55.168","10514","70.503689225414703401","0","0","0","0","0"," ","2355269632"," ","7745.2284396858858599"," ","4854337.9436598708853"," ","2736.2602540927514383"," ","2456465408"," ","125.15336293093557174"," ","57","18.653588232773486766"
|
||||
"01/27/2026 21:50:56.167","10510","70.501132907928237614","0","8521233.7350227460265","0.00012932806451612904441","620.70990591939994374","8.0276038548484951463"," ","2356051968"," ","3772.3143959746762448"," ","652540.31035295070615"," ","2830.2369419905544419"," ","2456469504"," ","126.70872186503223134"," ","57","17.09182396485544686"
|
||||
"01/27/2026 21:50:57.168","10517","70.460721610686903205","0","10013080.507485311478","0.00032016716981132076593","264.84829489668317137","8.4795500189640868882"," ","2356363264"," ","4185.6024868955064449"," ","705965.62289120792411"," ","3140.2012926995416819"," ","2456469504"," ","115.55849608550279584"," ","57","15.673529883552017594"
|
||||
"01/27/2026 21:50:58.169","10519","70.44155478254927516","4","4270160.7986819520593","0.0023528578947368422071","18.973060151890333458","4.4640802921923858904"," ","2356805632"," ","13085.420012124784989"," ","9217282.3807475771755"," ","3681.7722515799819121"," ","2456469504"," ","137.30469779785232731"," ","57","21.985967160311169266"
|
||||
"01/27/2026 21:50:59.161","10528","70.463038601554700335","0","5220309.3795784646645","0.00029643981042654028767","212.58284937991294328","6.3018387818669596712"," ","2357317632"," ","5001.2382195350137408"," ","709248.92565060930792"," ","3247.1778367367746796"," ","2456469504"," ","132.23536504617808873"," ","57","19.714242650534718138"
|
||||
"01/27/2026 21:51:00.153","10743","69.929794168549079814","0","7314739.5646521644667","0.00016994746716697935402","537.76540816444924076","9.1387627631384464877"," ","2156122112"," ","1243.0149772206407306"," ","176064.1928448950348"," ","763.76813129547485914"," ","2278170624"," ","110.34777077325159667"," ","57","16.447475288631409285"
|
||||
"01/27/2026 21:51:01.170","10750","69.964277038808049269","0","5823810.7487898729742","0.00012140000000000000996","257.44239431645803506","3.1253856766094259001"," ","2158878720"," ","5989.9573883707180357"," ","4503428.0125230988488"," ","1313.742294660703692"," ","2292432896"," ","116.68559839322702487"," ","57","17.09198274805759965"
|
||||
"01/27/2026 21:51:02.167","10847","69.624756735196513091","0","0","0","0","0"," ","2051203072"," ","1079.6855064763569771"," ","263821.55487757461378"," ","810.76755504916025075"," ","2168586240"," ","109.751006949810062"," ","57","12.202665547141954505"
|
||||
"01/27/2026 21:51:03.168","10849","69.684247851763913673","0","5250893.5505910031497","0.00016317461928934008501","196.686462110749261","3.2095340896803254971"," ","2055299072"," ","5555.144544082279026"," ","4536028.1175777697936"," ","1100.2460976956633658"," ","2191577088"," ","102.9644064153140306"," ","57","15.756394751106695296"
|
||||
"01/27/2026 21:51:04.169","10856","69.712802271757453809","0","8531379.4195607192814","0.0020068325000000002307","39.978048053813651563","8.0228925381371567482"," ","2060988416"," ","1731.0494807301311084"," ","393424.97034878149861"," ","1298.2871105475983313"," ","2203787264"," ","115.56108316490150401"," ","57","20.356550791757076269"
|
||||
"01/27/2026 21:51:05.170","10849","69.724191415525083926","0","4538432.9638415165246","0.00018337375000000000593","79.929007055932927983","1.5253854076654620453"," ","2065584128"," ","1798.4026587584908157"," ","440072.12793596729171"," ","1350.8002192452665895"," ","2214973440"," ","115.52014236677368331"," ","57","17.262600737310741295"
|
||||
"01/27/2026 21:51:06.170","10848","69.716261436631029369","0","0","0","0","0"," ","2069254144"," ","1792.3446678796333345"," ","412778.37728195131058"," ","1344.258500909724944"," ","2225627136"," ","115.64416223768279224"," ","57","18.73412877534982357"
|
||||
"01/27/2026 21:51:07.169","10969","69.12894308779597452","0","3536749.0025888914242","5.1693461538461537089e-005","260.13982515602134526","1.3448517043913830182"," ","1943576576"," ","5603.0116187450748839"," ","4545725.3273634575307"," ","1131.6082394286929684"," ","2009141248"," ","125.07642169365482232"," ","57","15.575915837467901426"
|
||||
"01/27/2026 21:51:08.167","10955","69.25419095653440138","0","3958417.8775537703186","0.00013134404761904762936","168.42010711518813082","2.2120007289864540567"," ","1958141952"," ","2125.3013516916598746"," ","423185.59414603788173"," ","1426.5584072911469775"," ","2031947776"," ","112.77636977176268829"," ","57","9.1523687949689467303"
|
||||
"01/27/2026 21:51:09.170","10944","69.313736462340159505","0","3431093.3259299322963","0.00015316785714285715832","55.844617935057492275","0.85535799904426157436"," ","1965596672"," ","1822.9278854515196144"," ","424424.08243303827476"," ","1368.1931394089085643"," ","2053844992"," ","104.39677585858899533"," ","57","15.859314979644690169"
|
||||
"01/27/2026 21:51:10.170","10942","69.351221637183627422","0","0","0","0","0"," ","1972744192"," ","2051.440572155972859"," ","529316.65534808661323"," ","1543.5790659887047696"," ","2068332544"," ","114.03177983851151112"," ","57","18.771882854758924708"
|
||||
"01/27/2026 21:51:11.159","10933","69.405426101154972685","0","3214128.124581430573","3.3966014234875444445e-005","568.11720197223610285","1.9297162698709551254"," ","1980702720"," ","6421.1396208676933384"," ","4731661.1615570662543"," ","1544.6318231558304888"," ","2090156032"," ","124.7842212291939461"," ","57","30.499927416651473777"
|
||||
"01/27/2026 21:51:12.153","10923","69.474990123294986688","0","4661357.2497754106298","6.2699632352941186411e-005","273.50850882959878163","1.7148467378917853221"," ","1989677056"," ","2043.2694483152376961"," ","446587.10652734088944"," ","1532.4520862364283857"," ","2116431872"," ","111.55005481601116912"," ","57","23.014750901626101154"
|
||||
"01/27/2026 21:51:13.168","10920","69.55603029801417847","1","2316338.9945661542006","4.0819999999999998809e-005","275.85973163183047063","1.1260116110341964468"," ","1996836864"," ","6147.7311620807940926"," ","4630412.9836929459125"," ","1586.1934568830254193"," ","2144813056"," ","113.91045401923862812"," ","57","13.794954945795257828"
|
||||
"01/27/2026 21:51:14.171","10908","69.643836531239529108","0","2255227.8631046907976","0.0017867749999999999952","3.9898024638825626553","0.71292974313840906664"," ","2006106112"," ","2146.5137255688186997"," ","471688.41158882016316"," ","1611.8801954085554371"," ","2178650112"," ","127.80585599698991928"," ","57","17.396239702272353611"
|
||||
"01/27/2026 21:51:15.161","10899","69.732219276421758991","0","3647323.3959229663014","0.00010993148148148149026","109.03589552543944308","1.1987011227360666599"," ","2017054720"," ","2511.8639635860495218"," ","499335.94110850134166"," ","1548.7135531113342495"," ","2214453248"," ","135.66991507139039186"," ","57","30.58748531231189105"
|
||||
"01/27/2026 21:51:16.154","11082","68.95247129000547659","0","3170305.795681017451","0.0028924333333333334013","3.0234392124948668013","0.87444063230039970058"," ","1825878016"," ","2326.0325674793839426"," ","580051.85198249435052"," ","1747.5478648220328068"," ","1922043904"," ","133.83961195002689237"," ","57","33.865966342941625555"
|
||||
"01/27/2026 21:51:17.172","11065","69.280210851989735943","0","0","0","0","0"," ","1843060736"," ","3058.8487753659401278"," ","568397.12495701562148"," ","1968.2258647653111439"," ","2044796928"," ","138.04804336961504418"," ","57","34.045188825372662222"
|
||||
"01/27/2026 21:51:18.168","11183","69.080406001089230017","0","2818655.3482180978172","3.3382317073170731465e-005","494.26124519677523494","1.6499834239157733506"," ","1725759488"," ","1679.6845568475775963"," ","424679.91059095360106"," ","1260.7680136625060641"," ","1968840704"," ","141.27344511306898767"," ","57","16.805637877859368245"
|
||||
"01/27/2026 21:51:19.170","11276","68.489465319665484344","0","3847505.9075129828416","4.1715081206496520125e-005","430.23624461855314394","1.7947277178083345106"," ","1631940608"," ","11783.082671641303023"," ","9209048.0978167559952"," ","2706.1959609301570708"," ","1750941696"," ","143.49476585996487188"," ","57","39.170697081101849335"
|
||||
"01/27/2026 21:51:20.170","11363","68.491999844203704129","0","2771359.9461277257651","3.621496598639455534e-005","440.74044795020216725","1.5960867552122313118"," ","1541435392"," ","4153.5539720658507576"," ","642452.65962874470279"," ","2785.3597016716857979"," ","1749553152"," ","251.40605001507560701"," ","57","57.837447582265568258"
|
||||
"01/27/2026 21:51:21.169","11182","69.436612974255481845","0","0","0","0","0"," ","1721573376"," ","12682.119062800207757"," ","8935040.2686303406954"," ","3364.6846819076940847"," ","2103476224"," ","283.05272684927700766"," ","57","64.033075459033028665"
|
||||
"01/27/2026 21:51:22.169","11094","69.772728502231814218","0","3809092.6258455528878","3.5215892857142855549e-005","560.4244094052426135","1.9735640549926740661"," ","1805754368"," ","23501.797911558423948"," ","17955458.668849918991"," ","5166.9129031415486679"," ","2228150272"," ","237.67752134068936698"," ","57","45.271623375499160602"
|
||||
"01/27/2026 21:51:23.170","11022","69.865658197055026335","0","7655567.20818094071","0.00041408310502283107507","218.65312867666730767","9.0536797040737972964"," ","1877463040"," ","11146.317481946638509"," ","5353247.6079946765676"," ","5293.6022294232425338"," ","2266091520"," ","179.39542630643049392"," ","57","37.599703194236241188"
|
||||
"01/27/2026 21:51:24.169","10978","69.928249500209531675","0","9695842.8733884748071","0.0024785974358974356864","39.068479230395034563","9.6838873997753580625"," ","1927356416"," ","10322.092563845908444"," ","5292950.4836076674983"," ","4665.1771224602480288"," ","2287968256"," ","183.14076150091423756"," ","57","40.520190627905428471"
|
||||
"01/27/2026 21:51:25.167","10930","69.9812682741277996","0","0","0","0","0"," ","1966698496"," ","9749.3517372041005729"," ","5138615.3336995020509"," ","4236.802300750878203"," ","2305388544"," ","153.33423990377269774"," ","57","28.026785351290371295"
|
||||
"01/27/2026 21:51:26.158","10897","70.023811671422748759","0","9472794.6024891883135","0.00044844450261780104865","192.80862199778803756","8.7297495675105949431"," ","1995489280"," ","26052.381761879129044"," ","22057618.282537512481"," ","3871.3144783325506069"," ","2318254080"," ","153.00035140237409337"," ","57","27.44313232464733332"
|
||||
"01/27/2026 21:51:27.171","10887","70.062765332419147057","0","8428257.3178220055997","0.004643081818181817727","10.861074904785398942","5.0427131694315052712"," ","2007629824"," ","4664.3379863823838605"," ","877255.93165263789706"," ","3501.2156011244569527"," ","2335494144"," ","134.21607151002669411"," ","57","27.492467115272933853"
|
||||
"01/27/2026 21:51:28.169","10865","70.088284851538091402","4","4611143.9571270849556","0.00099768703703703698858","54.084918730899389061","5.3961103206290186307"," ","2028404736"," ","9162.3858620419923682"," ","5182432.9379558842629"," ","3800.967899699317968"," ","2346729472"," ","143.97947448643648727"," ","57","28.010262756781756366"
|
||||
"01/27/2026 21:51:29.170","10798","70.193963421088170662","0","2123532.2839591512457","8.2523821339950366384e-005","402.56555125708337073","3.4769670747013661227"," ","2060161024"," ","13209.744044227471022"," ","9382995.8708561733365"," ","3772.9282558263125793"," ","2354552832"," ","195.09798976025859929"," ","57","36.007859358635187164"
|
||||
"01/27/2026 21:51:30.169","10794","70.203340139753038329","0","8483553.7887180037796","0.0039239888888888884919","18.018966764416223469","7.070866031376104921"," ","2081951744"," ","9878.3980017366284301"," ","5307519.6952312001958"," ","4337.5657216786385106"," ","2362077184"," ","150.16322742821446923"," ","57","21.789985714471626466"
|
||||
"01/27/2026 21:51:31.168","10768","70.183542410420159285","0","9212289.5628119334579","0.00054239588235294124065","170.15843451838006217","9.2288864898797555725"," ","2103881728"," ","5368.9990750387687513"," ","959846.7132747300202"," ","4030.7530341500969371"," ","2367655936"," ","136.05774783753881252"," ","57","15.550363411182798856"
|
||||
"01/27/2026 21:51:32.171","10758","70.185000046665081186","0","0","0","0","0"," ","2125774848"," ","4976.541369543786459"," ","857751.56630957254674"," ","3733.4029304770592717"," ","2374946816"," ","138.63467339789409039"," ","57","18.999966104601195838"
|
||||
"01/27/2026 21:51:33.171","10740","70.264397671042104321","0","8407394.7506972104311","0.0046668999999999998679","10.997785046091717476","5.1325906845937092626"," ","2137690112"," ","11989.585297521080065"," ","8999945.410994226113"," ","2521.4921714766646801"," ","2380029952"," ","145.28421316369701799"," ","57","26.576795497916560151"
|
||||
"01/27/2026 21:51:34.170","10745","70.275471352570562544","0","8719042.9648272171617","0.0010013129870129870453","77.133077698954011225","7.7233609704169436938"," ","2139545600"," ","6314.8950884961823249"," ","4559993.3565380349755"," ","1492.5751398888503445"," ","2382225408"," ","122.08446030855138531"," ","57","15.479989017156736253"
|
||||
"01/27/2026 21:51:35.170","10737","70.274601124756642889","0","6066180.3613462727517","0.00046712727272727274687","142.90361151403379836","6.6753347125859248123"," ","2141454336"," ","2270.4685689502434798"," ","457058.7138974762056"," ","1702.8514267126824961"," ","2384289792"," ","121.7913415275047555"," ","57","18.805772314996826111"
|
||||
"01/27/2026 21:51:36.168","10738","70.26768279500950598","0","0","0","0","0"," ","2144112640"," ","2457.3666430116732045"," ","455895.63046737771947"," ","1843.024982258755017"," ","2385657856"," ","123.70992161839586743"," ","57","15.438787754514216033"
|
||||
"01/27/2026 21:51:37.168","10741","70.264136598041318393","0","7740229.4281174419448","0.0010103707692307691609","64.989835589713763397","7.0307161229876662389"," ","2148913152"," ","10898.295506582770031"," ","8922516.5184165183455"," ","2032.6820885213551264"," ","2387492864"," ","131.22361093184161973"," ","57","14.077270117266838412"
|
||||
"01/27/2026 21:51:38.169","10730","70.266420969335925406","0","5991393.1992665911093","0.00042139046242774562054","345.70279930343883734","14.568267455385516485"," ","2153140224"," ","3049.3784493470966481"," ","541047.86115376616362"," ","2287.0338370103222587"," ","2388283392"," ","128.02093178455234579"," ","57","9.4512511014474664961"
|
||||
"01/27/2026 21:51:39.169","10739","70.21686136029121883","0","7275000.8850614232942","0.00013430194003527336417","567.03935253106567416","7.6153997701855216107"," ","2156089344"," ","16041.113253259776684"," ","13263696.50053713657"," ","2817.1955133686278714"," ","2388910080"," ","146.88425370798358927"," ","57","28.120471589710149374"
|
||||
"01/27/2026 21:51:40.170","10739","70.219276233161636469","0","0","0","0","0"," ","2159890432"," ","14936.049666661703668"," ","9651294.998871402815"," ","5065.9467360165135688"," ","2389733376"," ","138.95125562474680692"," ","57","20.376246776830463148"
|
||||
"01/27/2026 21:51:41.169","10713","70.235157937331862854","0","9738187.2361161895096","0.00045125491803278688788","244.15271752481174872","11.017220416751676737"," ","2184790016"," ","6816.2635728648265285"," ","1298510.2181414475199"," ","5114.1989314316106174"," ","2397507584"," ","151.65335552529518282"," ","57","24.952292172133827108"
|
||||
"01/27/2026 21:51:42.168","10692","70.238453934490323149","0","6191358.3524073306471","0.00013794331550802139204","374.63500633573909226","5.1679135145704551135"," ","2203250688"," ","14496.571689012876959"," ","9616176.4190302565694"," ","4720.0004006791514257"," ","2398257152"," ","153.38718471483306871"," ","57","24.874337304008420801"
|
||||
"01/27/2026 21:51:43.170","10674","70.245350517855342787","3","7691683.0553319035098","0.0014850056603773586616","52.858619051622625307","7.8494315130361105304"," ","2220285952"," ","14166.10990583486273"," ","9550378.6023522876203"," ","4496.9719491276682675"," ","2400722944"," ","171.414255622312794"," ","57","36.107632595923575991"
|
||||
"01/27/2026 21:51:44.168","10659","70.251572660694733941","0","1604540.3413351159543","0.00014607435897435897074","117.2194817576430097","1.7123349089709347659"," ","2238144512"," ","9529.8436793051314453"," ","5210695.5062659326941"," ","4073.6274600562092019"," ","2402811904"," ","145.5900966372582559"," ","57","26.424059510374718229"
|
||||
"01/27/2026 21:51:45.167","10649","70.252442888508653596","0","9943580.3513102997094","0.00036639831081081082912","296.5658476372919381","10.866102965472784092"," ","2249609216"," ","13425.616075472000375"," ","9375880.1793822608888"," ","3916.4726297776155661"," ","2404974592"," ","156.54841258406776205"," ","57","23.291277833806802278"
|
||||
"01/27/2026 21:51:46.169","10639","70.269031652777698582","0","9008396.8973790332675","0.00058435955882352944978","135.64940055931438678","7.9267280673931823642"," ","2262802432"," ","13664.682262225052909"," ","9445250.8047699909657"," ","4126.3350743667915594"," ","2408382464"," ","158.96265088360792106"," ","57","25.194046643008039865"
|
||||
"01/27/2026 21:51:47.161","10606","70.336746440813115555","0","0","0","0","0"," ","2273886208"," ","9721.8962820907036075"," ","5216159.4052135786042"," ","4026.9223915340439817"," ","2410160128"," ","149.69955708596896216"," ","57","29.089683485593642587"
|
||||
"01/27/2026 21:51:48.153","10609","70.345231185281932085","0","8536742.4601857867092","0.004004923809523809608","21.153928677414072013","8.4719061894694949189"," ","2274164736"," ","11979.167611038481482"," ","9165546.0121117327362"," ","2798.3625650407757348"," ","2410160128"," ","129.06291309575820492"," ","57","18.155225841714319301"
|
||||
"01/27/2026 21:51:49.172","10623","70.335321424141369562","0","8790511.1008961647749","0.00071812195121951228737","120.81138102141602531","8.6757696652841715945"," ","2274590720"," ","4203.8431770053703076"," ","735204.04354710073676"," ","3152.8823827540281854"," ","2410160128"," ","139.65809635369961939"," ","57","12.521851734495847097"
|
||||
"01/27/2026 21:51:50.168","10628","70.332667224652283267","0","7592319.3420785907656","0.00031383513513513517116","259.78395002599341979","8.1527646509353175475"," ","2275934208"," ","4710.214012826506405"," ","812151.83057913871016"," ","3533.6635364539570219"," ","2410766336"," ","126.94296074765681226"," ","57","9.0996446435542495124"
|
||||
"01/27/2026 21:51:51.169","10616","70.332656351461224631","0","0","0","0","0"," ","2279432192"," ","13356.162433235424032"," ","9301091.4426543768495"," ","3880.560659087851036"," ","2411986944"," ","128.01422928739759755"," ","57","15.700650236477786237"
|
||||
"01/27/2026 21:51:52.167","10612","70.345731561618336514","0","6618651.9876699792221","0.00052081891891891897092","74.178198285741828499","3.8634121618201175963"," ","2282975232"," ","4967.9344689748177188"," ","845738.71812254574616"," ","3560.5535177156079953"," ","2412523520"," ","122.17073870718849093"," ","57","7.5888002086651145106"
|
||||
"01/27/2026 21:51:53.170","10608","70.345448742235419104","0","5985047.7535923402756","0.00016649228855721392149","400.95543091138961245","6.6755401380790608812"," ","2283057152"," ","7995.1709805614409561"," ","4822575.2270185705274"," ","2766.7919536024746776"," ","2414170112"," ","118.44039680275758997"," ","57","15.844981219093289937"
|
||||
"01/27/2026 21:51:54.166","10824","69.66960622688513638","0","8589344.9906502962112","0.0018326512195121951246","41.15717100463248812","7.5424846783728112243"," ","2077839360"," ","2497.537596573796236"," ","452199.86082853202242"," ","1873.1531974303470633"," ","2123825152"," ","117.63377903088738208"," ","57","5.8886595081816590636"
|
||||
"01/27/2026 21:51:55.156","11042","69.546425125921700783","0","0","0","0","0.15782502707108853057"," ","2036101120"," ","1955.7523160915986864"," ","341986.93121045309817"," ","1467.8244397113082869"," ","2109165568"," ","110.49166188248294418"," ","57","16.345889455455552053"
|
||||
"01/27/2026 21:51:56.170","11047","69.609484185839420434","0","5081998.0377077050507","0.00024019839572192515126","184.28518437781971784","4.5038887948840429232"," ","2037841920"," ","5936.5451908662353162"," ","4457263.3788087414578"," ","1262.4027871015350684"," ","2129428480"," ","132.42528922422286541"," ","57","13.769579109808383066"
|
||||
"01/27/2026 21:51:57.172","11051","69.662644381090686352","0","5601166.2441976014525","0.0001617604240282685484","282.47783971328999542","4.5694464893083424073"," ","2038771712"," ","5777.3206228287017439"," ","4453791.1670277491212"," ","1100.9648664443775488"," ","2150416384"," ","106.05565301584844917"," ","57","14.219692413651985774"
|
||||
"01/27/2026 21:51:58.170","11153","69.217565159596873059","0","5608201.4268641658127","0.00099406376811594205321","69.161124672148702075","7.0715037672889806686"," ","1926578176"," ","1323.0823850324100022"," ","229492.6490244322049"," ","992.31178877430750163"," ","1982783488"," ","106.49704118644018536"," ","57","4.4658895239286566792"
|
||||
"01/27/2026 21:51:59.169","11159","69.242551636240108337","0","0","0","0","0"," ","1927266304"," ","1337.704369136717105"," ","247286.06717819173355"," ","1003.2782768525377151"," ","2009829376"," ","107.94752075896532517"," ","57","1.4392201765968892779"
|
||||
"01/27/2026 21:52:00.168","11161","69.250797054090256211","0","3183767.7654357752763","0.00097802222222222222683","9.0033249278958713546","0.88056861711551592808"," ","1927278592"," ","1568.5792763267475038"," ","294128.62169999378966"," ","1176.4344572450606847"," ","2014027776"," ","107.85519987362997085"," ","57","6.2128696751043710478"
|
||||
"01/27/2026 21:52:01.168","11164","69.256529717067380147","0","3953744.280777621083","8.0767295597484269109e-005","159.0444211068151219","1.2845569783842929468"," ","1927331840"," ","1564.4369472393639171"," ","285472.73253419680987"," ","1173.3277104295229947"," ","2016792576"," ","110.96833810115830943"," ","57","6.2239396328239600109"
|
||||
"01/27/2026 21:52:02.167","11171","69.208177544457882391","0","0","0","0","0"," ","1927405568"," ","1561.7754263046231245"," ","292495.50889451126568"," ","1171.331569728467457"," ","2019667968"," ","109.49760446772249622"," ","57","0"
|
||||
"01/27/2026 21:52:03.170","11174","69.217837105788717622","0","3835545.6203418713994","3.8384448818897639254e-005","506.6001624311701903","1.944587440466063688"," ","1927528448"," ","9697.2046840171242366"," ","8676928.9100357890129"," ","1147.8283207840097475"," ","2023600128"," ","104.40066960034002363"," ","57","3.3904251459540013514"
|
||||
"01/27/2026 21:52:04.171","11178","69.241355072995958153","0","5225551.4330876432359","0.0001444529411764705726","169.83617602460665807","2.4532940545207315708"," ","1927561216"," ","5566.630428288872281"," ","4492144.8770515955985"," ","1106.9322531486127446"," ","2030944256"," ","109.26784102830353618"," ","57","4.7808813896211947991"
|
||||
"01/27/2026 21:52:05.169","11182","69.264949175823701921","0","4022810.2774396105669","0.00010643759398496240975","133.15339270840007657","1.417270269501807789"," ","1927696384"," ","5538.3802140065354251"," ","4468549.7693342734128"," ","1079.2432882680848252"," ","2040180736"," ","103.24521873269235073"," ","57","1.4477457551573036376"
|
||||
"01/27/2026 21:52:06.159","11165","69.384845152819735858","0","24825.641687683000782","0.00015348000000000000097","5.0507897313808189921","0.077518886516047283419"," ","1876365312"," ","1438.4649154972571523"," ","250078.74181191221578"," ","910.15230959482357775"," ","1932718080"," ","105.75004472437711911"," ","57","5.298467411005558958"
|
||||
"01/27/2026 21:52:07.152","11267","68.847902251604921275","0","5296760.8292625145987","0.0034185142857142856651","7.0499077066368229794","2.4100513612384948381"," ","1820516352"," ","1361.6393170532835484"," ","265481.39589783997508"," ","1021.2294877899626044"," ","1863147520"," ","102.287895065435535"," ","57","8.727724403149817789"
|
||||
"01/27/2026 21:52:08.169","11271","68.867950180747541822","0","4083020.5788267301396","3.8631346153846156898e-005","511.63698647389236385","1.9765244976949747358"," ","1820717056"," ","1405.0338782398428066"," ","281079.58552681293804"," ","1053.7754086798820481"," ","1872273408"," ","103.00394177329224021"," ","57","9.2950363488919052202"
|
||||
"01/27/2026 21:52:09.170","11273","68.879904963280978336","0","3373549.3049142681994","0.00023990238095238097674","41.929767639204328589","1.0061301052173297066"," ","1820778496"," ","1433.5987221404145657"," ","279585.69396261259681"," ","1075.1990416053110948"," ","1879023616"," ","106.09605462473949444"," ","57","4.825598057218972059"
|
||||
"01/27/2026 21:52:10.169","11273","68.898473486513481134","0","0","0","0","0"," ","1820868608"," ","1433.7174501335148307"," ","311026.57873867102899"," ","1075.288087600136123"," ","1885958144"," ","107.91623988139798485"," ","57","1.4677809778540162888"
|
||||
"01/27/2026 21:52:11.170","11276","68.876434925216358351","0","3155191.4725937340409","5.8902898550724636496e-005","206.81485933792069432","1.218205675128568144"," ","1820938240"," ","1426.7227977514528448"," ","278181.97149911400629"," ","1070.0420983135898041"," ","1890983936"," ","103.03329006851001282"," ","57","0.088930842656953501546"
|
||||
"01/27/2026 21:52:12.170","11281","68.920990701047941229","0","3163592.8778261104599","5.8719323671497581415e-005","207.09694207858697723","1.2160476794658030553"," ","1821495296"," ","5310.4858384209646829"," ","4463004.1322342986241"," ","910.4261704904065482"," ","1917403136"," ","106.29874860611077736"," ","57","4.6437696327535800123"
|
||||
"01/27/2026 21:52:13.170","11284","68.964578367062983943","0","3462856.7556923464872","0.0002172895833333333374","47.96731986497598399","1.0423006223683655147"," ","1821605888"," ","1319.1012962868396698"," ","277259.10337287205039"," ","989.32597221512969554"," ","1933033472"," ","110.86417443502867286"," ","57","4.7504980206091733663"
|
||||
"01/27/2026 21:52:14.167","11283","68.994818853446005846","0","0","0","0","0"," ","1821683712"," ","1323.6434609907230424"," ","257075.62636914369068"," ","992.73259574304233865"," ","1945399296"," ","106.54333511022547043"," ","57","5.9911749027422249725"
|
||||
"01/27/2026 21:52:15.169","11284","69.128747277224604773","0","2547943.0252250363119","4.598987341772151912e-005","236.64103920762596545","1.088325335346309819"," ","1821855744"," ","1377.9098485507333862"," ","225839.42417746523279"," ","866.68532503046139936"," ","1974722560"," ","109.21096513037417708"," ","57","6.3906013168221313947"
|
||||
"01/27/2026 21:52:16.170","11378","68.564403003213087118","0","2744428.3456138232723","5.7699761336515509921e-005","418.39207631311705882","2.4140361219733184051"," ","1717841920"," ","1162.3111618817858925"," ","236884.8063763352111"," ","872.73192051948524295"," ","1761341440"," ","107.65267883300451501"," ","57","6.3889749278221552586"
|
||||
"01/27/2026 21:52:17.168","11378","68.589280701379578886","0","0","0","0","0"," ","1718079488"," ","1350.7081698806107397"," ","274689.75295467412798"," ","1013.0311274104579979"," ","1775206400"," ","109.59593444424615427"," ","57","2.9293152065248295735"
|
||||
"01/27/2026 21:52:18.168","11380","68.630181525049309244","0","3351713.1657754178159","0.00029478181818181817654","33.011672927547181189","0.97313120305118128162"," ","1718226944"," ","1336.4725767031220585"," ","261361.41739719163161"," ","1002.3544325273416007"," ","1791643648"," ","106.28835946893234166"," ","57","7.7792175196028212625"
|
||||
"01/27/2026 21:52:19.169","11381","68.672616120584450528","0","2274787.5192818092182","4.2674324324324325937e-005","221.74767332252628194","0.94627052827319946271"," ","1718464512"," ","1302.5178649215058613"," ","255188.62086831394117"," ","976.88839869112939596"," ","1808556032"," ","106.12669139726551748"," ","57","3.2374284319049650982"
|
||||
"01/27/2026 21:52:20.169","11380","68.680002207094801747","0","696272.86232722050045","4.2639743589743588027e-005","77.994719757472424249","0.33256585571887481434"," ","1718943744"," ","1311.9111836128693085"," ","260960.3329854568874"," ","983.93338770965203821"," ","1824301056"," ","110.92944652218248791"," ","57","1.5696460436972259345"
|
||||
"01/27/2026 21:52:21.169","11387","68.684636188830396009","0","0","0","0","0"," ","1718800384"," ","1356.4346016463675824"," ","274109.82478786201682"," ","1017.3259512347756299"," ","1833156608"," ","103.15905280333038263"," ","57","3.0930110029320667664"
|
||||
"01/27/2026 21:52:22.168","11388","68.697874566702608945","0","1975568.565649635857","4.4211764705882350965e-005","187.12280869934940597","0.82730569083367377914"," ","1718857728"," ","5495.6067667209990759"," ","4497437.6683417325839"," ","1048.688254101166649"," ","1839206400"," ","104.75659745167914139"," ","57","1.497527769316642221"
|
||||
"01/27/2026 21:52:23.170","11389","68.720903030764517894","0","3082931.8350497144274","4.1107430340557274851e-005","322.42978292889023351","1.325413150336072432"," ","1718882304"," ","9559.0947409506279655"," ","8673398.0954681634903"," ","1038.1640069537022555"," ","1848311808"," ","112.3003075930380561"," ","57","3.2969573504394444896"
|
||||
"01/27/2026 21:52:24.169","11393","68.737187205985463834","0","4635161.5854253908619","5.3479027355623097917e-005","329.47493812330475293","1.7620200389145388442"," ","1718923264"," ","1406.0267876143459489"," ","294844.01765144453384"," ","1055.5215342916815189"," ","1853861888"," ","107.96936869577808693"," ","57","4.5488189790947259894"
|
||||
"01/27/2026 21:52:25.170","11392","68.763479047632657171","0","53170.105795010305883","0.00011891428571428570642","6.9897600015976593113","0.08311651379545409446"," ","1718984704"," ","1393.9578517471902614"," ","289031.56875177862821"," ","1045.4683888103927529"," ","1862746112"," ","107.65256059073035999"," ","57","3.265593785991438569"
|
||||
"01/27/2026 21:52:26.170","11386","68.783820669349239552","0","2581663.6928031505086","0.00014103770491803277096","61.027981329439548119","0.8887668747299327654"," ","1719009280"," ","1396.6403596048787676"," ","281335.99255258537596"," ","1047.4802697036591326"," ","1871671296"," ","103.17455473864096405"," ","57","3.0815805480855384957"
|
||||
"01/27/2026 21:52:27.170","11499","68.429397667858140153","0","2608781.8138606129214","3.9983050847457625844e-005","294.95814543916219463","1.1793517554843357953"," ","1719103488"," ","1363.8064758610753415"," ","300029.42582447547466"," ","1022.8548568958065061"," ","1883553792"," ","109.36125329046137722"," ","57","4.6943810656870592624"
|
||||
"01/27/2026 21:52:28.161","11588","67.92617618756511888","0","1157020.7079952240456","3.6635329341317364785e-005","168.4766132770668321","0.61720598504485313374"," ","1616953344"," ","1154.1152430476913651"," ","200412.51549202989554"," ","697.10981900870172012"," ","1670135808"," ","111.91593626438501019"," ","57","10.156843831738360251"
|
||||
"01/27/2026 21:52:29.154","11521","68.474540852120739487","0","144337.32244400057243","0.00014359999999999999393","12.081807124218798322","0.17350364184466429696"," ","1618296832"," ","708.79935128750287276"," ","164595.49253953443258"," ","531.59951346562706931"," ","1778860032"," ","103.83335112134986389"," ","57","8.75250962063195459"
|
||||
"01/27/2026 21:52:30.169","11383","68.932401591197674406","0","3250164.1930545722134","3.9614784946236560189e-005","366.57054044026898509","1.4521451429747143091"," ","1618800640"," ","5317.2436457411058655"," ","4412382.9893531948328"," ","961.75496631640464784"," ","1806462976"," ","101.61872594900312095"," ","57","53.809670023180402154"
|
||||
"01/27/2026 21:52:31.168","11315","69.245673592492394732","0","4198254.5575386434793","0.0028305249999999999681","4.0037675452600893777","1.1332664309669473468"," ","1618788352"," ","5653.3197739072465993"," ","4508249.2625560648739"," ","998.94000254239233527"," ","1834594304"," ","112.60497035336341298"," ","57","10.854398470253967091"
|
||||
"01/27/2026 21:52:32.170","11490","68.264358506599521093","0","159474.66324125183746","0.00022950999999999998962","9.9831394757394242845","0.22911786477443854548"," ","1523314688"," ","1253.8823181528716759"," ","273488.10593788151164"," ","940.4117386146537001"," ","1588318208"," ","107.62829417452030611"," ","57","40.726446686496068139"
|
||||
"01/27/2026 21:52:33.170","11494","68.365566285412199932","0","6533100.7998062018305","0.00024039172932330829871","265.83255207544766563","6.3903525406079033644"," ","1523625984"," ","1427.1010690366138078"," ","297048.88890487881145"," ","1070.3258017774603559"," ","1626517504"," ","104.62090879155417156"," ","57","4.748127816644709398"
|
||||
"01/27/2026 21:52:34.170","11499","68.427711601468672598","0","5296768.9090379942209","0.00028078372093023255644","172.02081451855676164","4.8299832842582199888"," ","1523752960"," ","1524.1844263155842327"," ","323104.09559556707973"," ","1143.1383197366881177"," ","1649709056"," ","109.38639806267813981"," ","57","0"
|
||||
"01/27/2026 21:52:35.170","11504","68.592304752346194618","0","5870508.5471239397302","0.00026199523809523807966","231.01686423108887425","6.0525191236058635269"," ","1524805632"," ","1416.1033755464147816"," ","266127.42730219307123"," ","895.06533976980313128"," ","1711206400"," ","104.69492287003147624"," ","57","6.2456873016158764855"
|
||||
"01/27/2026 21:52:36.169","11588","68.239176219384916067","0","0","0","0","0"," ","1433886720"," ","913.58140941970555104"," ","218970.03713427943876"," ","685.18605706477910644"," ","1577254912"," ","106.43714843809870274"," ","57","7.6501212081202414339"
|
||||
"01/27/2026 21:52:37.169","11591","68.399885987956054123","0","4772604.4457504795864","0.00030543749999999999069","87.957472562016263851","2.6865164132524612661"," ","1434279936"," ","1383.3311593844375693"," ","330937.99148111883551"," ","1037.4983695383282338"," ","1642655744"," ","107.75900842821624792"," ","57","3.1730648905883174216"
|
||||
"01/27/2026 21:52:38.170","11595","68.40927360309503058","0","5460453.6617471762002","0.00090117777777777769745","44.936581003230138265","4.0495751156589934183"," ","1434284032"," ","1829.4181421759469686"," ","442200.92183900863165"," ","1372.0636066319602833"," ","1642659840"," ","113.90147748128471505"," ","57","4.8220530635840086475"
|
||||
"01/27/2026 21:52:39.170","11604","68.392782744111684679","0","4308829.0992017518729","0.0015428000000000002042","11.010049973615917196","1.6986344203580698853"," ","1434284032"," ","1833.6737774240327781"," ","452406.9570704139187"," ","1376.2562467019895394"," ","1642659840"," ","106.347318431096312"," ","57","6.164130796091504827"
|
||||
"01/27/2026 21:52:40.168","11605","68.352186486206989002","0","0","0","0","0"," ","1434284032"," ","1837.9701201718121411"," ","438786.33507256431039"," ","1378.4775901288592195"," ","1642659840"," ","104.80615104359645784"," ","57","1.4509326007973610828"
|
||||
"01/27/2026 21:52:41.168","11607","68.329604032526162882","0","3041500.0465847379528","0.0019922285714285715291","7.0052237953842180218","1.3954790625105695234"," ","1434284032"," ","10027.47748996426526"," ","8800582.5944406744093"," ","1374.024610151790057"," ","1642659840"," ","114.13767107016681734"," ","57","4.6228151932640741961"
|
||||
"01/27/2026 21:52:42.169","11607","68.328374826425772426","0","0","0","0","0"," ","1434284032"," ","5921.8115232742065928"," ","4551605.9619424780831"," ","1375.6332230397599687"," ","1642659840"," ","109.18892024212337333"," ","57","3.2912906257689944489"
|
||||
"01/27/2026 21:52:43.170","11606","68.328940465191607245","0","0","0","0","0"," ","1434284032"," ","2047.0198868781626516"," ","372783.51125481119379"," ","1368.3448364922874134"," ","1643044864"," ","110.88623729249967198"," ","57","7.8554946262074150098"
|
||||
"01/27/2026 21:52:44.170","11672","67.840164799205993518","0","0","0","0","0"," ","1347993600"," ","1407.780386259743409"," ","267300.30115302011836"," ","1055.8352896948076705"," ","1435299840"," ","118.73005335103702862"," ","57","10.952459986722228535"
|
||||
"01/27/2026 21:52:45.170","11674","68.021477262494499882","0","2239769.5164053118788","9.0798701298701301431e-005","307.89793183559652334","2.7956872081265466967"," ","1348235264"," ","5762.0898672090206674"," ","4531607.7720235744491"," ","1252.5847681493585242"," ","1500983296"," ","110.90129072857712345"," ","57","1.5946293535160749322"
|
||||
"01/27/2026 21:52:46.170","11679","68.202604811685759501","0","4129973.9523940989748","6.5709137055837562474e-005","197.05754080191414346","1.2948548308557303876"," ","1348526080"," ","1716.5012183557598746"," ","343563.32048958295491"," ","1288.3762058521087965"," ","1570168832"," ","106.28158688762299278"," ","57","1.5332356776433964107"
|
||||
"01/27/2026 21:52:47.170","11751","67.47875820553832682","0","0","0","0","0"," ","1267175424"," ","1779.6809032140536146"," ","366958.20439395215362"," ","1335.7604981426829909"," ","1300090880"," ","104.66886894132845498"," ","57","7.8289064546510740428"
|
||||
"01/27/2026 21:52:48.170","11753","67.831114415971356379","0","3665705.5562249608338","7.063435897435897475e-005","194.98859316729971169","1.3775355797766892785"," ","1267761152"," ","1599.9064054752798256"," ","315235.55871981492965"," ","1199.9298041064598692"," ","1445130240"," ","104.70008495021102135"," ","57","3.113354225177866752"
|
||||
"01/27/2026 21:52:49.169","11770","67.858755092103677953","0","3424651.4303509052843","8.5942281879194625774e-005","149.19567012136417361","1.282036136170911389"," ","1267572736"," ","2371.1097103851702741"," ","394777.75102046335815"," ","1779.3335960111687655"," ","1453629440"," ","109.50278975564484085"," ","57","4.5761403557952107235"
|
||||
"01/27/2026 21:52:50.171","11765","68.094989975936471183","0","3606016.7355129374191","5.3705952380952382756e-005","251.5357655910252106","1.3508657722180332783"," ","1267998720"," ","6420.1509693709285784"," ","4584962.9923014082015"," ","1583.078270743515759"," ","1527201792"," ","109.17100306367521512"," ","57","11.10361179100732798"
|
||||
"01/27/2026 21:52:51.169","11831","67.625185327517144174","0","65652.283324223870295","0.00017369999999999999373","1.0017743427158183334","0.017400438587220623532"," ","1190428672"," ","2007.5557828024998344"," ","366000.25965990964323"," ","1506.6686114445906242"," ","1349922816"," ","108.00142689606929025"," ","57","1.3900015296758660988"
|
||||
"01/27/2026 21:52:52.169","11904","67.192724432608287088","0","3145536.1222965396009","0.0026756333333333333357","2.9998170111623188028","0.80264697812197272064"," ","1116168192"," ","2003.8777634564289656"," ","337459.41497568646446"," ","1335.9185089709526437"," ","1189756928"," ","104.68188905074688932"," ","57","4.6926084761856774463"
|
||||
"01/27/2026 21:52:53.169","12032","67.104200266093059213","0","1936818.0452234249096","4.6982485875706210391e-005","176.94610221726460964","0.83134483640774337054"," ","983527424"," ","1403.5724718250821752"," ","95648.865355612681014"," ","885.73020657907602526"," ","1157603328"," ","106.21867611241444251"," ","57","10.963756788123180996"
|
||||
"01/27/2026 21:52:54.169","12875","64.284719727999700467","0","1425976.3941907244734","5.3624271844660192693e-005","103.04107217136750307","0.63337741194332253247"," ","89731072"," ","3822.5236579300508311"," ","68664.369617729622405"," ","587.23407150090031337"," ","64794624"," ","106.29487769736383029"," ","42","7.7735619978755003956"
|
||||
"01/27/2026 21:52:55.169","12799","64.438728723777160212","0","126998.7200710206962","0.00029823333333333333639","3.000536796032810205","0.089485964296030409693"," ","172609536"," ","11118068.022369202226"," ","11118180.042409587651"," ","1.000178932010936661"," ","150069248"," ","1358.0547769722118119"," ","43","95.311663600786374673"
|
||||
"01/27/2026 21:52:56.169","12800","64.459168250779427467","0","1224374.7656255231705","3.7970414201183433486e-005","168.95456811663342478","0.64159721612597653273"," ","178470912"," ","11192136.434512758628"," ","11192136.434512758628"," ","0"," ","160227328"," ","1399.7757559239009879"," ","43","100"
|
||||
"01/27/2026 21:52:57.170","12800","64.477943457774330227","0","2541887.4526651543565","9.3185714285714285566e-005","90.956468234303059717","0.84748176518541828983"," ","183078912"," ","11570004.595800448209"," ","11570004.595800448209"," ","0"," ","164810752"," ","1397.5993596640093983"," ","43","98.438436469649147398"
|
||||
"01/27/2026 21:52:58.169","12811","64.445549148238626458","0","2273558.7383013158105","0.0015530999999999999667","4.0004904601304120959","0.69753681219245655676"," ","182304768"," ","12124985.523225147277"," ","12124985.523225147277"," ","0"," ","163053568"," ","1400.2545662801496746"," ","43","100"
|
||||
"01/27/2026 21:52:59.168","12819","64.354555696331559034","0","36923.095414210445597","0.00011594285714285714796","7.0112214599466442522","0.081283217228997436954"," ","180166656"," ","13016855.477191245183"," ","13016855.477191245183"," ","0"," ","162537472"," ","1402.1254820181914056"," ","43","96.869869089411366758"
|
||||
"01/27/2026 21:53:00.169","12858","64.220801313459134008","0","1414978.3532321983948","5.0420720720720715456e-005","110.82474175339118005","0.55878036577964218523"," ","176037888"," ","12653208.216526383534"," ","12653208.216526383534"," ","0"," ","151994368"," ","1394.6545704583732004"," ","43","98.440041017187112971"
|
||||
"01/27/2026 21:53:01.168","12859","64.260842782505989135","0","2083708.9468075239565","0.0001200519230769230781","52.073496533006682796","0.62518433208567525394"," ","184410112"," ","12455951.329706747085"," ","12455951.329706747085"," ","0"," ","167632896"," ","1402.0504988545746983"," ","43","98.435347037523641234"
|
||||
"01/27/2026 21:53:02.169","12870","64.180302984123187571","0","0","0","0","0"," ","164106240"," ","8933820.4769572746009"," ","8933820.4769572746009"," ","0"," ","137252864"," ","808.64043103245023758"," ","43","71.900525562578948779"
|
||||
"01/27/2026 21:53:03.164","12691","64.694032483895782093","0","4990849.4276560340077","0.00081619999999999999843","19.085666011894389271","1.5578011656336272495"," ","70488064"," ","20955554.004147615284"," ","69340084.971394106746"," ","6.0270524248087538055"," ","44609536"," ","69.061266032133431736"," ","42","13.673417459833213883"
|
||||
"01/27/2026 21:53:04.169","12761","64.504017742468050756","0","3091323.2170643433928","5.8458910891089107121e-005","201.12526599314261944","1.1759938590627820876"," ","70488064"," ","0"," ","0"," ","0"," ","44609536"," ","0"," ","42","3.5250238411450807163"
|
||||
"01/27/2026 21:53:05.169","12778","64.441295891171620269","0","6685720.9896232718602","0.00028733437500000000298","64.010043175774285373","1.8388333476658353938"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0.005799663619510120327"
|
||||
"01/27/2026 21:53:06.170","12774","64.435378337380328162","0","0","0","0","0"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","1.7022989903758833918"
|
||||
"01/27/2026 21:53:07.167","12784","64.423793383041356719","0","4609387.847764544189","0.0018197631578947368481","38.104589477197009728","6.9329905821973598634"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0"
|
||||
"01/27/2026 21:53:08.170","12788","64.420519155548078061","0","4185490.194747899659","0.0028377250000000001431","3.9915945003012653913","1.1324953399266939336"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0.22858628596024166413"
|
||||
"01/27/2026 21:53:09.169","12792","64.407737637964942223","0","4194958.8330738423392","0.0028811250000000000054","4.0006244974840567963","1.1528838301852986081"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0"
|
||||
"01/27/2026 21:53:10.169","12792","64.413165711893967114","0","0","0","0","0"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0"
|
||||
"01/27/2026 21:53:11.168","12874","64.189027031927608391","0","3149801.0076830349863","0.0027905000000000000117","3.0038843228178357947","0.83821209855007272616"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0"
|
||||
"01/27/2026 21:53:12.168","12878","64.19145280127213482","0","3163343.2505624974146","0.0010999000000000000096","8.9947659456961996938","0.98911751904374212163"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","0.080055859245575788918"
|
||||
"01/27/2026 21:53:13.170","12881","64.188526655591218173","0","2093272.7469453609083","0.0019350666666666665756","2.9944506839924254216","0.57961608870965730667"," ","70406144"," ","0"," ","0"," ","0"," ","44494848"," ","0"," ","41","1.7157742500570338784"
|
||||
"01/27/2026 21:53:14.160","12866","64.376485502435016883","0","0","0","0","0"," ","70426624"," ","452.55249709982535933"," ","452.55249709982535933"," ","0"," ","44494848"," ","0"," ","41","5.3241936928644095772"
|
||||
|
196
arnis_before.csv
Normal file
196
arnis_before.csv
Normal file
@@ -0,0 +1,196 @@
|
||||
"(PDH-CSV 4.0) (Mitteleurop<6F>ische Zeit)(-60)","\\ROADRUNNER\Arbeitsspeicher\Verf<72>gbare MB","\\ROADRUNNER\Arbeitsspeicher\Zugesicherte verwendete Bytes (%)","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Aktuelle Warteschlangenl<6E>nge","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Bytes geschrieben/s","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Mittlere Sek./Schreibvorg<72>nge","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Schreibvorg<72>nge/s","\\ROADRUNNER\Physikalischer Datentr<74>ger(0 C:)\Zeit (%)","\\ROADRUNNER\Prozess(arnis-windows)\Arbeitsseiten","\\ROADRUNNER\Prozess(arnis-windows)\E/A-Bytes gelesen/s","\\ROADRUNNER\Prozess(arnis-windows)\E/A-Datenbytes/s","\\ROADRUNNER\Prozess(arnis-windows)\E/A-Schreibvorg<72>nge/s","\\ROADRUNNER\Prozess(arnis-windows)\Private Bytes","\\ROADRUNNER\Prozess(arnis-windows)\Prozessorzeit (%)","\\ROADRUNNER\Prozess(arnis-windows)\Threadanzahl","\\ROADRUNNER\Prozessorinformationen(0,0)\Prozessorzeit (%)"
|
||||
"01/27/2026 21:31:33.091","11421","64.050442880031283721","0"," "," "," "," ","31358976"," "," "," ","8216576"," ","39"," "
|
||||
"01/27/2026 21:31:34.097","11408","64.115242395576842682","0","246727.30288993721479","4.1506060606060607952e-005","33.199051553398710723","0.56725460153538675989","31358976","225.35113781700945879","225.35113781700945879","0","8216576","0","39","8.3468561592205166022"
|
||||
"01/27/2026 21:31:35.101","11387","64.1610600203650705","0","191824.80681207642192","0.00085968260869565221814","22.91789891891288633","16.690626749736260592","31358976","0","0","0","8216576","0","39","15.925976596382607653"
|
||||
"01/27/2026 21:31:36.096","11388","64.180194205646444061","0","177491.21235621796222","7.215000000000000547e-005","18.086740389560254982","0.67737982265856211406","31420416","225.07943595897205569","614.94917324504865519","3.0144567315933756824","8216576","1.5700557362250091575","39","10.506823035174473802"
|
||||
"01/27/2026 21:31:37.107","11380","64.110825970794721229","0","2007210.325699219713","0.0020881878787878788671","65.289416110816318906","16.830145160593072973","35909632","443.17664269160161439","4163470.4529779683799","4.9461678871830541127","9101312","0","39","2.6226761380818586211"
|
||||
"01/27/2026 21:31:38.099","11384","64.081042344700790636","0","0","0","0","0.27008660793354716256","37519360","505413.64501628652215","510546.31371673452668","6.0479206996637859817","9785344","6.2996018046897663822","44","10.227190600185853242"
|
||||
"01/27/2026 21:31:39.096","11399","64.048648035165072656","0","1051167.1269679761026","0.0025612999999999998615","1.0024710912399064089","1.5267090626088362093","37535744","449.10704887547808539","449.10704887547808539","0","9785344","0","44","2.8845323906524944491"
|
||||
"01/27/2026 21:31:40.091","11400","64.019734636885047507","0","1346647.1964355160017","0.00041656249999999999485","8.0433342677006649524","0.33505285238813004023","37588992","337.82003924342791379","563.03339873904656088","2.0108335669251662381","9785344","0","44","7.313773954896507945"
|
||||
"01/27/2026 21:31:41.097","11403","63.998490144498298093","0","1042045.915068630944","0.0025485999999999998239","0.99377242571700186158","0.67652123119748863722","37588992","222.60502336060841344","222.60502336060841344","0","9785344","0","44","3.7218092488283671671"
|
||||
"01/27/2026 21:31:42.100","11412","63.961516242465357607","0","236949.53801083788858","0.00042709999999999997316","3.9895867795467050421","0.26616860044348800152","37588992","558.5421491365386828","558.5421491365386828","0","9785344","0","44","1.8269683300157657513"
|
||||
"01/27/2026 21:31:43.091","11421","63.936007619820536263","0","143113.12005148778553","7.3613333333333335522e-005","30.272687285259429757","0.2228481290605907883","37588992","0","0","0","9785344","0","44","0"
|
||||
"01/27/2026 21:31:44.105","11422","63.933005315236044908","0","82291.606682091078255","8.2508333333333337602e-005","11.832573813567544008","0.097629614875278436514","37588992","331.3120667798912109","331.3120667798912109","0","9785344","0","44","0"
|
||||
"01/27/2026 21:31:45.106","11422","63.840934975035700916","0","98184.17603157107078","6.2114999999999997364e-005","19.975621751214816868","0.12407633146348247266","37588992","0","0","0","9785344","0","44","0.12369680151133044532"
|
||||
"01/27/2026 21:31:46.107","11450","63.83411452729117741","0","36814.910997675695398","0.0002331333333333333261","2.9960051267639729033","0.069862655054589065107","37588992","0","0","0","9785344","0","44","1.671359637745051252"
|
||||
"01/27/2026 21:31:47.107","11449","63.839118360504301108","0","106591.7407014980854","9.9259999999999994937e-005","10.008990074885261379","0.099326658120264504914","37588992","0","0","0","9785344","0","44","3.0557818282253568221"
|
||||
"01/27/2026 21:31:48.107","11449","63.84364356376315186","0","90035.649768995892373","0.0002737999999999999874","2.9974581554841495112","0.082071404719935084349","37564416","0","0","0","9728000","0","43","1.6489210930355491236"
|
||||
"01/27/2026 21:31:49.100","11449","63.901600885182062939","0","41257.088166175810329","0.00010826249999999999556","8.058025032456214376","0.087241190009720331888","38756352","225.62470090877397411","338.43705136316094695","1.007253129057026797","10551296","0","43","5.5667756216224333343"
|
||||
"01/27/2026 21:31:50.091","11434","63.934506467528294138","0","24816.266125372632814","0.00033240000000000000232","2.0195529073382676444","0.067127112073095851486","48848896","0","0","0","21000192","1.5777092751836985229","43","5.3374434889780992819"
|
||||
"01/27/2026 21:31:51.105","11431","63.957654583258005232","0","0","0","0","0","60133376","0","0","0","32718848","1.5396153119644908625","43","3.0042353462370718908"
|
||||
"01/27/2026 21:31:52.108","11416","63.982249462041593802","0","0","0","0","0","71974912","0","0","0","45096960","1.5586280561828305125","43","3.3614648317033091196"
|
||||
"01/27/2026 21:31:53.104","11401","64.011695879514263652","0","4110.6495328050104945","0.0006143999999999999722","1.0035765460949732653","0.061661531383802592465","84168704","0","0","0","57466880","9.4088030065616745645","43","7.483567074376241024"
|
||||
"01/27/2026 21:31:54.106","11395","64.010912683794956024","0","0","0","0","0","96387072","0","0","0","70115328","1.5603024700012502191","43","4.8215493299237355274"
|
||||
"01/27/2026 21:31:55.103","11392","64.015992652628568749","0","0","0","0","0","107159552","0","0","0","81117184","3.1336630113950012522","43","1.2896151410574541174"
|
||||
"01/27/2026 21:31:56.107","11375","64.044296872812395804","0","191652.22893577121431","0.00015568333333333334088","11.946407222558878658","0.18598557571455665016","118534144","0","0","0","93200384","1.5555211543410489838","43","3.5576884308549661107"
|
||||
"01/27/2026 21:31:57.105","11371","64.0751465372916158","0","0","0","0","0.067868783809043237154","130277376","0","0","0","105025536","0","43","5.9884976049378924046"
|
||||
"01/27/2026 21:31:58.107","11358","64.108791822592920084","0","110285.84649014337629","0.00029033333333333336121","2.9916950545286287166","0.08685816082282102335","142499840","0","0","0","117673984","1.5581616106275297806","43","4.9521417517206849368"
|
||||
"01/27/2026 21:31:59.106","11360","64.091822333655272814","0","41028.903941278986167","0.0001168571428571428593","7.0117755759021704876","0.081937663468393248656","153161728","0","0","0","128679936","3.1302591483952189044","43","1.3968368255506069531"
|
||||
"01/27/2026 21:32:00.108","11346","64.119256326025180215","0","0","0","0","0","164245504","0","0","0","139956224","1.5596997151314528907","43","4.8583173769813692289"
|
||||
"01/27/2026 21:32:01.106","11394","64.137433274681072248","0","45141.516088077252789","0.00024409999999999999732","3.0056939866883825019","0.199841617552878964","156962816","0","0","0","145809408","10.958426210691937897","43","2.9396535624428454803"
|
||||
"01/27/2026 21:32:02.108","11186","64.600689421547741631","0","8176.7519928836700274","0.00032000000000000002618","1.9962773420126147528","0.063877527616301263413","317206528","670.74918691623850009","1006.123780374357807","2.9944160130189221292","297693184","99.808636900470730779","42","17.343406865969413388"
|
||||
"01/27/2026 21:32:03.106","10722","66.327606855502267535","0","270664.88490164402174","8.4250000000000001429e-005","26.031631034870471808","0.21932538495334744089","808951808","2177724.15261784615","2177836.2888746117242","1.0012165782642488132","949456896","111.07696824132379732","51","6.1350623838890676609"
|
||||
"01/27/2026 21:32:04.105","10609","66.283246866958961618","0","0","0","0","0","920911872","0","0","0","934006784","100.16787133557127731","51","6.0926206229019230776"
|
||||
"01/27/2026 21:32:05.108","10745","65.813529250634260848","0","48981.314812273667485","0.00010804999999999999718","7.9722192077268339006","0.30873040922774841466","789962752","0","0","0","771710976","99.661181879962697394","51","1.8960240869117295226"
|
||||
"01/27/2026 21:32:06.106","10746","65.810972933147795061","0","0","0","0","0","789962752","0","0","0","771710976","100.21529251290547791","51","6.0481632691511189037"
|
||||
"01/27/2026 21:32:07.107","10751","65.770594255479650769","0","4091.6109289565088147","0.00062169999999999998701","0.99892844945227265985","0.062102072752822964907","789962752","0","0","0","771710976","99.890739509124912843","51","3.2308461005352340223"
|
||||
"01/27/2026 21:32:08.108","10763","65.72810527070613773","0","118684.13916530630377","0.00029340000000000003013","2.9974779220763645426","0.087946063744210239976","789950464","0","0","0","771670016","99.916000618280207846","50","3.2049140229826300619"
|
||||
"01/27/2026 21:32:09.109","10763","65.708198762896515177","0","0","0","0","0","789950464","0","0","0","771670016","99.936770005617447055","50","0.064738062682989649943"
|
||||
"01/27/2026 21:32:10.105","10764","65.707176240558538893","0","0","0","0","0","789950464","0","0","0","771670016","100.34158281622298148","50","2.794091646783980476"
|
||||
"01/27/2026 21:32:11.107","10764","65.708655623185592276","0","130780.49028714993619","0.00085130000000000003845","0.99777595739097546534","0.084942870855505323013","789950464","0","0","0","771670016","99.780184254088240436","50","0.21981574591175556677"
|
||||
"01/27/2026 21:32:12.107","10737","65.755332602596681113","0","1213328.4229740765877","0.0013396499999999999554","44.033112900901478781","17.437400217985516093","789950464","0","0","0","771670016","100.07598769745868594","50","10.869823456950866714"
|
||||
"01/27/2026 21:32:13.107","10751","65.702716299729118532","0","67073.428664030550863","6.7353333333333337859e-005","15.00031950680549464","0.33861335227218747335","789950464","0","0","0","771670016","100.0009900098011002","50","3.1240409280051895102"
|
||||
"01/27/2026 21:32:14.107","10751","65.704435008974854782","0","49141.179112359459396","9.6600000000000003464e-005","8.9980186362962868429","0.086918548502229645014","789950464","0","0","0","771670016","99.975326089521104223","50","3.1489028507764249554"
|
||||
"01/27/2026 21:32:15.107","10755","65.701291306340436904","0","350660.98375643382315","8.6031034482758625219e-005","28.995120121283591175","0.24944285530034823739","789950464","0","0","0","771670016","99.98110357142499538","50","3.1401101428177202735"
|
||||
"01/27/2026 21:32:16.107","10757","65.689967425002237178","0","0","0","0","0.041078963429820383735","789950464","0","0","0","771670016","100.02182476216310647","50","1.5442659980628437033"
|
||||
"01/27/2026 21:32:17.107","10995","65.110350624916691231","0","45057.198521480670024","8.7830000000000004422e-005","10.000266007075788721","0.087829200754273140106","668336128","1120.0297927924882515","1680.0446891887324909","5.0001330035378943606","673898496","634.36922724003204621","41","45.309705325942154275"
|
||||
"01/27/2026 21:32:18.100","10835","65.573639414639600886","0","137164.45483544687158","9.0741666666666672292e-005","12.085683872954081863","0.10967126519184715316","833945600","676.79829688542861277","1015.1974453281428623","3.0214209682385204658","854089728","122.74943011531244963","41","10.303931822427713882"
|
||||
"01/27/2026 21:32:19.095","10618","66.200988359330452226","0","0","0","0","0","1061310464","1125.0692244043989376","1687.6038366065981791","5.0226304660910665589","1091837952","98.884596816044052048","41","8.9633870582451535824"
|
||||
"01/27/2026 21:32:20.114","10380","66.903318503616219459","0","0","0","0","0","1317523456","1319.6504693645204043","1979.4757040467807201","5.8912967382344660905","1360404480","101.25514155198776223","41","11.01820893916227817"
|
||||
"01/27/2026 21:32:21.099","10134","67.621127881021507733","0","0","0","0","0","1574141952","1137.0409463673013306","1705.5614195509522233","5.0760756534254527494","1630420992","99.933565434696674856","41","7.9955590154301425798"
|
||||
"01/27/2026 21:32:22.094","9912","68.278478008759776685","0","0","0","0","0","1805234176","899.70986365178180222","1349.5647954776725328","4.0165618913025973313","1878568960","98.84850218724434967","41","2.7227293024845611313"
|
||||
"01/27/2026 21:32:23.114","9696","68.911287646952843033","0","27614.554556935945584","0.0003468999999999999753","1.961260977055109711","0.068035062474485624717","2037628928","658.98368829051685225","988.47553243577522153","2.9418914655826644555","2132140032","101.12591262152392346","41","5.0029305676593356367"
|
||||
"01/27/2026 21:32:24.088","9446","69.665766337342972747","0","0","0","0","0","2302824448","919.80954604216526604","1379.7143190632477854","4.1062926162596662394","2415419392","99.44998894886057883","41","5.3621072906004147995"
|
||||
"01/27/2026 21:32:25.093","9178","70.44045612459080985","0","0","0","0","0.19130000251946385759","2582384640","1115.342108287370138","1673.013162431055207","4.9792058405686168143","2711367680","99.583551545790655268","41","8.1964134187242354557"
|
||||
"01/27/2026 21:32:26.100","8998","71.104953012220789788","0","131060.24399771049502","0.00045479999999999999594","1.9843181322327776428","0.090251158376991283405","2766368768","4889.3598778215646234","7334.0398167323464804","21.827499454560555847","2954424320","100.77103422013166778","41","10.076725266708708162"
|
||||
"01/27/2026 21:32:27.096","8926","71.423402864351729136","0","0","0","0","0","2841747456","1799.7208022416166386","2699.5812033624247306","8.0344678671500737721","3075321856","98.868805276136612292","41","12.121074862700476515"
|
||||
"01/27/2026 21:32:28.090","8895","71.586299075648724966","0","115415.51012653157522","0.00028796666666666665508","3.0190301546769906516","0.086928697136152741076","2877227008","3606.7346914541117258","5410.1020371811673613","16.101494158277283475","3133210624","102.19580741857289752","41","10.38213810986684571"
|
||||
"01/27/2026 21:32:29.107","8769","72.043594175959839276","0","0","0","0","0.077168620664255921371","2935410688","2863.6250680873081365","4295.4376021309617499","12.784040482532624594","3225677824","99.878144975321674792","41","13.951136636645944833"
|
||||
"01/27/2026 21:32:30.105","8466","73.232774593223069814","0","354789.38736084837001","9.315000000000000092e-005","18.024659536712174912","0.16788561549598146616","3104595968","0","0","0","3503640576","100.12859515475724947","41","24.900485633512971617"
|
||||
"01/27/2026 21:32:31.107","8442","73.331458697405508929","0","3272347.3170057502575","0.0031957500000000002398","5.9918468939314273314","2.832207999519033681","3167928320","12303.258955539198723","18454.888433308795356","54.925263194371417796","3563728896","99.868403404833443915","41","15.739467932813278495"
|
||||
"01/27/2026 21:32:32.111","8403","73.396954399858827855","0","136061.19250214289059","0.00013690769230769231617","12.938871595036410156","0.17714236461468302331","3206610944","445.89342112125478934","668.84013168188209875","1.9905956300056015795","3600154624","99.529365442568277444","41","20.687536912953408574"
|
||||
"01/27/2026 21:32:33.103","8341","73.596617829426335788","0","4329182.2302357004955","0.00056137187500000004358","32.272649020721765112","1.811733754338871405","3273838592","1129.5427157252615871","1694.314073587892608","5.0426014094877755767","3668025344","100.8541438295064836","41","13.328470146517879868"
|
||||
"01/27/2026 21:32:34.094","8297","73.715926397991353269","0","3174102.8928102776408","0.0020274833333333332208","6.0541208130078842942","1.227438753682851047","3323801600","2034.1845931706488955","3051.2768897559735706","9.0811812195118264412","3713888256","99.323465310570284714","41","11.712475279493073543"
|
||||
"01/27/2026 21:32:35.107","8292","73.736344178611489042","0","105174.05676313841832","6.54117647058823445e-005","16.788977660882597576","0.10981905828774904399","3329404928","1990.9752331964305085","2986.4628497946459902","8.8882822910554928342","3718004736","100.30124197256755281","41","10.500430239862801329"
|
||||
"01/27/2026 21:32:36.109","8292","73.747135017473979701","0","3139086.9476535441354","0.0020241999999999998709","5.9873331978865511616","1.2119597350596862384","3320193024","2458.7981665987435917","3688.1972498981153876","10.976777529458678018","3704479744","101.34840150182738228","41","11.125247913782132514"
|
||||
"01/27/2026 21:32:37.108","8295","73.748973378387503885","0","1864122.1697786715813","0.0013898199999999999443","5.0011862813859453425","0.69509385232393960941","3322634240","4032.9566173096259263","6049.434925964438662","18.004270612989401457","3704479744","100.02645699787593969","41","10.913936736266737881"
|
||||
"01/27/2026 21:32:38.107","8264","73.746373568136803556","0","123046.35867693120963","0.00029179999999999999124","3.0040614911360159489","0.087655916183561138899","3353731072","3588.8521280771606143","5383.2781921157402394","16.021661286058751728","3704479744","100.13241510573583071","41","9.2549988104269065303"
|
||||
"01/27/2026 21:32:39.105","8255","73.729153902672493359","0","0","0","0","0","3372011520","3815.1155720534366083","5722.673358080154685","17.03176594666712873","3704483840","100.18795259907587081","41","7.6392311977269367063"
|
||||
"01/27/2026 21:32:40.108","8246","73.723965155362151336","0","106194.93735260536778","0.00028513333333333334323","2.9915190435116447709","0.085294386285252987712","3379941376","3797.2348392307808354","5695.8522588461719351","16.951941246565986887","3704483840","99.71286682868013429","41","9.63187140552977894"
|
||||
"01/27/2026 21:32:41.107","8201","73.72801172866671493","0","2364718.095375535544","0.00093906896551724139584","29.022559235293591939","2.7383549634516382021","3385401344","478139.65795612928923","59993269.76859112829","2228.7323936896150371","3705380864","150.11649039654773219","41","29.633747219500971681"
|
||||
"01/27/2026 21:32:42.103","8191","73.731449123875108853","0","45249.868536758891423","0.0006897999999999999618","1.0043028350665590409","0.069280365022555287502","3385413632","712220.43724131665658","1860524.2300110592041","3822.376590263324033","3705393152","133.39081588225752739","41","32.521634505536113124"
|
||||
"01/27/2026 21:32:43.105","8197","73.674590460414663085","0","10769392.93756887503","0.0012150461538461539233","77.859230511235679728","9.4599499081889693031","3385450496","707961.00650024751667","1814075.1521249581128","3776.1726797949304455","3705397248","146.6051878123501524","41","26.697406093824927353"
|
||||
"01/27/2026 21:32:44.105","8206","73.67389427350691733","0","8964501.7032879590988","0.00057407555555555561493","134.9752725300724876","7.7487313859705126973","3385454592","710156.89925605629105","1835702.6992654947098","3780.3074476755859905","3705397248","137.47713755202511265","41","31.261431223987447225"
|
||||
"01/27/2026 21:32:45.105","8210","73.673600580932941284","0","8996869.7105756998062","0.00080313560606060606633","131.97004280028434664","10.598958598897413097","3385462784","703095.3973448027391","1842493.7539178607985","3749.1489431898958173","3705405440","146.84131460243020229","41","32.827909277611709626"
|
||||
"01/27/2026 21:32:46.106","8225","73.640390409538611038","0","0","0","0","0","3385470976","701030.65371630515438","1831356.0794334874954","3744.5239086752008006","3705409536","151.50447378653976216","41","31.276321168992271993"
|
||||
"01/27/2026 21:32:47.106","8234","73.617949353907704335","0","8957696.3511126283556","0.00062185748031496062161","126.93833335765484094","7.893628976979351286","3385487360","699301.27943844872061","1792399.2524431629572","3745.1805912687614182","3705413632","156.17163307452318577","41","32.846197777955019603"
|
||||
"01/27/2026 21:32:48.107","8239","73.617905837860391216","0","9938171.0329610593617","0.0002836638888888888977","143.89984570738764091","4.082011173675646809","3385487360","709383.26924460567534","1838536.3786804382689","3789.3626036278747051","3705413632","149.89905797436006196","41","21.92757397168746536"
|
||||
"01/27/2026 21:32:49.108","8234","73.619733325582870975","0","4722118.0751299634576","0.00021927107438016529774","120.88055792071853034","2.6504897529008055734","3385503744","713169.31739747954998","1841499.4144286029041","3797.2479393111666468","3705413632","146.72607303586860894","41","34.441541835037433827"
|
||||
"01/27/2026 21:32:50.107","8233","73.610748204777650017","0","0","0","0","0","3385507840","712406.68731175595894","1853210.6529621593654","3800.6551376791826442","3705413632","142.3274363354049683","41","23.362149665551168454"
|
||||
"01/27/2026 21:32:51.106","8222","73.615545354228359543","0","4660295.3692265432328","0.00023683454545454546598","110.07424507830532434","2.8110694092459560522","3385516032","713292.11553192627616","10215671.470406789333","3766.540531588556405","3705417728","146.93246528717381238","41","29.659990022097638018"
|
||||
"01/27/2026 21:32:52.107","8210","73.614022432270928675","0","8484982.8137039877474","0.0039004428571428571837","13.990065654378824433","5.5536146841798110785","3385528320","733726.98047116736416","27015670.172610428184","3785.3120499133565318","3705417728","149.89567261186215319","41","28.174990206816051597"
|
||||
"01/27/2026 21:32:53.096","8189","73.674840660224390376","1","28986.805402715108357","0.00022921666666666669072","6.0658772466619224062","0.13904097214843424979","3385593856","722615.82464034156874","23067720.363463323563","3857.8979288769828599","3705417728","134.27164337572838804","41","41.552343471741757241"
|
||||
"01/27/2026 21:32:54.106","8185","73.677037976141320996","0","9049535.8860253747553","0.00023766320474777447133","333.65632982195523937","7.9294480349760014803","3385597952","738246.807270302088","5923064.1967649590224","3926.6498637206959756","3705421824","157.7870313168274663","41","25.746191373647942413"
|
||||
"01/27/2026 21:32:55.107","8193","73.667748289571093778","0","12101243.712429301813","0.00031502820512820513222","350.6623472258562515","11.046750349310400452","3385597952","731268.86121353751514","1730138.0500715861563","3896.2483025095134508","3705421824","146.73234681242894339","41","29.753973814579261159"
|
||||
"01/27/2026 21:32:56.106","8197","73.671512020209689808","0","9409318.9491815753281","0.00046907947598253275229","229.31914345194206817","10.756819270371391184","3385597952","682981.50536101090256","1670860.336330070626","4054.6428464494038053","3705421824","129.86737930707823807","41","23.330929992689842578"
|
||||
"01/27/2026 21:32:57.106","8189","73.668390063957403413","0","0","0","0","0","3385602048","639909.08786523889285","1579287.5051285496447","4149.8151223380882584","3705421824","126.50343554594360285","41","32.846915955572761447"
|
||||
"01/27/2026 21:32:58.107","8193","73.63537572641749307","0","8313593.0557949626818","0.00022954146341463415736","245.96192509399546111","5.6458488455231359282","3385606144","635243.66428076929878","1580454.3456672907341","4155.3567507749803553","3705425920","134.35426913627225076","41","21.887052827748675554"
|
||||
"01/27/2026 21:32:59.107","8212","73.575677926087678316","0","4296367.1648142784834","0.0017089583333333333796","11.999059273752937571","2.0505519166848484858","3385610240","725066.1548134626355","5979708.1908778352663","3946.6905794585704825","3705430016","148.42316232251963015","41","25.007244300200603959"
|
||||
"01/27/2026 21:33:00.106","8252","73.461318801497426989","0","10648108.854902390391","0.00036156298701298702613","308.19394645050130066","11.143045345200421892","3385610240","720904.66530587698799","5957482.0434499429539","3915.4640014961419183","3705430016","146.9660748766010272","41","42.151651378359169087"
|
||||
"01/27/2026 21:33:01.107","8245","73.462656786074546744","0","0","0","0","0","3385610240","738368.33458186208736","5971947.402203050442","3920.8562287135509905","3705430016","148.24476697845133799","41","28.218323357802521656"
|
||||
"01/27/2026 21:33:02.107","8255","73.463472624650108855","0","10026832.491504179314","0.00034494556574923547574","327.12790701164158236","11.284091457030454464","3385610240","735274.49232649966143","1796272.3424859121442","3935.5387956691065483","3705430016","123.4853382200264349","41","12.466089363019229097"
|
||||
"01/27/2026 21:33:03.107","8255","73.449733870441505701","0","7796265.1844468135387","0.00017896167247386757606","287.05807184793485476","5.1369171136290621149","3385610240","661372.79571657348424","10034154.909538200125","4107.8310142141754113","3705430016","150.02470906958376418","41","28.108184504355570255"
|
||||
"01/27/2026 21:33:04.102","8244","73.522517730836412397","0","11423832.143688943237","0.00015366815365551426292","810.49347000375723837","12.455436556772884416","3385671680","642979.43425547133666","31201949.764068063349","4121.7660481975453877","3705438208","139.66919504945582275","41","34.093275113801460918"
|
||||
"01/27/2026 21:33:05.107","8233","73.523116012458473278","0","0","0","0","0","3385671680","640731.61630296625663","5944267.0006540464237","4029.1889256627578106","3705438208","127.53127813464415397","41","33.123841953784157965"
|
||||
"01/27/2026 21:33:06.106","8238","73.520722885970187122","0","10451561.874567780644","0.00020501098901098900834","546.51421522510531759","11.203950209203261679","3385671680","724013.22404250164982","10312449.984190125018","3905.6748494658627351","3705438208","156.39447721805404967","41","31.184639170283933396"
|
||||
"01/27/2026 21:33:07.107","8237","73.505939863041490412","0","12420756.400261098519","0.00024187659574468085432","516.55901357011521213","12.494680651760669221","3385671680","737619.29440836363938","1935475.6844082206953","3948.6290553754261055","3705438208","138.94752092643335573","41","25.063957911922173594"
|
||||
"01/27/2026 21:33:08.106","8238","73.503394418746097472","2","5254583.6382454391569","0.00038778800000000002157","125.08359336544612006","4.8506435869955701889","3385675776","646770.23654908570461","1757666.6486212734599","4119.7532310843334926","3705438208","148.53835754477287878","41","31.20328703189465358"
|
||||
"01/27/2026 21:33:09.107","8238","73.47495965042034527","0","1890922.2736688789446","4.0785943775100405096e-005","248.81187334256568988","1.0148037217097369833","3385679872","646674.0497509832494","5961920.1921427203342","4096.9023321466638663","3705442304","145.20285732243581833","41","29.740552908498795404"
|
||||
"01/27/2026 21:33:10.107","8257","73.449429281393392444","0","12603932.430661311373","0.0075874599999999998642","14.995769693369501496","11.378063308202905901","3385679872","645699.84807285864372","1774447.4283804539591","4118.8380757788227129","3705442304","156.20707429598346039","41","37.517170281606617266"
|
||||
"01/27/2026 21:33:11.107","8255","73.455401224423056306","0","12068186.831245079637","0.00027742693965517246143","464.36796517560514985","12.882410938241815046","3385688064","735841.08047216618434","10388753.047915168107","3921.1070852543557521","3705442304","143.85944826824248821","41","31.197655176057949689"
|
||||
"01/27/2026 21:33:12.107","8258","73.426030965853954058","0","0","0","0","0","3385688064","735790.75217920681462","1968282.279914725339","3913.0045949825407661","3705442304","137.39523613244901412","41","25.057143927755088697"
|
||||
"01/27/2026 21:33:13.105","8259","73.438986524343249584","0","11474312.430410953239","0.00032581509433962261776","371.70933292001126347","12.110950636895610799","3385688064","724972.46495487331413","1968220.9560504308902","3876.3973290229746453","3705442304","156.55002812890904806","41","31.117987623280018994"
|
||||
"01/27/2026 21:33:14.106","8258","73.438747197724580928","0","10122618.759597938508","0.00043586516853932584861","355.76139083516687833","15.517454340500544063","3385688064","713211.64894705126062","1967360.4913184728939","3896.386693444706907","3705442304","126.47801268752473902","41","25.050066555540894342"
|
||||
"01/27/2026 21:33:15.105","8263","73.428271797818183586","0","9624246.0268020424992","0.00045226769230769230546","195.13864600798865467","8.8253659848718655212","3385688064","638423.59996777703054","14401316.135113997385","4107.9186762194540279","3705442304","142.28658838014794696","41","32.765678018171854546"
|
||||
"01/27/2026 21:33:16.106","8245","73.491015395496745555","0","49123.110698598153249","0.00035659999999999999424","1.9988244913166564043","0.071278815100634843049","3385688064","660224.72184108523652","22830012.669549036771","4126.5731623232377387","3705442304","143.66698921363237673","41","21.920114557808499711"
|
||||
"01/27/2026 21:33:17.107","8237","73.49823831429198151","0","9528464.2821826934814","0.00012933269754768393568","733.14442046132171527","9.481764197720885079","3385688064","663468.73198976798449","14425765.132090851665","4118.1940675232008289","3705442304","135.77631993061035587","41","20.40698486826288871"
|
||||
"01/27/2026 21:33:18.107","8245","73.494485456844444116","0","10700252.231917293742","0.00037792108843537412484","294.15384245960638054","11.116788573126616058","3385688064","746046.18215326615609","6092249.2463558446616","3960.0711171942930378","3705442304","145.38977466523456883","41","29.650109032951011301"
|
||||
"01/27/2026 21:33:19.107","8253","73.484347265559321727","0","11787184.106969796121","0.00026403530701754384996","455.87809819654222565","12.03611511314938376","3385688064","735510.32453921821434","6048117.7333181109279","3992.9322899056792266","3705442304","148.38948116389536835","41","28.143445577520253664"
|
||||
"01/27/2026 21:33:20.108","8247","73.485794028613199202","0","0","0","0","0","3385688064","741179.80620023724623","1823784.3091929613147","3957.5854771004210306","3705442304","148.35651218000094786","41","26.607474939357167898"
|
||||
"01/27/2026 21:33:21.106","8246","73.450343025254625218","0","9500665.0805103778839","0.00044792857142857139688","224.22943155436641405","10.04387086234383375","3385688064","738002.12377304455731","1823500.8060247246176","3997.0898223061835779","3705442304","156.40994480956226198","41","24.92322649141011226"
|
||||
"01/27/2026 21:33:22.107","8251","73.429359605868654626","0","8700328.4613892938942","0.00017130979729729729831","591.75028138125719579","10.137241809472783416","3385688064","729794.02692063956056","5961963.0515922289342","4010.3076501716277562","3705442304","142.12723805106634245","41","28.155462084076354756"
|
||||
"01/27/2026 21:33:23.107","8257","73.429359605868654626","0","9442443.3090156707913","0.00056637777777777779942","189.02328766904082613","10.705877164057790552","3385688064","663223.70916096866131","1681838.2024665437639","4122.5078929724140835","3705442304","151.58143252092187936","41","32.804107232993395371"
|
||||
"01/27/2026 21:33:24.107","8255","73.429196438153553572","0","352138.45618332602317","3.6866071428571427302e-005","55.981313437574542036","0.20638016095353334256","3385688064","667304.25384006823879","1702406.7366313126404","4144.6165270032861372","3705442304","139.01545716929391006","41","28.14931427205033998"
|
||||
"01/27/2026 21:33:25.107","8260","73.433406155890182276","0","8150276.6481214175001","0.00025212078651685391594","356.1456279472676556","8.979070078263116983","3385688064","667829.07530889380723","1682241.8687001115177","4135.6910840842820107","3705442304","129.73908425990171622","41","14.028317659101263715"
|
||||
"01/27/2026 21:33:26.108","8361","73.13167561617019885","0","10759175.221927553415","0.0012532486842105263359","75.935007227314144984","9.5163508217748784546","3385688064","714585.38636780786328","14316983.094169700518","4012.5656450643896278","3705442304","138.94074622408382425","41","40.676048260471866058"
|
||||
"01/27/2026 21:33:27.108","8362","73.13634221747898323","0","0","0","0","0","3385688064","755960.16281049617101","18648620.079941190779","3948.157531485506297","3705442304","148.44657008543222787","41","34.372027052825494309"
|
||||
"01/27/2026 21:33:28.105","8352","73.127074254007794707","0","9321504.4693941622972","0.00074525704225352114047","142.29750138614801358","10.604879949349827584","3385688064","764253.8254729162436","22973187.042149022222","3975.3111830904872477","3705442304","145.61774391479417545","41","20.145108175758018376"
|
||||
"01/27/2026 21:33:29.106","8278","73.328325867962163898","0","12569439.198358025402","0.00022335847701149424038","695.48117104639948138","15.689795510474310092","3385688064","745740.67745461896993","6070951.0705014066771","3958.0472967166497256","3705442304","153.0236218505240231","41","40.664309894694760317"
|
||||
"01/27/2026 21:33:30.105","8175","73.68937350662643837","0","10513108.143749916926","0.00030956630434782607224","276.26120496929848969","8.7681843687647553764","3385688064","692105.38564212468918","1724677.6827490392607","3690.4893576876938823","3705442304","136.06559214844855887","41","64.028636558456128114"
|
||||
"01/27/2026 21:33:31.104","8221","73.489525139678619325","0","520647.93139352131402","0.0015659999999999999008","1.0008764675226096141","0.22650505462963238523","3385688064","721552.8638428671984","1803442.2743996919598","3857.3779058321374578","3705442304","142.30376217369590108","41","35.885118141521623158"
|
||||
"01/27/2026 21:33:32.106","8205","73.586555797044781002","0","9369074.7735941242427","0.00012966219135802470146","646.97377020569967954","8.3888765472467188289","3385688064","727363.25640269403812","6069106.1837713019922","3864.8695439294192511","3705442304","148.20370864960523249","41","40.718516540157899897"
|
||||
"01/27/2026 21:33:33.106","8207","73.589612490867637007","0","10195611.812573723495","0.00030411058394160585825","274.0179481756055111","8.3328533204689883007","3385688064","707069.31304000411183","1776870.3850102182478","3977.2605105634420397","3705442304","137.50368509876065559","41","29.683527119282238971"
|
||||
"01/27/2026 21:33:34.103","8213","73.585500655133628811","0","10946178.488288458437","0.00033489974937343357925","400.25913518747273656","13.497825348744241225","3385688064","643366.90360536170192","1612611.9546870545018","4125.9795063310157275","3705442304","136.37027105519365477","41","31.033182117942391898"
|
||||
"01/27/2026 21:33:35.105","8212","73.578451777244552545","0","0","0","0","0.30544687828980870981","3385688064","645920.80235724058002","1647998.4613332671579","4175.8680912501995408","3705442304","143.4011194165714187","41","20.505901192987586512"
|
||||
"01/27/2026 21:33:36.106","8218","73.579691856536015848","0","11429971.967953160405","0.00022349675090252709114","553.31007766416064442","12.365797798754323722","3385688064","685319.4751464399742","10103586.837572231889","3994.0198566407548242","3705442304","131.08121982135801886","41","29.775056459606162207"
|
||||
"01/27/2026 21:33:37.106","8222","73.551572550449421328","0","10880158.344429560006","0.00024239960629921259859","508.43807024131990602","12.324839527564666497","3385688064","734023.43459124385845","14523976.85846124962","3963.4148782591082636","3705442304","139.18604153046243255","41","26.500260420524103466"
|
||||
"01/27/2026 21:33:38.105","8224","73.519722133297406685","5","8451719.1047615930438","0.00062652171428571423566","175.09826514640016626","10.970321649539000575","3385692160","741334.03666137438267","10391676.809025226161","3914.1966471583855309","3705446400","146.95794306306478916","41","37.464705079546902766"
|
||||
"01/27/2026 21:33:39.105","8220","73.516567534188865807","0","4030029.9657726860605","0.00025596335078534033884","190.97943151522579797","4.888216627315500773","3385692160","733551.99644998228177","14514916.743466727436","3852.5850765872514785","3705446400","149.97903293119620116","41","26.570584578637891582"
|
||||
"01/27/2026 21:33:40.108","8213","73.585391876656885302","0","6151092.1357134478167","0.0027918600000000001748","14.957484845076354674","4.1760773558400750005","3385696256","736286.18016150896437","14440259.007796239108","3855.0424274043457444","3705450496","158.92925112935751031","41","36.118565537839543822"
|
||||
"01/27/2026 21:33:41.107","8216","73.565452725991008265","0","12020973.212743533775","0.00024568720472440948397","508.65998633226610082","12.496513146314487841","3385696256","719472.51558897667564","1785323.4571856984403","3841.9849755057184666","3705450496","129.84963024832805445","41","24.903568672532394146"
|
||||
"01/27/2026 21:33:42.105","8212","73.567225824475116269","0","0","0","0","0","3385696256","721148.07909403520171","1813855.5360864156391","3858.196738971945706","3705450496","153.45643520876387811","41","35.801122324153197951"
|
||||
"01/27/2026 21:33:43.107","8218","73.564702149844890755","0","10972726.702423078939","0.00029280302267002515388","395.94887452280437401","11.593609107107148759","3385700352","709270.10066376801115","5995612.4476352632046","3934.5549370087233001","3705450496","135.57882621524757383","41","25.197888984690997916"
|
||||
"01/27/2026 21:33:44.107","8224","73.565931355945281211","0","9133774.5164243169129","0.00094089052631578945236","95.039441368167800306","9.0105913566662430014","3385700352","617594.30163517862093","1614698.0997113802005","4144.7200588244122628","3705450496","134.42902702596174436","41","26.532973602090681453"
|
||||
"01/27/2026 21:33:45.107","8200","73.638290975624300927","0","10729922.999066483229","0.00028707616580310880549","386.0899203424477264","11.08386663119127391","3385704448","571407.08070909709204","5779685.0886571481824","4237.9870271786294325","3705454592","142.22248673173601219","41","20.292892051444656687"
|
||||
"01/27/2026 21:33:46.105","8224","73.616372065994966079","0","0","0","0","0","3385716736","573215.56904937978834","1658937.5175761110149","4252.3172424954509552","3705454592","136.14145351150560259","41","35.841383977336448652"
|
||||
"01/27/2026 21:33:47.100","8219","73.611487907732723102","0","10567858.764713684097","0.00022798756756756759462","557.60289029188254517","12.712150732093640215","3385729024","652757.07000277296174","10257358.348772067577","4138.3176668689438884","3705454592","147.55800172257326608","41","32.498645703060958567"
|
||||
"01/27/2026 21:33:48.092","8225","73.598140751383766656","0","10416436.922348136082","0.00053727163461538462921","209.82137543157131176","11.273366619032083591","3385729024","657724.44279815373011","14571439.840681016445","4128.8408155837569211","3705454592","154.4694090547672829","41","35.376405641986551132"
|
||||
"01/27/2026 21:33:49.102","8233","73.600599140301483203","0","8585736.9486130997539","0.00012545989085948158108","725.7728986300219276","9.1054892786698289342","3385733120","666377.34764759475365","6036450.2358415368944","4158.5895965158151739","3705454592","139.2377326978119072","41","21.098618137906598236"
|
||||
"01/27/2026 21:33:50.093","8216","73.667139111474881474","0","70274.413551480858587","0.00035722500000000002298","4.0369033519922368214","0.14421213687083178634","3385733120","677187.50961918383837","18850930.78375973925","4141.8628391440352061","3705454592","138.77226411742859113","41","25.882995300918821613"
|
||||
"01/27/2026 21:33:51.105","8210","73.68666491789898032","0","11381638.169149212539","0.00027894565701559021452","443.37078713817879816","12.367556170718973618","3385733120","750391.72648387018126","10396393.232957083732","3956.7633498055288328","3705454592","152.74716799961765901","41","39.82687321227182764"
|
||||
"01/27/2026 21:33:52.107","8207","73.679006885196784538","0","10729973.141882600263","0.0022963686274509805159","50.914916083732471463","11.691369675021995533","3385733120","750007.66219570476096","6345048.788968754001","3986.3384298498776843","3705454592","163.78078067915379279","41","29.804915318147340741"
|
||||
"01/27/2026 21:33:53.108","8209","73.67556946670534046","0","9400419.4161849729717","0.00019082725563909774813","531.31110202511422358","10.232128342861274817","3385733120","745139.85166832676623","2144435.5248984163627","3979.8397397933836146","3705454592","156.05795508047796716","41","36.019267376293093719"
|
||||
"01/27/2026 21:33:54.099","8194","73.681497916970755568","0","4135.833035127923722","5.3699999999999997464e-005","1.0097248620917782524","0.10077925455864321369","3385733120","688259.76747248088941","1997763.8633204114158","4110.5899135756289979","3705454592","156.18966976796033919","41","33.737715856016826876"
|
||||
"01/27/2026 21:33:55.103","8203","73.6523669617372434","0","10136598.536697486416","0.00038810072463768113251","274.8621805175296231","10.667320221668276758","3385733120","660006.83570292417426","1939341.9002696436364","4102.0192809844365911","3705454592","149.38020658485874037","41","34.646159619124297535"
|
||||
"01/27/2026 21:33:56.107","8216","73.629458149343136597","0","10761314.931790962815","0.00022894880382775121869","624.4506800984978554","14.29654706940549147","3385733120","677473.2155973239569","6065321.3256878796965","4203.8378320506535601","3705454592","143.16375872439945738","41","26.861992825578539623"
|
||||
"01/27/2026 21:33:57.107","8215","73.613837518173696139","0","0","0","0","0","3385733120","714222.99420466448646","6205074.3488698359579","4169.3852511972108914","3705454592","146.89114333665270351","41","26.553965570799331175"
|
||||
"01/27/2026 21:33:58.106","8222","73.608757549340083415","0","10223832.412511598319","0.00042215743944636676408","289.12195163920142704","12.205545900890008681","3385733120","764705.5528021720238","10465172.209638025612","4024.6976174550427459","3705454592","159.44287483181588527","41","37.47377650186488296"
|
||||
"01/27/2026 21:33:59.105","8234","73.567497770666960832","0","9021694.9377210512757","0.0006260585937499999875","128.1490758199012987","8.0228862327090038065","3385733120","767464.79179229191504","10492934.430623143911","4056.7191814237503422","3705454592","140.78883596841643566","41","34.298543214738998586"
|
||||
"01/27/2026 21:34:00.096","8239","73.566290310948687647","0","11044346.263615325093","0.00024998416833667334684","503.55180619693658173","12.588079247254817972","3385733120","748821.90068908897229","10504691.104772480205","3953.739432223642325","3705454592","141.90867751506576155","41","41.659765910472955852"
|
||||
"01/27/2026 21:34:01.090","8240","73.569890897155246989","0","65902.006564055453055","0.00016630000000000000359","1.0055848169564125527","0.016722091904536665746","3385733120","749552.86671114037745","10523279.187954060733","3970.0488573439170068","3705454592","142.97489132148569979","41","21.438630875196274417"
|
||||
"01/27/2026 21:34:02.106","8250","73.557424841811283045","0","11411477.928586313501","0.00019542072829131651858","702.90029064041004858","13.736588454742094001","3385733120","739061.26189503690694","14382987.922814458609","4016.5730893737718361","3705454592","149.21133227395952758","41","27.705092101374617641"
|
||||
"01/27/2026 21:34:03.106","8241","73.566127143233586594","0","12637936.849253848195","0.00033720262529832932498","419.19479982347797886","14.13523283912807571","3385733120","669927.31522338429932","6016259.7559085702524","4154.9307963410592492","3705454592","150.06840117725658956","41","23.402586899108612783"
|
||||
"01/27/2026 21:34:04.106","8247","73.557664168429951701","0","10124307.373945007101","0.00014211002747252746039","727.63341828386865018","10.340544167412383914","3385733120","663300.82904232852161","1769759.3952166899107","4158.9047437900790101","3705454592","134.30920192197842766","41","20.351519790454652536"
|
||||
"01/27/2026 21:34:05.109","8246","73.528554959578571015","0","0","0","0","0","3385733120","677176.73285492823925","10163742.965447761118","4202.3237293807278547","3705454592","144.97535980078328066","41","26.732882681324575458"
|
||||
"01/27/2026 21:34:06.108","8252","73.503144242219434545","0","9980735.6300343964249","0.00032271467181467180553","259.28762776548023794","8.3676231645821808058","3385733120","754796.29553063213825","1903505.5587162838783","4024.4643382904655482","3705454592","142.34593102122664732","41","24.916431988803534381"
|
||||
"01/27/2026 21:34:07.107","8261","73.483781626793515329","0","6305711.3344156285748","0.00014825326370757180273","383.05730537288377491","5.6789989855394109597","3385733120","751303.3949878901476","1899300.1353002409451","4021.6016316040877427","3705454592","142.21001184487505498","41","20.299883471553549441"
|
||||
"01/27/2026 21:34:08.107","8264","73.485924553472074194","0","9038958.1691177729517","0.00073350489510489510093","143.1152363883398948","10.497416107950112263","3385733120","755032.95253337989561","10332017.340362459421","3986.2096960472572391","3705454592","137.60875219686116111","41","29.631888081150535186"
|
||||
"01/27/2026 21:34:09.107","8262","73.483640205460517336","0","229323.55370326802949","0.00016939999999999999763","2.9993140568751925912","0.050809990431884922979","3385733120","755010.32913772610482","6098625.2444066032767","3997.085866462339709","3705454592","143.72168682769495263","41","32.825733330533878984"
|
||||
"01/27/2026 21:34:10.107","8254","73.553737246793176041","0","10389091.86304683052","0.00029742534059945503148","366.91311497437408207","10.912661795271434428","3385733120","749479.5232488947222","1867517.7717916399706","4005.051603780224923","3705454592","137.46412186419345858","41","23.457477598346830661"
|
||||
"01/27/2026 21:34:11.107","8259","73.55073494220867758","0","7771335.2081617647782","0.00028901301587301586604","315.04958880527794918","9.105547177382494084","3385733120","753099.53786726028193","1861732.0366225643083","4024.6334773093285548","3705454592","129.71082200579664345","41","24.986512574960972444"
|
||||
"01/27/2026 21:34:12.107","8259","73.544175590748011473","0","0","0","0","0","3385733120","761776.8694205355132","1847257.6632643265184","4064.6097974594435982","3705454592","139.05621465909740664","41","25.003389846778922845"
|
||||
"01/27/2026 21:34:13.107","8265","73.548189521196334795","0","7095250.3469975283369","0.00036114941176470589023","169.92533480788540601","6.164927592347480001","3385737216","753700.8238579967292","1636298.0106541183777","4040.2247252557222055","3705458688","135.86648258955045776","41","17.230763479929045445"
|
||||
"01/27/2026 21:34:14.102","8496","72.850373706978359678","0","8279842.4809748930857","0.00021652010309278348988","389.91847688999416732","8.442640208106903188","3133726720","476114.57895135646686","1185209.4676225967705","2532.4602107288278603","3434455040","114.62813251991954644","41","21.487580465808530761"
|
||||
"01/27/2026 21:34:15.102","8830","71.770635589903832852","1","4223427.0620102221146","6.0866160849772388026e-005","659.07038871751501574","4.0114827528683880686","2783662080","0","0","0","3027333120","100.01004100811721287","41","12.491214117897442293"
|
||||
"01/27/2026 21:34:16.106","9008","71.325436716742217413","0","0","0","0","0","2602901504","0","0","0","2867236864","99.610880058140878646","41","5.0583799445844785936"
|
||||
"01/27/2026 21:34:17.106","9247","70.627968961053525732","0","3547437.1774163627997","3.4005872756933113333e-005","613.05204811888529548","2.0847574265282919903","2308984832","0","0","0","2547482624","100.0094708968939301","41","6.241121034161933423"
|
||||
"01/27/2026 21:34:18.107","9520","69.790339788838835489","0","2700274.935885750223","3.2701167315175099187e-005","513.41342516175268429","1.6788782307873248989","2031820800","0","0","0","2233516032","99.883286379865126037","41","3.2380663195056613723"
|
||||
"01/27/2026 21:34:19.107","9746","69.093383305960657026","0","4037786.1911667422391","9.2841875000000001223e-005","160.12794222583843862","1.4867148263240810291","1800921088","0","0","0","1985425408","100.08380016587889827","41","0"
|
||||
"01/27/2026 21:34:20.107","10033","68.115244565558441536","0","0","0","0","0","1507934208","0","0","0","1625628672","99.967310689404570212","41","3.156667769639331933"
|
||||
"01/27/2026 21:34:21.106","10360","67.2421208739378784","0","3155515.1128895659931","0.0021377750000000001786","4.0020230226379434058","0.92649408702433033724","1169424384","0","0","0","1298059264","100.05119619709405754","41","6.2020035652243237223"
|
||||
"01/27/2026 21:34:22.105","10822","65.711451244007662353","0","2099325.2214692649432","0.0029182000000000001341","2.0020725454991006309","0.58424907966555139627","682590208","0","0","0","719970304","100.10435879404278126","41","9.2804248428987268227"
|
||||
"01/27/2026 21:34:23.111","11290","64.376017745671802572","0","1899332.6828896303196","3.7539171974522297307e-005","311.78402621210011603","1.1703957290529867219","197574656","3013618.0113461585715","3013729.2209351258352","0.99294275863726155773","192450560","783.48339310603125796","42","53.401872096563842263"
|
||||
"01/27/2026 21:34:24.091","11281","64.34932343297388968","0","4183.7063838906060482","5.1199999999999997683e-005","1.0214126913795424922","0.0052294914309369564323","199487488","5291289.5355656920001","5291289.5355656920001","0","182243328","1383.6575532905094406","42","96.81186927296948852"
|
||||
"01/27/2026 21:34:25.107","11307","64.291213817030936184","0","2925371.0229666647501","0.00092301111111111103745","8.8659547436355001793","0.88779256408584172888","182259712","5476760.313149462454","5476760.313149462454","0","165285888","1397.6893375243162154","42","96.921521656873494521"
|
||||
"01/27/2026 21:34:26.108","11303","64.27447275823784878","0","1272028.4681582306512","3.3593032786885241962e-005","243.74791590537063257","0.81885507542893321009","183635968","5833032.4778114464134","5833032.4778114464134","0","163164160","1397.0471586676940206","42","92.195267270012877248"
|
||||
"01/27/2026 21:34:27.106","11291","64.304800276715496921","0","0","0","0","0","194453504","6038277.3346951240674","6038277.3346951240674","0","175382528","1397.2819689352140813","42","98.435294547664938136"
|
||||
"01/27/2026 21:34:28.108","11292","64.270632868695685147","0","1173203.1158814644441","0.00016060000000000000057","19.968734951686144541","0.32065376630909248057","194977792","6446278.0624376414344","6446278.0624376414344","0","176173056","1396.0593129603721536","42","100"
|
||||
"01/27/2026 21:34:29.108","11279","64.327154323534841751","0","1200035.1172819226049","0.00033412727272727271244","10.999148665893260457","0.50788905666845773901","188964864","6041477.3896500421688","6041477.3896500421688","0","170024960","1398.5450481141999717","42","95.312139503527831152"
|
||||
"01/27/2026 21:34:30.107","11290","64.331070371980558775","0","1189086.5193982853089","0.00012752692307692307006","26.027284402238866079","0.35861066966081073248","189038592","5962089.0579594587907","5962089.0579594587907","0","171126784","1398.1252035570207681","42","96.872203124033504196"
|
||||
"01/27/2026 21:34:31.106","11288","64.306018609624828741","0","0","0","0","0","189558784","5292224.931323970668","5292224.931323970668","0","167182336","1400.2359397558489036","42","98.436447729321827183"
|
||||
"01/27/2026 21:34:32.108","11301","64.280694901077239933","0","1092468.9575477945618","0.00034147000000000002221","9.9893653216785409654","0.34110477911300368659","181981184","5744505.3995516365394","5744505.3995516365394","0","161472512","1396.941794420713677","42","96.879914758272761333"
|
||||
"01/27/2026 21:34:33.108","11295","64.291442247175481839","0","1081494.003218246391","0.00098117499999999992916","4.000554876961434303","0.39257312896097806831","185393152","5526432.5161899961531","5526432.5161899961531","0","164974592","1398.8049660645851873","41","95.311268270174579698"
|
||||
"01/27/2026 21:34:34.107","11293","64.300993030029587771","0","1053097.4513703535777","0.0015861499999999999298","2.000808326563931594","0.3173516091366211378","187752448","5146855.329553139396","5146855.329553139396","0","165597184","1419.2938734122915321","41","96.872222220554078831"
|
||||
"01/27/2026 21:34:35.092","11289","64.311718606462633829","0","137244.06349974797922","0.00044024999999999999464","2.03071826911322173","0.089392832309097572385","191561728","5216245.0963230598718","5216245.0963230598718","0","171655168","1375.3475584182920102","41","93.657933488871250916"
|
||||
"01/27/2026 21:34:36.107","11287","64.317473015821875038","0","228091.87423683636007","4.4388372093023252291e-005","42.380870576016157258","0.18812007949002279572","191545344","5800675.6693491786718","5800675.6693491786718","0","171032576","1398.3096493763810031","41","96.920022798730443014"
|
||||
"01/27/2026 21:34:37.106","11298","64.305344169099200258","0","222316.13456785379094","5.0890909090909087239e-005","33.016059011103003229","0.16803768030356044938","185384960","5994757.8502183463424","5994757.8502183463424","0","163323904","1394.5606581105596433","41","98.436591190459012068"
|
||||
"01/27/2026 21:34:38.106","11300","64.320507939979549406","0","110638.46815662577865","5.6231818181818185381e-005","22.009243882430620687","0.12376669752413579917","190230528","6724260.1892794976011","6724260.1892794976011","0","174882816","1395.9519856046053974","41","96.873567781400666377"
|
||||
"01/27/2026 21:34:39.107","11293","64.303940945375686056","0","102226.03173918624816","0.00027953333333333331553","2.9949032736089722384","0.083710325937224244752","189734912","6293502.7170760799199","6293502.7170760799199","0","166821888","1395.9415564969833667","41","96.880577527381035452"
|
||||
"01/27/2026 21:34:40.106","11310","64.287754652157374835","0","172356.06387129079667","7.9088461538461541614e-005","26.048977287095198108","0.20600658002829169702","181878784","5501967.799857291393","5501967.799857291393","0","162775040","1163.0635802246506501","41","92.173192596065618432"
|
||||
"01/27/2026 21:34:41.107","11324","64.254435725569365445","0","66479.387095208352548","7.3715384615384620332e-005","12.984255292032882423","0.095714243194280831939","171872256","3338910.2374460734427","3338910.2374460734427","0","148455424","575.86604760203010756","41","68.787748097450943874"
|
||||
"01/27/2026 21:34:42.102","11197","64.560637079309827868","0","3997103.1871975827962","0.00073616666666666665322","18.090005010931388796","1.3317745878795346215","77455360","20837736.072052892298","69458145.054906174541","6.0300016703104626359","52105216","48.681481850739537265","40","7.3481474453666928426"
|
||||
"01/27/2026 21:34:43.093","11197","64.562334042173418425","0","3228690.9341432941146","0.00064717857142857144245","14.130044451101264613","0.91442909669887106894","77455360","113.04035560881011691","113.04035560881011691","0","52400128","0","40","0.65215583434123924889"
|
||||
"01/27/2026 21:34:44.106","11202","64.524903256568336474","0","6249070.275020962581","0.00063857857142857134183","27.631470784703175525","1.7648343127330383684","77455360","0","0","0","52400128","0","40","4.3812015268212327612"
|
||||
"01/27/2026 21:34:45.106","11194","64.523619661229574263","0","4276239.8220873419195","0.00074496250000000000836","16.0000592002190416","1.1917099999700058177","77455360","0","0","0","52400128","0","40","3.1436932671994322064"
|
||||
"01/27/2026 21:34:46.106","11212","64.525033781427197255","0","0","0","0","0","77455360","0","0","0","52400128","0","40","0.044140507551859720081"
|
||||
"01/27/2026 21:34:47.106","11219","64.501787760411815498","0","4198303.7239578142762","0.0028684249999999999678","4.0038144340112822306","1.1482473758198639135","77455360","0","0","0","52400128","0","40","0"
|
||||
|
Binary file not shown.
892
docs/PARALLEL_REGION_PROCESSING.md
Normal file
892
docs/PARALLEL_REGION_PROCESSING.md
Normal file
@@ -0,0 +1,892 @@
|
||||
# Parallel Region Processing Architecture
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document outlines a comprehensive plan to parallelize the Arnis world generation pipeline by splitting large user-selected areas into smaller processing units (**1 Minecraft region = 512×512 blocks per unit**). The goal is to:
|
||||
|
||||
1. **Reduce memory usage by ~90%** by processing and flushing regions incrementally
|
||||
2. **Utilize multiple CPU cores** for parallel generation
|
||||
3. **Maintain visual consistency** across region boundaries (colors, elevation, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Current Architecture Analysis
|
||||
|
||||
### Processing Pipeline Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ CURRENT PROCESSING FLOW │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ [1/7] Fetch Data (retrieve_data.rs) │
|
||||
│ └── Downloads OSM data for entire bbox from Overpass API │
|
||||
│ └── Single HTTP request with full bounding box │
|
||||
│ │
|
||||
│ [2/7] Parse Data (osm_parser.rs) │
|
||||
│ └── Transforms lat/lon → Minecraft X/Z coordinates │
|
||||
│ └── Clips ways/relations to bounding box (clipping.rs) │
|
||||
│ └── Sorts elements by priority │
|
||||
│ │
|
||||
│ [3/7] Fetch Elevation (ground.rs / elevation_data.rs) │
|
||||
│ └── Downloads Terrarium tiles for entire bbox │
|
||||
│ └── Builds height grid matching world dimensions │
|
||||
│ │
|
||||
│ [4/7] Process Data (data_processing.rs) │
|
||||
│ └── Pre-computes flood fills in parallel (floodfill_cache.rs) │
|
||||
│ └── Builds highway connectivity map │
|
||||
│ └── Collects building footprints │
|
||||
│ │
|
||||
│ [5/7] Process Terrain + Elements (data_processing.rs) │
|
||||
│ └── Iterates ALL elements sequentially │
|
||||
│ └── Calls element_processing/* for each element type │
|
||||
│ └── Places blocks via WorldEditor │
|
||||
│ │
|
||||
│ [6/7] Generate Ground (data_processing.rs) │
|
||||
│ └── Iterates ALL blocks in bbox │
|
||||
│ └── Sets grass, dirt, stone, bedrock layers │
|
||||
│ │
|
||||
│ [7/7] Save World (world_editor/mod.rs → java.rs) │
|
||||
│ └── Iterates ALL regions in memory │
|
||||
│ └── Writes .mca files in parallel │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Data Structures and Memory Usage
|
||||
|
||||
#### WorldToModify (world_editor/common.rs)
|
||||
|
||||
```rust
|
||||
pub struct WorldToModify {
|
||||
pub regions: FnvHashMap<(i32, i32), RegionToModify>, // Key: (region_x, region_z)
|
||||
}
|
||||
|
||||
pub struct RegionToModify {
|
||||
pub chunks: FnvHashMap<(i32, i32), ChunkToModify>, // 32×32 chunks per region
|
||||
}
|
||||
|
||||
pub struct ChunkToModify {
|
||||
pub sections: FnvHashMap<i8, SectionToModify>, // 24 sections per chunk (-4 to 19)
|
||||
}
|
||||
|
||||
pub struct SectionToModify {
|
||||
pub blocks: [Block; 4096], // 16×16×16 = 4096 blocks
|
||||
pub properties: FnvHashMap<usize, Value>, // Block properties (stairs, slabs, etc.)
|
||||
}
|
||||
```
|
||||
|
||||
**Memory estimate per region:**
|
||||
- Section: ~4KB (blocks) + ~variable (properties)
|
||||
- Chunk: ~24 sections × 4KB = ~96KB minimum, typically ~200-500KB with properties
|
||||
- Region: ~1024 chunks × 300KB = **~300MB per region**
|
||||
- **For a 10×10 region area: ~30GB of memory required!**
|
||||
|
||||
#### Why Elements Are "Scattered"
|
||||
|
||||
The current design processes elements in OSM priority order (entrance → building → highway → waterway → water → barrier → other), NOT by spatial location. This means:
|
||||
|
||||
1. A building in region (0,0) might be followed by a highway in region (5,5)
|
||||
2. Each `set_block()` call potentially accesses different regions
|
||||
3. ALL regions must remain in memory until the end because any element might touch any region
|
||||
|
||||
---
|
||||
|
||||
## Proposed Architecture: Region-Based Parallel Processing
|
||||
|
||||
### Core Concept
|
||||
|
||||
Split the user-selected area into **processing units** of **1 Minecraft region each** (512×512 blocks = 32×32 chunks). Process each unit independently in parallel, then flush to disk immediately.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ PROPOSED PARALLEL PROCESSING FLOW │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ GLOBAL PHASE (Run Once for Entire Area) │
|
||||
│ ═══════════════════════════════════════ │
|
||||
│ │
|
||||
│ [1] Fetch Elevation Data for ENTIRE bbox │
|
||||
│ └── Must be consistent across all units │
|
||||
│ └── Store as shared read-only Arc<Ground> │
|
||||
│ │
|
||||
│ [2] Compute Processing Unit Grid │
|
||||
│ └── Divide bbox into N×N region units │
|
||||
│ └── Create sub-bboxes with small overlap for boundary elements │
|
||||
│ │
|
||||
│ PARALLEL PHASE (Per Processing Unit) │
|
||||
│ ═════════════════════════════════════ │
|
||||
│ │
|
||||
│ For each processing unit (in parallel, using N-1 CPU cores): │
|
||||
│ │
|
||||
│ [3] Fetch OSM Data for Unit's Sub-BBox │
|
||||
│ └── Separate Overpass API query per unit │
|
||||
│ └── Include small buffer zone for boundary elements │
|
||||
│ │
|
||||
│ [4] Parse & Clip Elements to Unit Bounds │
|
||||
│ └── Same as current, but for smaller area │
|
||||
│ │
|
||||
│ [5] Pre-compute Flood Fills │
|
||||
│ └── Only for elements in this unit │
|
||||
│ │
|
||||
│ [6] Process Elements │
|
||||
│ └── Generate buildings, roads, etc. │
|
||||
│ └── Use deterministic RNG keyed by element ID │
|
||||
│ │
|
||||
│ [7] Generate Ground Layer │
|
||||
│ └── Only for this unit's blocks │
|
||||
│ │
|
||||
│ [8] Save Regions to Disk │
|
||||
│ └── Write .mca files immediately │
|
||||
│ └── FREE MEMORY for this unit │
|
||||
│ │
|
||||
│ FINALIZATION PHASE │
|
||||
│ ══════════════════ │
|
||||
│ │
|
||||
│ [9] Wait for all units to complete │
|
||||
│ [10] Generate map preview (optional) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Critical Considerations
|
||||
|
||||
### 1. Deterministic Randomness ✅ ALREADY IMPLEMENTED
|
||||
|
||||
The codebase already has `deterministic_rng.rs` which provides:
|
||||
|
||||
```rust
|
||||
// Creates RNG seeded by element ID - same element always produces same random values
|
||||
pub fn element_rng(element_id: u64) -> ChaCha8Rng {
|
||||
ChaCha8Rng::seed_from_u64(element_id)
|
||||
}
|
||||
|
||||
// For coordinate-based randomness
|
||||
pub fn coord_rng(x: i32, z: i32, element_id: u64) -> ChaCha8Rng
|
||||
```
|
||||
|
||||
**Impact on buildings crossing boundaries:**
|
||||
- Building colors are chosen using `element_rng(element.id)` in buildings.rs
|
||||
- Even if a building is processed in two different units, SAME element ID → SAME color
|
||||
- The existing implementation already supports this use case!
|
||||
|
||||
**Files using deterministic RNG:**
|
||||
- `element_processing/buildings.rs` - wall colors, window styles, accent blocks
|
||||
- `element_processing/natural.rs` - grass/flower distribution
|
||||
- `element_processing/tree.rs` - tree variations
|
||||
|
||||
### 2. Elevation Data Consistency ⚠️ REQUIRES CHANGES
|
||||
|
||||
**Current behavior:**
|
||||
- Elevation is fetched once in `ground.rs` → `Ground::new_enabled()`
|
||||
- Height grid dimensions match the world's XZ dimensions
|
||||
- Lookup uses relative coordinates: `ground.level(XZPoint::new(x - min_x, z - min_z))`
|
||||
|
||||
**Problem:**
|
||||
- If each unit downloads its own elevation tiles, slight differences in tile boundaries or interpolation could cause height discontinuities at unit boundaries
|
||||
|
||||
**Solution:**
|
||||
1. **Download elevation ONCE for the entire area** before parallel processing starts
|
||||
2. Pass `Arc<Ground>` (read-only) to all processing units
|
||||
3. The `Ground::level()` function already uses world-relative coordinates, so no changes needed
|
||||
|
||||
```rust
|
||||
// Proposed: Global elevation fetch before parallel processing
|
||||
let global_ground = Arc::new(Ground::new_enabled(&args.bbox, args.scale, args.ground_level));
|
||||
|
||||
// Each processing unit receives a clone of the Arc
|
||||
for unit in processing_units {
|
||||
let ground_ref = Arc::clone(&global_ground);
|
||||
// spawn task with ground_ref
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Element Clipping ⚠️ REQUIRES NEW LOGIC
|
||||
|
||||
**Current clipping (clipping.rs):**
|
||||
- Uses Sutherland-Hodgman algorithm to clip polygons to user's bbox
|
||||
- Works on the OUTER boundary of the entire selected area
|
||||
|
||||
**New requirement:**
|
||||
- Need to clip elements to each processing unit's internal boundary
|
||||
- But with OVERLAP to handle elements that straddle unit boundaries
|
||||
|
||||
**Proposed approach:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ UNIT BOUNDARY HANDLING │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Example: 4 processing units arranged in a 2×2 grid │
|
||||
│ │
|
||||
│ Unit A │ Unit B │
|
||||
│ (regions 0,0-1,1) │ (regions 2,0-3,1) │
|
||||
│ │ │
|
||||
│ ┌──────────────────┼──────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ │ ████████ │ │ ← Building straddles │
|
||||
│ │ █ BLD █─────┼──────────────────│ Unit A and B │
|
||||
│ │ ████████ │ │ │
|
||||
│ │ │ │ │
|
||||
│ ├──────────────────┼──────────────────┤ │
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
│ └──────────────────┴──────────────────┘ │
|
||||
│ Unit C │ Unit D │
|
||||
│ (regions 0,2-1,3) │ (regions 2,2-3,3) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Strategy for boundary elements:**
|
||||
|
||||
1. **Expanded Fetch BBox**: Each unit fetches OSM data with a buffer zone (e.g., +256 blocks)
|
||||
2. **Clip to Processing BBox**: Clip elements to the unit's actual processing bounds
|
||||
3. **Process Normally**: Elements partially in the unit are still processed, just clipped
|
||||
4. **Deterministic Results**: Same element in adjacent units produces identical blocks due to RNG seeding
|
||||
|
||||
**Example: Building straddling Unit A and B**
|
||||
|
||||
| Step | Unit A | Unit B |
|
||||
|------|--------|--------|
|
||||
| Fetch | Gets building (with buffer) | Gets building (with buffer) |
|
||||
| Clip | Clips to Unit A bounds → left half | Clips to Unit B bounds → right half |
|
||||
| Color | `element_rng(building_id)` → BLUE | `element_rng(building_id)` → BLUE |
|
||||
| Place | Places left half in blue | Places right half in blue |
|
||||
| **Result** | **Seamless blue building across boundary** |
|
||||
|
||||
### 4. OSM Data Downloading Strategy ⚠️ REQUIRES CAREFUL DESIGN
|
||||
|
||||
**Options:**
|
||||
|
||||
#### Option A: Download Once, Distribute Elements (RECOMMENDED)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ [1] Download ALL OSM data for entire bbox (single API call) │
|
||||
│ [2] Parse into ProcessedElements │
|
||||
│ [3] For each processing unit: │
|
||||
│ └── Filter elements that intersect unit's bbox │
|
||||
│ └── Clip filtered elements to unit bounds │
|
||||
│ └── Send to parallel processor │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Single Overpass API call (respects rate limits)
|
||||
- No duplicate data transfer
|
||||
- Elements are already parsed, just need filtering
|
||||
|
||||
**Cons:**
|
||||
- Must keep all elements in memory during distribution phase
|
||||
- For very large areas, this might still be memory-intensive
|
||||
|
||||
#### Option B: Download Per Unit (Simpler, Higher Bandwidth)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ For each processing unit (sequentially or with rate limiting): │
|
||||
│ [1] Download OSM data for unit's expanded bbox │
|
||||
│ [2] Parse into ProcessedElements │
|
||||
│ [3] Send to parallel processor │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Lower peak memory usage
|
||||
- Simpler code structure
|
||||
|
||||
**Cons:**
|
||||
- Multiple API calls (may hit rate limits)
|
||||
- Duplicate data transfer for overlapping areas
|
||||
- Slower due to network latency
|
||||
|
||||
#### Recommendation: Option A with Streaming
|
||||
|
||||
Download once, but use a streaming approach to distribute elements to units:
|
||||
|
||||
```rust
|
||||
// Pseudo-code for element distribution
|
||||
fn distribute_elements_to_units(
|
||||
elements: Vec<ProcessedElement>,
|
||||
units: &[ProcessingUnit],
|
||||
) -> Vec<Vec<ProcessedElement>> {
|
||||
let mut unit_elements = vec![Vec::new(); units.len()];
|
||||
|
||||
for element in elements {
|
||||
let element_bbox = compute_element_bbox(&element);
|
||||
for (i, unit) in units.iter().enumerate() {
|
||||
if unit.expanded_bbox.intersects(&element_bbox) {
|
||||
// Clone element for each unit that needs it
|
||||
// (or use Arc for large elements)
|
||||
unit_elements[i].push(element.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unit_elements
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Flood Fill Cache ⚠️ REQUIRES CHANGES
|
||||
|
||||
**Current behavior:**
|
||||
- `FloodFillCache::precompute()` runs in parallel for ALL elements
|
||||
- Results are stored in a `FnvHashMap<u64, Vec<(i32, i32)>>`
|
||||
- Cache is consumed during sequential element processing
|
||||
|
||||
**Problem:**
|
||||
- If we process units in parallel, each unit needs its own flood fill cache
|
||||
- But we don't want to re-compute the same flood fills multiple times
|
||||
|
||||
**Solution A: Per-Unit Flood Fill (Simpler)**
|
||||
|
||||
```rust
|
||||
// Each unit computes flood fills only for its elements
|
||||
fn process_unit(unit_elements: Vec<ProcessedElement>) {
|
||||
let flood_fill_cache = FloodFillCache::precompute(&unit_elements, timeout);
|
||||
// Process elements using this cache
|
||||
}
|
||||
```
|
||||
|
||||
**Pros:** Simple, no coordination needed
|
||||
**Cons:** Elements at boundaries may be flood-filled twice
|
||||
|
||||
**Solution B: Global Flood Fill + Distribution (More Complex)**
|
||||
|
||||
```rust
|
||||
// Compute flood fills globally, then distribute to units
|
||||
let global_cache = FloodFillCache::precompute(&all_elements, timeout);
|
||||
|
||||
// For each unit, create a view into the global cache
|
||||
let unit_caches: Vec<_> = units.iter()
|
||||
.map(|unit| global_cache.filter_for_bbox(&unit.bbox))
|
||||
.collect();
|
||||
```
|
||||
|
||||
**Recommendation:** Start with Solution A. The overhead of re-computing some flood fills at boundaries is acceptable given the simplicity.
|
||||
|
||||
### 6. Building Footprints Bitmap ⚠️ REQUIRES CHANGES
|
||||
|
||||
**Current behavior:**
|
||||
- `BuildingFootprintBitmap` is a memory-efficient bitmap covering the entire world
|
||||
- Used to prevent trees from spawning inside buildings
|
||||
- Created AFTER flood fill precomputation
|
||||
|
||||
**Problem:**
|
||||
- With parallel processing, each unit only knows about buildings in its own area
|
||||
- A tree in Unit B might spawn inside a building that exists in Unit A (near boundary)
|
||||
|
||||
**Solution:**
|
||||
- Compute building footprints GLOBALLY before parallel processing
|
||||
- Use `Arc<BuildingFootprintBitmap>` shared across all units (read-only)
|
||||
|
||||
```rust
|
||||
// Global building footprint computation
|
||||
let all_building_coords = compute_all_building_footprints(&all_elements, &global_xzbbox);
|
||||
let global_footprints = Arc::new(BuildingFootprintBitmap::from(all_building_coords));
|
||||
|
||||
// Each unit receives Arc clone
|
||||
for unit in units {
|
||||
let footprints = Arc::clone(&global_footprints);
|
||||
// spawn task
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Highway Connectivity ⚠️ REQUIRES CHANGES
|
||||
|
||||
**Current behavior:**
|
||||
- `highways::build_highway_connectivity_map()` creates a map of connected highway segments
|
||||
- Used for intersection detection and road marking placement
|
||||
|
||||
**Problem:**
|
||||
- Highway segments crossing unit boundaries won't see their full connectivity
|
||||
|
||||
**Solution:**
|
||||
- Build highway connectivity map GLOBALLY before parallel processing
|
||||
- Pass as `Arc<HighwayConnectivityMap>` to all units
|
||||
|
||||
### 8. Water Areas and Ring Merging ✅ ALREADY SUPPORTED
|
||||
|
||||
**Current behavior:**
|
||||
- Water relations contain multiple ways that must be merged into closed rings
|
||||
- `merge_way_segments()` in water_areas.rs handles this
|
||||
- **Clipping happens AFTER ring merging** via `clip_water_ring_to_bbox()`
|
||||
- Water uses `inverse_floodfill()` which iterates over bounding box (not flood fill)
|
||||
|
||||
**Why water CAN be clipped per-unit:**
|
||||
1. Ring merging happens on the UNCLIPPED ways (preserved in osm_parser.rs)
|
||||
2. After merging, `clip_water_ring_to_bbox()` clips the assembled polygon
|
||||
3. The `inverse_floodfill` algorithm iterates block-by-block within bounds
|
||||
4. Each unit can independently clip and fill its portion of a water body
|
||||
|
||||
**No special handling needed** - water relations work the same as other elements:
|
||||
- Distribute relation to units that intersect its bbox
|
||||
- Each unit clips to its own bounds
|
||||
- Each unit fills its portion independently
|
||||
|
||||
### 9. Element Priority Order ⚠️ MUST BE PRESERVED
|
||||
|
||||
**Current behavior:**
|
||||
- Elements are sorted by priority before processing (osm_parser.rs):
|
||||
```rust
|
||||
const PRIORITY_ORDER: [&str; 6] = [
|
||||
"entrance", "building", "highway", "waterway", "water", "barrier",
|
||||
];
|
||||
```
|
||||
- This ensures entrances are placed before buildings (so doors work)
|
||||
- Buildings before highways (so sidewalks don't overwrite buildings)
|
||||
|
||||
**Requirement:**
|
||||
- Each unit must process its elements in the SAME priority order
|
||||
- This is natural: just sort the unit's elements the same way
|
||||
|
||||
### 10. SPONGE Block as Placeholder ⚠️ MINOR CONSIDERATION
|
||||
|
||||
**Current behavior:**
|
||||
- `SPONGE` block is used as a blacklist marker in some places
|
||||
- Example: `editor.set_block(actual_block, x, 0, z, None, Some(&[SPONGE]));`
|
||||
- Prevents certain blocks from overwriting sponge blocks
|
||||
|
||||
**Impact on parallel processing:**
|
||||
- None - this is a per-block check, not cross-region coordination
|
||||
- Each unit handles its own sponge blocks independently
|
||||
|
||||
### 11. Tree Placement and Building Footprints ⚠️ REQUIRES GLOBAL FOOTPRINTS
|
||||
|
||||
**Current behavior:**
|
||||
- Trees check `building_footprints.contains(x, z)` before spawning
|
||||
- Prevents trees from appearing inside buildings
|
||||
- Uses `coord_rng(x, z, element_id)` for deterministic placement
|
||||
|
||||
**Problem with per-unit footprints:**
|
||||
- A tree near a unit boundary might not see a building from the adjacent unit
|
||||
- Could spawn a tree inside a building that exists in neighbor unit
|
||||
|
||||
**Solution (already planned):**
|
||||
- Compute building footprints GLOBALLY before parallel processing
|
||||
- Pass as `Arc<BuildingFootprintBitmap>` to all units
|
||||
- Tree placement will correctly avoid all buildings
|
||||
|
||||
### 12. Relations with Multiple Members Across Units ⚠️ REQUIRES CAREFUL HANDLING
|
||||
|
||||
**Current behavior:**
|
||||
- Relations (buildings, landuse, leisure, natural) process each member way
|
||||
- Member ways can be scattered across the entire bbox
|
||||
|
||||
**Example: Building relation with courtyard**
|
||||
```
|
||||
Building Relation:
|
||||
- Outer way 1 (in Unit A)
|
||||
- Outer way 2 (in Unit A and B) ← straddles boundary
|
||||
- Inner way (courtyard, in Unit A)
|
||||
```
|
||||
|
||||
**Strategy:**
|
||||
1. Distribute entire relation to all units that any member touches
|
||||
2. Each unit clips all members to its bounds
|
||||
3. Each unit processes the clipped relation independently
|
||||
4. Deterministic RNG ensures consistent colors/styles
|
||||
|
||||
**Important:** The relation-level tags (e.g., `building:levels`) must be preserved for all units processing that relation.
|
||||
|
||||
---
|
||||
|
||||
## Proposed Processing Unit Structure
|
||||
|
||||
### ProcessingUnit Definition
|
||||
|
||||
```rust
|
||||
struct ProcessingUnit {
|
||||
/// Which region this unit covers (1 region per unit)
|
||||
region_x: i32,
|
||||
region_z: i32,
|
||||
|
||||
/// Minecraft coordinate bounds for this unit (512×512 blocks)
|
||||
min_x: i32, // region_x * 512
|
||||
max_x: i32, // region_x * 512 + 511
|
||||
min_z: i32, // region_z * 512
|
||||
max_z: i32, // region_z * 512 + 511
|
||||
|
||||
/// Expanded bounds for element fetching (includes buffer for boundary elements)
|
||||
fetch_min_x: i32,
|
||||
fetch_max_x: i32,
|
||||
fetch_min_z: i32,
|
||||
fetch_max_z: i32,
|
||||
}
|
||||
```
|
||||
|
||||
### Unit Grid Calculation
|
||||
|
||||
```rust
|
||||
fn compute_processing_units(
|
||||
global_xzbbox: &XZBBox,
|
||||
buffer_blocks: i32, // e.g., 64-128 blocks overlap
|
||||
) -> Vec<ProcessingUnit> {
|
||||
let blocks_per_region = 512; // 32 chunks × 16 blocks
|
||||
|
||||
// Calculate which regions are covered by the bbox
|
||||
let min_region_x = global_xzbbox.min_x() >> 9; // divide by 512
|
||||
let max_region_x = global_xzbbox.max_x() >> 9;
|
||||
let min_region_z = global_xzbbox.min_z() >> 9;
|
||||
let max_region_z = global_xzbbox.max_z() >> 9;
|
||||
|
||||
let mut units = Vec::new();
|
||||
|
||||
// Create one unit per region
|
||||
for rx in min_region_x..=max_region_x {
|
||||
for rz in min_region_z..=max_region_z {
|
||||
// Compute Minecraft coordinate bounds for this region
|
||||
let min_x = rx * blocks_per_region;
|
||||
let max_x = min_x + blocks_per_region - 1;
|
||||
let min_z = rz * blocks_per_region;
|
||||
let max_z = min_z + blocks_per_region - 1;
|
||||
|
||||
// Add buffer for fetch bounds (clamped to global bbox)
|
||||
let fetch_min_x = (min_x - buffer_blocks).max(global_xzbbox.min_x());
|
||||
let fetch_max_x = (max_x + buffer_blocks).min(global_xzbbox.max_x());
|
||||
let fetch_min_z = (min_z - buffer_blocks).max(global_xzbbox.min_z());
|
||||
let fetch_max_z = (max_z + buffer_blocks).min(global_xzbbox.max_z());
|
||||
|
||||
units.push(ProcessingUnit {
|
||||
region_x: rx,
|
||||
region_z: rz,
|
||||
min_x, max_x, min_z, max_z,
|
||||
fetch_min_x, fetch_max_x, fetch_min_z, fetch_max_z,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
units
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel Execution Strategy
|
||||
|
||||
```rust
|
||||
fn process_units_parallel(
|
||||
units: Vec<ProcessingUnit>,
|
||||
elements: &[ProcessedElement],
|
||||
global_ground: Arc<Ground>,
|
||||
global_building_footprints: Arc<BuildingFootprintBitmap>,
|
||||
global_highway_connectivity: Arc<HighwayConnectivityMap>,
|
||||
args: &Args,
|
||||
) {
|
||||
// Use CPU-1 cores for parallel processing
|
||||
let num_threads = std::thread::available_parallelism()
|
||||
.map(|n| n.get().saturating_sub(1).max(1))
|
||||
.unwrap_or(1);
|
||||
|
||||
units.into_par_iter()
|
||||
.with_min_len(1) // Process 1 unit per task
|
||||
.for_each(|unit| {
|
||||
// 1. Filter elements that intersect this unit's fetch bounds
|
||||
let unit_elements = filter_elements_for_unit(elements, &unit);
|
||||
|
||||
// 2. Clip elements to unit's actual bounds
|
||||
let clipped_elements = clip_elements_to_unit(unit_elements, &unit);
|
||||
|
||||
// 3. Create per-unit structures
|
||||
let unit_xzbbox = XZBBox::new(unit.min_x, unit.max_x, unit.min_z, unit.max_z);
|
||||
let mut editor = WorldEditor::new(args.path.clone(), &unit_xzbbox, ...);
|
||||
editor.set_ground(Arc::clone(&global_ground));
|
||||
|
||||
// 4. Pre-compute flood fills for this unit's elements
|
||||
let flood_fill_cache = FloodFillCache::precompute(&clipped_elements, args.timeout.as_ref());
|
||||
|
||||
// 5. Process elements (same as current, just for this unit)
|
||||
for element in clipped_elements {
|
||||
process_element(&mut editor, &element, ...);
|
||||
}
|
||||
|
||||
// 6. Generate ground layer for this unit
|
||||
generate_ground_for_unit(&mut editor, &unit, &global_ground);
|
||||
|
||||
// 7. Save region immediately and FREE MEMORY
|
||||
editor.save_single_region(unit.region_x, unit.region_z);
|
||||
drop(editor); // Release memory
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Usage Comparison
|
||||
|
||||
### Understanding Minecraft Region Sizes
|
||||
|
||||
```
|
||||
1 Region = 32×32 chunks = 512×512 blocks (horizontally)
|
||||
1 Chunk = 16×16×384 blocks (with sections from Y=-64 to Y=319)
|
||||
```
|
||||
|
||||
### Current Architecture (All Regions in Memory)
|
||||
|
||||
| Stage | Memory Usage |
|
||||
|-------|--------------|
|
||||
| OSM Data (parsed) | ~50-200 MB |
|
||||
| Flood Fill Cache | ~100-500 MB |
|
||||
| Building Footprints | ~10-50 MB |
|
||||
| WorldToModify (all regions) | **~300 MB × N regions** |
|
||||
| **Total for 100 regions** | **~30+ GB** |
|
||||
|
||||
### Unit Size Analysis
|
||||
|
||||
The optimal unit size depends on balancing:
|
||||
1. **Memory per unit** - Larger units = more memory
|
||||
2. **Parallelism overhead** - Smaller units = more coordination
|
||||
3. **Boundary overhead** - More units = more elements processed multiple times
|
||||
|
||||
| Unit Size | Blocks | Memory per Unit | Parallel Units (8 cores) | Peak Memory |
|
||||
|-----------|--------|-----------------|--------------------------|-------------|
|
||||
| 1 region (32×32 chunks) | 512×512 | ~300 MB | 7 units | ~2.5 GB |
|
||||
| 2×2 regions | 1024×1024 | ~1.2 GB | 7 units | ~9 GB |
|
||||
| 4×4 regions | 2048×2048 | ~4.8 GB | 7 units | ~35 GB |
|
||||
|
||||
### Recommendation: 1 Region Per Unit
|
||||
|
||||
**1 region per unit is optimal because:**
|
||||
|
||||
1. **Lowest memory footprint** - Only ~300 MB per unit
|
||||
2. **Natural alignment** - Regions are the atomic save unit in Minecraft (.mca files)
|
||||
3. **Maximum parallelism** - More units = better CPU utilization
|
||||
4. **Simple boundary logic** - No partial region handling
|
||||
|
||||
**Memory calculation for 7 parallel units (8-core CPU, using 7):**
|
||||
- Per-unit WorldToModify: ~300 MB
|
||||
- Per-unit flood fill cache: ~50 MB
|
||||
- Per-unit OSM elements: ~20 MB
|
||||
- **Peak memory: ~370 MB × 7 = ~2.6 GB**
|
||||
|
||||
Plus global shared data:
|
||||
- Elevation data: ~50-100 MB
|
||||
- Building footprints: ~10-50 MB
|
||||
- Highway connectivity: ~20-50 MB
|
||||
|
||||
**Total peak: ~3 GB** (vs ~30 GB for 100 regions currently!)
|
||||
|
||||
### Why Not Smaller Than 1 Region?
|
||||
|
||||
- Regions are the minimum save unit for Minecraft
|
||||
- Going smaller would require buffering partial regions
|
||||
- No memory benefit (still need full region in memory to save)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Refactor Global Data Preparation
|
||||
|
||||
**Goal:** Extract global computations that must run before parallel processing
|
||||
|
||||
**Changes:**
|
||||
1. Move elevation fetching to a separate global phase
|
||||
2. Move building footprint collection to global phase
|
||||
3. Move highway connectivity map building to global phase
|
||||
4. Create shared data structures with `Arc<T>`
|
||||
|
||||
**Files affected:**
|
||||
- `data_processing.rs` - restructure `generate_world_with_options()`
|
||||
- `ground.rs` - no changes, already returns `Ground`
|
||||
- `floodfill_cache.rs` - add method to collect building footprints globally
|
||||
- `element_processing/highways.rs` - extract connectivity map building
|
||||
|
||||
### Phase 2: Implement Processing Unit Grid
|
||||
|
||||
**Goal:** Add logic to divide the world into processing units
|
||||
|
||||
**Changes:**
|
||||
1. Create `processing_unit.rs` module
|
||||
2. Implement grid computation
|
||||
3. Implement element-to-unit distribution
|
||||
4. Add unit-level bounding box clipping
|
||||
|
||||
**New files:**
|
||||
- `src/processing_unit.rs`
|
||||
|
||||
### Phase 3: Parallelize Unit Processing
|
||||
|
||||
**Goal:** Process units in parallel using rayon
|
||||
|
||||
**Changes:**
|
||||
1. Create per-unit WorldEditor instances
|
||||
2. Implement unit processing function
|
||||
3. Add parallel execution with CPU cap
|
||||
4. Implement region saving after unit completion
|
||||
|
||||
**Files affected:**
|
||||
- `data_processing.rs` - main parallel loop
|
||||
- `world_editor/mod.rs` - support per-unit saving
|
||||
|
||||
### Phase 4: Handle Boundary Cases
|
||||
|
||||
**Goal:** Ensure seamless results across unit boundaries
|
||||
|
||||
**Changes:**
|
||||
1. Verify deterministic RNG produces identical results
|
||||
2. Implement special handling for large water bodies
|
||||
3. Add boundary verification tests
|
||||
4. Optimize overlap buffer size
|
||||
|
||||
**Files affected:**
|
||||
- `element_processing/water_areas.rs` - global water handling
|
||||
- `clipping.rs` - potential optimizations
|
||||
|
||||
### Phase 5: Optimize Memory Management
|
||||
|
||||
**Goal:** Fine-tune memory usage and parallelism
|
||||
|
||||
**Changes:**
|
||||
1. Implement memory pressure monitoring
|
||||
2. Add dynamic unit size adjustment
|
||||
3. Optimize flood fill cache memory
|
||||
4. Profile and optimize hot paths
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
1. **Deterministic RNG Test**
|
||||
- Process same building in two units
|
||||
- Verify identical colors/styles
|
||||
|
||||
2. **Elevation Consistency Test**
|
||||
- Check ground level at unit boundaries
|
||||
- Verify no height discontinuities
|
||||
|
||||
3. **Clipping Accuracy Test**
|
||||
- Verify elements clipped correctly at unit boundaries
|
||||
- Check polygon integrity after clipping
|
||||
|
||||
### Integration Tests
|
||||
|
||||
1. **Small Area Test**
|
||||
- Process 2×2 region area
|
||||
- Verify world loads correctly in Minecraft
|
||||
|
||||
2. **Boundary Building Test**
|
||||
- Create world with buildings at unit boundaries
|
||||
- Verify buildings are complete and correctly colored
|
||||
|
||||
3. **Large Water Body Test**
|
||||
- Process area with lake spanning multiple units
|
||||
- Verify water body is continuous
|
||||
|
||||
### Performance Tests
|
||||
|
||||
1. **Memory Usage Test**
|
||||
- Monitor peak memory during processing
|
||||
- Compare with current architecture
|
||||
|
||||
2. **CPU Utilization Test**
|
||||
- Verify parallel units use expected cores
|
||||
- Measure speedup vs sequential processing
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Proposed CLI Arguments
|
||||
|
||||
```rust
|
||||
/// Number of CPU cores to use for parallel processing (default: available - 1)
|
||||
/// Set to 1 to disable parallel processing
|
||||
#[arg(long, default_value_t = 0)]
|
||||
pub parallel_cores: usize,
|
||||
|
||||
/// Buffer size for boundary overlap in blocks (default: 64)
|
||||
/// Larger values ensure buildings at boundaries are complete but increase processing time
|
||||
#[arg(long, default_value_t = 64)]
|
||||
pub boundary_buffer: i32,
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### High Risk
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Elevation discontinuities at boundaries | Use global elevation data (already planned) |
|
||||
| Race conditions in file writing | Each unit writes different regions (no overlap) |
|
||||
| Trees spawning inside buildings at boundaries | Use global building footprints bitmap |
|
||||
|
||||
### Medium Risk
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Overpass API rate limiting | Download once globally, distribute elements |
|
||||
| Complex relations broken at boundaries | Distribute full relation to all touching units |
|
||||
| Highway connectivity missing at boundaries | Build connectivity map globally |
|
||||
|
||||
### Low Risk
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Different random values at boundaries | Deterministic RNG already implemented |
|
||||
| Performance regression | Benchmark before/after, make parallel optional |
|
||||
| Water bodies split incorrectly | Water already supports clipping via `clip_water_ring_to_bbox` |
|
||||
|
||||
---
|
||||
|
||||
## Questions to Resolve
|
||||
|
||||
1. **Should we support Bedrock format with this change?**
|
||||
- Bedrock writes to a single .mcworld file (LevelDB database)
|
||||
- May need different handling (write to temp, merge at end)
|
||||
- Could be deferred to a follow-up implementation
|
||||
|
||||
2. **What buffer size for boundary overlap?**
|
||||
- Current thinking: 64-128 blocks should be sufficient
|
||||
- Most buildings are smaller than this
|
||||
- Larger buffers = more duplicate processing
|
||||
|
||||
3. **Should flood fills be computed globally or per-unit?**
|
||||
- Per-unit is simpler and avoids coordination
|
||||
- Some redundant computation at boundaries (acceptable)
|
||||
- **Recommendation:** Start per-unit
|
||||
|
||||
4. **How to report progress across parallel units?**
|
||||
- Current progress is linear (element by element)
|
||||
- With parallel, need aggregated progress reporting
|
||||
- Option: Track completed regions, report as percentage
|
||||
|
||||
5. **Should we limit parallelism based on available RAM?**
|
||||
- Could detect system RAM and adjust parallel units
|
||||
- Or just document memory requirements per parallel unit
|
||||
- **Recommendation:** Start with CPU-1 cores, let users override
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The proposed parallel region processing architecture will:
|
||||
|
||||
1. ✅ **Reduce memory usage by ~90%** by processing 1 region at a time per unit (~300 MB vs ~30 GB for 100 regions)
|
||||
2. ✅ **Utilize multiple CPU cores** through rayon-based parallel processing (CPU-1 cores)
|
||||
3. ✅ **Maintain visual consistency** using deterministic RNG and global shared data
|
||||
4. ✅ **Be backward compatible** with a `--no-parallel` flag for the current behavior
|
||||
|
||||
The main implementation work is:
|
||||
- Refactoring to extract global computations (elevation, building footprints, highway connectivity)
|
||||
- Adding element-to-unit distribution logic with proper clipping
|
||||
- Per-unit WorldEditor instances with immediate region saving
|
||||
|
||||
**The design is simpler than originally thought** because:
|
||||
- Water relations already support clipping (no special handling)
|
||||
- Deterministic RNG already exists (no changes needed)
|
||||
- Priority order is preserved naturally (just sort per-unit)
|
||||
|
||||
Estimated implementation effort: **3-4 weeks** for a fully tested solution.
|
||||
34
flake.lock
generated
34
flake.lock
generated
@@ -1,5 +1,23 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1755615617,
|
||||
@@ -17,8 +35,24 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
70
flake.nix
70
flake.nix
@@ -1,64 +1,36 @@
|
||||
{
|
||||
inputs = {
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs }: {
|
||||
|
||||
packages = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (system:
|
||||
outputs =
|
||||
{
|
||||
flake-utils,
|
||||
nixpkgs,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
lib = pkgs.lib;
|
||||
toml = lib.importTOML ./Cargo.toml;
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
stdenv = if pkgs.stdenv.isLinux then pkgs.stdenvAdapters.useMoldLinker pkgs.stdenv else pkgs.stdenv;
|
||||
in
|
||||
{
|
||||
default = self.packages.${system}.arnis;
|
||||
arnis = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "arnis";
|
||||
version = toml.package.version;
|
||||
|
||||
src = ./.;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
"bedrockrs_core-0.1.0" = "sha256-0HP6p2x6sulZ2u8FzEfAiNAeyaUjQQWgGyK/kPo0PuQ=";
|
||||
"nbtx-0.1.0" = "sha256-JoNSL1vrUbxX6hKWB4i/DX02+hsQemANJhQaEELlT2o=";
|
||||
};
|
||||
};
|
||||
|
||||
# Checks use internet connection, so we disable them in nix sandboxed environment
|
||||
doCheck = false;
|
||||
|
||||
devShell = pkgs.mkShell.override { inherit stdenv; } {
|
||||
buildInputs = with pkgs; [
|
||||
openssl.dev
|
||||
pkg-config
|
||||
wayland
|
||||
glib
|
||||
gdk-pixbuf
|
||||
pango
|
||||
gtk3
|
||||
libsoup_3.dev
|
||||
webkitgtk_4_1.dev
|
||||
];
|
||||
nativeBuildInputs = with pkgs; [
|
||||
gtk3
|
||||
pango
|
||||
gdk-pixbuf
|
||||
glib
|
||||
wayland
|
||||
pkg-config
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "Generate any location from the real world in Minecraft Java Edition with a high level of detail.";
|
||||
homepage = toml.package.homepage;
|
||||
license = lib.licenses.asl20;
|
||||
maintainers = [ ];
|
||||
mainProgram = "arnis";
|
||||
};
|
||||
};
|
||||
});
|
||||
apps = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (system: {
|
||||
default = self.apps.${system}.arnis;
|
||||
arnis = {
|
||||
type = "app";
|
||||
program = "${self.packages.${system}.arnis}/bin/arnis";
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
162
src/args.rs
162
src/args.rs
@@ -19,14 +19,9 @@ pub struct Args {
|
||||
#[arg(long, group = "location")]
|
||||
pub save_json_file: Option<String>,
|
||||
|
||||
/// Output directory for the generated world (required for Java, optional for Bedrock).
|
||||
/// Use --output-dir (or the deprecated --path alias) to specify where the world is created.
|
||||
#[arg(long = "output-dir", alias = "path")]
|
||||
pub path: Option<PathBuf>,
|
||||
|
||||
/// Generate a Bedrock Edition world (.mcworld) instead of Java Edition
|
||||
#[arg(long)]
|
||||
pub bedrock: bool,
|
||||
/// Path to the Minecraft world (required)
|
||||
#[arg(long, value_parser = validate_minecraft_world_path)]
|
||||
pub path: PathBuf,
|
||||
|
||||
/// Downloader method (requests/curl/wget) (optional)
|
||||
#[arg(long, default_value = "requests")]
|
||||
@@ -45,23 +40,17 @@ pub struct Args {
|
||||
pub terrain: bool,
|
||||
|
||||
/// Enable interior generation (optional)
|
||||
#[arg(long, default_value_t = true)]
|
||||
#[arg(long, default_value_t = true, action = clap::ArgAction::SetTrue)]
|
||||
pub interior: bool,
|
||||
|
||||
/// Enable roof generation (optional)
|
||||
#[arg(long, default_value_t = true)]
|
||||
#[arg(long, default_value_t = true, action = clap::ArgAction::SetTrue)]
|
||||
pub roof: bool,
|
||||
|
||||
/// Enable filling ground (optional)
|
||||
#[arg(long, default_value_t = false)]
|
||||
#[arg(long, default_value_t = false, action = clap::ArgAction::SetFalse)]
|
||||
pub fillground: bool,
|
||||
|
||||
/// Enable city ground generation (optional)
|
||||
/// When enabled, detects building clusters and places stone ground in urban areas.
|
||||
/// Isolated buildings in rural areas will keep grass around them.
|
||||
#[arg(long, default_value_t = true)]
|
||||
pub city_boundaries: bool,
|
||||
|
||||
/// Enable debug mode (optional)
|
||||
#[arg(long)]
|
||||
pub debug: bool,
|
||||
@@ -69,43 +58,38 @@ pub struct Args {
|
||||
/// Set floodfill timeout (seconds) (optional)
|
||||
#[arg(long, value_parser = parse_duration)]
|
||||
pub timeout: Option<Duration>,
|
||||
|
||||
/// Number of parallel threads (0 = auto, uses available cores - 1)
|
||||
#[arg(long, default_value_t = 0)]
|
||||
pub threads: usize,
|
||||
|
||||
/// Number of regions to batch per processing unit (1 = one region, 2 = 2x2=4 regions, etc.)
|
||||
/// Larger batches reduce element duplication overhead but use more memory per unit
|
||||
#[arg(long, default_value_t = 2)]
|
||||
pub region_batch_size: usize,
|
||||
|
||||
/// Disable parallel processing (process sequentially) - DEFAULT due to correctness issues
|
||||
#[arg(long, default_value_t = true)]
|
||||
pub no_parallel: bool,
|
||||
|
||||
/// Force parallel processing (experimental, may have visual bugs)
|
||||
#[arg(long)]
|
||||
pub force_parallel: bool,
|
||||
}
|
||||
|
||||
/// Validates CLI arguments after parsing.
|
||||
/// For Java Edition: `--path` is required and must point to an existing directory
|
||||
/// where a new world will be created automatically.
|
||||
/// For Bedrock Edition (`--bedrock`): `--path` is optional (defaults to Desktop output).
|
||||
pub fn validate_args(args: &Args) -> Result<(), String> {
|
||||
if args.bedrock {
|
||||
// Bedrock: path is optional; if provided, it must be an existing directory
|
||||
if let Some(ref path) = args.path {
|
||||
if !path.exists() {
|
||||
return Err(format!("Path does not exist: {}", path.display()));
|
||||
}
|
||||
if !path.is_dir() {
|
||||
return Err(format!("Path is not a directory: {}", path.display()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Java: path is required and must be an existing directory
|
||||
match &args.path {
|
||||
None => {
|
||||
return Err(
|
||||
"The --output-dir argument is required for Java Edition. Provide the directory where the world should be created. Use --bedrock for Bedrock Edition output."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Some(ref path) => {
|
||||
if !path.exists() {
|
||||
return Err(format!("Path does not exist: {}", path.display()));
|
||||
}
|
||||
if !path.is_dir() {
|
||||
return Err(format!("Path is not a directory: {}", path.display()));
|
||||
}
|
||||
}
|
||||
}
|
||||
fn validate_minecraft_world_path(path: &str) -> Result<PathBuf, String> {
|
||||
let mc_world_path = PathBuf::from(path);
|
||||
if !mc_world_path.exists() {
|
||||
return Err(format!("Path does not exist: {path}"));
|
||||
}
|
||||
Ok(())
|
||||
if !mc_world_path.is_dir() {
|
||||
return Err(format!("Path is not a directory: {path}"));
|
||||
}
|
||||
let region = mc_world_path.join("region");
|
||||
if !region.is_dir() {
|
||||
return Err(format!("No Minecraft world found at {region:?}"));
|
||||
}
|
||||
Ok(mc_world_path)
|
||||
}
|
||||
|
||||
fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntError> {
|
||||
@@ -117,15 +101,22 @@ fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntEr
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn minecraft_tmpdir() -> tempfile::TempDir {
|
||||
let tmpdir = tempfile::tempdir().unwrap();
|
||||
// create a `region` directory in the tempdir
|
||||
let region_path = tmpdir.path().join("region");
|
||||
std::fs::create_dir(®ion_path).unwrap();
|
||||
tmpdir
|
||||
}
|
||||
#[test]
|
||||
fn test_flags() {
|
||||
let tmpdir = tempfile::tempdir().unwrap();
|
||||
let tmpdir = minecraft_tmpdir();
|
||||
let tmp_path = tmpdir.path().to_str().unwrap();
|
||||
|
||||
// Test that terrain/debug are SetTrue
|
||||
let cmd = [
|
||||
"arnis",
|
||||
"--output-dir",
|
||||
"--path",
|
||||
tmp_path,
|
||||
"--bbox",
|
||||
"1,2,3,4",
|
||||
@@ -136,81 +127,24 @@ mod tests {
|
||||
assert!(args.debug);
|
||||
assert!(args.terrain);
|
||||
|
||||
let cmd = ["arnis", "--output-dir", tmp_path, "--bbox", "1,2,3,4"];
|
||||
let cmd = ["arnis", "--path", tmp_path, "--bbox", "1,2,3,4"];
|
||||
let args = Args::parse_from(cmd.iter());
|
||||
assert!(!args.debug);
|
||||
assert!(!args.terrain);
|
||||
assert!(!args.bedrock);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bedrock_flag() {
|
||||
// Bedrock mode doesn't require --output-dir
|
||||
let cmd = ["arnis", "--bedrock", "--bbox", "1,2,3,4"];
|
||||
let args = Args::parse_from(cmd.iter());
|
||||
assert!(args.bedrock);
|
||||
assert!(args.path.is_none());
|
||||
assert!(validate_args(&args).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_java_requires_path() {
|
||||
let cmd = ["arnis", "--bbox", "1,2,3,4"];
|
||||
let args = Args::parse_from(cmd.iter());
|
||||
assert!(!args.bedrock);
|
||||
assert!(args.path.is_none());
|
||||
assert!(validate_args(&args).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_java_path_must_exist() {
|
||||
let cmd = [
|
||||
"arnis",
|
||||
"--output-dir",
|
||||
"/nonexistent/path",
|
||||
"--bbox",
|
||||
"1,2,3,4",
|
||||
];
|
||||
let args = Args::parse_from(cmd.iter());
|
||||
let result = validate_args(&args);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("does not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bedrock_path_must_exist() {
|
||||
let cmd = [
|
||||
"arnis",
|
||||
"--bedrock",
|
||||
"--output-dir",
|
||||
"/nonexistent/path",
|
||||
"--bbox",
|
||||
"1,2,3,4",
|
||||
];
|
||||
let args = Args::parse_from(cmd.iter());
|
||||
let result = validate_args(&args);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("does not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_options() {
|
||||
let tmpdir = tempfile::tempdir().unwrap();
|
||||
let tmpdir = minecraft_tmpdir();
|
||||
let tmp_path = tmpdir.path().to_str().unwrap();
|
||||
|
||||
let cmd = ["arnis"];
|
||||
assert!(Args::try_parse_from(cmd.iter()).is_err());
|
||||
|
||||
let cmd = ["arnis", "--output-dir", tmp_path, "--bbox", "1,2,3,4"];
|
||||
let args = Args::try_parse_from(cmd.iter()).unwrap();
|
||||
assert!(validate_args(&args).is_ok());
|
||||
|
||||
// Verify --path still works as a deprecated alias
|
||||
let cmd = ["arnis", "--path", tmp_path, "--bbox", "1,2,3,4"];
|
||||
let args = Args::try_parse_from(cmd.iter()).unwrap();
|
||||
assert!(validate_args(&args).is_ok());
|
||||
assert!(Args::try_parse_from(cmd.iter()).is_ok());
|
||||
|
||||
let cmd = ["arnis", "--output-dir", tmp_path, "--file", ""];
|
||||
let cmd = ["arnis", "--path", tmp_path, "--file", ""];
|
||||
assert!(Args::try_parse_from(cmd.iter()).is_err());
|
||||
|
||||
// The --gui flag isn't used here, ugh. TODO clean up main.rs and its argparse usage.
|
||||
|
||||
@@ -129,81 +129,6 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
||||
)],
|
||||
),
|
||||
|
||||
// Dark oak log with axis
|
||||
"dark_oak_log" => BedrockBlock::with_states(
|
||||
"dark_oak_log",
|
||||
vec![(
|
||||
"pillar_axis",
|
||||
BedrockBlockStateValue::String("y".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Jungle log with axis
|
||||
"jungle_log" => BedrockBlock::with_states(
|
||||
"jungle_log",
|
||||
vec![(
|
||||
"pillar_axis",
|
||||
BedrockBlockStateValue::String("y".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Acacia log with axis
|
||||
"acacia_log" => BedrockBlock::with_states(
|
||||
"acacia_log",
|
||||
vec![(
|
||||
"pillar_axis",
|
||||
BedrockBlockStateValue::String("y".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Spruce leaves with persistence
|
||||
"spruce_leaves" => BedrockBlock::with_states(
|
||||
"leaves",
|
||||
vec![
|
||||
(
|
||||
"old_leaf_type",
|
||||
BedrockBlockStateValue::String("spruce".to_string()),
|
||||
),
|
||||
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||
],
|
||||
),
|
||||
|
||||
// Dark oak leaves with persistence
|
||||
"dark_oak_leaves" => BedrockBlock::with_states(
|
||||
"leaves2",
|
||||
vec![
|
||||
(
|
||||
"new_leaf_type",
|
||||
BedrockBlockStateValue::String("dark_oak".to_string()),
|
||||
),
|
||||
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||
],
|
||||
),
|
||||
|
||||
// Jungle leaves with persistence
|
||||
"jungle_leaves" => BedrockBlock::with_states(
|
||||
"leaves",
|
||||
vec![
|
||||
(
|
||||
"old_leaf_type",
|
||||
BedrockBlockStateValue::String("jungle".to_string()),
|
||||
),
|
||||
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||
],
|
||||
),
|
||||
|
||||
// Acacia leaves with persistence
|
||||
"acacia_leaves" => BedrockBlock::with_states(
|
||||
"leaves2",
|
||||
vec![
|
||||
(
|
||||
"new_leaf_type",
|
||||
BedrockBlockStateValue::String("acacia".to_string()),
|
||||
),
|
||||
("persistent_bit", BedrockBlockStateValue::Bool(true)),
|
||||
],
|
||||
),
|
||||
|
||||
// Stone slab (bottom half by default)
|
||||
"stone_slab" => BedrockBlock::with_states(
|
||||
"stone_block_slab",
|
||||
@@ -290,13 +215,6 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
||||
BedrockBlockStateValue::String("stone_brick".to_string()),
|
||||
)],
|
||||
),
|
||||
"brick_wall" => BedrockBlock::with_states(
|
||||
"cobblestone_wall",
|
||||
vec![(
|
||||
"wall_block_type",
|
||||
BedrockBlockStateValue::String("brick".to_string()),
|
||||
)],
|
||||
),
|
||||
|
||||
// Flowers - poppy is just "red_flower" in Bedrock
|
||||
"poppy" => BedrockBlock::with_states(
|
||||
@@ -403,10 +321,6 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||
),
|
||||
"green_concrete" => BedrockBlock::with_states(
|
||||
"concrete",
|
||||
vec![("color", BedrockBlockStateValue::String("green".to_string()))],
|
||||
),
|
||||
|
||||
// Terracotta colors
|
||||
"white_terracotta" => BedrockBlock::with_states(
|
||||
@@ -458,13 +372,6 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
||||
"stained_hardened_clay",
|
||||
vec![("color", BedrockBlockStateValue::String("black".to_string()))],
|
||||
),
|
||||
"light_gray_terracotta" => BedrockBlock::with_states(
|
||||
"stained_hardened_clay",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("silver".to_string()),
|
||||
)],
|
||||
),
|
||||
// Plain terracotta
|
||||
"terracotta" => BedrockBlock::simple("hardened_clay"),
|
||||
|
||||
@@ -496,17 +403,6 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
||||
BedrockBlockStateValue::String("yellow".to_string()),
|
||||
)],
|
||||
),
|
||||
"orange_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("orange".to_string()),
|
||||
)],
|
||||
),
|
||||
"blue_wool" => BedrockBlock::with_states(
|
||||
"wool",
|
||||
vec![("color", BedrockBlockStateValue::String("blue".to_string()))],
|
||||
),
|
||||
|
||||
// Carpets
|
||||
"white_carpet" => BedrockBlock::with_states(
|
||||
@@ -538,54 +434,6 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
||||
"stained_glass",
|
||||
vec![("color", BedrockBlockStateValue::String("brown".to_string()))],
|
||||
),
|
||||
"cyan_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![("color", BedrockBlockStateValue::String("cyan".to_string()))],
|
||||
),
|
||||
"blue_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![("color", BedrockBlockStateValue::String("blue".to_string()))],
|
||||
),
|
||||
"light_blue_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("light_blue".to_string()),
|
||||
)],
|
||||
),
|
||||
"red_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![("color", BedrockBlockStateValue::String("red".to_string()))],
|
||||
),
|
||||
"yellow_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("yellow".to_string()),
|
||||
)],
|
||||
),
|
||||
"purple_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("purple".to_string()),
|
||||
)],
|
||||
),
|
||||
"orange_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("orange".to_string()),
|
||||
)],
|
||||
),
|
||||
"magenta_stained_glass" => BedrockBlock::with_states(
|
||||
"stained_glass",
|
||||
vec![(
|
||||
"color",
|
||||
BedrockBlockStateValue::String("magenta".to_string()),
|
||||
)],
|
||||
),
|
||||
"daylight_detector" => BedrockBlock::simple("daylight_detector"),
|
||||
|
||||
// Planks - Bedrock uses single "planks" block with wood_type state
|
||||
"oak_planks" => BedrockBlock::with_states(
|
||||
@@ -691,34 +539,8 @@ pub fn to_bedrock_block(block: Block) -> BedrockBlock {
|
||||
// Oak items mapped to dark_oak in Bedrock (or generic equivalents)
|
||||
"oak_pressure_plate" => BedrockBlock::simple("wooden_pressure_plate"),
|
||||
"oak_door" => BedrockBlock::simple("wooden_door"),
|
||||
"spruce_door" => BedrockBlock::simple("spruce_door"),
|
||||
"dark_oak_door" => BedrockBlock::simple("dark_oak_door"),
|
||||
"oak_trapdoor" => BedrockBlock::simple("trapdoor"),
|
||||
|
||||
// Vegetation with different Bedrock names
|
||||
"fern" => BedrockBlock::with_states(
|
||||
"tallgrass",
|
||||
vec![(
|
||||
"tall_grass_type",
|
||||
BedrockBlockStateValue::String("fern".to_string()),
|
||||
)],
|
||||
),
|
||||
"large_fern" => BedrockBlock::with_states(
|
||||
"double_plant",
|
||||
vec![(
|
||||
"double_plant_type",
|
||||
BedrockBlockStateValue::String("fern".to_string()),
|
||||
)],
|
||||
),
|
||||
"cobweb" => BedrockBlock::simple("web"),
|
||||
|
||||
// Potted plants (Bedrock uses "flower_pot" for all variants;
|
||||
// the contained plant is a block entity, not a block state)
|
||||
"potted_poppy" => BedrockBlock::simple("flower_pot"),
|
||||
"potted_red_tulip" => BedrockBlock::simple("flower_pot"),
|
||||
"potted_dandelion" => BedrockBlock::simple("flower_pot"),
|
||||
"potted_blue_orchid" => BedrockBlock::simple("flower_pot"),
|
||||
|
||||
// Bed (Bedrock uses single "bed" block with color state)
|
||||
"red_bed" => BedrockBlock::with_states(
|
||||
"bed",
|
||||
@@ -742,14 +564,8 @@ pub fn to_bedrock_block_with_properties(
|
||||
) -> BedrockBlock {
|
||||
let java_name = block.name();
|
||||
|
||||
// If no stored properties were passed, fall back to block.properties()
|
||||
// so that blocks placed via set_block_absolute (e.g. doors with half=upper/lower)
|
||||
// still get their default properties forwarded to the Bedrock converter.
|
||||
let fallback_props = block.properties();
|
||||
let effective_properties = java_properties.or(fallback_props.as_ref());
|
||||
|
||||
// Extract Java properties as a map if present
|
||||
let props_map = effective_properties.and_then(|v| {
|
||||
let props_map = java_properties.and_then(|v| {
|
||||
if let fastnbt::Value::Compound(map) = v {
|
||||
Some(map)
|
||||
} else {
|
||||
@@ -762,11 +578,6 @@ pub fn to_bedrock_block_with_properties(
|
||||
return convert_stairs(java_name, props_map);
|
||||
}
|
||||
|
||||
// Handle barrel facing direction
|
||||
if java_name == "barrel" {
|
||||
return convert_barrel(java_name, props_map);
|
||||
}
|
||||
|
||||
// Handle slabs with type property (top/bottom/double)
|
||||
if java_name.ends_with("_slab") {
|
||||
return convert_slab(java_name, props_map);
|
||||
@@ -777,16 +588,6 @@ pub fn to_bedrock_block_with_properties(
|
||||
return convert_log(java_name, props_map);
|
||||
}
|
||||
|
||||
// Handle doors with half property (upper/lower → upper_block_bit)
|
||||
if java_name.ends_with("_door") && java_name != "iron_door" {
|
||||
return convert_door(java_name, props_map);
|
||||
}
|
||||
|
||||
// Handle trapdoors with facing/open/half properties
|
||||
if java_name.ends_with("_trapdoor") {
|
||||
return convert_trapdoor(java_name, props_map);
|
||||
}
|
||||
|
||||
// Fall back to basic conversion without properties
|
||||
to_bedrock_block(block)
|
||||
}
|
||||
@@ -849,46 +650,6 @@ fn convert_stairs(
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Java barrel to Bedrock format with facing direction.
|
||||
fn convert_barrel(
|
||||
java_name: &str,
|
||||
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||
) -> BedrockBlock {
|
||||
let mut states = HashMap::new();
|
||||
|
||||
if let Some(props) = props {
|
||||
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
|
||||
let facing_direction = match facing.as_str() {
|
||||
"down" => 0,
|
||||
"up" => 1,
|
||||
"north" => 2,
|
||||
"south" => 3,
|
||||
"west" => 4,
|
||||
"east" => 5,
|
||||
_ => 1,
|
||||
};
|
||||
states.insert(
|
||||
"facing_direction".to_string(),
|
||||
BedrockBlockStateValue::Int(facing_direction),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !states.contains_key("facing_direction") {
|
||||
states.insert(
|
||||
"facing_direction".to_string(),
|
||||
BedrockBlockStateValue::Int(1),
|
||||
);
|
||||
}
|
||||
|
||||
states.insert("open_bit".to_string(), BedrockBlockStateValue::Bool(false));
|
||||
|
||||
BedrockBlock {
|
||||
name: format!("minecraft:{java_name}"),
|
||||
states,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Java slab block to Bedrock format with proper type.
|
||||
fn convert_slab(
|
||||
java_name: &str,
|
||||
@@ -989,152 +750,6 @@ fn convert_log(
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Java door block to Bedrock format with upper_block_bit.
|
||||
///
|
||||
/// Java doors use `half=upper/lower`, Bedrock uses `upper_block_bit` (bool).
|
||||
/// Also maps door names: `oak_door` → `wooden_door`, others keep their names.
|
||||
fn convert_door(
|
||||
java_name: &str,
|
||||
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||
) -> BedrockBlock {
|
||||
let bedrock_name = match java_name {
|
||||
"oak_door" => "wooden_door",
|
||||
_ => java_name, // spruce_door, dark_oak_door, etc. keep their name
|
||||
};
|
||||
|
||||
let mut states = HashMap::new();
|
||||
|
||||
if let Some(props) = props {
|
||||
// Convert half: Java "upper"/"lower" → Bedrock upper_block_bit true/false
|
||||
if let Some(fastnbt::Value::String(half)) = props.get("half") {
|
||||
let is_upper = half == "upper";
|
||||
states.insert(
|
||||
"upper_block_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(is_upper),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert facing if present
|
||||
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
|
||||
let direction = match facing.as_str() {
|
||||
"east" => 0,
|
||||
"south" => 1,
|
||||
"west" => 2,
|
||||
"north" => 3,
|
||||
_ => 0,
|
||||
};
|
||||
states.insert(
|
||||
"direction".to_string(),
|
||||
BedrockBlockStateValue::Int(direction),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert hinge if present
|
||||
if let Some(fastnbt::Value::String(hinge)) = props.get("hinge") {
|
||||
let door_hinge = hinge == "right";
|
||||
states.insert(
|
||||
"door_hinge_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(door_hinge),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert open if present
|
||||
if let Some(fastnbt::Value::String(open)) = props.get("open") {
|
||||
let is_open = open == "true";
|
||||
states.insert(
|
||||
"open_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(is_open),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Defaults if no properties were set
|
||||
if !states.contains_key("upper_block_bit") {
|
||||
states.insert(
|
||||
"upper_block_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(false),
|
||||
);
|
||||
}
|
||||
if !states.contains_key("direction") {
|
||||
states.insert("direction".to_string(), BedrockBlockStateValue::Int(0));
|
||||
}
|
||||
|
||||
BedrockBlock {
|
||||
name: format!("minecraft:{bedrock_name}"),
|
||||
states,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Java trapdoor block to Bedrock format with facing/open/half states.
|
||||
fn convert_trapdoor(
|
||||
java_name: &str,
|
||||
props: Option<&std::collections::HashMap<String, fastnbt::Value>>,
|
||||
) -> BedrockBlock {
|
||||
// Map Java trapdoor names to Bedrock equivalents
|
||||
let bedrock_name = match java_name {
|
||||
"oak_trapdoor" => "trapdoor",
|
||||
"iron_trapdoor" => "iron_trapdoor",
|
||||
_ => java_name, // spruce_trapdoor, dark_oak_trapdoor, birch_trapdoor, etc.
|
||||
};
|
||||
|
||||
let mut states = HashMap::new();
|
||||
|
||||
if let Some(props) = props {
|
||||
// Convert facing: Java "north/south/east/west" → Bedrock "direction" (0-3)
|
||||
// Bedrock trapdoor: 0=south, 1=north, 2=east, 3=west
|
||||
if let Some(fastnbt::Value::String(facing)) = props.get("facing") {
|
||||
let direction = match facing.as_str() {
|
||||
"south" => 0,
|
||||
"north" => 1,
|
||||
"east" => 2,
|
||||
"west" => 3,
|
||||
_ => 0,
|
||||
};
|
||||
states.insert(
|
||||
"direction".to_string(),
|
||||
BedrockBlockStateValue::Int(direction),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert open: Java "true"/"false" → Bedrock open_bit
|
||||
if let Some(fastnbt::Value::String(open)) = props.get("open") {
|
||||
let is_open = open == "true";
|
||||
states.insert(
|
||||
"open_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(is_open),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert half: Java "top"/"bottom" → Bedrock upside_down_bit
|
||||
if let Some(fastnbt::Value::String(half)) = props.get("half") {
|
||||
let upside_down = half == "top";
|
||||
states.insert(
|
||||
"upside_down_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(upside_down),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Defaults if no properties were set
|
||||
if !states.contains_key("direction") {
|
||||
states.insert("direction".to_string(), BedrockBlockStateValue::Int(0));
|
||||
}
|
||||
if !states.contains_key("open_bit") {
|
||||
states.insert("open_bit".to_string(), BedrockBlockStateValue::Bool(false));
|
||||
}
|
||||
if !states.contains_key("upside_down_bit") {
|
||||
states.insert(
|
||||
"upside_down_bit".to_string(),
|
||||
BedrockBlockStateValue::Bool(false),
|
||||
);
|
||||
}
|
||||
|
||||
BedrockBlock {
|
||||
name: format!("minecraft:{bedrock_name}"),
|
||||
states,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -266,67 +266,7 @@ impl Block {
|
||||
185 => "quartz_stairs",
|
||||
186 => "polished_andesite_stairs",
|
||||
187 => "nether_brick_stairs",
|
||||
188 => "barrel",
|
||||
189 => "fern",
|
||||
190 => "cobweb",
|
||||
191 => "chiseled_bookshelf",
|
||||
192 => "chiseled_bookshelf",
|
||||
193 => "chiseled_bookshelf",
|
||||
194 => "chiseled_bookshelf",
|
||||
195 => "chipped_anvil",
|
||||
196 => "damaged_anvil",
|
||||
197 => "large_fern",
|
||||
198 => "large_fern",
|
||||
199 => "chain",
|
||||
200 => "end_rod",
|
||||
201 => "lightning_rod",
|
||||
202 => "gold_block",
|
||||
203 => "sea_lantern",
|
||||
204 => "orange_concrete",
|
||||
205 => "orange_wool",
|
||||
206 => "blue_wool",
|
||||
207 => "green_concrete",
|
||||
208 => "brick_wall",
|
||||
209 => "redstone_block",
|
||||
210 => "chain",
|
||||
211 => "chain",
|
||||
212 => "spruce_door",
|
||||
213 => "spruce_door",
|
||||
214 => "smooth_stone_slab",
|
||||
215 => "glass_pane",
|
||||
216 => "light_gray_terracotta",
|
||||
217 => "oak_slab",
|
||||
218 => "oak_door",
|
||||
219 => "dark_oak_log",
|
||||
220 => "dark_oak_leaves",
|
||||
221 => "jungle_log",
|
||||
222 => "jungle_leaves",
|
||||
223 => "acacia_log",
|
||||
224 => "acacia_leaves",
|
||||
225 => "spruce_leaves",
|
||||
226 => "cyan_stained_glass",
|
||||
227 => "blue_stained_glass",
|
||||
228 => "light_blue_stained_glass",
|
||||
229 => "daylight_detector",
|
||||
230 => "red_stained_glass",
|
||||
231 => "yellow_stained_glass",
|
||||
232 => "purple_stained_glass",
|
||||
233 => "orange_stained_glass",
|
||||
234 => "magenta_stained_glass",
|
||||
235 => "potted_poppy",
|
||||
236 => "oak_trapdoor",
|
||||
237 => "oak_trapdoor",
|
||||
238 => "oak_trapdoor",
|
||||
239 => "oak_trapdoor",
|
||||
240 => "quartz_slab",
|
||||
241 => "dark_oak_trapdoor",
|
||||
242 => "spruce_trapdoor",
|
||||
243 => "birch_trapdoor",
|
||||
244 => "mud_brick_slab",
|
||||
245 => "brick_slab",
|
||||
246 => "potted_red_tulip",
|
||||
247 => "potted_dandelion",
|
||||
248 => "potted_blue_orchid",
|
||||
188 => "fern",
|
||||
_ => panic!("Invalid id"),
|
||||
}
|
||||
}
|
||||
@@ -385,13 +325,6 @@ impl Block {
|
||||
map
|
||||
})),
|
||||
|
||||
// Oak door lower
|
||||
159 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("half".to_string(), Value::String("lower".to_string()));
|
||||
map
|
||||
})),
|
||||
|
||||
116 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert(
|
||||
@@ -531,140 +464,6 @@ impl Block {
|
||||
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||
map
|
||||
})),
|
||||
191 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("north".to_string()));
|
||||
map
|
||||
})),
|
||||
192 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("east".to_string()));
|
||||
map
|
||||
})),
|
||||
193 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("south".to_string()));
|
||||
map
|
||||
})),
|
||||
194 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("west".to_string()));
|
||||
map
|
||||
})),
|
||||
|
||||
197 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("half".to_string(), Value::String("lower".to_string()));
|
||||
map
|
||||
})),
|
||||
198 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("half".to_string(), Value::String("upper".to_string()));
|
||||
map
|
||||
})),
|
||||
|
||||
210 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("axis".to_string(), Value::String("x".to_string()));
|
||||
map
|
||||
})),
|
||||
211 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("axis".to_string(), Value::String("z".to_string()));
|
||||
map
|
||||
})),
|
||||
// Spruce door lower
|
||||
212 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("half".to_string(), Value::String("lower".to_string()));
|
||||
map
|
||||
})),
|
||||
// Spruce door upper
|
||||
213 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("half".to_string(), Value::String("upper".to_string()));
|
||||
map
|
||||
})),
|
||||
// Smooth stone slab (bottom by default)
|
||||
214 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("type".to_string(), Value::String("bottom".to_string()));
|
||||
map
|
||||
})),
|
||||
// Oak slab top
|
||||
217 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("type".to_string(), Value::String("top".to_string()));
|
||||
map
|
||||
})),
|
||||
// Oak door upper
|
||||
218 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("half".to_string(), Value::String("upper".to_string()));
|
||||
map
|
||||
})),
|
||||
// Dark oak leaves
|
||||
220 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("persistent".to_string(), Value::String("true".to_string()));
|
||||
map
|
||||
})),
|
||||
// Jungle leaves
|
||||
222 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("persistent".to_string(), Value::String("true".to_string()));
|
||||
map
|
||||
})),
|
||||
// Acacia leaves
|
||||
224 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("persistent".to_string(), Value::String("true".to_string()));
|
||||
map
|
||||
})),
|
||||
// Spruce leaves
|
||||
225 => Some(Value::Compound({
|
||||
let mut map: HashMap<String, Value> = HashMap::new();
|
||||
map.insert("persistent".to_string(), Value::String("true".to_string()));
|
||||
map
|
||||
})),
|
||||
// Quartz slab (top half) used as window sill
|
||||
240 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("type".to_string(), Value::String("top".to_string()));
|
||||
map
|
||||
})),
|
||||
// Open oak trapdoor facing north (hangs flat against wall, looks like shutter)
|
||||
236 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("north".to_string()));
|
||||
map.insert("open".to_string(), Value::String("true".to_string()));
|
||||
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||
map
|
||||
})),
|
||||
// Open oak trapdoor facing south
|
||||
237 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("south".to_string()));
|
||||
map.insert("open".to_string(), Value::String("true".to_string()));
|
||||
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||
map
|
||||
})),
|
||||
// Open oak trapdoor facing east
|
||||
238 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("east".to_string()));
|
||||
map.insert("open".to_string(), Value::String("true".to_string()));
|
||||
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||
map
|
||||
})),
|
||||
// Open oak trapdoor facing west
|
||||
239 => Some(Value::Compound({
|
||||
let mut map = HashMap::new();
|
||||
map.insert("facing".to_string(), Value::String("west".to_string()));
|
||||
map.insert("open".to_string(), Value::String("true".to_string()));
|
||||
map.insert("half".to_string(), Value::String("top".to_string()));
|
||||
map
|
||||
})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -899,69 +698,7 @@ pub const SMOOTH_SANDSTONE_STAIRS: Block = Block::new(184);
|
||||
pub const QUARTZ_STAIRS: Block = Block::new(185);
|
||||
pub const POLISHED_ANDESITE_STAIRS: Block = Block::new(186);
|
||||
pub const NETHER_BRICK_STAIRS: Block = Block::new(187);
|
||||
pub const BARREL: Block = Block::new(188);
|
||||
pub const FERN: Block = Block::new(189);
|
||||
pub const COBWEB: Block = Block::new(190);
|
||||
pub const CHISELLED_BOOKSHELF_NORTH: Block = Block::new(191);
|
||||
pub const CHISELLED_BOOKSHELF_EAST: Block = Block::new(192);
|
||||
pub const CHISELLED_BOOKSHELF_SOUTH: Block = Block::new(193);
|
||||
pub const CHISELLED_BOOKSHELF_WEST: Block = Block::new(194);
|
||||
// Backwards-compatible alias (defaults to north-facing)
|
||||
pub const CHISELLED_BOOKSHELF: Block = CHISELLED_BOOKSHELF_NORTH;
|
||||
pub const CHIPPED_ANVIL: Block = Block::new(195);
|
||||
pub const DAMAGED_ANVIL: Block = Block::new(196);
|
||||
pub const LARGE_FERN_LOWER: Block = Block::new(197);
|
||||
pub const LARGE_FERN_UPPER: Block = Block::new(198);
|
||||
pub const CHAIN: Block = Block::new(199);
|
||||
pub const END_ROD: Block = Block::new(200);
|
||||
pub const LIGHTNING_ROD: Block = Block::new(201);
|
||||
pub const GOLD_BLOCK: Block = Block::new(202);
|
||||
pub const SEA_LANTERN: Block = Block::new(203);
|
||||
pub const ORANGE_CONCRETE: Block = Block::new(204);
|
||||
pub const ORANGE_WOOL: Block = Block::new(205);
|
||||
pub const BLUE_WOOL: Block = Block::new(206);
|
||||
pub const GREEN_CONCRETE: Block = Block::new(207);
|
||||
pub const BRICK_WALL: Block = Block::new(208);
|
||||
pub const REDSTONE_BLOCK: Block = Block::new(209);
|
||||
pub const CHAIN_X: Block = Block::new(210);
|
||||
pub const CHAIN_Z: Block = Block::new(211);
|
||||
pub const SPRUCE_DOOR_LOWER: Block = Block::new(212);
|
||||
pub const SPRUCE_DOOR_UPPER: Block = Block::new(213);
|
||||
pub const SMOOTH_STONE_SLAB: Block = Block::new(214);
|
||||
pub const GLASS_PANE: Block = Block::new(215);
|
||||
pub const LIGHT_GRAY_TERRACOTTA: Block = Block::new(216);
|
||||
pub const OAK_SLAB_TOP: Block = Block::new(217);
|
||||
pub const OAK_DOOR_UPPER: Block = Block::new(218);
|
||||
pub const DARK_OAK_LOG: Block = Block::new(219);
|
||||
pub const DARK_OAK_LEAVES: Block = Block::new(220);
|
||||
pub const JUNGLE_LOG: Block = Block::new(221);
|
||||
pub const JUNGLE_LEAVES: Block = Block::new(222);
|
||||
pub const ACACIA_LOG: Block = Block::new(223);
|
||||
pub const ACACIA_LEAVES: Block = Block::new(224);
|
||||
pub const SPRUCE_LEAVES: Block = Block::new(225);
|
||||
pub const CYAN_STAINED_GLASS: Block = Block::new(226);
|
||||
pub const BLUE_STAINED_GLASS: Block = Block::new(227);
|
||||
pub const LIGHT_BLUE_STAINED_GLASS: Block = Block::new(228);
|
||||
pub const DAYLIGHT_DETECTOR: Block = Block::new(229);
|
||||
pub const RED_STAINED_GLASS: Block = Block::new(230);
|
||||
pub const YELLOW_STAINED_GLASS: Block = Block::new(231);
|
||||
pub const PURPLE_STAINED_GLASS: Block = Block::new(232);
|
||||
pub const ORANGE_STAINED_GLASS: Block = Block::new(233);
|
||||
pub const MAGENTA_STAINED_GLASS: Block = Block::new(234);
|
||||
pub const FLOWER_POT: Block = Block::new(235);
|
||||
pub const OAK_TRAPDOOR_OPEN_NORTH: Block = Block::new(236);
|
||||
pub const OAK_TRAPDOOR_OPEN_SOUTH: Block = Block::new(237);
|
||||
pub const OAK_TRAPDOOR_OPEN_EAST: Block = Block::new(238);
|
||||
pub const OAK_TRAPDOOR_OPEN_WEST: Block = Block::new(239);
|
||||
pub const QUARTZ_SLAB_TOP: Block = Block::new(240);
|
||||
pub const DARK_OAK_TRAPDOOR: Block = Block::new(241);
|
||||
pub const SPRUCE_TRAPDOOR: Block = Block::new(242);
|
||||
pub const BIRCH_TRAPDOOR: Block = Block::new(243);
|
||||
pub const MUD_BRICK_SLAB: Block = Block::new(244);
|
||||
pub const BRICK_SLAB: Block = Block::new(245);
|
||||
pub const POTTED_RED_TULIP: Block = Block::new(246);
|
||||
pub const POTTED_DANDELION: Block = Block::new(247);
|
||||
pub const POTTED_BLUE_ORCHID: Block = Block::new(248);
|
||||
pub const FERN: Block = Block::new(188);
|
||||
|
||||
/// Maps a block to its corresponding stair variant
|
||||
#[inline]
|
||||
@@ -1013,80 +750,58 @@ pub static WINDOW_VARIATIONS: [Block; 7] = [
|
||||
TINTED_GLASS,
|
||||
];
|
||||
|
||||
// Residential window options
|
||||
pub static RESIDENTIAL_WINDOW_OPTIONS: [Block; 4] = [
|
||||
GLASS,
|
||||
WHITE_STAINED_GLASS,
|
||||
LIGHT_GRAY_STAINED_GLASS,
|
||||
BROWN_STAINED_GLASS,
|
||||
];
|
||||
|
||||
// Institutional window options (hospital, school, etc.)
|
||||
pub static INSTITUTIONAL_WINDOW_OPTIONS: [Block; 3] =
|
||||
[GLASS, WHITE_STAINED_GLASS, LIGHT_GRAY_STAINED_GLASS];
|
||||
|
||||
// Hospitality window options (hotel, restaurant)
|
||||
pub static HOSPITALITY_WINDOW_OPTIONS: [Block; 2] = [GLASS, WHITE_STAINED_GLASS];
|
||||
|
||||
// Industrial window options
|
||||
pub static INDUSTRIAL_WINDOW_OPTIONS: [Block; 4] = [
|
||||
GLASS,
|
||||
GRAY_STAINED_GLASS,
|
||||
LIGHT_GRAY_STAINED_GLASS,
|
||||
BROWN_STAINED_GLASS,
|
||||
];
|
||||
|
||||
// Window types for different building styles (non-deterministic, for backwards compatibility)
|
||||
// Window types for different building styles
|
||||
pub fn get_window_block_for_building_type(building_type: &str) -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::rng();
|
||||
get_window_block_for_building_type_with_rng(building_type, &mut rng)
|
||||
}
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
/// Deterministic window block selection using provided RNG
|
||||
pub fn get_window_block_for_building_type_with_rng(
|
||||
building_type: &str,
|
||||
rng: &mut impl rand::Rng,
|
||||
) -> Block {
|
||||
match building_type {
|
||||
"residential" | "house" | "apartment" | "apartments" => {
|
||||
RESIDENTIAL_WINDOW_OPTIONS[rng.random_range(0..RESIDENTIAL_WINDOW_OPTIONS.len())]
|
||||
"residential" | "house" | "apartment" => {
|
||||
let residential_windows = [
|
||||
GLASS,
|
||||
WHITE_STAINED_GLASS,
|
||||
LIGHT_GRAY_STAINED_GLASS,
|
||||
BROWN_STAINED_GLASS,
|
||||
];
|
||||
residential_windows[rng.gen_range(0..residential_windows.len())]
|
||||
}
|
||||
"hospital" | "school" | "university" => {
|
||||
INSTITUTIONAL_WINDOW_OPTIONS[rng.random_range(0..INSTITUTIONAL_WINDOW_OPTIONS.len())]
|
||||
let institutional_windows = [GLASS, WHITE_STAINED_GLASS, LIGHT_GRAY_STAINED_GLASS];
|
||||
institutional_windows[rng.gen_range(0..institutional_windows.len())]
|
||||
}
|
||||
"hotel" | "restaurant" => {
|
||||
HOSPITALITY_WINDOW_OPTIONS[rng.random_range(0..HOSPITALITY_WINDOW_OPTIONS.len())]
|
||||
let hospitality_windows = [GLASS, WHITE_STAINED_GLASS];
|
||||
hospitality_windows[rng.gen_range(0..hospitality_windows.len())]
|
||||
}
|
||||
"industrial" | "warehouse" => {
|
||||
INDUSTRIAL_WINDOW_OPTIONS[rng.random_range(0..INDUSTRIAL_WINDOW_OPTIONS.len())]
|
||||
let industrial_windows = [
|
||||
GLASS,
|
||||
GRAY_STAINED_GLASS,
|
||||
LIGHT_GRAY_STAINED_GLASS,
|
||||
BROWN_STAINED_GLASS,
|
||||
];
|
||||
industrial_windows[rng.gen_range(0..industrial_windows.len())]
|
||||
}
|
||||
_ => WINDOW_VARIATIONS[rng.random_range(0..WINDOW_VARIATIONS.len())],
|
||||
_ => WINDOW_VARIATIONS[rng.gen_range(0..WINDOW_VARIATIONS.len())],
|
||||
}
|
||||
}
|
||||
|
||||
// Floor block options for buildings
|
||||
pub static FLOOR_BLOCK_OPTIONS: [Block; 8] = [
|
||||
WHITE_CONCRETE,
|
||||
GRAY_CONCRETE,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
POLISHED_ANDESITE,
|
||||
SMOOTH_STONE,
|
||||
STONE_BRICKS,
|
||||
MUD_BRICKS,
|
||||
OAK_PLANKS,
|
||||
];
|
||||
|
||||
// Random floor block selection (non-deterministic, for backwards compatibility)
|
||||
// Random floor block selection
|
||||
pub fn get_random_floor_block() -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::rng();
|
||||
FLOOR_BLOCK_OPTIONS[rng.random_range(0..FLOOR_BLOCK_OPTIONS.len())]
|
||||
}
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
/// Deterministic floor block selection using provided RNG
|
||||
pub fn get_floor_block_with_rng(rng: &mut impl rand::Rng) -> Block {
|
||||
FLOOR_BLOCK_OPTIONS[rng.random_range(0..FLOOR_BLOCK_OPTIONS.len())]
|
||||
let floor_options = [
|
||||
WHITE_CONCRETE,
|
||||
GRAY_CONCRETE,
|
||||
LIGHT_GRAY_CONCRETE,
|
||||
POLISHED_ANDESITE,
|
||||
SMOOTH_STONE,
|
||||
STONE_BRICKS,
|
||||
MUD_BRICKS,
|
||||
OAK_PLANKS,
|
||||
];
|
||||
floor_options[rng.gen_range(0..floor_options.len())]
|
||||
}
|
||||
|
||||
// Define all predefined colors with their blocks
|
||||
@@ -1221,7 +936,7 @@ static DEFINED_COLORS: &[ColorBlockMapping] = &[
|
||||
// Function to randomly select building wall block with alternatives
|
||||
pub fn get_building_wall_block_for_color(color: RGBTuple) -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// Find the closest color match
|
||||
let closest_color = DEFINED_COLORS
|
||||
@@ -1229,7 +944,7 @@ pub fn get_building_wall_block_for_color(color: RGBTuple) -> Block {
|
||||
.min_by_key(|(defined_color, _)| crate::colors::rgb_distance(&color, defined_color));
|
||||
|
||||
if let Some((_, options)) = closest_color {
|
||||
options[rng.random_range(0..options.len())]
|
||||
options[rng.gen_range(0..options.len())]
|
||||
} else {
|
||||
// This should never happen, but fallback just in case
|
||||
get_fallback_building_block()
|
||||
@@ -1239,7 +954,7 @@ pub fn get_building_wall_block_for_color(color: RGBTuple) -> Block {
|
||||
// Function to get a random fallback building block when no color attribute is specified
|
||||
pub fn get_fallback_building_block() -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let fallback_options = [
|
||||
BLACKSTONE,
|
||||
@@ -1268,14 +983,15 @@ pub fn get_fallback_building_block() -> Block {
|
||||
STONE_BRICKS,
|
||||
WHITE_CONCRETE,
|
||||
WHITE_TERRACOTTA,
|
||||
OAK_PLANKS,
|
||||
];
|
||||
fallback_options[rng.random_range(0..fallback_options.len())]
|
||||
fallback_options[rng.gen_range(0..fallback_options.len())]
|
||||
}
|
||||
|
||||
// Function to get a random castle wall block
|
||||
pub fn get_castle_wall_block() -> Block {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::rng();
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let castle_wall_options = [
|
||||
STONE_BRICKS,
|
||||
@@ -1289,5 +1005,5 @@ pub fn get_castle_wall_block() -> Block {
|
||||
SMOOTH_STONE,
|
||||
BRICK,
|
||||
];
|
||||
castle_wall_options[rng.random_range(0..castle_wall_options.len())]
|
||||
castle_wall_options[rng.gen_range(0..castle_wall_options.len())]
|
||||
}
|
||||
|
||||
@@ -14,9 +14,6 @@ pub fn clip_way_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<Process
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Get way ID for ID generation
|
||||
let way_id = nodes.first().map(|n| n.id).unwrap_or(0);
|
||||
|
||||
let is_closed = is_closed_polygon(nodes);
|
||||
|
||||
if !is_closed {
|
||||
@@ -57,13 +54,12 @@ pub fn clip_way_to_bbox(nodes: &[ProcessedNode], xzbbox: &XZBBox) -> Vec<Process
|
||||
}
|
||||
|
||||
let polygon = insert_bbox_corners(polygon, min_x, min_z, max_x, max_z);
|
||||
|
||||
let polygon = remove_consecutive_duplicates(polygon);
|
||||
|
||||
if polygon.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let way_id = nodes.first().map(|n| n.id).unwrap_or(0);
|
||||
assign_node_ids_preserving_endpoints(nodes, polygon, way_id)
|
||||
}
|
||||
|
||||
@@ -500,15 +496,12 @@ fn find_bbox_intersections(
|
||||
|
||||
/// Returns which bbox edge a point lies on: 0=bottom, 1=right, 2=top, 3=left, -1=interior.
|
||||
fn get_bbox_edge(point: (f64, f64), min_x: f64, min_z: f64, max_x: f64, max_z: f64) -> i32 {
|
||||
// Use a slightly larger epsilon to handle floating-point errors from Sutherland-Hodgman.
|
||||
// Points should be clamped to bbox before this function is called, so any point
|
||||
// at or very near the boundary should be considered ON that edge.
|
||||
let eps = 1.0;
|
||||
let eps = 0.5;
|
||||
|
||||
let on_left = (point.0 - min_x).abs() <= eps;
|
||||
let on_right = (point.0 - max_x).abs() <= eps;
|
||||
let on_bottom = (point.1 - min_z).abs() <= eps;
|
||||
let on_top = (point.1 - max_z).abs() <= eps;
|
||||
let on_left = (point.0 - min_x).abs() < eps;
|
||||
let on_right = (point.0 - max_x).abs() < eps;
|
||||
let on_bottom = (point.1 - min_z).abs() < eps;
|
||||
let on_top = (point.1 - max_z).abs() < eps;
|
||||
|
||||
// Handle corners (assign to edge in counter-clockwise order)
|
||||
if on_bottom && on_left {
|
||||
@@ -563,21 +556,20 @@ fn get_corners_between_edges(
|
||||
let ccw_dist = ((edge2 - edge1 + 4) % 4) as usize;
|
||||
let cw_dist = ((edge1 - edge2 + 4) % 4) as usize;
|
||||
|
||||
// For opposite edges (distance = 2), we need to pick a direction.
|
||||
// Use counter-clockwise by default to ensure corners are inserted.
|
||||
// This prevents diagonal lines when polygon spans opposite bbox edges.
|
||||
// Opposite edges: don't insert corners
|
||||
if ccw_dist == 2 && cw_dist == 2 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
if ccw_dist <= cw_dist {
|
||||
// Go counter-clockwise
|
||||
let mut current = edge1;
|
||||
for _ in 0..ccw_dist {
|
||||
result.push(corners[current as usize]);
|
||||
current = (current + 1) % 4;
|
||||
}
|
||||
} else {
|
||||
// Go clockwise
|
||||
let mut current = edge1;
|
||||
for _ in 0..cw_dist {
|
||||
current = (current + 4 - 1) % 4;
|
||||
@@ -588,12 +580,6 @@ fn get_corners_between_edges(
|
||||
result
|
||||
}
|
||||
|
||||
/// Checks if two points are approximately equal (within epsilon tolerance).
|
||||
fn points_approx_equal(p1: (f64, f64), p2: (f64, f64)) -> bool {
|
||||
let eps = 1.0;
|
||||
(p1.0 - p2.0).abs() <= eps && (p1.1 - p2.1).abs() <= eps
|
||||
}
|
||||
|
||||
/// Inserts bbox corners where polygon transitions between different bbox edges.
|
||||
fn insert_bbox_corners(
|
||||
polygon: Vec<(f64, f64)>,
|
||||
@@ -618,13 +604,8 @@ fn insert_bbox_corners(
|
||||
let edge2 = get_bbox_edge(next, min_x, min_z, max_x, max_z);
|
||||
|
||||
if edge1 >= 0 && edge2 >= 0 && edge1 != edge2 {
|
||||
let corners = get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z);
|
||||
|
||||
// Filter out corners that match the current point or the next point
|
||||
for corner in corners {
|
||||
if !points_approx_equal(corner, current) && !points_approx_equal(corner, next) {
|
||||
result.push(corner);
|
||||
}
|
||||
for corner in get_corners_between_edges(edge1, edge2, min_x, min_z, max_x, max_z) {
|
||||
result.push(corner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mod xzbbox;
|
||||
pub mod xzbbox;
|
||||
mod xzpoint;
|
||||
mod xzvector;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mod rectangle;
|
||||
pub mod rectangle;
|
||||
mod xzbbox_enum;
|
||||
|
||||
pub use xzbbox_enum::XZBBox;
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, SMOOTH_STONE, STONE};
|
||||
use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, STONE};
|
||||
use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::element_processing::*;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::ground::Ground;
|
||||
use crate::map_renderer;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole};
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::parallel_processing::{
|
||||
calculate_parallel_threads, compute_processing_units, distribute_elements_to_units_indices,
|
||||
ParallelConfig, ProcessingStats,
|
||||
};
|
||||
use crate::progress::{emit_gui_progress_update, emit_map_preview_ready, emit_open_mcworld_file};
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use crate::urban_ground;
|
||||
use crate::unit_processing::{process_unit_refs, SharedProcessingData};
|
||||
use crate::world_editor::{WorldEditor, WorldFormat};
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use std::collections::HashSet;
|
||||
use rayon::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -29,6 +33,38 @@ pub struct GenerationOptions {
|
||||
pub spawn_point: Option<(i32, i32)>,
|
||||
}
|
||||
|
||||
pub fn generate_world(
|
||||
elements: Vec<ProcessedElement>,
|
||||
xzbbox: XZBBox,
|
||||
llbbox: LLBBox,
|
||||
ground: Ground,
|
||||
args: &Args,
|
||||
) -> Result<(), String> {
|
||||
// Default to Java format when called from CLI
|
||||
let options = GenerationOptions {
|
||||
path: args.path.clone(),
|
||||
format: WorldFormat::JavaAnvil,
|
||||
level_name: None,
|
||||
spawn_point: None,
|
||||
};
|
||||
|
||||
// Use sequential by default (parallel has correctness issues)
|
||||
// Use --force-parallel to enable experimental parallel mode
|
||||
let parallel_config = if args.force_parallel {
|
||||
ParallelConfig {
|
||||
num_threads: args.threads,
|
||||
buffer_blocks: 64,
|
||||
enabled: true,
|
||||
region_batch_size: args.region_batch_size,
|
||||
}
|
||||
} else {
|
||||
ParallelConfig::sequential()
|
||||
};
|
||||
|
||||
generate_world_with_options(elements, xzbbox, llbbox, ground, args, options, parallel_config)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Generate world with explicit format options (used by GUI for Bedrock support)
|
||||
pub fn generate_world_with_options(
|
||||
elements: Vec<ProcessedElement>,
|
||||
@@ -37,6 +73,362 @@ pub fn generate_world_with_options(
|
||||
ground: Ground,
|
||||
args: &Args,
|
||||
options: GenerationOptions,
|
||||
parallel_config: ParallelConfig,
|
||||
) -> Result<PathBuf, String> {
|
||||
let _output_path = options.path.clone();
|
||||
let _world_format = options.format;
|
||||
|
||||
// Determine if we should use parallel processing
|
||||
let num_threads = calculate_parallel_threads(parallel_config.num_threads);
|
||||
|
||||
// Calculate region count to decide if parallel is worth the overhead
|
||||
let min_region_x = xzbbox.min_x() >> 9;
|
||||
let max_region_x = xzbbox.max_x() >> 9;
|
||||
let min_region_z = xzbbox.min_z() >> 9;
|
||||
let max_region_z = xzbbox.max_z() >> 9;
|
||||
let region_count = ((max_region_x - min_region_x + 1) * (max_region_z - min_region_z + 1)) as usize;
|
||||
|
||||
// Auto-disable parallel for small areas (< 6 regions) - overhead isn't worth it
|
||||
// User can still force parallel with explicit --threads > 1 and region count check
|
||||
let use_parallel = parallel_config.enabled && num_threads > 1 && region_count >= 6;
|
||||
|
||||
let mode_reason = if !parallel_config.enabled {
|
||||
"disabled by --no-parallel"
|
||||
} else if num_threads <= 1 {
|
||||
"single thread"
|
||||
} else if region_count < 6 {
|
||||
"small area (< 6 regions)"
|
||||
} else {
|
||||
"parallel"
|
||||
};
|
||||
|
||||
println!(
|
||||
"{} Processing data ({} mode, {} thread(s), {} regions)...",
|
||||
"[4/7]".bold(),
|
||||
if use_parallel { "parallel" } else { "sequential" },
|
||||
num_threads,
|
||||
region_count
|
||||
);
|
||||
|
||||
if !use_parallel && parallel_config.enabled && region_count < 6 {
|
||||
println!(" (auto-selected sequential: {})", mode_reason);
|
||||
}
|
||||
|
||||
// Build highway connectivity map once before processing (needed for all units)
|
||||
let highway_connectivity = Arc::new(highways::build_highway_connectivity_map(&elements));
|
||||
|
||||
let ground = Arc::new(ground);
|
||||
|
||||
println!("{} Processing terrain...", "[5/7]".bold());
|
||||
emit_gui_progress_update(25.0, "Processing terrain...");
|
||||
|
||||
// Pre-compute all flood fills in parallel for better CPU utilization
|
||||
let flood_fill_cache = Arc::new(FloodFillCache::precompute(
|
||||
&elements,
|
||||
args.timeout.as_ref(),
|
||||
));
|
||||
|
||||
// Collect building footprints to prevent trees from spawning inside buildings
|
||||
let building_footprints =
|
||||
Arc::new(flood_fill_cache.collect_building_footprints(&elements, &xzbbox));
|
||||
|
||||
if use_parallel {
|
||||
// === PARALLEL PROCESSING PATH ===
|
||||
generate_world_parallel(
|
||||
elements,
|
||||
xzbbox,
|
||||
llbbox,
|
||||
ground,
|
||||
highway_connectivity,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
args,
|
||||
options,
|
||||
parallel_config,
|
||||
)
|
||||
} else {
|
||||
// === SEQUENTIAL PROCESSING PATH (original logic) ===
|
||||
generate_world_sequential(
|
||||
elements,
|
||||
xzbbox,
|
||||
llbbox,
|
||||
ground,
|
||||
highway_connectivity,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
args,
|
||||
options,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parallel world generation - processes regions in parallel, saving each immediately
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn generate_world_parallel(
|
||||
elements: Vec<ProcessedElement>,
|
||||
xzbbox: XZBBox,
|
||||
llbbox: LLBBox,
|
||||
ground: Arc<Ground>,
|
||||
highway_connectivity: Arc<highways::HighwayConnectivityMap>,
|
||||
flood_fill_cache: Arc<FloodFillCache>,
|
||||
building_footprints: Arc<crate::floodfill_cache::BuildingFootprintBitmap>,
|
||||
args: &Args,
|
||||
options: GenerationOptions,
|
||||
parallel_config: ParallelConfig,
|
||||
) -> Result<PathBuf, String> {
|
||||
let output_path = options.path.clone();
|
||||
let world_format = options.format;
|
||||
|
||||
// Compute processing units (one or more regions per unit depending on batch size)
|
||||
let units = compute_processing_units(
|
||||
&xzbbox,
|
||||
parallel_config.buffer_blocks,
|
||||
parallel_config.region_batch_size
|
||||
);
|
||||
let total_units = units.len();
|
||||
|
||||
println!(
|
||||
" {} unit(s) to process across {} thread(s) (batch size: {})",
|
||||
total_units,
|
||||
calculate_parallel_threads(parallel_config.num_threads),
|
||||
parallel_config.region_batch_size
|
||||
);
|
||||
|
||||
// Distribute elements to units based on spatial intersection
|
||||
// Returns indices into the elements vector for each unit
|
||||
let unit_element_indices = distribute_elements_to_units_indices(&elements, &units);
|
||||
|
||||
// Wrap elements in Arc for shared access across threads
|
||||
let elements = Arc::new(elements);
|
||||
|
||||
// Create shared data for all units
|
||||
let shared = Arc::new(SharedProcessingData {
|
||||
ground: Arc::clone(&ground),
|
||||
highway_connectivity: Arc::clone(&highway_connectivity),
|
||||
building_footprints: Arc::clone(&building_footprints),
|
||||
floodfill_cache: Arc::clone(&flood_fill_cache),
|
||||
llbbox,
|
||||
world_dir: options.path.clone(),
|
||||
format: options.format,
|
||||
level_name: options.level_name.clone(),
|
||||
terrain_enabled: args.terrain,
|
||||
ground_level: args.ground_level,
|
||||
fill_ground: args.fillground,
|
||||
interior: args.interior,
|
||||
roof: args.roof,
|
||||
debug: args.debug,
|
||||
timeout: args.timeout,
|
||||
});
|
||||
|
||||
// Set up progress tracking
|
||||
let stats = Arc::new(ProcessingStats::new(total_units, 0));
|
||||
let process_pb = ProgressBar::new(total_units as u64);
|
||||
process_pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45}] {pos}/{len} regions ({eta})")
|
||||
.unwrap()
|
||||
.progress_chars("█▓░"),
|
||||
);
|
||||
|
||||
// Process units in parallel
|
||||
println!("{} Processing regions in parallel...", "[5/7]".bold());
|
||||
|
||||
// Log element distribution stats
|
||||
let total_element_refs: usize = unit_element_indices.iter().map(|v| v.len()).sum();
|
||||
let avg_elements_per_unit = total_element_refs as f64 / total_units as f64;
|
||||
println!(
|
||||
" Total element references: {} (avg {:.1} per unit, original: {})",
|
||||
total_element_refs, avg_elements_per_unit, elements.len()
|
||||
);
|
||||
println!(
|
||||
" Element processing overhead: {:.1}x (elements processed multiple times across regions)",
|
||||
total_element_refs as f64 / elements.len() as f64
|
||||
);
|
||||
|
||||
// Configure thread pool to use requested number of threads
|
||||
let num_threads = calculate_parallel_threads(parallel_config.num_threads);
|
||||
|
||||
// Process each unit: generate blocks, save region, free memory
|
||||
let units_with_indices: Vec<_> = units
|
||||
.into_iter()
|
||||
.zip(unit_element_indices.into_iter())
|
||||
.collect();
|
||||
|
||||
// Track timing for each unit
|
||||
let unit_times = std::sync::Mutex::new(Vec::with_capacity(total_units));
|
||||
let parallel_start = std::time::Instant::now();
|
||||
|
||||
// Track which thread processes each unit
|
||||
let thread_ids = std::sync::Mutex::new(std::collections::HashSet::new());
|
||||
|
||||
// Use rayon's parallel iterator with configured thread count
|
||||
println!(" Starting parallel processing with {} threads...", num_threads);
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(num_threads)
|
||||
.build()
|
||||
.unwrap()
|
||||
.install(|| {
|
||||
units_with_indices
|
||||
.par_iter()
|
||||
.for_each(|(unit, element_indices)| {
|
||||
// Track thread usage
|
||||
let thread_id = std::thread::current().id();
|
||||
thread_ids.lock().unwrap().insert(format!("{:?}", thread_id));
|
||||
|
||||
let unit_start = std::time::Instant::now();
|
||||
|
||||
// Collect elements for this unit using indices - only clone what's needed
|
||||
let unit_elements: Vec<&ProcessedElement> = element_indices
|
||||
.iter()
|
||||
.map(|&idx| &elements[idx])
|
||||
.collect();
|
||||
|
||||
// Create bbox for this specific unit
|
||||
let unit_bbox = unit.bbox();
|
||||
|
||||
// Process this unit and save immediately
|
||||
let process_start = std::time::Instant::now();
|
||||
let mut editor = process_unit_refs(unit, &unit_elements, &shared, &unit_bbox, args);
|
||||
let process_time = process_start.elapsed();
|
||||
|
||||
// Save this region silently (no progress output)
|
||||
let save_start = std::time::Instant::now();
|
||||
editor.save_silent();
|
||||
let save_time = save_start.elapsed();
|
||||
|
||||
// editor is dropped here, freeing its memory
|
||||
let total_time = unit_start.elapsed();
|
||||
|
||||
// Update progress
|
||||
let completed = stats.increment_completed();
|
||||
process_pb.inc(1);
|
||||
|
||||
// Store timing info
|
||||
unit_times.lock().unwrap().push((
|
||||
unit.region_x,
|
||||
unit.region_z,
|
||||
element_indices.len(),
|
||||
process_time,
|
||||
save_time,
|
||||
total_time,
|
||||
));
|
||||
|
||||
// Progress: 25% (terrain done) to 90% (regions done)
|
||||
// This covers the full parallel processing phase
|
||||
let progress = 25.0 + (completed as f64 / total_units as f64) * 65.0;
|
||||
emit_gui_progress_update(progress, &format!("Processing unit {}/{}...", completed, total_units));
|
||||
});
|
||||
});
|
||||
|
||||
process_pb.finish();
|
||||
let parallel_duration = parallel_start.elapsed();
|
||||
|
||||
// Report thread usage
|
||||
let unique_threads = thread_ids.into_inner().unwrap();
|
||||
println!(" Threads actually used: {} (requested: {})", unique_threads.len(), num_threads);
|
||||
|
||||
// Print timing summary
|
||||
let times = unit_times.into_inner().unwrap();
|
||||
println!("\n === Unit Processing Times ===");
|
||||
|
||||
let mut total_process = std::time::Duration::ZERO;
|
||||
let mut total_save = std::time::Duration::ZERO;
|
||||
|
||||
// Sort by total time descending to show slowest first
|
||||
let mut sorted_times = times.clone();
|
||||
sorted_times.sort_by(|a, b| b.5.cmp(&a.5));
|
||||
|
||||
for (rx, rz, elem_count, process, save, total) in sorted_times.iter().take(10) {
|
||||
println!(
|
||||
" Region ({:3},{:3}): {} elements, process: {:>6.2}s, save: {:>5.2}s, total: {:>6.2}s",
|
||||
rx, rz, elem_count,
|
||||
process.as_secs_f64(),
|
||||
save.as_secs_f64(),
|
||||
total.as_secs_f64()
|
||||
);
|
||||
total_process += *process;
|
||||
total_save += *save;
|
||||
}
|
||||
|
||||
if times.len() > 10 {
|
||||
for (_, _, _, process, save, _) in times.iter().skip(10) {
|
||||
total_process += *process;
|
||||
total_save += *save;
|
||||
}
|
||||
println!(" ... and {} more units", times.len() - 10);
|
||||
}
|
||||
|
||||
let sum_total: std::time::Duration = times.iter().map(|t| t.5).sum();
|
||||
println!(" Sum of all unit times: {:.2}s (process: {:.2}s, save: {:.2}s)",
|
||||
sum_total.as_secs_f64(), total_process.as_secs_f64(), total_save.as_secs_f64());
|
||||
println!(" Actual wall time: {:.2}s", parallel_duration.as_secs_f64());
|
||||
println!(" Parallelism factor: {:.2}x (sum/wall)", sum_total.as_secs_f64() / parallel_duration.as_secs_f64());
|
||||
println!();
|
||||
|
||||
// Final save for any remaining metadata or global operations
|
||||
println!("{} Finalizing world...", "[7/7]".bold());
|
||||
emit_gui_progress_update(90.0, "Finalizing world...");
|
||||
|
||||
// Save metadata file (regions already saved individually during processing)
|
||||
let mut metadata_editor = WorldEditor::new_with_format_and_name(
|
||||
options.path.clone(),
|
||||
&xzbbox,
|
||||
llbbox,
|
||||
options.format,
|
||||
options.level_name,
|
||||
options.spawn_point,
|
||||
);
|
||||
metadata_editor.set_ground(Arc::clone(&ground));
|
||||
// Only save metadata, not the world data (already saved per-region)
|
||||
if let Err(e) = metadata_editor.save_metadata() {
|
||||
eprintln!("Warning: Failed to save metadata: {}", e);
|
||||
}
|
||||
|
||||
emit_gui_progress_update(99.0, "World generation complete!");
|
||||
|
||||
// Handle spawn point update for GUI
|
||||
#[cfg(feature = "gui")]
|
||||
if world_format == WorldFormat::JavaAnvil {
|
||||
use crate::gui::update_player_spawn_y_after_generation;
|
||||
let bbox_string = format!(
|
||||
"{},{},{},{}",
|
||||
args.bbox.min().lat(),
|
||||
args.bbox.min().lng(),
|
||||
args.bbox.max().lat(),
|
||||
args.bbox.max().lng()
|
||||
);
|
||||
|
||||
if let Err(e) =
|
||||
update_player_spawn_y_after_generation(&args.path, bbox_string, args.scale, &ground)
|
||||
{
|
||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||
eprintln!("Warning: {}", warning_msg);
|
||||
send_log(LogLevel::Warning, &warning_msg);
|
||||
}
|
||||
}
|
||||
|
||||
// For Bedrock format, emit event to open the mcworld file
|
||||
if world_format == WorldFormat::BedrockMcWorld {
|
||||
if let Some(path_str) = output_path.to_str() {
|
||||
emit_open_mcworld_file(path_str);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output_path)
|
||||
}
|
||||
|
||||
/// Sequential world generation - original logic preserved for debugging/comparison
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn generate_world_sequential(
|
||||
elements: Vec<ProcessedElement>,
|
||||
xzbbox: XZBBox,
|
||||
llbbox: LLBBox,
|
||||
ground: Arc<Ground>,
|
||||
highway_connectivity: Arc<highways::HighwayConnectivityMap>,
|
||||
flood_fill_cache: Arc<FloodFillCache>,
|
||||
building_footprints: Arc<crate::floodfill_cache::BuildingFootprintBitmap>,
|
||||
args: &Args,
|
||||
options: GenerationOptions,
|
||||
) -> Result<PathBuf, String> {
|
||||
let output_path = options.path.clone();
|
||||
let world_format = options.format;
|
||||
@@ -50,36 +442,13 @@ pub fn generate_world_with_options(
|
||||
options.level_name.clone(),
|
||||
options.spawn_point,
|
||||
);
|
||||
let ground = Arc::new(ground);
|
||||
|
||||
println!("{} Processing data...", "[4/7]".bold());
|
||||
|
||||
// Build highway connectivity map once before processing
|
||||
let highway_connectivity = highways::build_highway_connectivity_map(&elements);
|
||||
|
||||
// Set ground reference in the editor to enable elevation-aware block placement
|
||||
editor.set_ground(Arc::clone(&ground));
|
||||
|
||||
println!("{} Processing terrain...", "[5/7]".bold());
|
||||
emit_gui_progress_update(25.0, "Processing terrain...");
|
||||
|
||||
// Pre-compute all flood fills in parallel for better CPU utilization
|
||||
let mut flood_fill_cache = FloodFillCache::precompute(&elements, args.timeout.as_ref());
|
||||
|
||||
// Collect building footprints to prevent trees from spawning inside buildings
|
||||
// Uses a memory-efficient bitmap (~1 bit per coordinate) instead of a HashSet (~24 bytes per coordinate)
|
||||
let building_footprints = flood_fill_cache.collect_building_footprints(&elements, &xzbbox);
|
||||
|
||||
// Collect building centroids for urban ground generation (only if enabled)
|
||||
// This must be done before the processing loop clears the flood fill cache
|
||||
let building_centroids = if args.city_boundaries {
|
||||
flood_fill_cache.collect_building_centroids(&elements)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Process all elements (no longer need to partition boundaries)
|
||||
// Process data
|
||||
let elements_count: usize = elements.len();
|
||||
let mut elements = elements; // Take ownership for consuming
|
||||
let process_pb: ProgressBar = ProgressBar::new(elements_count as u64);
|
||||
process_pb.set_style(ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} elements ({eta}) {msg}")
|
||||
@@ -90,35 +459,8 @@ pub fn generate_world_with_options(
|
||||
let mut current_progress_prcs: f64 = 25.0;
|
||||
let mut last_emitted_progress: f64 = current_progress_prcs;
|
||||
|
||||
// Pre-scan: detect building relation outlines that should be suppressed.
|
||||
// Only applies to type=building relations (NOT type=multipolygon).
|
||||
// When a type=building relation has "part" members, the outline way should not
|
||||
// render as a standalone building, the individual parts render instead.
|
||||
let suppressed_building_outlines: HashSet<u64> = {
|
||||
let mut outlines = HashSet::new();
|
||||
for element in &elements {
|
||||
if let ProcessedElement::Relation(rel) = element {
|
||||
let is_building_type = rel.tags.get("type").map(|t| t.as_str()) == Some("building");
|
||||
if is_building_type {
|
||||
let has_parts = rel
|
||||
.members
|
||||
.iter()
|
||||
.any(|m| m.role == ProcessedMemberRole::Part);
|
||||
if has_parts {
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
outlines.insert(member.way.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
outlines
|
||||
};
|
||||
|
||||
// Process all elements
|
||||
for element in elements.into_iter() {
|
||||
// Process elements by draining in insertion order
|
||||
for element in elements.drain(..) {
|
||||
process_pb.inc(1);
|
||||
current_progress_prcs += progress_increment_prcs;
|
||||
if (current_progress_prcs - last_emitted_progress).abs() > 0.25 {
|
||||
@@ -139,18 +481,7 @@ pub fn generate_world_with_options(
|
||||
match &element {
|
||||
ProcessedElement::Way(way) => {
|
||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
||||
// Skip building outlines that are suppressed by building relations with parts.
|
||||
// The individual building:part ways will render instead.
|
||||
if !suppressed_building_outlines.contains(&way.id) {
|
||||
buildings::generate_buildings(
|
||||
&mut editor,
|
||||
way,
|
||||
args,
|
||||
None,
|
||||
None,
|
||||
&flood_fill_cache,
|
||||
);
|
||||
}
|
||||
buildings::generate_buildings(&mut editor, way, args, None, &flood_fill_cache);
|
||||
} else if way.tags.contains_key("highway") {
|
||||
highways::generate_highways(
|
||||
&mut editor,
|
||||
@@ -205,17 +536,10 @@ pub fn generate_world_with_options(
|
||||
highways::generate_aeroway(&mut editor, way, args);
|
||||
} else if way.tags.get("service") == Some(&"siding".to_string()) {
|
||||
highways::generate_siding(&mut editor, way);
|
||||
} else if way.tags.get("tomb") == Some(&"pyramid".to_string()) {
|
||||
historic::generate_pyramid(&mut editor, way, args, &flood_fill_cache);
|
||||
} else if way.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made(&mut editor, &element, args);
|
||||
} else if way.tags.contains_key("power") {
|
||||
power::generate_power(&mut editor, &element);
|
||||
} else if way.tags.contains_key("place") {
|
||||
landuse::generate_place(&mut editor, way, args, &flood_fill_cache);
|
||||
}
|
||||
// Release flood fill cache entry for this way
|
||||
flood_fill_cache.remove_way(way.id);
|
||||
// Note: flood fill cache entries are managed by Arc, not removed per-element in Arc version
|
||||
}
|
||||
ProcessedElement::Node(node) => {
|
||||
if node.tags.contains_key("door") || node.tags.contains_key("entrance") {
|
||||
@@ -246,27 +570,15 @@ pub fn generate_world_with_options(
|
||||
tourisms::generate_tourisms(&mut editor, node);
|
||||
} else if node.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made_nodes(&mut editor, node);
|
||||
} else if node.tags.contains_key("power") {
|
||||
power::generate_power_nodes(&mut editor, node);
|
||||
} else if node.tags.contains_key("historic") {
|
||||
historic::generate_historic(&mut editor, node);
|
||||
} else if node.tags.contains_key("emergency") {
|
||||
emergency::generate_emergency(&mut editor, node);
|
||||
} else if node.tags.contains_key("advertising") {
|
||||
advertising::generate_advertising(&mut editor, node);
|
||||
}
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
let is_building_relation = rel.tags.contains_key("building")
|
||||
|| rel.tags.contains_key("building:part")
|
||||
|| rel.tags.get("type").map(|t| t.as_str()) == Some("building");
|
||||
if is_building_relation {
|
||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||
buildings::generate_building_from_relation(
|
||||
&mut editor,
|
||||
rel,
|
||||
args,
|
||||
&flood_fill_cache,
|
||||
&xzbbox,
|
||||
);
|
||||
} else if rel.tags.contains_key("water")
|
||||
|| rel
|
||||
@@ -303,9 +615,7 @@ pub fn generate_world_with_options(
|
||||
} else if rel.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made(&mut editor, &element, args);
|
||||
}
|
||||
// Release flood fill cache entries for all ways in this relation
|
||||
let way_ids: Vec<u64> = rel.members.iter().map(|m| m.way.id).collect();
|
||||
flood_fill_cache.remove_relation_ways(&way_ids);
|
||||
// Note: flood fill cache entries are managed by Arc, dropped when no longer referenced
|
||||
}
|
||||
}
|
||||
// Element is dropped here, freeing its memory immediately
|
||||
@@ -313,16 +623,6 @@ pub fn generate_world_with_options(
|
||||
|
||||
process_pb.finish();
|
||||
|
||||
// Compute urban ground lookup (if enabled)
|
||||
// Uses a compact cell-based representation instead of storing all coordinates.
|
||||
// Memory usage: ~270 KB vs ~560 MB for coordinate-based approach.
|
||||
let urban_lookup = if args.city_boundaries && !building_centroids.is_empty() {
|
||||
urban_ground::compute_urban_ground_lookup(building_centroids, &xzbbox)
|
||||
} else {
|
||||
urban_ground::UrbanGroundLookup::empty()
|
||||
};
|
||||
let has_urban_ground = !urban_lookup.is_empty();
|
||||
|
||||
// Drop remaining caches
|
||||
drop(highway_connectivity);
|
||||
drop(flood_fill_cache);
|
||||
@@ -379,31 +679,26 @@ pub fn generate_world_with_options(
|
||||
args.ground_level
|
||||
};
|
||||
|
||||
// Check if this coordinate is in an urban area (O(1) lookup)
|
||||
let is_urban = has_urban_ground && urban_lookup.is_urban(x, z);
|
||||
|
||||
// Add default dirt and grass layer if there isn't a stone layer already
|
||||
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
|
||||
if is_urban {
|
||||
// Urban area: smooth stone ground
|
||||
editor.set_block_if_absent_absolute(SMOOTH_STONE, x, ground_y, z);
|
||||
} else {
|
||||
// Rural/natural area: grass and dirt
|
||||
editor.set_block_if_absent_absolute(GRASS_BLOCK, x, ground_y, z);
|
||||
}
|
||||
editor.set_block_if_absent_absolute(DIRT, x, ground_y - 1, z);
|
||||
editor.set_block_if_absent_absolute(DIRT, x, ground_y - 2, z);
|
||||
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
|
||||
editor.set_block_absolute(DIRT, x, ground_y - 1, z, None, None);
|
||||
editor.set_block_absolute(DIRT, x, ground_y - 2, z, None, None);
|
||||
}
|
||||
|
||||
// Fill underground with stone
|
||||
if args.fillground {
|
||||
editor.fill_column_absolute(
|
||||
// Fill from bedrock+1 to 3 blocks below ground with stone
|
||||
editor.fill_blocks_absolute(
|
||||
STONE,
|
||||
x,
|
||||
z,
|
||||
MIN_Y + 1,
|
||||
z,
|
||||
x,
|
||||
ground_y - 3,
|
||||
true, // skip_existing: don't overwrite blocks placed by element processing
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
// Generate a bedrock level at MIN_Y
|
||||
@@ -460,18 +755,16 @@ pub fn generate_world_with_options(
|
||||
);
|
||||
|
||||
// Always update spawn Y since we now always set a spawn point (user-selected or default)
|
||||
if let Some(ref world_path) = args.path {
|
||||
if let Err(e) = update_player_spawn_y_after_generation(
|
||||
world_path,
|
||||
bbox_string,
|
||||
args.scale,
|
||||
ground.as_ref(),
|
||||
) {
|
||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||
eprintln!("Warning: {}", warning_msg);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, &warning_msg);
|
||||
}
|
||||
if let Err(e) = update_player_spawn_y_after_generation(
|
||||
&args.path,
|
||||
bbox_string,
|
||||
args.scale,
|
||||
ground.as_ref(),
|
||||
) {
|
||||
let warning_msg = format!("Failed to update spawn point Y coordinate: {}", e);
|
||||
eprintln!("Warning: {}", warning_msg);
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(LogLevel::Warning, &warning_msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//! # Example
|
||||
//! ```ignore
|
||||
//! let mut rng = element_rng(element_id);
|
||||
//! let color = rng.random_bool(0.5); // Always same result for same element_id
|
||||
//! let color = rng.gen_bool(0.5); // Always same result for same element_id
|
||||
//! ```
|
||||
|
||||
use rand::SeedableRng;
|
||||
@@ -77,7 +77,7 @@ mod tests {
|
||||
|
||||
// Same seed should produce same sequence
|
||||
for _ in 0..100 {
|
||||
assert_eq!(rng1.random::<u64>(), rng2.random::<u64>());
|
||||
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ mod tests {
|
||||
let mut rng2 = element_rng(12346);
|
||||
|
||||
// Different seeds should (almost certainly) produce different values
|
||||
let v1: u64 = rng1.random();
|
||||
let v2: u64 = rng2.random();
|
||||
let v1: u64 = rng1.gen();
|
||||
let v2: u64 = rng2.gen();
|
||||
assert_ne!(v1, v2);
|
||||
}
|
||||
|
||||
@@ -97,8 +97,8 @@ mod tests {
|
||||
let mut rng1 = element_rng(12345);
|
||||
let mut rng2 = element_rng_salted(12345, 1);
|
||||
|
||||
let v1: u64 = rng1.random();
|
||||
let v2: u64 = rng2.random();
|
||||
let v1: u64 = rng1.gen();
|
||||
let v2: u64 = rng2.gen();
|
||||
assert_ne!(v1, v2);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ mod tests {
|
||||
let mut rng1 = coord_rng(100, 200, 12345);
|
||||
let mut rng2 = coord_rng(100, 200, 12345);
|
||||
|
||||
assert_eq!(rng1.random::<u64>(), rng2.random::<u64>());
|
||||
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -116,12 +116,12 @@ mod tests {
|
||||
let mut rng1 = coord_rng(-100, -200, 12345);
|
||||
let mut rng2 = coord_rng(-100, -200, 12345);
|
||||
|
||||
assert_eq!(rng1.random::<u64>(), rng2.random::<u64>());
|
||||
assert_eq!(rng1.gen::<u64>(), rng2.gen::<u64>());
|
||||
|
||||
// Ensure different negative coords produce different seeds
|
||||
let mut rng3 = coord_rng(-100, -200, 12345);
|
||||
let mut rng4 = coord_rng(-101, -200, 12345);
|
||||
|
||||
assert_ne!(rng3.random::<u64>(), rng4.random::<u64>());
|
||||
assert_ne!(rng3.gen::<u64>(), rng4.gen::<u64>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
//! Processing of advertising elements.
|
||||
//!
|
||||
//! This module handles advertising-related OSM elements including:
|
||||
//! - `advertising=column` - Cylindrical advertising columns (Litfaßsäule)
|
||||
//! - `advertising=flag` - Advertising flags on poles
|
||||
//! - `advertising=poster_box` - Illuminated poster display boxes
|
||||
|
||||
use crate::block_definitions::*;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::osm_parser::ProcessedNode;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
/// Generate advertising structures from node elements
|
||||
pub fn generate_advertising(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
// Skip if 'layer' or 'level' is negative in the tags
|
||||
if let Some(layer) = node.tags.get("layer") {
|
||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(level) = node.tags.get("level") {
|
||||
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(advertising_type) = node.tags.get("advertising") {
|
||||
match advertising_type.as_str() {
|
||||
"column" => generate_advertising_column(editor, node),
|
||||
"flag" => generate_advertising_flag(editor, node),
|
||||
"poster_box" => generate_poster_box(editor, node),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate an advertising column (Litfaßsäule)
|
||||
///
|
||||
/// Creates a simple advertising column.
|
||||
fn generate_advertising_column(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
let x = node.x;
|
||||
let z = node.z;
|
||||
|
||||
// Two green concrete blocks stacked
|
||||
editor.set_block(GREEN_CONCRETE, x, 1, z, None, None);
|
||||
editor.set_block(GREEN_CONCRETE, x, 2, z, None, None);
|
||||
|
||||
// Stone brick slab on top
|
||||
editor.set_block(STONE_BRICK_SLAB, x, 3, z, None, None);
|
||||
}
|
||||
|
||||
/// Generate an advertising flag
|
||||
///
|
||||
/// Creates a flagpole with a banner/flag for advertising.
|
||||
fn generate_advertising_flag(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
let x = node.x;
|
||||
let z = node.z;
|
||||
|
||||
// Use deterministic RNG for flag color
|
||||
let mut rng = element_rng(node.id);
|
||||
|
||||
// Get height from tags or default
|
||||
let height = node
|
||||
.tags
|
||||
.get("height")
|
||||
.and_then(|h| h.parse::<i32>().ok())
|
||||
.unwrap_or(6)
|
||||
.clamp(4, 12);
|
||||
|
||||
// Flagpole
|
||||
for y in 1..=height {
|
||||
editor.set_block(IRON_BARS, x, y, z, None, None);
|
||||
}
|
||||
|
||||
// Flag/banner at top (using colored wool)
|
||||
// Random bright advertising colors
|
||||
let flag_colors = [
|
||||
RED_WOOL,
|
||||
YELLOW_WOOL,
|
||||
BLUE_WOOL,
|
||||
GREEN_WOOL,
|
||||
ORANGE_WOOL,
|
||||
WHITE_WOOL,
|
||||
];
|
||||
let flag_block = flag_colors[rng.random_range(0..flag_colors.len())];
|
||||
|
||||
// Flag extends to one side (2-3 blocks)
|
||||
let flag_length = 3;
|
||||
for dx in 1..=flag_length {
|
||||
editor.set_block(flag_block, x + dx, height, z, None, None);
|
||||
editor.set_block(flag_block, x + dx, height - 1, z, None, None);
|
||||
}
|
||||
|
||||
// Finial at top
|
||||
editor.set_block(IRON_BLOCK, x, height + 1, z, None, None);
|
||||
}
|
||||
|
||||
/// Generate a poster box (city light / lollipop display)
|
||||
///
|
||||
/// Creates an illuminated poster display box on a pole.
|
||||
fn generate_poster_box(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
let x = node.x;
|
||||
let z = node.z;
|
||||
|
||||
// Y=1: Two iron bars next to each other
|
||||
editor.set_block(IRON_BARS, x, 1, z, None, None);
|
||||
editor.set_block(IRON_BARS, x + 1, 1, z, None, None);
|
||||
|
||||
// Y=2 and Y=3: Two sea lanterns
|
||||
editor.set_block(SEA_LANTERN, x, 2, z, None, None);
|
||||
editor.set_block(SEA_LANTERN, x + 1, 2, z, None, None);
|
||||
editor.set_block(SEA_LANTERN, x, 3, z, None, None);
|
||||
editor.set_block(SEA_LANTERN, x + 1, 3, z, None, None);
|
||||
|
||||
// Y=4: Two polished stone brick slabs
|
||||
editor.set_block(STONE_BRICK_SLAB, x, 4, z, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x + 1, 4, z, None, None);
|
||||
}
|
||||
@@ -7,12 +7,7 @@ use crate::floodfill::flood_fill_area; // Needed for inline amenity flood fills
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::world_editor::WorldEditor;
|
||||
use fastnbt::Value;
|
||||
use rand::{
|
||||
prelude::{IndexedRandom, SliceRandom},
|
||||
Rng,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_amenities(
|
||||
editor: &mut WorldEditor,
|
||||
@@ -39,49 +34,6 @@ pub fn generate_amenities(
|
||||
.map(|n: &crate::osm_parser::ProcessedNode| XZPoint::new(n.x, n.z))
|
||||
.next();
|
||||
match amenity_type.as_str() {
|
||||
"recycling" => {
|
||||
let is_container = element
|
||||
.tags()
|
||||
.get("recycling_type")
|
||||
.is_some_and(|value| value == "container");
|
||||
|
||||
if !is_container {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(pt) = first_node {
|
||||
let mut rng = rand::rng();
|
||||
let loot_pool = build_recycling_loot_pool(element.tags());
|
||||
let items = build_recycling_items(&loot_pool, &mut rng);
|
||||
|
||||
let properties = Value::Compound(recycling_barrel_properties());
|
||||
let barrel_block = BlockWithProperties::new(BARREL, Some(properties));
|
||||
let absolute_y = editor.get_absolute_y(pt.x, 1, pt.z);
|
||||
|
||||
editor.set_block_entity_with_items(
|
||||
barrel_block,
|
||||
pt.x,
|
||||
1,
|
||||
pt.z,
|
||||
"minecraft:barrel",
|
||||
items,
|
||||
);
|
||||
|
||||
if let Some(category) = single_loot_category(&loot_pool) {
|
||||
if let Some(display_item) =
|
||||
build_display_item_for_category(category, &mut rng)
|
||||
{
|
||||
place_item_frame_on_random_side(
|
||||
editor,
|
||||
pt.x,
|
||||
absolute_y,
|
||||
pt.z,
|
||||
display_item,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"waste_disposal" | "waste_basket" => {
|
||||
// Place a cauldron for waste disposal or waste basket
|
||||
if let Some(pt) = first_node {
|
||||
@@ -135,7 +87,7 @@ pub fn generate_amenities(
|
||||
// Use deterministic RNG for consistent bench orientation across region boundaries
|
||||
let mut rng = element_rng(element.id());
|
||||
// 50% chance to 90 degrees rotate the bench
|
||||
if rng.random_bool(0.5) {
|
||||
if rng.gen_bool(0.5) {
|
||||
editor.set_block(SMOOTH_STONE, pt.x, 1, pt.z, None, None);
|
||||
editor.set_block(OAK_LOG, pt.x + 1, 1, pt.z, None, None);
|
||||
editor.set_block(OAK_LOG, pt.x - 1, 1, pt.z, None, None);
|
||||
@@ -311,423 +263,3 @@ pub fn generate_amenities(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum RecyclingLootKind {
|
||||
GlassBottle,
|
||||
Paper,
|
||||
GlassBlock,
|
||||
GlassPane,
|
||||
LeatherArmor,
|
||||
EmptyBucket,
|
||||
LeatherBoots,
|
||||
ScrapMetal,
|
||||
GreenWaste,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum LeatherPiece {
|
||||
Helmet,
|
||||
Chestplate,
|
||||
Leggings,
|
||||
Boots,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum LootCategory {
|
||||
GlassBottle,
|
||||
Paper,
|
||||
Glass,
|
||||
Leather,
|
||||
EmptyBucket,
|
||||
ScrapMetal,
|
||||
GreenWaste,
|
||||
}
|
||||
|
||||
fn recycling_barrel_properties() -> HashMap<String, Value> {
|
||||
let mut props = HashMap::new();
|
||||
props.insert("facing".to_string(), Value::String("up".to_string()));
|
||||
props
|
||||
}
|
||||
|
||||
fn build_recycling_loot_pool(tags: &HashMap<String, String>) -> Vec<RecyclingLootKind> {
|
||||
let mut loot_pool: Vec<RecyclingLootKind> = Vec::new();
|
||||
|
||||
if tag_enabled(tags, "recycling:glass_bottles") {
|
||||
loot_pool.push(RecyclingLootKind::GlassBottle);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:paper") {
|
||||
loot_pool.push(RecyclingLootKind::Paper);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:glass") {
|
||||
loot_pool.push(RecyclingLootKind::GlassBlock);
|
||||
loot_pool.push(RecyclingLootKind::GlassPane);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:clothes") {
|
||||
loot_pool.push(RecyclingLootKind::LeatherArmor);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:cans") {
|
||||
loot_pool.push(RecyclingLootKind::EmptyBucket);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:shoes") {
|
||||
loot_pool.push(RecyclingLootKind::LeatherBoots);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:scrap_metal") {
|
||||
loot_pool.push(RecyclingLootKind::ScrapMetal);
|
||||
}
|
||||
if tag_enabled(tags, "recycling:green_waste") {
|
||||
loot_pool.push(RecyclingLootKind::GreenWaste);
|
||||
}
|
||||
|
||||
loot_pool
|
||||
}
|
||||
|
||||
fn build_recycling_items(
|
||||
loot_pool: &[RecyclingLootKind],
|
||||
rng: &mut impl Rng,
|
||||
) -> Vec<HashMap<String, Value>> {
|
||||
if loot_pool.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut items = Vec::new();
|
||||
for slot in 0..27 {
|
||||
if rng.random_bool(0.2) {
|
||||
let kind = loot_pool[rng.random_range(0..loot_pool.len())];
|
||||
if let Some(item) = build_item_for_kind(kind, slot as i8, rng) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
fn kind_to_category(kind: RecyclingLootKind) -> LootCategory {
|
||||
match kind {
|
||||
RecyclingLootKind::GlassBottle => LootCategory::GlassBottle,
|
||||
RecyclingLootKind::Paper => LootCategory::Paper,
|
||||
RecyclingLootKind::GlassBlock | RecyclingLootKind::GlassPane => LootCategory::Glass,
|
||||
RecyclingLootKind::LeatherArmor | RecyclingLootKind::LeatherBoots => LootCategory::Leather,
|
||||
RecyclingLootKind::EmptyBucket => LootCategory::EmptyBucket,
|
||||
RecyclingLootKind::ScrapMetal => LootCategory::ScrapMetal,
|
||||
RecyclingLootKind::GreenWaste => LootCategory::GreenWaste,
|
||||
}
|
||||
}
|
||||
|
||||
fn single_loot_category(loot_pool: &[RecyclingLootKind]) -> Option<LootCategory> {
|
||||
let mut categories: HashSet<LootCategory> = HashSet::new();
|
||||
for kind in loot_pool {
|
||||
categories.insert(kind_to_category(*kind));
|
||||
if categories.len() > 1 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
categories.iter().next().copied()
|
||||
}
|
||||
|
||||
fn build_display_item_for_category(
|
||||
category: LootCategory,
|
||||
rng: &mut impl Rng,
|
||||
) -> Option<HashMap<String, Value>> {
|
||||
match category {
|
||||
LootCategory::GlassBottle => Some(make_display_item("minecraft:glass_bottle", 1)),
|
||||
LootCategory::Paper => Some(make_display_item(
|
||||
"minecraft:paper",
|
||||
rng.random_range(1..=4),
|
||||
)),
|
||||
LootCategory::Glass => Some(make_display_item("minecraft:glass", 1)),
|
||||
LootCategory::Leather => Some(build_leather_display_item(rng)),
|
||||
LootCategory::EmptyBucket => Some(make_display_item("minecraft:bucket", 1)),
|
||||
LootCategory::ScrapMetal => {
|
||||
let metals = [
|
||||
"minecraft:copper_ingot",
|
||||
"minecraft:iron_ingot",
|
||||
"minecraft:gold_ingot",
|
||||
];
|
||||
let metal = metals.choose(rng)?;
|
||||
Some(make_display_item(metal, rng.random_range(1..=2)))
|
||||
}
|
||||
LootCategory::GreenWaste => {
|
||||
let options = [
|
||||
"minecraft:oak_sapling",
|
||||
"minecraft:birch_sapling",
|
||||
"minecraft:tall_grass",
|
||||
"minecraft:sweet_berries",
|
||||
"minecraft:wheat_seeds",
|
||||
];
|
||||
let choice = options.choose(rng)?;
|
||||
Some(make_display_item(choice, rng.random_range(1..=3)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn place_item_frame_on_random_side(
|
||||
editor: &mut WorldEditor,
|
||||
x: i32,
|
||||
barrel_absolute_y: i32,
|
||||
z: i32,
|
||||
item: HashMap<String, Value>,
|
||||
) {
|
||||
let mut rng = rand::rng();
|
||||
let mut directions = [
|
||||
((0, 0, -1), 2), // North
|
||||
((0, 0, 1), 3), // South
|
||||
((-1, 0, 0), 4), // West
|
||||
((1, 0, 0), 5), // East
|
||||
];
|
||||
directions.shuffle(&mut rng);
|
||||
|
||||
let (min_x, min_z) = editor.get_min_coords();
|
||||
let (max_x, max_z) = editor.get_max_coords();
|
||||
|
||||
let ((dx, _dy, dz), facing) = directions
|
||||
.into_iter()
|
||||
.find(|((dx, _dy, dz), _)| {
|
||||
let target_x = x + dx;
|
||||
let target_z = z + dz;
|
||||
target_x >= min_x && target_x <= max_x && target_z >= min_z && target_z <= max_z
|
||||
})
|
||||
.unwrap_or(((0, 0, 1), 3)); // Fallback south if all directions are out of bounds
|
||||
|
||||
let target_x = x + dx;
|
||||
let target_y = barrel_absolute_y;
|
||||
let target_z = z + dz;
|
||||
|
||||
let ground_y = editor.get_absolute_y(target_x, 0, target_z);
|
||||
|
||||
let mut extra = HashMap::new();
|
||||
extra.insert("Facing".to_string(), Value::Byte(facing)); // 2=north, 3=south, 4=west, 5=east
|
||||
extra.insert("ItemRotation".to_string(), Value::Byte(0));
|
||||
extra.insert("Item".to_string(), Value::Compound(item));
|
||||
extra.insert("ItemDropChance".to_string(), Value::Float(1.0));
|
||||
extra.insert(
|
||||
"block_pos".to_string(),
|
||||
Value::List(vec![
|
||||
Value::Int(target_x),
|
||||
Value::Int(target_y),
|
||||
Value::Int(target_z),
|
||||
]),
|
||||
);
|
||||
extra.insert("TileX".to_string(), Value::Int(target_x));
|
||||
extra.insert("TileY".to_string(), Value::Int(target_y));
|
||||
extra.insert("TileZ".to_string(), Value::Int(target_z));
|
||||
extra.insert("Fixed".to_string(), Value::Byte(1));
|
||||
|
||||
let relative_y = target_y - ground_y;
|
||||
editor.add_entity(
|
||||
"minecraft:item_frame",
|
||||
target_x,
|
||||
relative_y,
|
||||
target_z,
|
||||
Some(extra),
|
||||
);
|
||||
}
|
||||
|
||||
fn make_display_item(id: &str, count: i8) -> HashMap<String, Value> {
|
||||
let mut item = HashMap::new();
|
||||
item.insert("id".to_string(), Value::String(id.to_string()));
|
||||
item.insert("Count".to_string(), Value::Byte(count));
|
||||
item
|
||||
}
|
||||
|
||||
fn build_leather_display_item(rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
let mut item = make_display_item("minecraft:leather_chestplate", 1);
|
||||
let damage = biased_damage(80, rng);
|
||||
|
||||
let mut tag = HashMap::new();
|
||||
tag.insert("Damage".to_string(), Value::Int(damage));
|
||||
|
||||
if let Some(color) = maybe_leather_color(rng) {
|
||||
let mut display = HashMap::new();
|
||||
display.insert("color".to_string(), Value::Int(color));
|
||||
tag.insert("display".to_string(), Value::Compound(display));
|
||||
}
|
||||
|
||||
item.insert("tag".to_string(), Value::Compound(tag));
|
||||
|
||||
let mut components = HashMap::new();
|
||||
components.insert("minecraft:damage".to_string(), Value::Int(damage));
|
||||
item.insert("components".to_string(), Value::Compound(components));
|
||||
|
||||
item
|
||||
}
|
||||
|
||||
fn build_item_for_kind(
|
||||
kind: RecyclingLootKind,
|
||||
slot: i8,
|
||||
rng: &mut impl Rng,
|
||||
) -> Option<HashMap<String, Value>> {
|
||||
match kind {
|
||||
RecyclingLootKind::GlassBottle => Some(make_basic_item(
|
||||
"minecraft:glass_bottle",
|
||||
slot,
|
||||
rng.random_range(1..=4),
|
||||
)),
|
||||
RecyclingLootKind::Paper => Some(make_basic_item(
|
||||
"minecraft:paper",
|
||||
slot,
|
||||
rng.random_range(1..=10),
|
||||
)),
|
||||
RecyclingLootKind::GlassBlock => Some(build_glass_item(false, slot, rng)),
|
||||
RecyclingLootKind::GlassPane => Some(build_glass_item(true, slot, rng)),
|
||||
RecyclingLootKind::LeatherArmor => {
|
||||
Some(build_leather_item(random_leather_piece(rng), slot, rng))
|
||||
}
|
||||
RecyclingLootKind::EmptyBucket => Some(make_basic_item("minecraft:bucket", slot, 1)),
|
||||
RecyclingLootKind::LeatherBoots => Some(build_leather_item(LeatherPiece::Boots, slot, rng)),
|
||||
RecyclingLootKind::ScrapMetal => Some(build_scrap_metal_item(slot, rng)),
|
||||
RecyclingLootKind::GreenWaste => Some(build_green_waste_item(slot, rng)),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_scrap_metal_item(slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
let metals = ["copper_ingot", "iron_ingot", "gold_ingot"];
|
||||
let metal = metals.choose(rng).expect("scrap metal list is non-empty");
|
||||
let count = rng.random_range(1..=3);
|
||||
make_basic_item(&format!("minecraft:{metal}"), slot, count)
|
||||
}
|
||||
|
||||
fn build_green_waste_item(slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
#[allow(clippy::match_same_arms)]
|
||||
let (id, count) = match rng.random_range(0..8) {
|
||||
0 => ("minecraft:tall_grass", rng.random_range(1..=4)),
|
||||
1 => ("minecraft:sweet_berries", rng.random_range(2..=6)),
|
||||
2 => ("minecraft:oak_sapling", rng.random_range(1..=2)),
|
||||
3 => ("minecraft:birch_sapling", rng.random_range(1..=2)),
|
||||
4 => ("minecraft:spruce_sapling", rng.random_range(1..=2)),
|
||||
5 => ("minecraft:jungle_sapling", rng.random_range(1..=2)),
|
||||
6 => ("minecraft:acacia_sapling", rng.random_range(1..=2)),
|
||||
_ => ("minecraft:dark_oak_sapling", rng.random_range(1..=2)),
|
||||
};
|
||||
|
||||
// 25% chance to replace with seeds instead
|
||||
let id = if rng.random_bool(0.25) {
|
||||
match rng.random_range(0..4) {
|
||||
0 => "minecraft:wheat_seeds",
|
||||
1 => "minecraft:pumpkin_seeds",
|
||||
2 => "minecraft:melon_seeds",
|
||||
_ => "minecraft:beetroot_seeds",
|
||||
}
|
||||
} else {
|
||||
id
|
||||
};
|
||||
|
||||
make_basic_item(id, slot, count)
|
||||
}
|
||||
|
||||
fn build_glass_item(is_pane: bool, slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
const GLASS_COLORS: &[&str] = &[
|
||||
"white",
|
||||
"orange",
|
||||
"magenta",
|
||||
"light_blue",
|
||||
"yellow",
|
||||
"lime",
|
||||
"pink",
|
||||
"gray",
|
||||
"light_gray",
|
||||
"cyan",
|
||||
"purple",
|
||||
"blue",
|
||||
"brown",
|
||||
"green",
|
||||
"red",
|
||||
"black",
|
||||
];
|
||||
|
||||
let use_colorless = rng.random_bool(0.7);
|
||||
|
||||
let id = if use_colorless {
|
||||
if is_pane {
|
||||
"minecraft:glass_pane".to_string()
|
||||
} else {
|
||||
"minecraft:glass".to_string()
|
||||
}
|
||||
} else {
|
||||
let color = GLASS_COLORS
|
||||
.choose(rng)
|
||||
.expect("glass color array is non-empty");
|
||||
if is_pane {
|
||||
format!("minecraft:{color}_stained_glass_pane")
|
||||
} else {
|
||||
format!("minecraft:{color}_stained_glass")
|
||||
}
|
||||
};
|
||||
|
||||
let count = if is_pane {
|
||||
rng.random_range(4..=16)
|
||||
} else {
|
||||
rng.random_range(1..=6)
|
||||
};
|
||||
|
||||
make_basic_item(&id, slot, count)
|
||||
}
|
||||
|
||||
fn build_leather_item(piece: LeatherPiece, slot: i8, rng: &mut impl Rng) -> HashMap<String, Value> {
|
||||
let (id, max_damage) = match piece {
|
||||
LeatherPiece::Helmet => ("minecraft:leather_helmet", 55),
|
||||
LeatherPiece::Chestplate => ("minecraft:leather_chestplate", 80),
|
||||
LeatherPiece::Leggings => ("minecraft:leather_leggings", 75),
|
||||
LeatherPiece::Boots => ("minecraft:leather_boots", 65),
|
||||
};
|
||||
|
||||
let mut item = make_basic_item(id, slot, 1);
|
||||
let damage = biased_damage(max_damage, rng);
|
||||
|
||||
let mut tag = HashMap::new();
|
||||
tag.insert("Damage".to_string(), Value::Int(damage));
|
||||
|
||||
if let Some(color) = maybe_leather_color(rng) {
|
||||
let mut display = HashMap::new();
|
||||
display.insert("color".to_string(), Value::Int(color));
|
||||
tag.insert("display".to_string(), Value::Compound(display));
|
||||
}
|
||||
|
||||
item.insert("tag".to_string(), Value::Compound(tag));
|
||||
|
||||
let mut components = HashMap::new();
|
||||
components.insert("minecraft:damage".to_string(), Value::Int(damage));
|
||||
item.insert("components".to_string(), Value::Compound(components));
|
||||
|
||||
item
|
||||
}
|
||||
|
||||
fn biased_damage(max_damage: i32, rng: &mut impl Rng) -> i32 {
|
||||
let safe_max = max_damage.max(1);
|
||||
let upper = safe_max.saturating_sub(1);
|
||||
let lower = (safe_max / 2).min(upper);
|
||||
|
||||
let heavy_wear = rng.random_range(lower..=upper);
|
||||
let random_wear = rng.random_range(0..=upper);
|
||||
heavy_wear.max(random_wear)
|
||||
}
|
||||
|
||||
fn maybe_leather_color(rng: &mut impl Rng) -> Option<i32> {
|
||||
if rng.random_bool(0.3) {
|
||||
Some(rng.random_range(0..=0x00FF_FFFF))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn random_leather_piece(rng: &mut impl Rng) -> LeatherPiece {
|
||||
match rng.random_range(0..4) {
|
||||
0 => LeatherPiece::Helmet,
|
||||
1 => LeatherPiece::Chestplate,
|
||||
2 => LeatherPiece::Leggings,
|
||||
_ => LeatherPiece::Boots,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_basic_item(id: &str, slot: i8, count: i8) -> HashMap<String, Value> {
|
||||
let mut item = HashMap::new();
|
||||
item.insert("id".to_string(), Value::String(id.to_string()));
|
||||
item.insert("Slot".to_string(), Value::Byte(slot));
|
||||
item.insert("Count".to_string(), Value::Byte(count));
|
||||
item
|
||||
}
|
||||
|
||||
fn tag_enabled(tags: &HashMap<String, String>, key: &str) -> bool {
|
||||
tags.get(key).is_some_and(|value| value == "yes")
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
barrier_material = LIGHT_GRAY_CONCRETE;
|
||||
}
|
||||
if barrier_mat == "metal" {
|
||||
barrier_material = STONE_BRICK_WALL;
|
||||
barrier_material = STONE_BRICK_WALL; // IRON_BARS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +80,7 @@ pub fn generate_barriers(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
.get("height")
|
||||
.and_then(|height: &String| height.parse::<f32>().ok())
|
||||
.map(|height: f32| height.round() as i32)
|
||||
.unwrap_or(barrier_height)
|
||||
.max(2); // Minimum height of 2
|
||||
.unwrap_or(barrier_height);
|
||||
|
||||
// Process nodes to create the barrier wall
|
||||
for i in 1..way.nodes.len() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,55 +0,0 @@
|
||||
//! Processing of emergency infrastructure elements.
|
||||
//!
|
||||
//! This module handles emergency-related OSM elements including:
|
||||
//! - `emergency=fire_hydrant` - Fire hydrants
|
||||
|
||||
use crate::block_definitions::*;
|
||||
use crate::osm_parser::ProcessedNode;
|
||||
use crate::world_editor::WorldEditor;
|
||||
|
||||
/// Generate emergency infrastructure from node elements
|
||||
pub fn generate_emergency(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
// Skip if 'layer' or 'level' is negative in the tags
|
||||
if let Some(layer) = node.tags.get("layer") {
|
||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(level) = node.tags.get("level") {
|
||||
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(emergency_type) = node.tags.get("emergency") {
|
||||
if emergency_type.as_str() == "fire_hydrant" {
|
||||
generate_fire_hydrant(editor, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a fire hydrant
|
||||
///
|
||||
/// Creates a simple fire hydrant structure using brick wall with redstone block on top.
|
||||
/// Skips underground, wall-mounted, and pond hydrant types.
|
||||
fn generate_fire_hydrant(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
let x = node.x;
|
||||
let z = node.z;
|
||||
|
||||
// Get hydrant type - skip underground, wall, and pond types
|
||||
let hydrant_type = node
|
||||
.tags
|
||||
.get("fire_hydrant:type")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("pillar");
|
||||
|
||||
// Skip non-visible hydrant types
|
||||
if matches!(hydrant_type, "underground" | "wall" | "pond") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple hydrant: brick wall with redstone block on top
|
||||
editor.set_block(BRICK_WALL, x, 1, z, None, None);
|
||||
editor.set_block(REDSTONE_BLOCK, x, 2, z, None, None);
|
||||
}
|
||||
@@ -166,28 +166,17 @@ fn generate_highways_internal(
|
||||
// Check if this is a bridge - bridges need special elevation handling
|
||||
// to span across valleys instead of following terrain
|
||||
// Accept any bridge tag value except "no" (e.g., "yes", "viaduct", "aqueduct", etc.)
|
||||
// Indoor highways are never treated as bridges (indoor corridors should not
|
||||
// generate elevated decks or support pillars).
|
||||
let is_indoor = element.tags().get("indoor").is_some_and(|v| v == "yes");
|
||||
let is_bridge = !is_indoor && element.tags().get("bridge").is_some_and(|v| v != "no");
|
||||
let is_bridge = element.tags().get("bridge").is_some_and(|v| v != "no");
|
||||
|
||||
// Parse the layer value for elevation calculation
|
||||
let mut layer_value = element
|
||||
let layer_value = element
|
||||
.tags()
|
||||
.get("layer")
|
||||
.and_then(|layer| layer.parse::<i32>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Treat negative layers as ground level (0)
|
||||
if layer_value < 0 {
|
||||
layer_value = 0;
|
||||
}
|
||||
|
||||
// If the way is indoor, treat it as ground level to avoid creating
|
||||
// bridges/supports inside buildings (indoor=yes should not produce bridges)
|
||||
if is_indoor {
|
||||
layer_value = 0;
|
||||
}
|
||||
let layer_value = if layer_value < 0 { 0 } else { layer_value };
|
||||
|
||||
// Skip if 'level' is negative in the tags (indoor mapping)
|
||||
if let Some(level) = element.tags().get("level") {
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
//! Processing of historic elements.
|
||||
//!
|
||||
//! This module handles historic OSM elements including:
|
||||
//! - `historic=memorial` - Memorials, monuments, and commemorative structures
|
||||
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::floodfill_cache::FloodFillCache;
|
||||
use crate::osm_parser::{ProcessedNode, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::Rng;
|
||||
|
||||
/// Generate historic structures from node elements
|
||||
pub fn generate_historic(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
// Skip if 'layer' or 'level' is negative in the tags
|
||||
if let Some(layer) = node.tags.get("layer") {
|
||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(level) = node.tags.get("level") {
|
||||
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(historic_type) = node.tags.get("historic") {
|
||||
match historic_type.as_str() {
|
||||
"memorial" => generate_memorial(editor, node),
|
||||
"monument" => generate_monument(editor, node),
|
||||
"wayside_cross" => generate_wayside_cross(editor, node),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a memorial structure
|
||||
///
|
||||
/// Memorials come in many forms. We determine the type from the `memorial` tag:
|
||||
/// - plaque: Simple wall-mounted or standing plaque
|
||||
/// - statue: A statue on a pedestal
|
||||
/// - sculpture: Artistic sculpture
|
||||
/// - stone/stolperstein: Memorial stone
|
||||
/// - bench: Memorial bench (already handled by amenity=bench typically)
|
||||
/// - cross: Memorial cross
|
||||
/// - obelisk: Tall pointed pillar
|
||||
/// - stele: Upright stone slab
|
||||
/// - bust: Bust on a pedestal
|
||||
/// - Default: A general monument/pillar
|
||||
fn generate_memorial(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
let x = node.x;
|
||||
let z = node.z;
|
||||
|
||||
// Use deterministic RNG for consistent results
|
||||
let mut rng = element_rng(node.id);
|
||||
|
||||
// Get memorial subtype
|
||||
let memorial_type = node
|
||||
.tags
|
||||
.get("memorial")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("yes");
|
||||
|
||||
match memorial_type {
|
||||
"plaque" => {
|
||||
// Simple plaque on a small stand
|
||||
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x, 2, z, None, None);
|
||||
}
|
||||
"statue" | "sculpture" | "bust" => {
|
||||
// Statue on a pedestal
|
||||
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
|
||||
editor.set_block(CHISELED_STONE_BRICKS, x, 2, z, None, None);
|
||||
|
||||
// Use polished andesite for bronze/metal statue appearance
|
||||
let statue_block = if rng.random_bool(0.5) {
|
||||
POLISHED_ANDESITE
|
||||
} else {
|
||||
POLISHED_DIORITE
|
||||
};
|
||||
editor.set_block(statue_block, x, 3, z, None, None);
|
||||
editor.set_block(statue_block, x, 4, z, None, None);
|
||||
editor.set_block(STONE_BRICK_WALL, x, 5, z, None, None);
|
||||
}
|
||||
"stone" | "stolperstein" => {
|
||||
// Simple memorial stone embedded in ground
|
||||
let stone_block = if memorial_type == "stolperstein" {
|
||||
GOLD_BLOCK // Stolpersteine are brass/gold colored
|
||||
} else {
|
||||
STONE
|
||||
};
|
||||
editor.set_block(stone_block, x, 0, z, None, None);
|
||||
}
|
||||
"cross" | "war_memorial" => {
|
||||
// Memorial cross
|
||||
generate_cross(editor, x, z, 5);
|
||||
}
|
||||
"obelisk" => {
|
||||
// Tall pointed pillar with fixed height
|
||||
// Base layer at Y=1
|
||||
for dx in -1..=1 {
|
||||
for dz in -1..=1 {
|
||||
editor.set_block(STONE_BRICKS, x + dx, 1, z + dz, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Second base layer at Y=2
|
||||
for dx in -1..=1 {
|
||||
for dz in -1..=1 {
|
||||
editor.set_block(STONE_BRICKS, x + dx, 2, z + dz, None, None);
|
||||
}
|
||||
}
|
||||
// Stone brick slabs on the 4 corners at Y=3 (on top of corner blocks)
|
||||
editor.set_block(STONE_BRICK_SLAB, x - 1, 3, z - 1, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x + 1, 3, z - 1, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x - 1, 3, z + 1, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x + 1, 3, z + 1, None, None);
|
||||
|
||||
// Main shaft, fixed height of 4 blocks (Y=3 to Y=6)
|
||||
for y in 3..=6 {
|
||||
editor.set_block(SMOOTH_QUARTZ, x, y, z, None, None);
|
||||
}
|
||||
|
||||
editor.set_block(STONE_BRICK_SLAB, x, 7, z, None, None);
|
||||
}
|
||||
"stele" => {
|
||||
// Upright stone slab
|
||||
// Base
|
||||
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
|
||||
|
||||
// Upright slab (using wall blocks for thin appearance)
|
||||
for y in 2..=4 {
|
||||
editor.set_block(STONE_BRICK_WALL, x, y, z, None, None);
|
||||
}
|
||||
editor.set_block(STONE_BRICK_SLAB, x, 5, z, None, None);
|
||||
}
|
||||
_ => {
|
||||
// Default: simple stone pillar monument
|
||||
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
|
||||
editor.set_block(STONE_BRICKS, x, 2, z, None, None);
|
||||
editor.set_block(CHISELED_STONE_BRICKS, x, 3, z, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x, 4, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a monument (larger than memorial)
|
||||
fn generate_monument(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
let x = node.x;
|
||||
let z = node.z;
|
||||
|
||||
// Monuments are typically larger structures
|
||||
let height = node
|
||||
.tags
|
||||
.get("height")
|
||||
.and_then(|h| h.parse::<i32>().ok())
|
||||
.unwrap_or(10)
|
||||
.clamp(5, 20);
|
||||
|
||||
// Large base platform
|
||||
for dx in -2..=2 {
|
||||
for dz in -2..=2 {
|
||||
editor.set_block(STONE_BRICKS, x + dx, 1, z + dz, None, None);
|
||||
}
|
||||
}
|
||||
for dx in -1..=1 {
|
||||
for dz in -1..=1 {
|
||||
editor.set_block(STONE_BRICKS, x + dx, 2, z + dz, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Main structure
|
||||
for y in 3..height {
|
||||
editor.set_block(POLISHED_ANDESITE, x, y, z, None, None);
|
||||
}
|
||||
|
||||
// Decorative top
|
||||
editor.set_block(CHISELED_STONE_BRICKS, x, height, z, None, None);
|
||||
}
|
||||
|
||||
/// Generate a wayside cross
|
||||
fn generate_wayside_cross(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
let x = node.x;
|
||||
let z = node.z;
|
||||
|
||||
// Simple roadside cross
|
||||
generate_cross(editor, x, z, 4);
|
||||
}
|
||||
|
||||
/// Helper function to generate a cross structure
|
||||
fn generate_cross(editor: &mut WorldEditor, x: i32, z: i32, height: i32) {
|
||||
// Base
|
||||
editor.set_block(STONE_BRICKS, x, 1, z, None, None);
|
||||
|
||||
// Vertical beam
|
||||
for y in 2..=height {
|
||||
editor.set_block(STONE_BRICK_WALL, x, y, z, None, None);
|
||||
}
|
||||
|
||||
// Horizontal beam (cross arm) at approximately 2/3 height, but at least 2 and at most height-1
|
||||
let arm_y = ((height * 2 + 2) / 3).clamp(2, height - 1);
|
||||
// Only place horizontal arms if height allows for them (height >= 3)
|
||||
if height >= 3 {
|
||||
editor.set_block(STONE_BRICK_WALL, x - 1, arm_y, z, None, None);
|
||||
editor.set_block(STONE_BRICK_WALL, x + 1, arm_y, z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Pyramid Generation (tomb=pyramid)
|
||||
// ============================================================================
|
||||
|
||||
/// Generates a solid sandstone pyramid from a way outline.
|
||||
///
|
||||
/// The pyramid is built by flood-filling the footprint at ground level,
|
||||
/// then shrinking the filled area inward by one block per layer until
|
||||
/// only a single apex block (or nothing) remains.
|
||||
pub fn generate_pyramid(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
if element.nodes.len() < 3 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the footprint via flood fill
|
||||
let footprint: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
if footprint.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine base Y from terrain or ground level
|
||||
// Use the MINIMUM ground level so the pyramid sits on the lowest point
|
||||
// and doesn't float in areas with elevation differences
|
||||
let base_y = if args.terrain {
|
||||
footprint
|
||||
.iter()
|
||||
.map(|&(x, z)| editor.get_ground_level(x, z))
|
||||
.min()
|
||||
.unwrap_or(args.ground_level)
|
||||
} else {
|
||||
args.ground_level
|
||||
};
|
||||
|
||||
// Bounding box of the footprint
|
||||
let min_x = footprint.iter().map(|&(x, _)| x).min().unwrap();
|
||||
let max_x = footprint.iter().map(|&(x, _)| x).max().unwrap();
|
||||
let min_z = footprint.iter().map(|&(_, z)| z).min().unwrap();
|
||||
let max_z = footprint.iter().map(|&(_, z)| z).max().unwrap();
|
||||
|
||||
let center_x = (min_x + max_x) as f64 / 2.0;
|
||||
let center_z = (min_z + max_z) as f64 / 2.0;
|
||||
|
||||
// The pyramid height is half the shorter side of the bounding box (classic proportions)
|
||||
let width = (max_x - min_x + 1) as f64;
|
||||
let length = (max_z - min_z + 1) as f64;
|
||||
let half_base = width.min(length) / 2.0;
|
||||
// Height = half the shorter side (classic pyramid proportions).
|
||||
// Footprint is already in scaled Minecraft coordinates, so no extra scale factor needed.
|
||||
let pyramid_height = half_base.max(3.0) as i32;
|
||||
|
||||
// Build the pyramid layer by layer.
|
||||
// For each layer, only place blocks whose Chebyshev distance from the
|
||||
// footprint centre is within the shrinking radius AND that were in the
|
||||
// original footprint.
|
||||
let mut last_placed_layer: Option<i32> = None;
|
||||
for layer in 0..pyramid_height {
|
||||
// The allowed radius shrinks linearly from half_base at layer 0 to 0
|
||||
let radius = half_base * (1.0 - layer as f64 / pyramid_height as f64);
|
||||
if radius < 0.0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let y = base_y + 1 + layer;
|
||||
let mut placed = false;
|
||||
|
||||
for &(x, z) in &footprint {
|
||||
let dx = (x as f64 - center_x).abs();
|
||||
let dz = (z as f64 - center_z).abs();
|
||||
|
||||
// Use Chebyshev distance (max of dx, dz) for a square-footprint pyramid
|
||||
if dx <= radius && dz <= radius {
|
||||
// Allow overwriting common terrain blocks so the pyramid is
|
||||
// solid even when it intersects higher ground.
|
||||
editor.set_block_absolute(
|
||||
SANDSTONE,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
Some(&[
|
||||
GRASS_BLOCK,
|
||||
DIRT,
|
||||
STONE,
|
||||
SAND,
|
||||
GRAVEL,
|
||||
COARSE_DIRT,
|
||||
PODZOL,
|
||||
DIRT_PATH,
|
||||
SANDSTONE,
|
||||
]),
|
||||
None,
|
||||
);
|
||||
placed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if placed {
|
||||
last_placed_layer = Some(y);
|
||||
} else {
|
||||
break; // Nothing placed, we've reached the apex
|
||||
}
|
||||
}
|
||||
|
||||
// Cap with smooth sandstone one block above the last placed layer
|
||||
if let Some(top_y) = last_placed_layer {
|
||||
editor.set_block_absolute(
|
||||
SMOOTH_SANDSTONE,
|
||||
center_x.round() as i32,
|
||||
top_y + 1,
|
||||
center_z.round() as i32,
|
||||
Some(&[
|
||||
GRASS_BLOCK,
|
||||
DIRT,
|
||||
STONE,
|
||||
SAND,
|
||||
GRAVEL,
|
||||
COARSE_DIRT,
|
||||
PODZOL,
|
||||
DIRT_PATH,
|
||||
SANDSTONE,
|
||||
]),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::{Tree, TreeType};
|
||||
use crate::element_processing::tree::Tree;
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::osm_parser::{ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::prelude::IndexedRandom;
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_landuse(
|
||||
@@ -60,34 +58,11 @@ pub fn generate_landuse(
|
||||
let floor_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
|
||||
let trees_ok_to_generate: Vec<TreeType> = {
|
||||
let mut trees: Vec<TreeType> = vec![];
|
||||
if let Some(leaf_type) = element.tags.get("leaf_type") {
|
||||
match leaf_type.as_str() {
|
||||
"broadleaved" => {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
"needleleaved" => trees.push(TreeType::Spruce),
|
||||
_ => {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Spruce);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Spruce);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
trees
|
||||
};
|
||||
|
||||
for (x, z) in floor_area {
|
||||
// Apply per-block randomness for certain landuse types
|
||||
let actual_block = if landuse_tag == "residential" && block_type == STONE_BRICKS {
|
||||
// Urban residential: mix of stone bricks, cracked stone bricks, stone, cobblestone
|
||||
let random_value = rng.random_range(0..100);
|
||||
let random_value = rng.gen_range(0..100);
|
||||
if random_value < 72 {
|
||||
STONE_BRICKS
|
||||
} else if random_value < 87 {
|
||||
@@ -99,7 +74,7 @@ pub fn generate_landuse(
|
||||
}
|
||||
} else if landuse_tag == "commercial" {
|
||||
// Commercial: mix of smooth stone, stone, cobblestone, stone bricks
|
||||
let random_value = rng.random_range(0..100);
|
||||
let random_value = rng.gen_range(0..100);
|
||||
if random_value < 40 {
|
||||
SMOOTH_STONE
|
||||
} else if random_value < 70 {
|
||||
@@ -111,7 +86,7 @@ pub fn generate_landuse(
|
||||
}
|
||||
} else if landuse_tag == "industrial" {
|
||||
// Industrial: primarily stone, with some stone bricks and smooth stone
|
||||
let random_value = rng.random_range(0..100);
|
||||
let random_value = rng.gen_range(0..100);
|
||||
if random_value < 70 {
|
||||
STONE
|
||||
} else if random_value < 90 {
|
||||
@@ -135,11 +110,11 @@ pub fn generate_landuse(
|
||||
match landuse_tag.as_str() {
|
||||
"cemetery" => {
|
||||
if (x % 3 == 0) && (z % 3 == 0) {
|
||||
let random_choice: i32 = rng.random_range(0..100);
|
||||
let random_choice: i32 = rng.gen_range(0..100);
|
||||
if random_choice < 15 {
|
||||
// Place graves
|
||||
if editor.check_for_block(x, 0, z, Some(&[PODZOL])) {
|
||||
if rng.random_bool(0.5) {
|
||||
if rng.gen_bool(0.5) {
|
||||
editor.set_block(COBBLESTONE, x - 1, 1, z, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x - 1, 2, z, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, x, 1, z, None, None);
|
||||
@@ -159,39 +134,25 @@ pub fn generate_landuse(
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice < 35 {
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||
} else if random_choice < 37 {
|
||||
editor.set_block(FERN, x, 1, z, None, None);
|
||||
} else if random_choice < 41 {
|
||||
editor.set_block(LARGE_FERN_LOWER, x, 1, z, None, None);
|
||||
editor.set_block(LARGE_FERN_UPPER, x, 2, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
"forest" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
let random_choice: i32 = rng.random_range(0..30);
|
||||
let random_choice: i32 = rng.gen_range(0..30);
|
||||
if random_choice == 20 {
|
||||
let tree_type = *trees_ok_to_generate
|
||||
.choose(&mut rng)
|
||||
.unwrap_or(&TreeType::Oak);
|
||||
Tree::create_of_type(
|
||||
editor,
|
||||
(x, 1, z),
|
||||
tree_type,
|
||||
Some(building_footprints),
|
||||
);
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice == 2 {
|
||||
let flower_block: Block = match rng.random_range(1..=6) {
|
||||
let flower_block: Block = match rng.gen_range(1..=5) {
|
||||
1 => OAK_LEAVES,
|
||||
2 => RED_FLOWER,
|
||||
3 => BLUE_FLOWER,
|
||||
4 => YELLOW_FLOWER,
|
||||
5 => FERN,
|
||||
_ => WHITE_FLOWER,
|
||||
};
|
||||
editor.set_block(flower_block, x, 1, z, None, None);
|
||||
} else if random_choice <= 12 {
|
||||
if rng.random_range(0..100) < 12 {
|
||||
if rng.gen_range(0..100) < 12 {
|
||||
editor.set_block(FERN, x, 1, z, None, None);
|
||||
} else {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
@@ -205,8 +166,8 @@ pub fn generate_landuse(
|
||||
if x % 9 == 0 && z % 9 == 0 {
|
||||
// Place water in dot pattern
|
||||
editor.set_block(WATER, x, 0, z, Some(&[FARMLAND]), None);
|
||||
} else if rng.random_range(0..76) == 0 {
|
||||
let special_choice: i32 = rng.random_range(1..=10);
|
||||
} else if rng.gen_range(0..76) == 0 {
|
||||
let special_choice: i32 = rng.gen_range(1..=10);
|
||||
if special_choice <= 4 {
|
||||
editor.set_block(HAY_BALE, x, 1, z, None, Some(&[SPONGE]));
|
||||
} else {
|
||||
@@ -215,14 +176,14 @@ pub fn generate_landuse(
|
||||
} else {
|
||||
// Set crops only if the block below is farmland
|
||||
if editor.check_for_block(x, 0, z, Some(&[FARMLAND])) {
|
||||
let crop_choice = [WHEAT, CARROTS, POTATOES][rng.random_range(0..3)];
|
||||
let crop_choice = [WHEAT, CARROTS, POTATOES][rng.gen_range(0..3)];
|
||||
editor.set_block(crop_choice, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"construction" => {
|
||||
let random_choice: i32 = rng.random_range(0..1501);
|
||||
let random_choice: i32 = rng.gen_range(0..1501);
|
||||
if random_choice < 15 {
|
||||
editor.set_block(SCAFFOLDING, x, 1, z, None, None);
|
||||
if random_choice < 2 {
|
||||
@@ -258,7 +219,7 @@ pub fn generate_landuse(
|
||||
FURNACE,
|
||||
];
|
||||
editor.set_block(
|
||||
construction_items[rng.random_range(0..construction_items.len())],
|
||||
construction_items[rng.gen_range(0..construction_items.len())],
|
||||
x,
|
||||
1,
|
||||
z,
|
||||
@@ -295,7 +256,7 @@ pub fn generate_landuse(
|
||||
}
|
||||
"grass" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.random_range(0..200) {
|
||||
match rng.gen_range(0..200) {
|
||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||
1..=8 => editor.set_block(FERN, x, 1, z, None, None),
|
||||
9..=170 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
@@ -305,17 +266,17 @@ pub fn generate_landuse(
|
||||
}
|
||||
"greenfield" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.random_range(0..200) {
|
||||
match rng.gen_range(0..200) {
|
||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
|
||||
3..=16 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
3..=17 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
"meadow" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
let random_choice: i32 = rng.random_range(0..1001);
|
||||
let random_choice: i32 = rng.gen_range(0..1001);
|
||||
if random_choice < 5 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice < 6 {
|
||||
@@ -324,10 +285,7 @@ pub fn generate_landuse(
|
||||
editor.set_block(OAK_LEAVES, x, 1, z, None, None);
|
||||
} else if random_choice < 40 {
|
||||
editor.set_block(FERN, x, 1, z, None, None);
|
||||
} else if random_choice < 65 {
|
||||
editor.set_block(LARGE_FERN_LOWER, x, 1, z, None, None);
|
||||
editor.set_block(LARGE_FERN_UPPER, x, 2, z, None, None);
|
||||
} else if random_choice < 825 {
|
||||
} else if random_choice < 800 {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
@@ -336,7 +294,7 @@ pub fn generate_landuse(
|
||||
if x % 18 == 0 && z % 10 == 0 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
match rng.random_range(0..100) {
|
||||
match rng.gen_range(0..100) {
|
||||
0 => editor.set_block(OAK_LEAVES, x, 1, z, None, None),
|
||||
1..=2 => editor.set_block(FERN, x, 1, z, None, None),
|
||||
3..=20 => editor.set_block(GRASS, x, 1, z, None, None),
|
||||
@@ -358,8 +316,7 @@ pub fn generate_landuse(
|
||||
"clay" | "kaolinite" => CLAY,
|
||||
_ => STONE,
|
||||
};
|
||||
let random_choice: i32 =
|
||||
rng.random_range(0..100 + editor.get_absolute_y(x, 0, z)); // The deeper it is the more resources are there
|
||||
let random_choice: i32 = rng.gen_range(0..100 + editor.get_absolute_y(x, 0, z)); // The deeper it is the more resources are there
|
||||
if random_choice < 5 {
|
||||
editor.set_block(ore_block, x, 0, z, Some(&[STONE]), None);
|
||||
}
|
||||
@@ -368,26 +325,6 @@ pub fn generate_landuse(
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a stone brick wall fence around cemeteries
|
||||
if landuse_tag == "cemetery" {
|
||||
generate_cemetery_fence(editor, element);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws a stone-brick wall fence (with slab cap) along the outline of a
|
||||
/// cemetery way.
|
||||
fn generate_cemetery_fence(editor: &mut WorldEditor, element: &ProcessedWay) {
|
||||
for i in 1..element.nodes.len() {
|
||||
let prev = &element.nodes[i - 1];
|
||||
let cur = &element.nodes[i];
|
||||
|
||||
let points = bresenham_line(prev.x, 0, prev.z, cur.x, 0, cur.z);
|
||||
for (bx, _, bz) in points {
|
||||
editor.set_block(STONE_BRICK_WALL, bx, 1, bz, None, None);
|
||||
editor.set_block(STONE_BRICK_SLAB, bx, 2, bz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_landuse_from_relation(
|
||||
@@ -398,53 +335,44 @@ pub fn generate_landuse_from_relation(
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if rel.tags.contains_key("landuse") {
|
||||
// Process each outer member way individually using cached flood fill.
|
||||
// We intentionally do not combine all outer nodes into one mega-way,
|
||||
// because that creates a nonsensical polygon spanning the whole relation
|
||||
// extent, misses the flood fill cache, and can cause multi-GB allocations.
|
||||
// Generate individual ways with their original tags
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
// Use relation tags so the member inherits the relation's landuse=* type
|
||||
let way_with_rel_tags = ProcessedWay {
|
||||
id: member.way.id,
|
||||
nodes: member.way.nodes.clone(),
|
||||
tags: rel.tags.clone(),
|
||||
};
|
||||
generate_landuse(
|
||||
editor,
|
||||
&way_with_rel_tags,
|
||||
&member.way.clone(),
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates ground blocks for place=* areas (squares, neighbourhoods, etc.)
|
||||
pub fn generate_place(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedWay,
|
||||
args: &Args,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
) {
|
||||
let binding = String::new();
|
||||
let place_tag = element.tags.get("place").unwrap_or(&binding);
|
||||
|
||||
// Determine block type based on place tag
|
||||
let block_type = match place_tag.as_str() {
|
||||
"square" => STONE_BRICKS,
|
||||
"neighbourhood" | "city_block" | "quarter" | "suburb" => SMOOTH_STONE,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
// Get the area using flood fill cache
|
||||
let floor_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(element, args.timeout.as_ref());
|
||||
|
||||
// Place ground blocks
|
||||
for (x, z) in floor_area {
|
||||
editor.set_block(block_type, x, 0, z, None, None);
|
||||
|
||||
// Combine all outer ways into one with relation tags
|
||||
let mut combined_nodes = Vec::new();
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
combined_nodes.extend(member.way.nodes.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Only process if we have nodes
|
||||
if !combined_nodes.is_empty() {
|
||||
// Create combined way with relation tags
|
||||
let combined_way = ProcessedWay {
|
||||
id: rel.id,
|
||||
nodes: combined_nodes,
|
||||
tags: rel.tags.clone(),
|
||||
};
|
||||
|
||||
// Generate landuse area from combined way
|
||||
generate_landuse(
|
||||
editor,
|
||||
&combined_way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,20 +96,18 @@ pub fn generate_leisure(
|
||||
if matches!(leisure_type.as_str(), "park" | "garden" | "nature_reserve")
|
||||
&& editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK]))
|
||||
{
|
||||
let random_choice: i32 = rng.random_range(0..1000);
|
||||
let random_choice: i32 = rng.gen_range(0..1000);
|
||||
|
||||
match random_choice {
|
||||
0..30 => {
|
||||
// Plants
|
||||
let plant_choice = match random_choice {
|
||||
0..5 => RED_FLOWER,
|
||||
5..10 => YELLOW_FLOWER,
|
||||
10..16 => BLUE_FLOWER,
|
||||
16..22 => WHITE_FLOWER,
|
||||
22..30 => FERN,
|
||||
_ => unreachable!(),
|
||||
// Flowers
|
||||
let flower_choice = match random_choice {
|
||||
0..10 => RED_FLOWER,
|
||||
10..20 => YELLOW_FLOWER,
|
||||
20..30 => BLUE_FLOWER,
|
||||
_ => WHITE_FLOWER,
|
||||
};
|
||||
editor.set_block(plant_choice, x, 1, z, None, None);
|
||||
editor.set_block(flower_choice, x, 1, z, None, None);
|
||||
}
|
||||
30..90 => {
|
||||
// Grass
|
||||
@@ -129,7 +127,7 @@ pub fn generate_leisure(
|
||||
|
||||
// Add playground or recreation ground features
|
||||
if matches!(leisure_type.as_str(), "playground" | "recreation_ground") {
|
||||
let random_choice: i32 = rng.random_range(0..5000);
|
||||
let random_choice: i32 = rng.gen_range(0..5000);
|
||||
|
||||
match random_choice {
|
||||
0..10 => {
|
||||
@@ -185,26 +183,41 @@ pub fn generate_leisure_from_relation(
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
||||
// Process each outer member way individually using cached flood fill.
|
||||
// We intentionally do not combine all outer nodes into one mega-way,
|
||||
// because that creates a nonsensical polygon spanning the whole relation
|
||||
// extent, misses the flood fill cache, and can cause multi-GB allocations.
|
||||
// First generate individual ways with their original tags
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
// Use relation tags so the member inherits the relation's leisure=* type
|
||||
let way_with_rel_tags = ProcessedWay {
|
||||
id: member.way.id,
|
||||
nodes: member.way.nodes.clone(),
|
||||
tags: rel.tags.clone(),
|
||||
};
|
||||
generate_leisure(
|
||||
editor,
|
||||
&way_with_rel_tags,
|
||||
&member.way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Then combine all outer ways into one
|
||||
let mut combined_nodes = Vec::new();
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
combined_nodes.extend(member.way.nodes.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Create combined way with relation tags
|
||||
let combined_way = ProcessedWay {
|
||||
id: rel.id,
|
||||
nodes: combined_nodes,
|
||||
tags: rel.tags.clone(),
|
||||
};
|
||||
|
||||
// Generate leisure area from combined way
|
||||
generate_leisure(
|
||||
editor,
|
||||
&combined_way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +1,16 @@
|
||||
pub mod advertising;
|
||||
pub mod amenities;
|
||||
pub mod barriers;
|
||||
pub mod bridges;
|
||||
pub mod buildings;
|
||||
pub mod doors;
|
||||
pub mod emergency;
|
||||
pub mod highways;
|
||||
pub mod historic;
|
||||
pub mod landuse;
|
||||
pub mod leisure;
|
||||
pub mod man_made;
|
||||
pub mod natural;
|
||||
pub mod power;
|
||||
pub mod railways;
|
||||
pub mod subprocessor;
|
||||
pub mod tourisms;
|
||||
pub mod tree;
|
||||
pub mod water_areas;
|
||||
pub mod waterways;
|
||||
|
||||
use crate::osm_parser::ProcessedNode;
|
||||
|
||||
/// Merges way segments that share endpoints into closed rings.
|
||||
/// Used by water_areas.rs and boundaries.rs for assembling relation members.
|
||||
pub fn merge_way_segments(rings: &mut Vec<Vec<ProcessedNode>>) {
|
||||
let mut removed: Vec<usize> = vec![];
|
||||
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
|
||||
|
||||
// Match nodes by ID or proximity (handles synthetic nodes from bbox clipping)
|
||||
let nodes_match = |a: &ProcessedNode, b: &ProcessedNode| -> bool {
|
||||
if a.id == b.id {
|
||||
return true;
|
||||
}
|
||||
let dx = (a.x - b.x).abs();
|
||||
let dz = (a.z - b.z).abs();
|
||||
dx <= 1 && dz <= 1
|
||||
};
|
||||
|
||||
for i in 0..rings.len() {
|
||||
for j in 0..rings.len() {
|
||||
if i == j {
|
||||
continue;
|
||||
}
|
||||
|
||||
if removed.contains(&i) || removed.contains(&j) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let x: &Vec<ProcessedNode> = &rings[i];
|
||||
let y: &Vec<ProcessedNode> = &rings[j];
|
||||
|
||||
// Skip empty rings (can happen after clipping)
|
||||
if x.is_empty() || y.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let x_first = &x[0];
|
||||
let x_last = x.last().unwrap();
|
||||
let y_first = &y[0];
|
||||
let y_last = y.last().unwrap();
|
||||
|
||||
// Skip already-closed rings
|
||||
if nodes_match(x_first, x_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if nodes_match(y_first, y_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if nodes_match(x_first, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut x: Vec<ProcessedNode> = x.clone();
|
||||
x.reverse();
|
||||
x.extend(y.iter().skip(1).cloned());
|
||||
merged.push(x);
|
||||
} else if nodes_match(x_last, y_last) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut x: Vec<ProcessedNode> = x.clone();
|
||||
x.extend(y.iter().rev().skip(1).cloned());
|
||||
|
||||
merged.push(x);
|
||||
} else if nodes_match(x_first, y_last) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut y: Vec<ProcessedNode> = y.clone();
|
||||
y.extend(x.iter().skip(1).cloned());
|
||||
|
||||
merged.push(y);
|
||||
} else if nodes_match(x_last, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut x: Vec<ProcessedNode> = x.clone();
|
||||
x.extend(y.iter().skip(1).cloned());
|
||||
|
||||
merged.push(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removed.sort();
|
||||
|
||||
for r in removed.iter().rev() {
|
||||
rings.remove(*r);
|
||||
}
|
||||
|
||||
let merged_len: usize = merged.len();
|
||||
for m in merged {
|
||||
rings.push(m);
|
||||
}
|
||||
|
||||
if merged_len > 0 {
|
||||
merge_way_segments(rings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ use crate::args::Args;
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::deterministic_rng::element_rng;
|
||||
use crate::element_processing::tree::{Tree, TreeType};
|
||||
use crate::element_processing::tree::Tree;
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedMemberRole, ProcessedRelation, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
use rand::{prelude::IndexedRandom, Rng};
|
||||
use rand::Rng;
|
||||
|
||||
pub fn generate_natural(
|
||||
editor: &mut WorldEditor,
|
||||
@@ -21,66 +21,7 @@ pub fn generate_natural(
|
||||
let x: i32 = node.x;
|
||||
let z: i32 = node.z;
|
||||
|
||||
let mut trees_ok_to_generate: Vec<TreeType> = vec![];
|
||||
if let Some(species) = element.tags().get("species") {
|
||||
if species.contains("Betula") {
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
if species.contains("Quercus") {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
}
|
||||
if species.contains("Picea") {
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
}
|
||||
} else if let Some(genus_wikidata) = element.tags().get("genus:wikidata") {
|
||||
match genus_wikidata.as_str() {
|
||||
"Q12004" => trees_ok_to_generate.push(TreeType::Birch),
|
||||
"Q26782" => trees_ok_to_generate.push(TreeType::Oak),
|
||||
"Q25243" => trees_ok_to_generate.push(TreeType::Spruce),
|
||||
_ => {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
}
|
||||
} else if let Some(genus) = element.tags().get("genus") {
|
||||
match genus.as_str() {
|
||||
"Betula" => trees_ok_to_generate.push(TreeType::Birch),
|
||||
"Quercus" => trees_ok_to_generate.push(TreeType::Oak),
|
||||
"Picea" => trees_ok_to_generate.push(TreeType::Spruce),
|
||||
_ => trees_ok_to_generate.push(TreeType::Oak),
|
||||
}
|
||||
} else if let Some(leaf_type) = element.tags().get("leaf_type") {
|
||||
match leaf_type.as_str() {
|
||||
"broadleaved" => {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
"needleleaved" => trees_ok_to_generate.push(TreeType::Spruce),
|
||||
_ => {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
|
||||
if trees_ok_to_generate.is_empty() {
|
||||
trees_ok_to_generate.push(TreeType::Oak);
|
||||
trees_ok_to_generate.push(TreeType::Spruce);
|
||||
trees_ok_to_generate.push(TreeType::Birch);
|
||||
}
|
||||
|
||||
let mut rng = element_rng(element.id());
|
||||
let tree_type = *trees_ok_to_generate
|
||||
.choose(&mut rng)
|
||||
.unwrap_or(&TreeType::Oak);
|
||||
|
||||
Tree::create_of_type(editor, (x, 1, z), tree_type, Some(building_footprints));
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
}
|
||||
} else {
|
||||
let mut previous_node: Option<(i32, i32)> = None;
|
||||
@@ -140,29 +81,6 @@ pub fn generate_natural(
|
||||
let filled_area: Vec<(i32, i32)> =
|
||||
flood_fill_cache.get_or_compute(way, args.timeout.as_ref());
|
||||
|
||||
let trees_ok_to_generate: Vec<TreeType> = {
|
||||
let mut trees: Vec<TreeType> = vec![];
|
||||
if let Some(leaf_type) = element.tags().get("leaf_type") {
|
||||
match leaf_type.as_str() {
|
||||
"broadleaved" => {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
"needleleaved" => trees.push(TreeType::Spruce),
|
||||
_ => {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Spruce);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trees.push(TreeType::Oak);
|
||||
trees.push(TreeType::Spruce);
|
||||
trees.push(TreeType::Birch);
|
||||
}
|
||||
trees
|
||||
};
|
||||
|
||||
// Use deterministic RNG seeded by element ID for consistent results across region boundaries
|
||||
let mut rng = element_rng(way.id);
|
||||
|
||||
@@ -192,7 +110,7 @@ pub fn generate_natural(
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
if rng.random_bool(0.6) {
|
||||
if rng.gen_bool(0.6) {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
@@ -200,7 +118,7 @@ pub fn generate_natural(
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let random_choice = rng.random_range(0..500);
|
||||
let random_choice = rng.gen_range(0..500);
|
||||
if random_choice < 33 {
|
||||
if random_choice <= 2 {
|
||||
editor.set_block(COBBLESTONE, x, 0, z, None, None);
|
||||
@@ -215,11 +133,11 @@ pub fn generate_natural(
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let random_choice = rng.random_range(0..500);
|
||||
let random_choice = rng.gen_range(0..500);
|
||||
if random_choice == 0 {
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice == 1 {
|
||||
let flower_block = match rng.random_range(1..=4) {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
2 => BLUE_FLOWER,
|
||||
3 => YELLOW_FLOWER,
|
||||
@@ -244,19 +162,11 @@ pub fn generate_natural(
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let random_choice: i32 = rng.random_range(0..30);
|
||||
let random_choice: i32 = rng.gen_range(0..30);
|
||||
if random_choice == 0 {
|
||||
let tree_type = *trees_ok_to_generate
|
||||
.choose(&mut rng)
|
||||
.unwrap_or(&TreeType::Oak);
|
||||
Tree::create_of_type(
|
||||
editor,
|
||||
(x, 1, z),
|
||||
tree_type,
|
||||
Some(building_footprints),
|
||||
);
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if random_choice == 1 {
|
||||
let flower_block = match rng.random_range(1..=4) {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
2 => BLUE_FLOWER,
|
||||
3 => YELLOW_FLOWER,
|
||||
@@ -269,13 +179,13 @@ pub fn generate_natural(
|
||||
}
|
||||
"sand" => {
|
||||
if editor.check_for_block(x, 0, z, Some(&[SAND]))
|
||||
&& rng.random_range(0..100) == 1
|
||||
&& rng.gen_range(0..100) == 1
|
||||
{
|
||||
editor.set_block(DEAD_BUSH, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
"shoal" => {
|
||||
if rng.random_bool(0.05) {
|
||||
if rng.gen_bool(0.05) {
|
||||
editor.set_block(WATER, x, 0, z, Some(&[SAND, GRAVEL]), None);
|
||||
}
|
||||
}
|
||||
@@ -283,14 +193,14 @@ pub fn generate_natural(
|
||||
if let Some(wetland_type) = element.tags().get("wetland") {
|
||||
// Wetland without water blocks
|
||||
if matches!(wetland_type.as_str(), "wet_meadow" | "fen") {
|
||||
if rng.random_bool(0.3) {
|
||||
if rng.gen_bool(0.3) {
|
||||
editor.set_block(GRASS_BLOCK, x, 0, z, Some(&[MUD]), None);
|
||||
}
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
continue;
|
||||
}
|
||||
// All the other types of wetland
|
||||
if rng.random_bool(0.3) {
|
||||
if rng.gen_bool(0.3) {
|
||||
editor.set_block(
|
||||
WATER,
|
||||
x,
|
||||
@@ -311,7 +221,7 @@ pub fn generate_natural(
|
||||
}
|
||||
"swamp" | "mangrove" => {
|
||||
// TODO implement mangrove
|
||||
let random_choice: i32 = rng.random_range(0..40);
|
||||
let random_choice: i32 = rng.gen_range(0..40);
|
||||
if random_choice == 0 {
|
||||
Tree::create(
|
||||
editor,
|
||||
@@ -323,7 +233,7 @@ pub fn generate_natural(
|
||||
}
|
||||
}
|
||||
"bog" => {
|
||||
if rng.random_bool(0.2) {
|
||||
if rng.gen_bool(0.2) {
|
||||
editor.set_block(
|
||||
MOSS_BLOCK,
|
||||
x,
|
||||
@@ -333,7 +243,7 @@ pub fn generate_natural(
|
||||
None,
|
||||
);
|
||||
}
|
||||
if rng.random_bool(0.15) {
|
||||
if rng.gen_bool(0.15) {
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
}
|
||||
@@ -346,7 +256,7 @@ pub fn generate_natural(
|
||||
}
|
||||
} else {
|
||||
// Generic natural=wetland without wetland=... tag
|
||||
if rng.random_bool(0.3) {
|
||||
if rng.gen_bool(0.3) {
|
||||
editor.set_block(WATER, x, 0, z, Some(&[MUD]), None);
|
||||
continue;
|
||||
}
|
||||
@@ -355,11 +265,11 @@ pub fn generate_natural(
|
||||
}
|
||||
"mountain_range" => {
|
||||
// Create block clusters instead of random placement
|
||||
let cluster_chance = rng.random_range(0..1000);
|
||||
let cluster_chance = rng.gen_range(0..1000);
|
||||
|
||||
if cluster_chance < 50 {
|
||||
// 5% chance to start a new cluster
|
||||
let cluster_block = match rng.random_range(0..7) {
|
||||
let cluster_block = match rng.gen_range(0..7) {
|
||||
0 => DIRT,
|
||||
1 => STONE,
|
||||
2 => GRAVEL,
|
||||
@@ -370,7 +280,7 @@ pub fn generate_natural(
|
||||
};
|
||||
|
||||
// Generate cluster size (5-10 blocks radius)
|
||||
let cluster_size = rng.random_range(5..=10);
|
||||
let cluster_size = rng.gen_range(5..=10);
|
||||
|
||||
// Create cluster around current position
|
||||
for dx in -(cluster_size as i32)..=(cluster_size as i32) {
|
||||
@@ -383,7 +293,7 @@ pub fn generate_natural(
|
||||
if distance <= cluster_size as f32 {
|
||||
// Probability decreases with distance from center
|
||||
let place_prob = 1.0 - (distance / cluster_size as f32);
|
||||
if rng.random::<f32>() < place_prob {
|
||||
if rng.gen::<f32>() < place_prob {
|
||||
editor.set_block(
|
||||
cluster_block,
|
||||
cluster_x,
|
||||
@@ -395,8 +305,7 @@ pub fn generate_natural(
|
||||
|
||||
// Add vegetation on grass blocks
|
||||
if cluster_block == GRASS_BLOCK {
|
||||
let vegetation_chance =
|
||||
rng.random_range(0..100);
|
||||
let vegetation_chance = rng.gen_range(0..100);
|
||||
if vegetation_chance == 0 {
|
||||
// 1% chance for rare trees
|
||||
Tree::create(
|
||||
@@ -426,7 +335,7 @@ pub fn generate_natural(
|
||||
}
|
||||
"saddle" => {
|
||||
// Saddle areas - lowest point between peaks, mix of stone and grass
|
||||
let terrain_chance = rng.random_range(0..100);
|
||||
let terrain_chance = rng.gen_range(0..100);
|
||||
if terrain_chance < 30 {
|
||||
// 30% chance for exposed stone
|
||||
editor.set_block(STONE, x, 0, z, None, None);
|
||||
@@ -436,7 +345,7 @@ pub fn generate_natural(
|
||||
} else {
|
||||
// 50% chance for grass
|
||||
editor.set_block(GRASS_BLOCK, x, 0, z, None, None);
|
||||
if rng.random_bool(0.4) {
|
||||
if rng.gen_bool(0.4) {
|
||||
// 40% chance for grass on top
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
}
|
||||
@@ -444,10 +353,10 @@ pub fn generate_natural(
|
||||
}
|
||||
"ridge" => {
|
||||
// Ridge areas - elevated crest, mostly rocky with some vegetation
|
||||
let ridge_chance = rng.random_range(0..100);
|
||||
let ridge_chance = rng.gen_range(0..100);
|
||||
if ridge_chance < 60 {
|
||||
// 60% chance for stone/rocky terrain
|
||||
let rock_type = match rng.random_range(0..4) {
|
||||
let rock_type = match rng.gen_range(0..4) {
|
||||
0 => STONE,
|
||||
1 => COBBLESTONE,
|
||||
2 => GRANITE,
|
||||
@@ -457,7 +366,7 @@ pub fn generate_natural(
|
||||
} else {
|
||||
// 40% chance for grass with sparse vegetation
|
||||
editor.set_block(GRASS_BLOCK, x, 0, z, None, None);
|
||||
let vegetation_chance = rng.random_range(0..100);
|
||||
let vegetation_chance = rng.gen_range(0..100);
|
||||
if vegetation_chance < 20 {
|
||||
// 20% chance for grass
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
@@ -477,7 +386,7 @@ pub fn generate_natural(
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let tundra_chance = rng.random_range(0..100);
|
||||
let tundra_chance = rng.gen_range(0..100);
|
||||
if tundra_chance < 40 {
|
||||
// 40% chance for grass (sedges, grasses)
|
||||
editor.set_block(GRASS, x, 1, z, None, None);
|
||||
@@ -492,10 +401,10 @@ pub fn generate_natural(
|
||||
}
|
||||
"cliff" => {
|
||||
// Cliff areas - predominantly stone with minimal vegetation
|
||||
let cliff_chance = rng.random_range(0..100);
|
||||
let cliff_chance = rng.gen_range(0..100);
|
||||
if cliff_chance < 90 {
|
||||
// 90% chance for stone variants
|
||||
let stone_type = match rng.random_range(0..4) {
|
||||
let stone_type = match rng.gen_range(0..4) {
|
||||
0 => STONE,
|
||||
1 => COBBLESTONE,
|
||||
2 => ANDESITE,
|
||||
@@ -512,13 +421,13 @@ pub fn generate_natural(
|
||||
if !editor.check_for_block(x, 0, z, Some(&[GRASS_BLOCK])) {
|
||||
continue;
|
||||
}
|
||||
let hill_chance = rng.random_range(0..1000);
|
||||
let hill_chance = rng.gen_range(0..1000);
|
||||
if hill_chance == 0 {
|
||||
// 0.1% chance for rare trees
|
||||
Tree::create(editor, (x, 1, z), Some(building_footprints));
|
||||
} else if hill_chance < 50 {
|
||||
// 5% chance for flowers
|
||||
let flower_block = match rng.random_range(1..=4) {
|
||||
let flower_block = match rng.gen_range(1..=4) {
|
||||
1 => RED_FLOWER,
|
||||
2 => BLUE_FLOWER,
|
||||
3 => YELLOW_FLOWER,
|
||||
@@ -551,26 +460,44 @@ pub fn generate_natural_from_relation(
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
) {
|
||||
if rel.tags.contains_key("natural") {
|
||||
// Process each outer member way individually using cached flood fill.
|
||||
// We intentionally do not combine all outer nodes into one mega-way,
|
||||
// because that creates a nonsensical polygon spanning the whole relation
|
||||
// extent, misses the flood fill cache, and can cause multi-GB allocations.
|
||||
// Generate individual ways with their original tags
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
// Use relation tags so the member inherits the relation's natural=* type
|
||||
let way_with_rel_tags = ProcessedWay {
|
||||
id: member.way.id,
|
||||
nodes: member.way.nodes.clone(),
|
||||
tags: rel.tags.clone(),
|
||||
};
|
||||
generate_natural(
|
||||
editor,
|
||||
&ProcessedElement::Way(way_with_rel_tags),
|
||||
&ProcessedElement::Way((*member.way).clone()),
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all outer ways into one with relation tags
|
||||
let mut combined_nodes = Vec::new();
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
combined_nodes.extend(member.way.nodes.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Only process if we have nodes
|
||||
if !combined_nodes.is_empty() {
|
||||
// Create combined way with relation tags
|
||||
let combined_way = ProcessedWay {
|
||||
id: rel.id,
|
||||
nodes: combined_nodes,
|
||||
tags: rel.tags.clone(),
|
||||
};
|
||||
|
||||
// Generate natural area from combined way
|
||||
generate_natural(
|
||||
editor,
|
||||
&ProcessedElement::Way(combined_way),
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,385 +0,0 @@
|
||||
//! Processing of power infrastructure elements.
|
||||
//!
|
||||
//! This module handles power-related OSM elements including:
|
||||
//! - `power=tower` - Large electricity pylons
|
||||
//! - `power=pole` - Smaller wooden/concrete poles
|
||||
//! - `power=line` - Power lines connecting towers/poles
|
||||
|
||||
use crate::block_definitions::*;
|
||||
use crate::bresenham::bresenham_line;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedNode, ProcessedWay};
|
||||
use crate::world_editor::WorldEditor;
|
||||
|
||||
/// Generate power infrastructure from way elements (power lines)
|
||||
pub fn generate_power(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
// Skip if 'layer' or 'level' is negative in the tags
|
||||
if let Some(layer) = element.tags().get("layer") {
|
||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(level) = element.tags().get("level") {
|
||||
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip underground power infrastructure
|
||||
if element
|
||||
.tags()
|
||||
.get("location")
|
||||
.map(|v| v == "underground" || v == "underwater")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if element
|
||||
.tags()
|
||||
.get("tunnel")
|
||||
.map(|v| v == "yes")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(power_type) = element.tags().get("power") {
|
||||
match power_type.as_str() {
|
||||
"line" | "minor_line" => {
|
||||
if let ProcessedElement::Way(way) = element {
|
||||
generate_power_line(editor, way);
|
||||
}
|
||||
}
|
||||
"tower" => generate_power_tower(editor, element),
|
||||
"pole" => generate_power_pole(editor, element),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate power infrastructure from node elements
|
||||
pub fn generate_power_nodes(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
// Skip if 'layer' or 'level' is negative in the tags
|
||||
if let Some(layer) = node.tags.get("layer") {
|
||||
if layer.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(level) = node.tags.get("level") {
|
||||
if level.parse::<i32>().unwrap_or(0) < 0 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip underground power infrastructure
|
||||
if node
|
||||
.tags
|
||||
.get("location")
|
||||
.map(|v| v == "underground" || v == "underwater")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if node.tags.get("tunnel").map(|v| v == "yes").unwrap_or(false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(power_type) = node.tags.get("power") {
|
||||
match power_type.as_str() {
|
||||
"tower" => generate_power_tower_from_node(editor, node),
|
||||
"pole" => generate_power_pole_from_node(editor, node),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a high-voltage transmission tower (pylon) from a ProcessedElement
|
||||
fn generate_power_tower(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
let Some(first_node) = element.nodes().next() else {
|
||||
return;
|
||||
};
|
||||
let height = element
|
||||
.tags()
|
||||
.get("height")
|
||||
.and_then(|h| h.parse::<i32>().ok())
|
||||
.unwrap_or(25)
|
||||
.clamp(15, 40);
|
||||
generate_power_tower_impl(editor, first_node.x, first_node.z, height);
|
||||
}
|
||||
|
||||
/// Generate a high-voltage transmission tower (pylon) from a ProcessedNode
|
||||
fn generate_power_tower_from_node(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
let height = node
|
||||
.tags
|
||||
.get("height")
|
||||
.and_then(|h| h.parse::<i32>().ok())
|
||||
.unwrap_or(25)
|
||||
.clamp(15, 40);
|
||||
generate_power_tower_impl(editor, node.x, node.z, height);
|
||||
}
|
||||
|
||||
/// Generate a high-voltage transmission tower (pylon)
|
||||
///
|
||||
/// Creates a realistic lattice tower structure using iron bars and iron blocks.
|
||||
/// The design is a tapered lattice tower with cross-bracing and insulators.
|
||||
fn generate_power_tower_impl(editor: &mut WorldEditor, x: i32, z: i32, height: i32) {
|
||||
// Tower design constants
|
||||
let base_width = 3; // Half-width at base (so 7x7 footprint)
|
||||
let top_width = 1; // Half-width at top (so 3x3)
|
||||
let arm_height = height - 4; // Height where arms extend
|
||||
let arm_length = 5; // How far arms extend horizontally
|
||||
|
||||
// Build the four corner legs with tapering
|
||||
for y in 1..=height {
|
||||
// Calculate taper: legs get closer together as we go up
|
||||
let progress = y as f32 / height as f32;
|
||||
let current_width = base_width - ((base_width - top_width) as f32 * progress) as i32;
|
||||
|
||||
// Four corner positions
|
||||
let corners = [
|
||||
(x - current_width, z - current_width),
|
||||
(x + current_width, z - current_width),
|
||||
(x - current_width, z + current_width),
|
||||
(x + current_width, z + current_width),
|
||||
];
|
||||
|
||||
for (cx, cz) in corners {
|
||||
editor.set_block(IRON_BLOCK, cx, y, cz, None, None);
|
||||
}
|
||||
|
||||
// Add horizontal cross-bracing every 5 blocks
|
||||
if y % 5 == 0 && y < height - 2 {
|
||||
// Connect corners horizontally
|
||||
for dx in -current_width..=current_width {
|
||||
editor.set_block(IRON_BLOCK, x + dx, y, z - current_width, None, None);
|
||||
editor.set_block(IRON_BLOCK, x + dx, y, z + current_width, None, None);
|
||||
}
|
||||
for dz in -current_width..=current_width {
|
||||
editor.set_block(IRON_BLOCK, x - current_width, y, z + dz, None, None);
|
||||
editor.set_block(IRON_BLOCK, x + current_width, y, z + dz, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Add diagonal bracing between cross-brace levels
|
||||
if y % 5 >= 1 && y % 5 <= 4 && y > 1 && y < height - 2 {
|
||||
let prev_width = base_width
|
||||
- ((base_width - top_width) as f32 * ((y - 1) as f32 / height as f32)) as i32;
|
||||
|
||||
// Only add center vertical support if the width changed
|
||||
if current_width != prev_width || y % 5 == 2 {
|
||||
editor.set_block(IRON_BARS, x, y, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the cross-arms at arm_height for holding power lines
|
||||
// These extend outward in two directions (perpendicular to typical line direction)
|
||||
for arm_offset in [-arm_length, arm_length] {
|
||||
// Main arm beam (iron blocks for strength)
|
||||
for dx in 0..=arm_length {
|
||||
let arm_x = if arm_offset < 0 { x - dx } else { x + dx };
|
||||
editor.set_block(IRON_BLOCK, arm_x, arm_height, z, None, None);
|
||||
// Add second arm perpendicular
|
||||
editor.set_block(
|
||||
IRON_BLOCK,
|
||||
x,
|
||||
arm_height,
|
||||
z + if arm_offset < 0 { -dx } else { dx },
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
// Insulators hanging from arm ends (end rods to simulate ceramic insulators)
|
||||
let end_x = if arm_offset < 0 {
|
||||
x - arm_length
|
||||
} else {
|
||||
x + arm_length
|
||||
};
|
||||
editor.set_block(END_ROD, end_x, arm_height - 1, z, None, None);
|
||||
editor.set_block(END_ROD, x, arm_height - 1, z + arm_offset, None, None);
|
||||
}
|
||||
|
||||
// Add a second, smaller arm set lower for additional circuits
|
||||
let lower_arm_height = arm_height - 6;
|
||||
if lower_arm_height > 5 {
|
||||
let lower_arm_length = arm_length - 1;
|
||||
for arm_offset in [-lower_arm_length, lower_arm_length] {
|
||||
for dx in 0..=lower_arm_length {
|
||||
let arm_x = if arm_offset < 0 { x - dx } else { x + dx };
|
||||
editor.set_block(IRON_BLOCK, arm_x, lower_arm_height, z, None, None);
|
||||
}
|
||||
let end_x = if arm_offset < 0 {
|
||||
x - lower_arm_length
|
||||
} else {
|
||||
x + lower_arm_length
|
||||
};
|
||||
editor.set_block(END_ROD, end_x, lower_arm_height - 1, z, None, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Top finial/lightning rod
|
||||
editor.set_block(IRON_BLOCK, x, height, z, None, None);
|
||||
editor.set_block(LIGHTNING_ROD, x, height + 1, z, None, None);
|
||||
|
||||
// Concrete foundation at base
|
||||
for dx in -3..=3 {
|
||||
for dz in -3..=3 {
|
||||
editor.set_block(GRAY_CONCRETE, x + dx, 0, z + dz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a wooden/concrete power pole from a ProcessedElement
|
||||
fn generate_power_pole(editor: &mut WorldEditor, element: &ProcessedElement) {
|
||||
let Some(first_node) = element.nodes().next() else {
|
||||
return;
|
||||
};
|
||||
let height = element
|
||||
.tags()
|
||||
.get("height")
|
||||
.and_then(|h| h.parse::<i32>().ok())
|
||||
.unwrap_or(10)
|
||||
.clamp(6, 15);
|
||||
let pole_material = element
|
||||
.tags()
|
||||
.get("material")
|
||||
.map(|m| m.as_str())
|
||||
.unwrap_or("wood");
|
||||
generate_power_pole_impl(editor, first_node.x, first_node.z, height, pole_material);
|
||||
}
|
||||
|
||||
/// Generate a wooden/concrete power pole from a ProcessedNode
|
||||
fn generate_power_pole_from_node(editor: &mut WorldEditor, node: &ProcessedNode) {
|
||||
let height = node
|
||||
.tags
|
||||
.get("height")
|
||||
.and_then(|h| h.parse::<i32>().ok())
|
||||
.unwrap_or(10)
|
||||
.clamp(6, 15);
|
||||
let pole_material = node
|
||||
.tags
|
||||
.get("material")
|
||||
.map(|m| m.as_str())
|
||||
.unwrap_or("wood");
|
||||
generate_power_pole_impl(editor, node.x, node.z, height, pole_material);
|
||||
}
|
||||
|
||||
/// Generate a wooden/concrete power pole
|
||||
///
|
||||
/// Creates a simpler single-pole structure for lower voltage distribution lines.
|
||||
fn generate_power_pole_impl(
|
||||
editor: &mut WorldEditor,
|
||||
x: i32,
|
||||
z: i32,
|
||||
height: i32,
|
||||
pole_material: &str,
|
||||
) {
|
||||
let pole_block = match pole_material {
|
||||
"concrete" => LIGHT_GRAY_CONCRETE,
|
||||
"steel" | "metal" => IRON_BLOCK,
|
||||
_ => OAK_LOG, // Default to wood
|
||||
};
|
||||
|
||||
// Build the main pole
|
||||
for y in 1..=height {
|
||||
editor.set_block(pole_block, x, y, z, None, None);
|
||||
}
|
||||
|
||||
// Cross-arm at top (perpendicular beam for wires)
|
||||
let arm_length = 2;
|
||||
for dx in -arm_length..=arm_length {
|
||||
editor.set_block(OAK_FENCE, x + dx, height, z, None, None);
|
||||
}
|
||||
|
||||
// Insulators at arm ends
|
||||
editor.set_block(END_ROD, x - arm_length, height + 1, z, None, None);
|
||||
editor.set_block(END_ROD, x + arm_length, height + 1, z, None, None);
|
||||
editor.set_block(END_ROD, x, height + 1, z, None, None); // Center insulator
|
||||
}
|
||||
|
||||
/// Generate power lines connecting towers/poles
|
||||
///
|
||||
/// Creates a catenary-like curve (simplified) between nodes to simulate
|
||||
/// the natural sag of power cables.
|
||||
fn generate_power_line(editor: &mut WorldEditor, way: &ProcessedWay) {
|
||||
if way.nodes.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine line height based on voltage (higher voltage = taller structures)
|
||||
let base_height = way
|
||||
.tags
|
||||
.get("voltage")
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.map(|voltage| {
|
||||
if voltage >= 220000 {
|
||||
22 // High voltage transmission
|
||||
} else if voltage >= 110000 {
|
||||
18
|
||||
} else if voltage >= 33000 {
|
||||
14
|
||||
} else {
|
||||
10 // Distribution lines
|
||||
}
|
||||
})
|
||||
.unwrap_or(15);
|
||||
|
||||
// Process consecutive node pairs
|
||||
for i in 1..way.nodes.len() {
|
||||
let start = &way.nodes[i - 1];
|
||||
let end = &way.nodes[i];
|
||||
|
||||
// Calculate distance between nodes
|
||||
let dx = (end.x - start.x) as f64;
|
||||
let dz = (end.z - start.z) as f64;
|
||||
let distance = (dx * dx + dz * dz).sqrt();
|
||||
|
||||
// Calculate sag based on span length (longer spans = more sag)
|
||||
let max_sag = (distance / 15.0).clamp(1.0, 6.0) as i32;
|
||||
|
||||
// Determine chain orientation based on line direction
|
||||
// If the line runs more along X-axis, use CHAIN_X; if more along Z-axis, use CHAIN_Z
|
||||
let chain_block = if dx.abs() >= dz.abs() {
|
||||
CHAIN_X // Line runs primarily along X-axis
|
||||
} else {
|
||||
CHAIN_Z // Line runs primarily along Z-axis
|
||||
};
|
||||
|
||||
// Generate points along the line using Bresenham
|
||||
let line_points = bresenham_line(start.x, 0, start.z, end.x, 0, end.z);
|
||||
|
||||
for (idx, (lx, _, lz)) in line_points.iter().enumerate() {
|
||||
// Calculate position along the span (0.0 to 1.0)
|
||||
// Use len-1 as denominator so last point reaches t=1.0
|
||||
let denom = (line_points.len().saturating_sub(1)).max(1) as f64;
|
||||
let t = idx as f64 / denom;
|
||||
|
||||
// Catenary approximation: sag is maximum at center, zero at ends
|
||||
// Using parabola: sag = 4 * max_sag * t * (1 - t)
|
||||
let sag = (4.0 * max_sag as f64 * t * (1.0 - t)) as i32;
|
||||
|
||||
// Ensure wire doesn't go underground (minimum height of 3 blocks above ground)
|
||||
let wire_y = (base_height - sag).max(3);
|
||||
|
||||
// Place the wire block (chain aligned with line direction)
|
||||
editor.set_block(chain_block, *lx, wire_y, *lz, None, None);
|
||||
|
||||
// For high voltage lines, add parallel wires offset to sides
|
||||
if base_height >= 18 {
|
||||
// Three-phase power: 3 parallel lines
|
||||
// Offset perpendicular to the line direction
|
||||
if dx.abs() >= dz.abs() {
|
||||
// Line runs along X, offset in Z
|
||||
editor.set_block(chain_block, *lx, wire_y, *lz + 1, None, None);
|
||||
editor.set_block(chain_block, *lx, wire_y, *lz - 1, None, None);
|
||||
} else {
|
||||
// Line runs along Z, offset in X
|
||||
editor.set_block(chain_block, *lx + 1, wire_y, *lz, None, None);
|
||||
editor.set_block(chain_block, *lx - 1, wire_y, *lz, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ const INTERIOR1_LAYER2: [[char; 23]; 23] = [
|
||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building level floors (1st layer above floor)
|
||||
/// Interior layout for building level floors (1nd layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const INTERIOR2_LAYER1: [[char; 23]; 23] = [
|
||||
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||
@@ -114,119 +114,6 @@ const INTERIOR2_LAYER2: [[char; 23]; 23] = [
|
||||
['P', 'P', ' ', ' ', ' ', 'E', 'B', 'B', 'B', ' ', ' ', 'W', 'B', 'B', 'B', 'B', 'B', 'B', 'B', ' ', 'B', ' ', 'D',],
|
||||
];
|
||||
|
||||
// Generic Abandoned Building Interiors
|
||||
/// Interior layout for building ground floors (1st layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const ABANDONED_INTERIOR1_LAYER1: [[char; 23]; 23] = [
|
||||
['1', 'U', ' ', 'W', 'C', ' ', ' ', ' ', 'S', 'S', 'W', 'b', 'T', 'T', 'd', 'W', '7', '8', ' ', ' ', ' ', ' ', 'W',],
|
||||
['2', ' ', ' ', 'W', 'F', ' ', ' ', ' ', 'U', 'U', 'W', 'b', 'T', 'T', 'd', 'W', '7', '8', ' ', ' ', ' ', 'B', 'W',],
|
||||
[' ', ' ', ' ', 'W', 'F', ' ', ' ', ' ', ' ', ' ', 'W', 'b', 'T', 'T', 'd', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
|
||||
['W', 'W', 'D', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'M', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'c', 'c', 'c', ' ', ' ', 'J', 'W', ' ', ' ', ' ', 'd', 'W', 'W', 'W',],
|
||||
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', 'T', 'S', 'S', 'T', ' ', ' ', 'W', 'S', 'S', ' ', 'd', 'W', 'W', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'T', 'T', 'T', 'T', ' ', ' ', 'W', 'U', 'U', ' ', 'd', 'W', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', 'T', 'T', 'T', 'T', ' ', 'B', 'W', ' ', ' ', ' ', 'd', 'W', ' ', ' ',],
|
||||
['L', ' ', 'M', 'L', 'W', 'W', ' ', ' ', 'W', 'J', 'U', 'U', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
|
||||
['c', 'c', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
|
||||
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
|
||||
[' ', '6', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['U', '5', ' ', 'W', ' ', ' ', 'W', 'C', 'F', 'F', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', 'W', 'M', ' ', 'b', 'W', ' ', ' ', 'W',],
|
||||
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'b', 'W', 'J', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'U', ' ', ' ', 'W', 'B', ' ', 'D',],
|
||||
['J', ' ', ' ', 'C', 'a', 'a', 'W', 'L', 'F', ' ', 'W', 'F', ' ', 'W', 'L', 'W', '7', '8', ' ', 'W', 'B', ' ', 'W',],
|
||||
['B', ' ', ' ', 'd', 'W', 'W', 'W', 'W', 'W', ' ', 'W', 'M', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'C', ' ', 'W',],
|
||||
['B', ' ', ' ', 'd', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'C', ' ', ' ', 'W', 'W', 'c', 'c', 'c', 'c', 'W', 'D', 'W',],
|
||||
['W', 'W', 'D', 'W', 'C', ' ', ' ', ' ', 'W', 'W', 'W', 'b', 'T', 'T', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building ground floors (2nd layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const ABANDONED_INTERIOR1_LAYER2: [[char; 23]; 23] = [
|
||||
[' ', 'P', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'P', 'P', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'B', 'W',],
|
||||
[' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', ' ', ' ', 'B', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W',],
|
||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'B', 'B', 'B', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
|
||||
['W', 'W', 'W', 'W', 'D', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', 'B', 'W', 'W', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', 'P', ' ', 'B', 'W', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'B', 'W', ' ', ' ',],
|
||||
[' ', ' ', ' ', ' ', 'W', 'W', ' ', ' ', 'W', ' ', 'P', 'P', ' ', ' ', 'B', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W', 'C', 'C', 'W', 'W',],
|
||||
['B', 'B', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', 'W', ' ', ' ', 'W', 'W',],
|
||||
[' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['P', ' ', ' ', 'W', ' ', ' ', 'W', 'N', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'D', 'W', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'B', 'W', ' ', ' ', 'W',],
|
||||
['B', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'C', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', 'W', 'P', ' ', ' ', 'W', 'B', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', 'B', 'B', 'W', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'P', 'W', ' ', ' ', ' ', 'W', 'B', ' ', 'W',],
|
||||
['B', ' ', ' ', 'B', 'W', 'W', 'W', 'W', 'W', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W',],
|
||||
['B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', 'D', ' ', 'W', 'N', ' ', ' ', 'W', 'W', 'B', 'B', 'B', 'B', 'W', 'D', 'W',],
|
||||
['W', 'W', 'D', 'W', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'B', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building level floors (1st layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const ABANDONED_INTERIOR2_LAYER1: [[char; 23]; 23] = [
|
||||
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||
['U', ' ', ' ', ' ', ' ', ' ', 'C', 'W', 'L', ' ', ' ', 'L', 'W', 'M', 'M', 'W', ' ', ' ', ' ', ' ', ' ', 'L', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'S', 'S', 'S', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'Q', 'C', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'J', ' ', 'U', 'U', 'U', ' ', 'D',],
|
||||
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
|
||||
['U', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', 'T', 'T', 'W', ' ', ' ', ' ', ' ', ' ', 'U', 'W', ' ', 'L', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'T', 'J', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', 'L', ' ', 'W',],
|
||||
['J', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'C', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', 'M', 'c', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'd', 'W', 'L', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'd', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'U', ' ', ' ', ' ', 'D', ' ', ' ', 'F', 'F', 'W', 'M', 'M', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'U', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', 'C', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
['C', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'L', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'L', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'U', 'U', ' ', 'Q', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'U', 'U', ' ', 'Q', 'b', ' ', 'U', 'U', 'B', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['S', 'S', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'Q', 'b', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'd', ' ', 'W',],
|
||||
['U', 'U', ' ', ' ', ' ', 'L', 'a', 'a', 'a', ' ', ' ', 'Q', 'B', 'a', 'a', 'a', 'a', 'a', 'a', ' ', 'd', 'D', 'W',],
|
||||
];
|
||||
|
||||
/// Interior layout for building level floors (2nd layer above floor)
|
||||
#[rustfmt::skip]
|
||||
const ABANDONED_INTERIOR2_LAYER2: [[char; 23]; 23] = [
|
||||
['W', 'W', 'W', 'D', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W',],
|
||||
['P', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', 'O', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'O', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', 'W', 'F', ' ', ' ', ' ', 'Q', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', 'P', 'P', ' ', 'D',],
|
||||
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W',],
|
||||
['P', ' ', 'W', 'F', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', 'P', 'W', ' ', 'P', 'W',],
|
||||
[' ', ' ', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'W', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'D', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'O', ' ', ' ', ' ', ' ', 'W', 'P', ' ', ' ', ' ', 'B', 'W', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', ' ', 'c', 'B', 'W', 'W', 'W', 'W', ' ', ' ', 'W', ' ', ' ', ' ', ' ', 'B', 'W', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'd', 'W', 'O', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', 'B', 'W', 'W', 'B', 'B', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', 'd', 'W', ' ', ' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'D',],
|
||||
[' ', ' ', ' ', ' ', 'D', ' ', ' ', 'P', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', 'W', ' ', ' ', 'P', ' ', ' ', 'W', 'W', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', ' ', ' ', 'O', ' ', ' ', 'W', 'W', 'D', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'D', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W', 'O', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
['W', 'W', 'W', 'W', 'W', 'W', ' ', ' ', 'P', 'P', ' ', 'Q', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', 'W', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'P', 'P', ' ', 'Q', 'b', ' ', 'P', 'P', 'c', ' ', ' ', ' ', ' ', ' ', 'W',],
|
||||
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'Q', 'b', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'd', ' ', 'W',],
|
||||
['P', 'P', ' ', ' ', ' ', 'O', 'a', 'a', 'a', ' ', ' ', 'Q', 'b', 'a', 'a', 'a', 'a', 'a', 'a', ' ', 'd', ' ', 'D',],
|
||||
];
|
||||
|
||||
/// Maps interior layout characters to actual block types for different floor layers
|
||||
#[inline(always)]
|
||||
pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option<Block> {
|
||||
@@ -258,19 +145,12 @@ pub fn get_interior_block(c: char, is_layer2: bool, wall_block: Block) -> Option
|
||||
Some(DARK_OAK_DOOR_LOWER)
|
||||
}
|
||||
}
|
||||
'J' => Some(NOTE_BLOCK), // Note block
|
||||
'G' => Some(GLOWSTONE), // Glowstone
|
||||
'N' => Some(BREWING_STAND), // Brewing Stand
|
||||
'T' => Some(WHITE_CARPET), // White Carpet
|
||||
'E' => Some(OAK_LEAVES), // Oak Leaves
|
||||
'O' => Some(COBWEB), // Cobweb
|
||||
'a' => Some(CHISELLED_BOOKSHELF_NORTH), // Chiseled Bookshelf
|
||||
'b' => Some(CHISELLED_BOOKSHELF_EAST), // Chiseled Bookshelf East
|
||||
'c' => Some(CHISELLED_BOOKSHELF_SOUTH), // Chiseled Bookshelf South
|
||||
'd' => Some(CHISELLED_BOOKSHELF_WEST), // Chiseled Bookshelf West
|
||||
'M' => Some(DAMAGED_ANVIL), // Damaged Anvil
|
||||
'Q' => Some(SCAFFOLDING), // Scaffolding
|
||||
_ => None, // Default case for unknown characters
|
||||
'J' => Some(NOTE_BLOCK), // Note block
|
||||
'G' => Some(GLOWSTONE), // Glowstone
|
||||
'N' => Some(BREWING_STAND), // Brewing Stand
|
||||
'T' => Some(WHITE_CARPET), // White Carpet
|
||||
'E' => Some(OAK_LEAVES), // Oak Leaves
|
||||
_ => None, // Default case for unknown characters
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +170,6 @@ pub fn generate_building_interior(
|
||||
args: &crate::args::Args,
|
||||
element: &crate::osm_parser::ProcessedWay,
|
||||
abs_terrain_offset: i32,
|
||||
is_abandoned_building: bool,
|
||||
) {
|
||||
// Skip interior generation for very small buildings
|
||||
let width = max_x - min_x + 1;
|
||||
@@ -335,13 +214,7 @@ pub fn generate_building_interior(
|
||||
};
|
||||
|
||||
// Choose the appropriate interior pattern based on floor number
|
||||
let (layer1, layer2) = if is_abandoned_building {
|
||||
if floor_index == 0 {
|
||||
(&ABANDONED_INTERIOR1_LAYER1, &ABANDONED_INTERIOR1_LAYER2)
|
||||
} else {
|
||||
(&ABANDONED_INTERIOR2_LAYER1, &ABANDONED_INTERIOR2_LAYER2)
|
||||
}
|
||||
} else if floor_index == 0 {
|
||||
let (layer1, layer2) = if floor_index == 0 {
|
||||
// Ground floor uses INTERIOR1 patterns
|
||||
(&INTERIOR1_LAYER1, &INTERIOR1_LAYER2)
|
||||
} else {
|
||||
|
||||
@@ -83,33 +83,6 @@ const BIRCH_LEAVES_FILL: [(Coord, Coord); 5] = [
|
||||
((0, 7, 0), (0, 8, 0)),
|
||||
];
|
||||
|
||||
/// Dark oak: short but wide canopy, leaves start at y=3 up to y=6 with a cap
|
||||
const DARK_OAK_LEAVES_FILL: [(Coord, Coord); 5] = [
|
||||
((-1, 3, 0), (-1, 6, 0)),
|
||||
((1, 3, 0), (1, 6, 0)),
|
||||
((0, 3, -1), (0, 6, -1)),
|
||||
((0, 3, 1), (0, 6, 1)),
|
||||
((0, 6, 0), (0, 7, 0)),
|
||||
];
|
||||
|
||||
/// Jungle: tall tree with canopy only near the top, leaves from y=7 to y=11
|
||||
const JUNGLE_LEAVES_FILL: [(Coord, Coord); 5] = [
|
||||
((-1, 7, 0), (-1, 11, 0)),
|
||||
((1, 7, 0), (1, 11, 0)),
|
||||
((0, 7, -1), (0, 11, -1)),
|
||||
((0, 7, 1), (0, 11, 1)),
|
||||
((0, 11, 0), (0, 12, 0)),
|
||||
];
|
||||
|
||||
/// Acacia: umbrella-shaped canopy with a gentle dome, leaves from y=5 to y=8
|
||||
const ACACIA_LEAVES_FILL: [(Coord, Coord); 5] = [
|
||||
((-1, 5, 0), (-1, 8, 0)),
|
||||
((1, 5, 0), (1, 8, 0)),
|
||||
((0, 5, -1), (0, 8, -1)),
|
||||
((0, 5, 1), (0, 8, 1)),
|
||||
((0, 8, 0), (0, 9, 0)),
|
||||
];
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
/// Helper function to set blocks in various patterns.
|
||||
@@ -119,14 +92,10 @@ fn round(editor: &mut WorldEditor, material: Block, (x, y, z): Coord, block_patt
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum TreeType {
|
||||
Oak,
|
||||
Spruce,
|
||||
Birch,
|
||||
DarkOak,
|
||||
Jungle,
|
||||
Acacia,
|
||||
}
|
||||
|
||||
// TODO what should be moved in, and what should be referenced?
|
||||
@@ -151,30 +120,6 @@ impl Tree<'_> {
|
||||
editor: &mut WorldEditor,
|
||||
(x, y, z): Coord,
|
||||
building_footprints: Option<&BuildingFootprintBitmap>,
|
||||
) {
|
||||
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
||||
// The element_id of 0 is used as a salt for tree-specific randomness
|
||||
let mut rng = coord_rng(x, z, 0);
|
||||
|
||||
let tree_type = match rng.random_range(1..=10) {
|
||||
1..=3 => TreeType::Oak,
|
||||
4..=5 => TreeType::Spruce,
|
||||
6..=7 => TreeType::Birch,
|
||||
8 => TreeType::DarkOak,
|
||||
9 => TreeType::Jungle,
|
||||
10 => TreeType::Acacia,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Self::create_of_type(editor, (x, y, z), tree_type, building_footprints);
|
||||
}
|
||||
|
||||
/// Creates a tree of a specific type at the specified coordinates.
|
||||
pub fn create_of_type(
|
||||
editor: &mut WorldEditor,
|
||||
(x, y, z): Coord,
|
||||
tree_type: TreeType,
|
||||
building_footprints: Option<&BuildingFootprintBitmap>,
|
||||
) {
|
||||
// Skip if this coordinate is inside a building
|
||||
if let Some(footprints) = building_footprints {
|
||||
@@ -190,7 +135,16 @@ impl Tree<'_> {
|
||||
blacklist.extend(Self::get_functional_blocks());
|
||||
blacklist.push(WATER);
|
||||
|
||||
let tree = Self::get_tree(tree_type);
|
||||
// Use deterministic RNG based on coordinates for consistent tree types across region boundaries
|
||||
// The element_id of 0 is used as a salt for tree-specific randomness
|
||||
let mut rng = coord_rng(x, z, 0);
|
||||
|
||||
let tree = Self::get_tree(match rng.gen_range(1..=3) {
|
||||
1 => TreeType::Oak,
|
||||
2 => TreeType::Spruce,
|
||||
3 => TreeType::Birch,
|
||||
_ => unreachable!(),
|
||||
});
|
||||
|
||||
// Build the logs
|
||||
editor.fill_blocks(
|
||||
@@ -247,9 +201,9 @@ impl Tree<'_> {
|
||||
// kind,
|
||||
log_block: SPRUCE_LOG,
|
||||
log_height: 9,
|
||||
leaves_block: SPRUCE_LEAVES,
|
||||
leaves_block: BIRCH_LEAVES, // TODO Is this correct?
|
||||
leaves_fill: &SPRUCE_LEAVES_FILL,
|
||||
// Conical shape: wide at bottom, narrow at top
|
||||
// TODO can I omit the third empty vec? May cause issues with iter zip
|
||||
round_ranges: [vec![9, 7, 6, 4, 3], vec![6, 3], vec![]],
|
||||
},
|
||||
|
||||
@@ -261,44 +215,6 @@ impl Tree<'_> {
|
||||
leaves_fill: &BIRCH_LEAVES_FILL,
|
||||
round_ranges: [(2..=6).rev().collect(), (2..=4).collect(), vec![]],
|
||||
},
|
||||
|
||||
TreeType::DarkOak => Self {
|
||||
// Short trunk with a very wide, bushy canopy
|
||||
log_block: DARK_OAK_LOG,
|
||||
log_height: 5,
|
||||
leaves_block: DARK_OAK_LEAVES,
|
||||
leaves_fill: &DARK_OAK_LEAVES_FILL,
|
||||
// All 3 round patterns used for maximum width
|
||||
round_ranges: [
|
||||
(3..=6).rev().collect(),
|
||||
(3..=5).rev().collect(),
|
||||
(4..=5).rev().collect(),
|
||||
],
|
||||
},
|
||||
|
||||
TreeType::Jungle => Self {
|
||||
// Tall trunk, canopy clustered at the top
|
||||
log_block: JUNGLE_LOG,
|
||||
log_height: 10,
|
||||
leaves_block: JUNGLE_LEAVES,
|
||||
leaves_fill: &JUNGLE_LEAVES_FILL,
|
||||
// Canopy only near the top of the tree
|
||||
round_ranges: [(7..=11).rev().collect(), (8..=10).rev().collect(), vec![]],
|
||||
},
|
||||
|
||||
TreeType::Acacia => Self {
|
||||
// Medium trunk with umbrella-shaped canopy, domed center
|
||||
log_block: ACACIA_LOG,
|
||||
log_height: 6,
|
||||
leaves_block: ACACIA_LEAVES,
|
||||
leaves_fill: &ACACIA_LEAVES_FILL,
|
||||
// Inner rounds reach higher → gentle dome, outer stays low → wide brim
|
||||
round_ranges: [
|
||||
(5..=8).rev().collect(),
|
||||
(5..=7).rev().collect(),
|
||||
(6..=7).rev().collect(),
|
||||
],
|
||||
},
|
||||
} // match
|
||||
} // fn get_tree
|
||||
|
||||
@@ -434,9 +350,6 @@ impl Tree<'_> {
|
||||
GRAY_STAINED_GLASS,
|
||||
LIGHT_GRAY_STAINED_GLASS,
|
||||
BROWN_STAINED_GLASS,
|
||||
CYAN_STAINED_GLASS,
|
||||
BLUE_STAINED_GLASS,
|
||||
LIGHT_BLUE_STAINED_GLASS,
|
||||
TINTED_GLASS,
|
||||
// Carpets
|
||||
WHITE_CARPET,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use geo::orient::{Direction, Orient};
|
||||
use geo::{Contains, Intersects, LineString, Point, Polygon, Rect};
|
||||
|
||||
use crate::clipping::clip_water_ring_to_bbox;
|
||||
use crate::{
|
||||
block_definitions::WATER,
|
||||
@@ -51,19 +54,18 @@ pub fn generate_water_areas_from_relation(
|
||||
match mem.role {
|
||||
ProcessedMemberRole::Outer => outers.push(mem.way.nodes.clone()),
|
||||
ProcessedMemberRole::Inner => inners.push(mem.way.nodes.clone()),
|
||||
ProcessedMemberRole::Part => {} // Not applicable to water areas
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve OSM-defined outer/inner roles without modification
|
||||
super::merge_way_segments(&mut outers);
|
||||
merge_way_segments(&mut outers);
|
||||
|
||||
// Clip assembled rings to bbox (must happen after merging to preserve ring connectivity)
|
||||
outers = outers
|
||||
.into_iter()
|
||||
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
||||
.collect();
|
||||
super::merge_way_segments(&mut inners);
|
||||
merge_way_segments(&mut inners);
|
||||
inners = inners
|
||||
.into_iter()
|
||||
.filter_map(|ring| clip_water_ring_to_bbox(&ring, xzbbox))
|
||||
@@ -110,7 +112,7 @@ pub fn generate_water_areas_from_relation(
|
||||
}
|
||||
}
|
||||
|
||||
super::merge_way_segments(&mut inners);
|
||||
merge_way_segments(&mut inners);
|
||||
if !verify_closed_rings(&inners) {
|
||||
println!("Skipping relation {} due to invalid polygon", element.id);
|
||||
return;
|
||||
@@ -161,7 +163,106 @@ fn generate_water_areas(
|
||||
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
|
||||
.collect();
|
||||
|
||||
scanline_fill_water(min_x, min_z, max_x, max_z, &outers_xz, &inners_xz, editor);
|
||||
inverse_floodfill(min_x, min_z, max_x, max_z, outers_xz, inners_xz, editor);
|
||||
}
|
||||
|
||||
/// Merges way segments that share endpoints into closed rings.
|
||||
fn merge_way_segments(rings: &mut Vec<Vec<ProcessedNode>>) {
|
||||
let mut removed: Vec<usize> = vec![];
|
||||
let mut merged: Vec<Vec<ProcessedNode>> = vec![];
|
||||
|
||||
// Match nodes by ID or proximity (handles synthetic nodes from bbox clipping)
|
||||
let nodes_match = |a: &ProcessedNode, b: &ProcessedNode| -> bool {
|
||||
if a.id == b.id {
|
||||
return true;
|
||||
}
|
||||
let dx = (a.x - b.x).abs();
|
||||
let dz = (a.z - b.z).abs();
|
||||
dx <= 1 && dz <= 1
|
||||
};
|
||||
|
||||
for i in 0..rings.len() {
|
||||
for j in 0..rings.len() {
|
||||
if i == j {
|
||||
continue;
|
||||
}
|
||||
|
||||
if removed.contains(&i) || removed.contains(&j) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let x: &Vec<ProcessedNode> = &rings[i];
|
||||
let y: &Vec<ProcessedNode> = &rings[j];
|
||||
|
||||
// Skip empty rings (can happen after clipping)
|
||||
if x.is_empty() || y.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let x_first = &x[0];
|
||||
let x_last = x.last().unwrap();
|
||||
let y_first = &y[0];
|
||||
let y_last = y.last().unwrap();
|
||||
|
||||
// Skip already-closed rings
|
||||
if nodes_match(x_first, x_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if nodes_match(y_first, y_last) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if nodes_match(x_first, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut x: Vec<ProcessedNode> = x.clone();
|
||||
x.reverse();
|
||||
x.extend(y.iter().skip(1).cloned());
|
||||
merged.push(x);
|
||||
} else if nodes_match(x_last, y_last) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut x: Vec<ProcessedNode> = x.clone();
|
||||
x.extend(y.iter().rev().skip(1).cloned());
|
||||
|
||||
merged.push(x);
|
||||
} else if nodes_match(x_first, y_last) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut y: Vec<ProcessedNode> = y.clone();
|
||||
y.extend(x.iter().skip(1).cloned());
|
||||
|
||||
merged.push(y);
|
||||
} else if nodes_match(x_last, y_first) {
|
||||
removed.push(i);
|
||||
removed.push(j);
|
||||
|
||||
let mut x: Vec<ProcessedNode> = x.clone();
|
||||
x.extend(y.iter().skip(1).cloned());
|
||||
|
||||
merged.push(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removed.sort();
|
||||
|
||||
for r in removed.iter().rev() {
|
||||
rings.remove(*r);
|
||||
}
|
||||
|
||||
let merged_len: usize = merged.len();
|
||||
for m in merged {
|
||||
rings.push(m);
|
||||
}
|
||||
|
||||
if merged_len > 0 {
|
||||
merge_way_segments(rings);
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies all rings are properly closed (first node matches last).
|
||||
@@ -187,248 +288,156 @@ fn verify_closed_rings(rings: &[Vec<ProcessedNode>]) -> bool {
|
||||
valid
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Scanline rasterization for water area filling
|
||||
// ============================================================================
|
||||
//
|
||||
// For each row (z coordinate) in the fill area, computes polygon edge
|
||||
// crossings to determine which x-ranges are inside the outer polygons but
|
||||
// outside the inner polygons, then fills those ranges with water blocks.
|
||||
//
|
||||
// Complexity: O(E * H + A) where E = total edges, H = height of fill area,
|
||||
// A = total filled area. This is dramatically faster than the previous
|
||||
// quadtree + per-block point-in-polygon approach O(A * V * P) for large or
|
||||
// complex water bodies (e.g. the Venetian Lagoon with dozens of inner island
|
||||
// rings).
|
||||
|
||||
/// A polygon edge segment for scanline intersection testing.
|
||||
struct ScanlineEdge {
|
||||
x1: f64,
|
||||
z1: f64,
|
||||
x2: f64,
|
||||
z2: f64,
|
||||
}
|
||||
|
||||
/// Collects all non-horizontal edges from a single polygon ring.
|
||||
///
|
||||
/// If the ring is not perfectly closed (last point != first point),
|
||||
/// the closing edge is added explicitly.
|
||||
fn collect_ring_edges(ring: &[XZPoint]) -> Vec<ScanlineEdge> {
|
||||
let mut edges = Vec::new();
|
||||
if ring.len() < 2 {
|
||||
return edges;
|
||||
}
|
||||
for i in 0..ring.len() - 1 {
|
||||
let a = &ring[i];
|
||||
let b = &ring[i + 1];
|
||||
// Skip horizontal edges, they produce no scanline crossings
|
||||
if a.z != b.z {
|
||||
edges.push(ScanlineEdge {
|
||||
x1: a.x as f64,
|
||||
z1: a.z as f64,
|
||||
x2: b.x as f64,
|
||||
z2: b.z as f64,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Add closing edge if the ring isn't perfectly closed by coordinates
|
||||
let first = ring.first().unwrap();
|
||||
let last = ring.last().unwrap();
|
||||
if first.z != last.z {
|
||||
edges.push(ScanlineEdge {
|
||||
x1: last.x as f64,
|
||||
z1: last.z as f64,
|
||||
x2: first.x as f64,
|
||||
z2: first.z as f64,
|
||||
});
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
/// Collects edges from multiple rings into a single list.
|
||||
/// Used for inner rings where even-odd on combined edges is correct
|
||||
/// (inner rings of a valid multipolygon do not overlap).
|
||||
fn collect_all_ring_edges(rings: &[Vec<XZPoint>]) -> Vec<ScanlineEdge> {
|
||||
let mut edges = Vec::new();
|
||||
for ring in rings {
|
||||
edges.extend(collect_ring_edges(ring));
|
||||
}
|
||||
edges
|
||||
}
|
||||
|
||||
/// Computes the integer x-spans that are "inside" the polygon rings at
|
||||
/// scanline `z`, using the even-odd (parity) rule.
|
||||
///
|
||||
/// The crossing test uses the same convention as `geo::Contains`:
|
||||
/// an edge crosses the scanline when one endpoint is strictly above `z`
|
||||
/// and the other is at or below.
|
||||
fn compute_scanline_spans(
|
||||
edges: &[ScanlineEdge],
|
||||
z: f64,
|
||||
min_x: i32,
|
||||
max_x: i32,
|
||||
) -> Vec<(i32, i32)> {
|
||||
let mut xs: Vec<f64> = Vec::new();
|
||||
for edge in edges {
|
||||
// Crossing test: (z1 > z) != (z2 > z)
|
||||
// Matches geo's convention (bottom-inclusive, top-exclusive).
|
||||
if (edge.z1 > z) != (edge.z2 > z) {
|
||||
let t = (z - edge.z1) / (edge.z2 - edge.z1);
|
||||
xs.push(edge.x1 + t * (edge.x2 - edge.x1));
|
||||
}
|
||||
}
|
||||
|
||||
if xs.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
xs.sort_unstable_by(|a, b| {
|
||||
a.partial_cmp(b)
|
||||
.expect("NaN encountered while sorting scanline intersections")
|
||||
});
|
||||
|
||||
debug_assert!(
|
||||
xs.len().is_multiple_of(2),
|
||||
"Odd number of scanline crossings ({}) at z={}, possible malformed polygon",
|
||||
xs.len(),
|
||||
z
|
||||
);
|
||||
|
||||
// Pair consecutive crossings into fill spans (even-odd rule)
|
||||
let mut spans = Vec::with_capacity(xs.len() / 2);
|
||||
let mut i = 0;
|
||||
while i + 1 < xs.len() {
|
||||
let start = (xs[i].ceil() as i32).max(min_x);
|
||||
let end = (xs[i + 1].floor() as i32).min(max_x);
|
||||
if start <= end {
|
||||
spans.push((start, end));
|
||||
}
|
||||
i += 2;
|
||||
}
|
||||
|
||||
spans
|
||||
}
|
||||
|
||||
/// Merges two sorted, non-overlapping span lists into their union.
|
||||
fn union_spans(a: &[(i32, i32)], b: &[(i32, i32)]) -> Vec<(i32, i32)> {
|
||||
if a.is_empty() {
|
||||
return b.to_vec();
|
||||
}
|
||||
if b.is_empty() {
|
||||
return a.to_vec();
|
||||
}
|
||||
|
||||
// Merge both sorted lists and combine overlapping/adjacent spans
|
||||
let mut all: Vec<(i32, i32)> = Vec::with_capacity(a.len() + b.len());
|
||||
all.extend_from_slice(a);
|
||||
all.extend_from_slice(b);
|
||||
all.sort_unstable_by_key(|&(start, _)| start);
|
||||
|
||||
let mut result: Vec<(i32, i32)> = Vec::new();
|
||||
let mut current = all[0];
|
||||
for &(start, end) in &all[1..] {
|
||||
if start <= current.1 + 1 {
|
||||
// Overlapping or adjacent, extend
|
||||
current.1 = current.1.max(end);
|
||||
} else {
|
||||
result.push(current);
|
||||
current = (start, end);
|
||||
}
|
||||
}
|
||||
result.push(current);
|
||||
result
|
||||
}
|
||||
|
||||
/// Subtracts spans in `b` from spans in `a`.
|
||||
///
|
||||
/// Both inputs must be sorted and non-overlapping.
|
||||
/// Returns sorted, non-overlapping spans representing `a \ b`.
|
||||
fn subtract_spans(a: &[(i32, i32)], b: &[(i32, i32)]) -> Vec<(i32, i32)> {
|
||||
if b.is_empty() {
|
||||
return a.to_vec();
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
let mut bi = 0;
|
||||
|
||||
for &(a_start, a_end) in a {
|
||||
let mut pos = a_start;
|
||||
|
||||
// Skip B spans that end before this A span starts
|
||||
while bi < b.len() && b[bi].1 < a_start {
|
||||
bi += 1;
|
||||
}
|
||||
|
||||
// Walk through B spans that overlap with [pos .. a_end]
|
||||
let mut j = bi;
|
||||
while j < b.len() && b[j].0 <= a_end {
|
||||
if b[j].0 > pos {
|
||||
result.push((pos, (b[j].0 - 1).min(a_end)));
|
||||
}
|
||||
pos = pos.max(b[j].1 + 1);
|
||||
j += 1;
|
||||
}
|
||||
|
||||
if pos <= a_end {
|
||||
result.push((pos, a_end));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Fills water blocks using scanline rasterization.
|
||||
///
|
||||
/// For each row z in [min_z, max_z], computes which x positions are inside
|
||||
/// any outer polygon ring but outside all inner polygon rings, and places
|
||||
/// water blocks at those positions.
|
||||
// Water areas are absolutely huge. We can't easily flood fill the entire thing.
|
||||
// Instead, we'll iterate over all the blocks in our MC world, and check if each
|
||||
// one is in the river or not
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn scanline_fill_water(
|
||||
fn inverse_floodfill(
|
||||
min_x: i32,
|
||||
min_z: i32,
|
||||
max_x: i32,
|
||||
max_z: i32,
|
||||
outers: &[Vec<XZPoint>],
|
||||
inners: &[Vec<XZPoint>],
|
||||
outers: Vec<Vec<XZPoint>>,
|
||||
inners: Vec<Vec<XZPoint>>,
|
||||
editor: &mut WorldEditor,
|
||||
) {
|
||||
// Collect edges per outer ring so we can union their spans correctly,
|
||||
// even if multiple outer rings happen to overlap (invalid OSM, but
|
||||
// we handle it gracefully).
|
||||
let outer_edge_groups: Vec<Vec<ScanlineEdge>> =
|
||||
outers.iter().map(|ring| collect_ring_edges(ring)).collect();
|
||||
let inner_edges = collect_all_ring_edges(inners);
|
||||
// Convert to geo Polygons with normalized winding order
|
||||
let inners: Vec<_> = inners
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
Polygon::new(
|
||||
LineString::from(
|
||||
x.iter()
|
||||
.map(|pt| (pt.x as f64, pt.z as f64))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
vec![],
|
||||
)
|
||||
.orient(Direction::Default)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for z in min_z..=max_z {
|
||||
let z_f = z as f64;
|
||||
let outers: Vec<_> = outers
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
Polygon::new(
|
||||
LineString::from(
|
||||
x.iter()
|
||||
.map(|pt| (pt.x as f64, pt.z as f64))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
vec![],
|
||||
)
|
||||
.orient(Direction::Default)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Compute spans for each outer ring and union them together
|
||||
let mut outer_spans: Vec<(i32, i32)> = Vec::new();
|
||||
for ring_edges in &outer_edge_groups {
|
||||
let ring_spans = compute_scanline_spans(ring_edges, z_f, min_x, max_x);
|
||||
if !ring_spans.is_empty() {
|
||||
outer_spans = union_spans(&outer_spans, &ring_spans);
|
||||
}
|
||||
}
|
||||
if outer_spans.is_empty() {
|
||||
inverse_floodfill_recursive((min_x, min_z), (max_x, max_z), &outers, &inners, editor);
|
||||
}
|
||||
|
||||
fn inverse_floodfill_recursive(
|
||||
min: (i32, i32),
|
||||
max: (i32, i32),
|
||||
outers: &[Polygon],
|
||||
inners: &[Polygon],
|
||||
editor: &mut WorldEditor,
|
||||
) {
|
||||
// Check if we've exceeded 40 seconds
|
||||
// if start_time.elapsed().as_secs() > 40 {
|
||||
// println!("Water area generation exceeded 40 seconds, continuing anyway");
|
||||
// }
|
||||
|
||||
const ITERATIVE_THRES: i64 = 10_000;
|
||||
|
||||
if min.0 > max.0 || min.1 > max.1 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Multiply as i64 to avoid overflow; in release builds where unchecked math is
|
||||
// enabled, this could cause the rest of this code to end up in an infinite loop.
|
||||
if ((max.0 - min.0) as i64) * ((max.1 - min.1) as i64) < ITERATIVE_THRES {
|
||||
inverse_floodfill_iterative(min, max, 0, outers, inners, editor);
|
||||
return;
|
||||
}
|
||||
|
||||
let center_x: i32 = (min.0 + max.0) / 2;
|
||||
let center_z: i32 = (min.1 + max.1) / 2;
|
||||
let quadrants: [(i32, i32, i32, i32); 4] = [
|
||||
(min.0, center_x, min.1, center_z),
|
||||
(center_x, max.0, min.1, center_z),
|
||||
(min.0, center_x, center_z, max.1),
|
||||
(center_x, max.0, center_z, max.1),
|
||||
];
|
||||
|
||||
for (min_x, max_x, min_z, max_z) in quadrants {
|
||||
let rect: Rect = Rect::new(
|
||||
Point::new(min_x as f64, min_z as f64),
|
||||
Point::new(max_x as f64, max_z as f64),
|
||||
);
|
||||
|
||||
if outers.iter().any(|outer: &Polygon| outer.contains(&rect))
|
||||
&& !inners.iter().any(|inner: &Polygon| inner.intersects(&rect))
|
||||
{
|
||||
rect_fill(min_x, max_x, min_z, max_z, 0, editor);
|
||||
continue;
|
||||
}
|
||||
|
||||
let fill_spans = if inner_edges.is_empty() {
|
||||
outer_spans
|
||||
} else {
|
||||
let inner_spans = compute_scanline_spans(&inner_edges, z_f, min_x, max_x);
|
||||
if inner_spans.is_empty() {
|
||||
outer_spans
|
||||
} else {
|
||||
subtract_spans(&outer_spans, &inner_spans)
|
||||
}
|
||||
};
|
||||
let outers_intersects: Vec<_> = outers
|
||||
.iter()
|
||||
.filter(|poly| poly.intersects(&rect))
|
||||
.cloned()
|
||||
.collect();
|
||||
let inners_intersects: Vec<_> = inners
|
||||
.iter()
|
||||
.filter(|poly| poly.intersects(&rect))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
for (start, end) in fill_spans {
|
||||
for x in start..=end {
|
||||
editor.set_block(WATER, x, 0, z, None, None);
|
||||
if !outers_intersects.is_empty() {
|
||||
inverse_floodfill_recursive(
|
||||
(min_x, min_z),
|
||||
(max_x, max_z),
|
||||
&outers_intersects,
|
||||
&inners_intersects,
|
||||
editor,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// once we "zoom in" enough, it's more efficient to switch to iteration
|
||||
fn inverse_floodfill_iterative(
|
||||
min: (i32, i32),
|
||||
max: (i32, i32),
|
||||
ground_level: i32,
|
||||
outers: &[Polygon],
|
||||
inners: &[Polygon],
|
||||
editor: &mut WorldEditor,
|
||||
) {
|
||||
for x in min.0..max.0 {
|
||||
for z in min.1..max.1 {
|
||||
let p: Point = Point::new(x as f64, z as f64);
|
||||
|
||||
if outers.iter().any(|poly: &Polygon| poly.contains(&p))
|
||||
&& inners.iter().all(|poly: &Polygon| !poly.contains(&p))
|
||||
{
|
||||
editor.set_block(WATER, x, ground_level, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_fill(
|
||||
min_x: i32,
|
||||
max_x: i32,
|
||||
min_z: i32,
|
||||
max_z: i32,
|
||||
ground_level: i32,
|
||||
editor: &mut WorldEditor,
|
||||
) {
|
||||
for x in min_x..max_x {
|
||||
for z in min_z..max_z {
|
||||
editor.set_block(WATER, x, ground_level, z, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,6 +326,11 @@ pub fn fetch_elevation_data(
|
||||
Ok(tile_data) => successful_tiles.push(tile_data),
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to download tile: {e}");
|
||||
#[cfg(feature = "gui")]
|
||||
send_log(
|
||||
LogLevel::Warning,
|
||||
&format!("Failed to download elevation tile: {e}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
123
src/floodfill.rs
123
src/floodfill.rs
@@ -1,64 +1,8 @@
|
||||
use geo::orient::{Direction, Orient};
|
||||
use geo::{Contains, LineString, Point, Polygon};
|
||||
use itertools::Itertools;
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Maximum bounding box area (in blocks) for flood fill.
|
||||
/// Polygons exceeding this are skipped to prevent excessive memory allocations.
|
||||
/// 25 million blocks ≈ 5000×5000; bitmap uses only ~3 MB at this size.
|
||||
const MAX_FLOOD_FILL_AREA: i64 = 25_000_000;
|
||||
|
||||
/// A compact bitmap for visited-coordinate tracking during flood fill.
|
||||
///
|
||||
/// Uses 1 bit per coordinate instead of ~48 bytes per entry in a `HashSet`.
|
||||
/// For a 5000×5000 bounding box this is ~3 MB instead of ~1.2 GB.
|
||||
struct FloodBitmap {
|
||||
bits: Vec<u8>,
|
||||
min_x: i32,
|
||||
min_z: i32,
|
||||
width: usize,
|
||||
}
|
||||
|
||||
impl FloodBitmap {
|
||||
#[inline]
|
||||
fn new(min_x: i32, max_x: i32, min_z: i32, max_z: i32) -> Self {
|
||||
let width = (max_x - min_x + 1) as usize;
|
||||
let height = (max_z - min_z + 1) as usize;
|
||||
let num_bytes = (width * height).div_ceil(8);
|
||||
Self {
|
||||
bits: vec![0u8; num_bytes],
|
||||
min_x,
|
||||
min_z,
|
||||
width,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark (x, z) as visited. Returns `true` if it was NOT already visited
|
||||
/// (i.e. this is the first visit).
|
||||
#[inline]
|
||||
fn insert(&mut self, x: i32, z: i32) -> bool {
|
||||
let idx = (z - self.min_z) as usize * self.width + (x - self.min_x) as usize;
|
||||
let byte = idx / 8;
|
||||
let bit = idx % 8;
|
||||
let mask = 1u8 << bit;
|
||||
if self.bits[byte] & mask != 0 {
|
||||
false // already visited
|
||||
} else {
|
||||
self.bits[byte] |= mask;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn contains(&self, x: i32, z: i32) -> bool {
|
||||
let idx = (z - self.min_z) as usize * self.width + (x - self.min_x) as usize;
|
||||
let byte = idx / 8;
|
||||
let bit = idx % 8;
|
||||
(self.bits[byte] >> bit) & 1 == 1
|
||||
}
|
||||
}
|
||||
|
||||
/// Main flood fill function with automatic algorithm selection
|
||||
/// Chooses the best algorithm based on polygon size and complexity
|
||||
pub fn flood_fill_area(
|
||||
@@ -85,13 +29,6 @@ pub fn flood_fill_area(
|
||||
|
||||
let area = (max_x - min_x + 1) as i64 * (max_z - min_z + 1) as i64;
|
||||
|
||||
// Safety cap: reject polygons whose bounding box is too large.
|
||||
// This prevents multi-GB memory allocations when ocean-adjacent elements
|
||||
// (e.g. natural=water, large landuse) produce huge clipped polygons.
|
||||
if area > MAX_FLOOD_FILL_AREA {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
// For small and medium areas, use optimized flood fill with span filling
|
||||
if area < 50000 {
|
||||
optimized_flood_fill_area(polygon_coords, timeout, min_x, max_x, min_z, max_z)
|
||||
@@ -113,16 +50,15 @@ fn optimized_flood_fill_area(
|
||||
let start_time = Instant::now();
|
||||
|
||||
let mut filled_area = Vec::new();
|
||||
let mut visited = FloodBitmap::new(min_x, max_x, min_z, max_z);
|
||||
let mut global_visited = HashSet::new();
|
||||
|
||||
// Create polygon for containment testing, with normalized winding order
|
||||
// to avoid "polygon had no winding order" warnings from geo::Contains
|
||||
// Create polygon for containment testing
|
||||
let exterior_coords: Vec<(f64, f64)> = polygon_coords
|
||||
.iter()
|
||||
.map(|&(x, z)| (x as f64, z as f64))
|
||||
.collect();
|
||||
let exterior = LineString::from(exterior_coords);
|
||||
let polygon = Polygon::new(exterior, vec![]).orient(Direction::Default);
|
||||
let polygon = Polygon::new(exterior, vec![]);
|
||||
|
||||
// Optimized step sizes: larger steps for efficiency, but still catch U-shapes
|
||||
let width = max_x - min_x + 1;
|
||||
@@ -145,14 +81,16 @@ fn optimized_flood_fill_area(
|
||||
}
|
||||
|
||||
// Skip if already visited or not inside polygon
|
||||
if visited.contains(x, z) || !polygon.contains(&Point::new(x as f64, z as f64)) {
|
||||
if global_visited.contains(&(x, z))
|
||||
|| !polygon.contains(&Point::new(x as f64, z as f64))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start flood fill from this seed point
|
||||
queue.clear(); // Reuse queue instead of creating new one
|
||||
queue.push_back((x, z));
|
||||
visited.insert(x, z);
|
||||
global_visited.insert((x, z));
|
||||
|
||||
while let Some((curr_x, curr_z)) = queue.pop_front() {
|
||||
// Add current point to filled area
|
||||
@@ -166,16 +104,17 @@ fn optimized_flood_fill_area(
|
||||
(curr_x, curr_z + 1),
|
||||
];
|
||||
|
||||
for &(nx, nz) in &neighbors {
|
||||
if nx >= min_x
|
||||
&& nx <= max_x
|
||||
&& nz >= min_z
|
||||
&& nz <= max_z
|
||||
&& visited.insert(nx, nz)
|
||||
for (nx, nz) in neighbors.iter() {
|
||||
if *nx >= min_x
|
||||
&& *nx <= max_x
|
||||
&& *nz >= min_z
|
||||
&& *nz <= max_z
|
||||
&& !global_visited.contains(&(*nx, *nz))
|
||||
{
|
||||
// Only check polygon containment for unvisited points
|
||||
if polygon.contains(&Point::new(nx as f64, nz as f64)) {
|
||||
queue.push_back((nx, nz));
|
||||
if polygon.contains(&Point::new(*nx as f64, *nz as f64)) {
|
||||
global_visited.insert((*nx, *nz));
|
||||
queue.push_back((*nx, *nz));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,16 +136,15 @@ fn original_flood_fill_area(
|
||||
) -> Vec<(i32, i32)> {
|
||||
let start_time = Instant::now();
|
||||
let mut filled_area: Vec<(i32, i32)> = Vec::new();
|
||||
let mut visited = FloodBitmap::new(min_x, max_x, min_z, max_z);
|
||||
let mut global_visited: HashSet<(i32, i32)> = HashSet::new();
|
||||
|
||||
// Convert input to a geo::Polygon for efficient point-in-polygon testing,
|
||||
// with normalized winding order to avoid undefined Contains results
|
||||
// Convert input to a geo::Polygon for efficient point-in-polygon testing
|
||||
let exterior_coords: Vec<(f64, f64)> = polygon_coords
|
||||
.iter()
|
||||
.map(|&(x, z)| (x as f64, z as f64))
|
||||
.collect::<Vec<_>>();
|
||||
let exterior: LineString = LineString::from(exterior_coords);
|
||||
let polygon: Polygon<f64> = Polygon::new(exterior, vec![]).orient(Direction::Default);
|
||||
let polygon: Polygon<f64> = Polygon::new(exterior, vec![]);
|
||||
|
||||
// Optimized step sizes for large polygons - coarser sampling for speed
|
||||
let width = max_x - min_x + 1;
|
||||
@@ -230,14 +168,16 @@ fn original_flood_fill_area(
|
||||
}
|
||||
|
||||
// Skip if already processed or not inside polygon
|
||||
if visited.contains(x, z) || !polygon.contains(&Point::new(x as f64, z as f64)) {
|
||||
if global_visited.contains(&(x, z))
|
||||
|| !polygon.contains(&Point::new(x as f64, z as f64))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start flood-fill from this seed point
|
||||
queue.clear(); // Reuse queue
|
||||
queue.push_back((x, z));
|
||||
visited.insert(x, z);
|
||||
global_visited.insert((x, z));
|
||||
|
||||
while let Some((curr_x, curr_z)) = queue.pop_front() {
|
||||
// Only check polygon containment once per point when adding to filled_area
|
||||
@@ -252,14 +192,15 @@ fn original_flood_fill_area(
|
||||
(curr_x, curr_z + 1),
|
||||
];
|
||||
|
||||
for &(nx, nz) in &neighbors {
|
||||
if nx >= min_x
|
||||
&& nx <= max_x
|
||||
&& nz >= min_z
|
||||
&& nz <= max_z
|
||||
&& visited.insert(nx, nz)
|
||||
for (nx, nz) in neighbors.iter() {
|
||||
if *nx >= min_x
|
||||
&& *nx <= max_x
|
||||
&& *nz >= min_z
|
||||
&& *nz <= max_z
|
||||
&& !global_visited.contains(&(*nx, *nz))
|
||||
{
|
||||
queue.push_back((nx, nz));
|
||||
global_visited.insert((*nx, *nz));
|
||||
queue.push_back((*nx, *nz));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ use fnv::FnvHashMap;
|
||||
use rayon::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
||||
/// A memory-efficient bitmap for storing coordinates.
|
||||
/// A memory-efficient bitmap for storing building footprint coordinates.
|
||||
///
|
||||
/// Instead of storing each coordinate individually (~24 bytes per entry in a HashSet),
|
||||
/// this uses 1 bit per coordinate in the world bounds, reducing memory usage by ~200x.
|
||||
///
|
||||
/// For a world of size W x H blocks, the bitmap uses only (W * H) / 8 bytes.
|
||||
pub struct CoordinateBitmap {
|
||||
pub struct BuildingFootprintBitmap {
|
||||
/// The bitmap data, where each bit represents one (x, z) coordinate
|
||||
bits: Vec<u8>,
|
||||
/// Minimum x coordinate (offset for indexing)
|
||||
@@ -27,13 +27,12 @@ pub struct CoordinateBitmap {
|
||||
/// Width of the world (max_x - min_x + 1)
|
||||
width: usize,
|
||||
/// Height of the world (max_z - min_z + 1)
|
||||
#[allow(dead_code)]
|
||||
height: usize,
|
||||
/// Number of coordinates marked
|
||||
/// Number of coordinates marked as building footprints
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl CoordinateBitmap {
|
||||
impl BuildingFootprintBitmap {
|
||||
/// Creates a new empty bitmap covering the given world bounds.
|
||||
pub fn new(xzbbox: &XZBBox) -> Self {
|
||||
let min_x = xzbbox.min_x();
|
||||
@@ -45,7 +44,7 @@ impl CoordinateBitmap {
|
||||
// Calculate number of bytes needed (round up to nearest byte)
|
||||
let total_bits = width
|
||||
.checked_mul(height)
|
||||
.expect("CoordinateBitmap: world size too large (width * height overflowed)");
|
||||
.expect("BuildingFootprintBitmap: world size too large (width * height overflowed)");
|
||||
let num_bytes = total_bits.div_ceil(8);
|
||||
|
||||
Self {
|
||||
@@ -80,7 +79,7 @@ impl CoordinateBitmap {
|
||||
Some(local_z * self.width + local_x)
|
||||
}
|
||||
|
||||
/// Sets a coordinate.
|
||||
/// Sets a coordinate as part of a building footprint.
|
||||
#[inline]
|
||||
pub fn set(&mut self, x: i32, z: i32) {
|
||||
if let Some(bit_index) = self.coord_to_index(x, z) {
|
||||
@@ -97,7 +96,7 @@ impl CoordinateBitmap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a coordinate is set.
|
||||
/// Checks if a coordinate is part of a building footprint.
|
||||
#[inline]
|
||||
pub fn contains(&self, x: i32, z: i32) -> bool {
|
||||
if let Some(bit_index) = self.coord_to_index(x, z) {
|
||||
@@ -112,119 +111,12 @@ impl CoordinateBitmap {
|
||||
|
||||
/// Returns true if no coordinates are marked.
|
||||
#[must_use]
|
||||
#[allow(dead_code)]
|
||||
#[allow(dead_code)] // Standard API method for collection-like types
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.count == 0
|
||||
}
|
||||
|
||||
/// Returns the number of coordinates that are set.
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
|
||||
/// Counts how many coordinates from the given iterator are set in this bitmap.
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
pub fn count_contained<'a, I>(&self, coords: I) -> usize
|
||||
where
|
||||
I: Iterator<Item = &'a (i32, i32)>,
|
||||
{
|
||||
coords.filter(|(x, z)| self.contains(*x, *z)).count()
|
||||
}
|
||||
|
||||
/// Counts the number of set bits in a rectangular range.
|
||||
///
|
||||
/// This is optimized to iterate row-by-row and use `count_ones()` on bytes
|
||||
/// where possible, which is much faster than checking individual coordinates.
|
||||
///
|
||||
/// Returns `(urban_count, total_count)` for the given range.
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
pub fn count_in_range(&self, min_x: i32, min_z: i32, max_x: i32, max_z: i32) -> (usize, usize) {
|
||||
let mut urban_count = 0usize;
|
||||
let mut total_count = 0usize;
|
||||
|
||||
for z in min_z..=max_z {
|
||||
// Calculate local z coordinate
|
||||
let local_z = i64::from(z) - i64::from(self.min_z);
|
||||
if local_z < 0 || local_z >= self.height as i64 {
|
||||
// Row is out of bounds, still counts toward total
|
||||
total_count += (i64::from(max_x) - i64::from(min_x) + 1) as usize;
|
||||
continue;
|
||||
}
|
||||
let local_z = local_z as usize;
|
||||
|
||||
// Calculate x range in local coordinates
|
||||
let local_min_x = (i64::from(min_x) - i64::from(self.min_x)).max(0) as usize;
|
||||
let local_max_x =
|
||||
((i64::from(max_x) - i64::from(self.min_x)) as usize).min(self.width - 1);
|
||||
|
||||
// Count out-of-bounds x coordinates toward total
|
||||
let x_start_offset = (i64::from(self.min_x) - i64::from(min_x)).max(0) as usize;
|
||||
let x_end_offset = (i64::from(max_x) - i64::from(self.min_x) - (self.width as i64 - 1))
|
||||
.max(0) as usize;
|
||||
total_count += x_start_offset + x_end_offset;
|
||||
|
||||
if local_min_x > local_max_x {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process this row
|
||||
let row_start_bit = local_z * self.width + local_min_x;
|
||||
let row_end_bit = local_z * self.width + local_max_x;
|
||||
let num_bits = row_end_bit - row_start_bit + 1;
|
||||
total_count += num_bits;
|
||||
|
||||
// Count set bits using byte-wise popcount where possible
|
||||
let start_byte = row_start_bit / 8;
|
||||
let end_byte = row_end_bit / 8;
|
||||
let start_bit_in_byte = row_start_bit % 8;
|
||||
let end_bit_in_byte = row_end_bit % 8;
|
||||
|
||||
if start_byte == end_byte {
|
||||
// All bits are in the same byte
|
||||
let byte = self.bits[start_byte];
|
||||
// Create mask for bits from start_bit to end_bit (inclusive)
|
||||
let num_bits_in_mask = end_bit_in_byte - start_bit_in_byte + 1;
|
||||
let mask = if num_bits_in_mask >= 8 {
|
||||
0xFFu8
|
||||
} else {
|
||||
((1u16 << num_bits_in_mask) - 1) as u8
|
||||
};
|
||||
let masked = (byte >> start_bit_in_byte) & mask;
|
||||
urban_count += masked.count_ones() as usize;
|
||||
} else {
|
||||
// First partial byte
|
||||
let first_byte = self.bits[start_byte];
|
||||
let first_mask = !((1u8 << start_bit_in_byte) - 1); // bits from start_bit to 7
|
||||
urban_count += (first_byte & first_mask).count_ones() as usize;
|
||||
|
||||
// Full bytes in between
|
||||
for byte_idx in (start_byte + 1)..end_byte {
|
||||
urban_count += self.bits[byte_idx].count_ones() as usize;
|
||||
}
|
||||
|
||||
// Last partial byte
|
||||
let last_byte = self.bits[end_byte];
|
||||
// Handle case where end_bit_in_byte is 7 (would overflow 1u8 << 8)
|
||||
let last_mask = if end_bit_in_byte >= 7 {
|
||||
0xFFu8
|
||||
} else {
|
||||
(1u8 << (end_bit_in_byte + 1)) - 1
|
||||
};
|
||||
urban_count += (last_byte & last_mask).count_ones() as usize;
|
||||
}
|
||||
}
|
||||
|
||||
(urban_count, total_count)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type alias for building footprint bitmap (for backwards compatibility).
|
||||
pub type BuildingFootprintBitmap = CoordinateBitmap;
|
||||
|
||||
/// A cache of pre-computed flood fill results, keyed by element ID.
|
||||
pub struct FloodFillCache {
|
||||
/// Cached results: element_id -> filled coordinates
|
||||
@@ -341,8 +233,6 @@ impl FloodFillCache {
|
||||
// Highway areas (like pedestrian plazas) use flood fill when area=yes
|
||||
|| (way.tags.contains_key("highway")
|
||||
&& way.tags.get("area").map(|v| v == "yes").unwrap_or(false))
|
||||
// Historic tomb polygons (e.g. tomb=pyramid)
|
||||
|| way.tags.get("tomb").map(|v| v == "pyramid").unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Collects all building footprint coordinates from the pre-computed cache.
|
||||
@@ -371,10 +261,7 @@ impl FloodFillCache {
|
||||
}
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
let is_building = rel.tags.contains_key("building")
|
||||
|| rel.tags.contains_key("building:part")
|
||||
|| rel.tags.get("type").map(|t| t.as_str()) == Some("building");
|
||||
if is_building {
|
||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||
for member in &rel.members {
|
||||
// Only treat outer members as building footprints.
|
||||
// Inner members represent courtyards/holes where trees can spawn.
|
||||
@@ -395,67 +282,10 @@ impl FloodFillCache {
|
||||
footprints
|
||||
}
|
||||
|
||||
/// Collects centroids of all buildings from the pre-computed cache.
|
||||
///
|
||||
/// This is used for urban ground detection - building clusters are identified
|
||||
/// using their centroids, and a concave hull is computed around dense clusters
|
||||
/// to determine where city ground (smooth stone) should be placed.
|
||||
///
|
||||
/// Returns a vector of (x, z) centroid coordinates for all buildings.
|
||||
pub fn collect_building_centroids(&self, elements: &[ProcessedElement]) -> Vec<(i32, i32)> {
|
||||
let mut centroids = Vec::new();
|
||||
|
||||
for element in elements {
|
||||
match element {
|
||||
ProcessedElement::Way(way) => {
|
||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
||||
if let Some(cached) = self.way_cache.get(&way.id) {
|
||||
if let Some(centroid) = Self::compute_centroid(cached) {
|
||||
centroids.push(centroid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
let is_building = rel.tags.contains_key("building")
|
||||
|| rel.tags.contains_key("building:part")
|
||||
|| rel.tags.get("type").map(|t| t.as_str()) == Some("building");
|
||||
if is_building {
|
||||
// For building relations, compute centroid from outer ways
|
||||
let mut all_coords = Vec::new();
|
||||
for member in &rel.members {
|
||||
if member.role == ProcessedMemberRole::Outer {
|
||||
if let Some(cached) = self.way_cache.get(&member.way.id) {
|
||||
all_coords.extend(cached.iter().copied());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(centroid) = Self::compute_centroid(&all_coords) {
|
||||
centroids.push(centroid);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
centroids
|
||||
}
|
||||
|
||||
/// Computes the centroid of a set of coordinates.
|
||||
fn compute_centroid(coords: &[(i32, i32)]) -> Option<(i32, i32)> {
|
||||
if coords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let sum_x: i64 = coords.iter().map(|(x, _)| i64::from(*x)).sum();
|
||||
let sum_z: i64 = coords.iter().map(|(_, z)| i64::from(*z)).sum();
|
||||
let len = coords.len() as i64;
|
||||
Some(((sum_x / len) as i32, (sum_z / len) as i32))
|
||||
}
|
||||
|
||||
/// Removes a way's cached flood fill result, freeing memory.
|
||||
///
|
||||
/// Call this after processing an element to release its cached data.
|
||||
#[allow(dead_code)]
|
||||
pub fn remove_way(&mut self, way_id: u64) {
|
||||
self.way_cache.remove(&way_id);
|
||||
}
|
||||
@@ -463,6 +293,7 @@ impl FloodFillCache {
|
||||
/// Removes all cached flood fill results for ways in a relation.
|
||||
///
|
||||
/// Relations contain multiple ways, so we need to remove all of them.
|
||||
#[allow(dead_code)]
|
||||
pub fn remove_relation_ways(&mut self, way_ids: &[u64]) {
|
||||
for &id in way_ids {
|
||||
self.way_cache.remove(&id);
|
||||
|
||||
299
src/gui.rs
299
src/gui.rs
@@ -6,6 +6,7 @@ use crate::data_processing::{self, GenerationOptions};
|
||||
use crate::ground::{self, Ground};
|
||||
use crate::map_transformation;
|
||||
use crate::osm_parser;
|
||||
use crate::parallel_processing::ParallelConfig;
|
||||
use crate::progress::{self, emit_gui_progress_update};
|
||||
use crate::retrieve_data;
|
||||
use crate::telemetry::{self, send_log, LogLevel};
|
||||
@@ -62,6 +63,24 @@ impl Drop for SessionLock {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the Desktop directory for Bedrock .mcworld file output.
|
||||
fn get_bedrock_output_directory() -> PathBuf {
|
||||
dirs::desktop_dir()
|
||||
.or_else(dirs::home_dir)
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
}
|
||||
|
||||
/// Gets the area name for a given bounding box using the center point
|
||||
fn get_area_name_for_bedrock(bbox: &LLBBox) -> String {
|
||||
let center_lat = (bbox.min().lat() + bbox.max().lat()) / 2.0;
|
||||
let center_lon = (bbox.min().lng() + bbox.max().lng()) / 2.0;
|
||||
|
||||
match retrieve_data::fetch_area_name(center_lat, center_lon) {
|
||||
Ok(Some(name)) => name,
|
||||
_ => "Unknown Location".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_gui() {
|
||||
// Configure thread pool with 90% CPU cap to keep system responsive
|
||||
crate::floodfill_cache::configure_rayon_thread_pool(0.9);
|
||||
@@ -105,10 +124,7 @@ pub fn run_gui() {
|
||||
)
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
gui_create_world,
|
||||
gui_get_default_save_path,
|
||||
gui_set_save_path,
|
||||
gui_pick_save_directory,
|
||||
gui_select_world,
|
||||
gui_start_generation,
|
||||
gui_get_version,
|
||||
gui_check_for_updates,
|
||||
@@ -126,17 +142,15 @@ pub fn run_gui() {
|
||||
.expect("Error while starting the application UI (Tauri)");
|
||||
}
|
||||
|
||||
/// Detects the default Minecraft Java Edition saves directory for the current OS.
|
||||
/// Checks standard install paths including Flatpak on Linux.
|
||||
/// Falls back to Desktop, then current directory.
|
||||
fn detect_minecraft_saves_directory() -> PathBuf {
|
||||
// Try standard Minecraft saves directories per OS
|
||||
let mc_saves: Option<PathBuf> = if cfg!(target_os = "windows") {
|
||||
#[tauri::command]
|
||||
fn gui_select_world(generate_new: bool) -> Result<String, i32> {
|
||||
// Determine the default Minecraft 'saves' directory based on the OS
|
||||
let default_dir: Option<PathBuf> = if cfg!(target_os = "windows") {
|
||||
env::var("APPDATA")
|
||||
.ok()
|
||||
.map(|appdata| PathBuf::from(appdata).join(".minecraft").join("saves"))
|
||||
.map(|appdata: String| PathBuf::from(appdata).join(".minecraft").join("saves"))
|
||||
} else if cfg!(target_os = "macos") {
|
||||
dirs::home_dir().map(|home| {
|
||||
dirs::home_dir().map(|home: PathBuf| {
|
||||
home.join("Library/Application Support/minecraft")
|
||||
.join("saves")
|
||||
})
|
||||
@@ -153,75 +167,174 @@ fn detect_minecraft_saves_directory() -> PathBuf {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(saves_dir) = mc_saves {
|
||||
if saves_dir.exists() {
|
||||
return saves_dir;
|
||||
if generate_new {
|
||||
// Handle new world generation
|
||||
// Try Minecraft saves directory first, fall back to current directory
|
||||
let target_path = if let Some(default_path) = &default_dir {
|
||||
if default_path.exists() {
|
||||
default_path.clone()
|
||||
} else {
|
||||
// Minecraft directory doesn't exist, use current directory
|
||||
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
}
|
||||
} else {
|
||||
// No default directory configured, use current directory
|
||||
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
};
|
||||
|
||||
create_new_world(&target_path).map_err(|_| 3) // Error code 3: Failed to create new world
|
||||
} else {
|
||||
// Handle existing world selection
|
||||
// Open the directory picker dialog
|
||||
let dialog: FileDialog = FileDialog::new();
|
||||
let dialog: FileDialog = if let Some(start_dir) = default_dir.filter(|dir| dir.exists()) {
|
||||
dialog.set_directory(start_dir)
|
||||
} else {
|
||||
dialog
|
||||
};
|
||||
|
||||
if let Some(path) = dialog.pick_folder() {
|
||||
// Check if the "region" folder exists within the selected directory
|
||||
if path.join("region").exists() {
|
||||
// Check the 'session.lock' file
|
||||
let session_lock_path = path.join("session.lock");
|
||||
if session_lock_path.exists() {
|
||||
// Try to acquire a lock on the session.lock file
|
||||
if let Ok(file) = fs::File::open(&session_lock_path) {
|
||||
if fs2::FileExt::try_lock_shared(&file).is_err() {
|
||||
return Err(2); // Error code 2: The selected world is currently in use
|
||||
} else {
|
||||
// Release the lock immediately
|
||||
let _ = fs2::FileExt::unlock(&file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(path.display().to_string());
|
||||
} else {
|
||||
// No Minecraft directory found, generating new world in custom user selected directory
|
||||
return create_new_world(&path).map_err(|_| 3); // Error code 3: Failed to create new world
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to Desktop
|
||||
if let Some(desktop) = dirs::desktop_dir() {
|
||||
if desktop.exists() {
|
||||
return desktop;
|
||||
}
|
||||
// If no folder was selected, return an error message
|
||||
Err(4) // Error code 4: No world selected
|
||||
}
|
||||
|
||||
// Last resort: current directory
|
||||
env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
}
|
||||
|
||||
/// Returns the default save path (auto-detected on first run).
|
||||
/// The frontend stores/retrieves this via localStorage and passes it here for validation.
|
||||
#[tauri::command]
|
||||
fn gui_get_default_save_path() -> String {
|
||||
detect_minecraft_saves_directory().display().to_string()
|
||||
}
|
||||
|
||||
/// Validates and returns a user-provided save path.
|
||||
/// Returns the path string if valid, or an error message.
|
||||
#[tauri::command]
|
||||
fn gui_set_save_path(path: String) -> Result<String, String> {
|
||||
let p = PathBuf::from(&path);
|
||||
if !p.exists() {
|
||||
return Err("Path does not exist.".to_string());
|
||||
}
|
||||
if !p.is_dir() {
|
||||
return Err("Path is not a directory.".to_string());
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Opens a native folder-picker dialog and returns the chosen path.
|
||||
#[tauri::command]
|
||||
fn gui_pick_save_directory(start_path: String) -> Result<String, String> {
|
||||
let start = PathBuf::from(&start_path);
|
||||
let mut dialog = FileDialog::new();
|
||||
if start.is_dir() {
|
||||
dialog = dialog.set_directory(&start);
|
||||
}
|
||||
match dialog.pick_folder() {
|
||||
Some(folder) => Ok(folder.display().to_string()),
|
||||
None => Ok(start_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new Java Edition world in the given base save directory.
|
||||
/// Called when the user clicks "Create World".
|
||||
#[tauri::command]
|
||||
fn gui_create_world(save_path: String) -> Result<String, i32> {
|
||||
let trimmed = save_path.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(3);
|
||||
}
|
||||
let base = PathBuf::from(trimmed);
|
||||
if !base.is_dir() {
|
||||
return Err(3); // Error code 3: Failed to create new world
|
||||
}
|
||||
create_new_world(&base).map_err(|_| 3)
|
||||
}
|
||||
|
||||
fn create_new_world(base_path: &Path) -> Result<String, String> {
|
||||
crate::world_utils::create_new_world(base_path)
|
||||
// Generate a unique world name with proper counter
|
||||
// Check for both "Arnis World X" and "Arnis World X: Location" patterns
|
||||
let mut counter: i32 = 1;
|
||||
let unique_name: String = loop {
|
||||
let candidate_name: String = format!("Arnis World {counter}");
|
||||
let candidate_path: PathBuf = base_path.join(&candidate_name);
|
||||
|
||||
// Check for exact match (no location suffix)
|
||||
let exact_match_exists = candidate_path.exists();
|
||||
|
||||
// Check for worlds with location suffix (Arnis World X: Location)
|
||||
let location_pattern = format!("Arnis World {counter}: ");
|
||||
let location_match_exists = fs::read_dir(base_path)
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.any(|name| name.starts_with(&location_pattern))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if !exact_match_exists && !location_match_exists {
|
||||
break candidate_name;
|
||||
}
|
||||
counter += 1;
|
||||
};
|
||||
|
||||
let new_world_path: PathBuf = base_path.join(&unique_name);
|
||||
|
||||
// Create the new world directory structure
|
||||
fs::create_dir_all(new_world_path.join("region"))
|
||||
.map_err(|e| format!("Failed to create world directory: {e}"))?;
|
||||
|
||||
// Copy the region template file
|
||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/region.template");
|
||||
let region_path = new_world_path.join("region").join("r.0.0.mca");
|
||||
fs::write(®ion_path, REGION_TEMPLATE)
|
||||
.map_err(|e| format!("Failed to create region file: {e}"))?;
|
||||
|
||||
// Add the level.dat file
|
||||
const LEVEL_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/level.dat");
|
||||
|
||||
// Decompress the gzipped level.template
|
||||
let mut decoder = GzDecoder::new(LEVEL_TEMPLATE);
|
||||
let mut decompressed_data = Vec::new();
|
||||
decoder
|
||||
.read_to_end(&mut decompressed_data)
|
||||
.map_err(|e| format!("Failed to decompress level.template: {e}"))?;
|
||||
|
||||
// Parse the decompressed NBT data
|
||||
let mut level_data: Value = fastnbt::from_bytes(&decompressed_data)
|
||||
.map_err(|e| format!("Failed to parse level.dat template: {e}"))?;
|
||||
|
||||
// Modify the LevelName, LastPlayed and player position fields
|
||||
if let Value::Compound(ref mut root) = level_data {
|
||||
if let Some(Value::Compound(ref mut data)) = root.get_mut("Data") {
|
||||
// Update LevelName
|
||||
data.insert("LevelName".to_string(), Value::String(unique_name.clone()));
|
||||
|
||||
// Update LastPlayed to the current Unix time in milliseconds
|
||||
let current_time = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_err(|e| format!("Failed to get current time: {e}"))?;
|
||||
let current_time_millis = current_time.as_millis() as i64;
|
||||
data.insert("LastPlayed".to_string(), Value::Long(current_time_millis));
|
||||
|
||||
// Update player position and rotation
|
||||
if let Some(Value::Compound(ref mut player)) = data.get_mut("Player") {
|
||||
if let Some(Value::List(ref mut pos)) = player.get_mut("Pos") {
|
||||
if let Value::Double(ref mut x) = pos.get_mut(0).unwrap() {
|
||||
*x = -5.0;
|
||||
}
|
||||
if let Value::Double(ref mut y) = pos.get_mut(1).unwrap() {
|
||||
*y = -61.0;
|
||||
}
|
||||
if let Value::Double(ref mut z) = pos.get_mut(2).unwrap() {
|
||||
*z = -5.0;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Value::List(ref mut rot)) = player.get_mut("Rotation") {
|
||||
if let Value::Float(ref mut x) = rot.get_mut(0).unwrap() {
|
||||
*x = -45.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the updated NBT data back to bytes
|
||||
let serialized_level_data: Vec<u8> = fastnbt::to_bytes(&level_data)
|
||||
.map_err(|e| format!("Failed to serialize updated level.dat: {e}"))?;
|
||||
|
||||
// Compress the serialized data back to gzip
|
||||
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
encoder
|
||||
.write_all(&serialized_level_data)
|
||||
.map_err(|e| format!("Failed to compress updated level.dat: {e}"))?;
|
||||
let compressed_level_data = encoder
|
||||
.finish()
|
||||
.map_err(|e| format!("Failed to finalize compression for level.dat: {e}"))?;
|
||||
|
||||
// Write the level.dat file
|
||||
fs::write(new_world_path.join("level.dat"), compressed_level_data)
|
||||
.map_err(|e| format!("Failed to create level.dat file: {e}"))?;
|
||||
|
||||
// Add the icon.png file
|
||||
const ICON_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/icon.png");
|
||||
fs::write(new_world_path.join("icon.png"), ICON_TEMPLATE)
|
||||
.map_err(|e| format!("Failed to create icon.png file: {e}"))?;
|
||||
|
||||
Ok(new_world_path.display().to_string())
|
||||
}
|
||||
|
||||
/// Adds localized area name to the world name in level.dat
|
||||
@@ -605,22 +718,12 @@ fn gui_get_world_map_data(world_path: String) -> Result<Option<WorldMapData>, St
|
||||
.as_f64()
|
||||
.ok_or("Missing maxGeoLon in metadata")?;
|
||||
|
||||
// Extract Minecraft coordinate bounds
|
||||
let min_mc_x = metadata["minMcX"].as_i64().unwrap_or(0) as i32;
|
||||
let max_mc_x = metadata["maxMcX"].as_i64().unwrap_or(0) as i32;
|
||||
let min_mc_z = metadata["minMcZ"].as_i64().unwrap_or(0) as i32;
|
||||
let max_mc_z = metadata["maxMcZ"].as_i64().unwrap_or(0) as i32;
|
||||
|
||||
Ok(Some(WorldMapData {
|
||||
image_base64: format!("data:image/png;base64,{}", base64_image),
|
||||
min_lat,
|
||||
max_lat,
|
||||
min_lon,
|
||||
max_lon,
|
||||
min_mc_x,
|
||||
max_mc_x,
|
||||
min_mc_z,
|
||||
max_mc_z,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -632,11 +735,6 @@ struct WorldMapData {
|
||||
max_lat: f64,
|
||||
min_lon: f64,
|
||||
max_lon: f64,
|
||||
// Minecraft coordinate bounds for coordinate copying
|
||||
min_mc_x: i32,
|
||||
max_mc_x: i32,
|
||||
min_mc_z: i32,
|
||||
max_mc_z: i32,
|
||||
}
|
||||
|
||||
/// Opens the file with default application (Windows) or shows in file explorer (macOS/Linux)
|
||||
@@ -703,7 +801,6 @@ fn gui_start_generation(
|
||||
interior_enabled: bool,
|
||||
roof_enabled: bool,
|
||||
fillground_enabled: bool,
|
||||
city_boundaries_enabled: bool,
|
||||
is_new_world: bool,
|
||||
spawn_point: Option<(f64, f64)>,
|
||||
telemetry_consent: bool,
|
||||
@@ -837,9 +934,11 @@ fn gui_start_generation(
|
||||
}
|
||||
WorldFormat::BedrockMcWorld => {
|
||||
// Bedrock: generate .mcworld on Desktop with location-based name
|
||||
let output_dir = crate::world_utils::get_bedrock_output_directory();
|
||||
let (output_path, lvl_name) =
|
||||
crate::world_utils::build_bedrock_output(&bbox, output_dir);
|
||||
let area_name = get_area_name_for_bedrock(&bbox);
|
||||
let filename = format!("Arnis {}.mcworld", area_name);
|
||||
let lvl_name = format!("Arnis World: {}", area_name);
|
||||
|
||||
let output_path = get_bedrock_output_directory().join(&filename);
|
||||
(output_path, Some(lvl_name))
|
||||
}
|
||||
};
|
||||
@@ -882,12 +981,11 @@ fn gui_start_generation(
|
||||
bbox,
|
||||
file: None,
|
||||
save_json_file: None,
|
||||
path: Some(if world_format == WorldFormat::JavaAnvil {
|
||||
path: if world_format == WorldFormat::JavaAnvil {
|
||||
generation_path
|
||||
} else {
|
||||
world_path
|
||||
}),
|
||||
bedrock: world_format == WorldFormat::BedrockMcWorld,
|
||||
},
|
||||
downloader: "requests".to_string(),
|
||||
scale: world_scale,
|
||||
ground_level,
|
||||
@@ -895,9 +993,12 @@ fn gui_start_generation(
|
||||
interior: interior_enabled,
|
||||
roof: roof_enabled,
|
||||
fillground: fillground_enabled,
|
||||
city_boundaries: city_boundaries_enabled,
|
||||
debug: false,
|
||||
timeout: Some(std::time::Duration::from_secs(40)),
|
||||
threads: 0, // Auto-detect thread count
|
||||
region_batch_size: 2, // Four regions per unit (default)
|
||||
no_parallel: true, // Use sequential processing (parallel has bugs)
|
||||
force_parallel: false,
|
||||
};
|
||||
|
||||
// If skip_osm_objects is true (terrain-only mode), skip fetching and processing OSM data
|
||||
@@ -918,6 +1019,7 @@ fn gui_start_generation(
|
||||
ground,
|
||||
&args,
|
||||
generation_options.clone(),
|
||||
ParallelConfig::default(), // Use parallel processing
|
||||
);
|
||||
// Explicitly release session lock before showing Done message
|
||||
// so Minecraft can open the world immediately
|
||||
@@ -971,6 +1073,7 @@ fn gui_start_generation(
|
||||
ground,
|
||||
&args,
|
||||
generation_options.clone(),
|
||||
ParallelConfig::default(), // Use parallel processing
|
||||
);
|
||||
// Explicitly release session lock before showing Done message
|
||||
// so Minecraft can open the world immediately
|
||||
|
||||
40
src/gui/css/bbox.css
vendored
40
src/gui/css/bbox.css
vendored
@@ -375,42 +375,4 @@ body,
|
||||
accent-color: #3887BE;
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Context menu for coordinate copying */
|
||||
.coordinate-context-menu {
|
||||
position: fixed;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
z-index: 10000;
|
||||
min-width: 160px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.coordinate-context-menu-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.coordinate-context-menu-item:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.coordinate-context-menu-item svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.coordinate-context-menu-separator {
|
||||
height: 1px;
|
||||
background: #e0e0e0;
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
42
src/gui/css/styles.css
vendored
42
src/gui/css/styles.css
vendored
@@ -417,10 +417,6 @@ button:hover {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
#city-boundaries-toggle {
|
||||
accent-color: #fecc44;
|
||||
}
|
||||
|
||||
#telemetry-toggle {
|
||||
accent-color: #fecc44;
|
||||
}
|
||||
@@ -578,44 +574,6 @@ button:hover {
|
||||
border: 1px solid #fecc44;
|
||||
}
|
||||
|
||||
/* Save Path Setting */
|
||||
.save-path-control {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.save-path-input {
|
||||
max-width: 200px !important;
|
||||
font-size: 0.85em;
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.save-path-browse {
|
||||
background: none;
|
||||
border: 1px solid #fecc44;
|
||||
border-radius: 0 4px 4px 0;
|
||||
padding: 0 6px;
|
||||
margin-top: 0;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
justify-content: center;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.save-path-browse:hover {
|
||||
background: rgba(254, 204, 68, 0.15);
|
||||
}
|
||||
|
||||
.save-path-browse svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: #fecc44;
|
||||
}
|
||||
|
||||
.license-button-row {
|
||||
justify-content: center;
|
||||
margin-top: 5px;
|
||||
|
||||
42
src/gui/index.html
vendored
42
src/gui/index.html
vendored
@@ -32,11 +32,11 @@
|
||||
<!-- World Selection Container -->
|
||||
<div class="world-selection-container">
|
||||
<div class="tooltip" style="width: 100%;">
|
||||
<button type="button" id="choose-world-btn" onclick="createWorld()" class="choose-world-btn">
|
||||
<span id="choose_world">Create World</span>
|
||||
<button type="button" id="choose-world-btn" onclick="openWorldPicker()" class="choose-world-btn">
|
||||
<span id="choose_world">Choose World</span>
|
||||
<br>
|
||||
<span id="selected-world" style="font-size: 0.8em; color: #fecc44; display: block; margin-top: 4px;" data-localize="no_world_selected">
|
||||
No world created
|
||||
No world selected
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -73,6 +73,17 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- World Picker Modal -->
|
||||
<div id="world-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<span class="close-button" onclick="closeWorldPicker()">×</span>
|
||||
<h2 data-localize="choose_world_modal_title">Choose World</h2>
|
||||
|
||||
<button type="button" id="select-world-button" class="select-world-button" onclick="selectWorld(false)" data-localize="select_existing_world">Select existing world</button>
|
||||
<button type="button" id="generate-world-button" class="generate-world-button" onclick="selectWorld(true)" data-localize="generate_new_world">Generate new world</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div id="settings-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
@@ -127,17 +138,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- City Ground Toggle Button -->
|
||||
<div class="settings-row">
|
||||
<label for="city-boundaries-toggle">
|
||||
<span data-localize="city_boundaries">City Ground</span>
|
||||
<span class="tooltip-icon" data-tooltip="Detect urban areas and place smooth stone ground where cities are located.">?</span>
|
||||
</label>
|
||||
<div class="settings-control">
|
||||
<input type="checkbox" id="city-boundaries-toggle" name="city-boundaries-toggle" checked>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- World Scale Slider -->
|
||||
<div class="settings-row">
|
||||
<label for="scale-value-slider">
|
||||
@@ -178,20 +178,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Path -->
|
||||
<div class="settings-row">
|
||||
<label for="save-path-input">
|
||||
<span data-localize="save_path">Save Path</span>
|
||||
<span class="tooltip-icon" data-tooltip="Directory where new Minecraft Java worlds are created">?</span>
|
||||
</label>
|
||||
<div class="settings-control save-path-control">
|
||||
<input type="text" id="save-path-input" name="save-path-input" class="save-path-input" placeholder="Minecraft saves directory">
|
||||
<button type="button" id="save-path-browse" class="save-path-browse" title="Browse...">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Language Selector -->
|
||||
<div class="settings-row">
|
||||
<label for="language-select">
|
||||
|
||||
182
src/gui/js/bbox.js
vendored
182
src/gui/js/bbox.js
vendored
@@ -749,188 +749,6 @@ $(document).ready(function () {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Context Menu for Coordinate Copying ==========
|
||||
var contextMenuElement = null;
|
||||
|
||||
// Create the context menu element
|
||||
function createContextMenu() {
|
||||
if (contextMenuElement) return contextMenuElement;
|
||||
|
||||
contextMenuElement = document.createElement('div');
|
||||
contextMenuElement.className = 'coordinate-context-menu';
|
||||
contextMenuElement.style.display = 'none';
|
||||
contextMenuElement.innerHTML = `
|
||||
<div class="coordinate-context-menu-item" id="copy-coords-item">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
<span id="copy-coords-text">Copy coordinates</span>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(contextMenuElement);
|
||||
|
||||
// Handle click on the copy coordinates item
|
||||
var copyItem = contextMenuElement.querySelector('#copy-coords-item');
|
||||
copyItem.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
copyMinecraftCoordinates();
|
||||
hideContextMenu();
|
||||
});
|
||||
|
||||
return contextMenuElement;
|
||||
}
|
||||
|
||||
// Show context menu at position
|
||||
function showContextMenu(x, y, latLng) {
|
||||
if (!worldPreviewAvailable || !worldOverlayData) return;
|
||||
|
||||
var menu = createContextMenu();
|
||||
|
||||
// Position the menu, ensuring it stays within viewport
|
||||
var menuWidth = 180;
|
||||
var menuHeight = 40;
|
||||
var viewportWidth = window.innerWidth;
|
||||
var viewportHeight = window.innerHeight;
|
||||
|
||||
var posX = x;
|
||||
var posY = y;
|
||||
|
||||
// Adjust if menu would go off-screen
|
||||
if (x + menuWidth > viewportWidth) {
|
||||
posX = viewportWidth - menuWidth - 10;
|
||||
}
|
||||
if (y + menuHeight > viewportHeight) {
|
||||
posY = viewportHeight - menuHeight - 10;
|
||||
}
|
||||
|
||||
menu.style.left = posX + 'px';
|
||||
menu.style.top = posY + 'px';
|
||||
menu.style.display = 'block';
|
||||
|
||||
// Store the latLng for copying
|
||||
menu.dataset.lat = latLng.lat;
|
||||
menu.dataset.lng = latLng.lng;
|
||||
}
|
||||
|
||||
// Hide context menu
|
||||
function hideContextMenu() {
|
||||
if (contextMenuElement) {
|
||||
contextMenuElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate Minecraft coordinates from lat/lng
|
||||
function calculateMinecraftCoords(lat, lng) {
|
||||
if (!worldOverlayData) return null;
|
||||
|
||||
var data = worldOverlayData;
|
||||
|
||||
// Check if Minecraft coordinate bounds are available (not all zeros)
|
||||
if (data.min_mc_x === 0 && data.max_mc_x === 0 &&
|
||||
data.min_mc_z === 0 && data.max_mc_z === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate the relative position within the geo bounds (0 to 1)
|
||||
// Note: Latitude increases northward, but Minecraft Z increases southward
|
||||
var relX = (lng - data.min_lon) / (data.max_lon - data.min_lon);
|
||||
var relZ = (data.max_lat - lat) / (data.max_lat - data.min_lat);
|
||||
|
||||
// Clamp to 0-1 range
|
||||
relX = Math.max(0, Math.min(1, relX));
|
||||
relZ = Math.max(0, Math.min(1, relZ));
|
||||
|
||||
// Calculate Minecraft X and Z coordinates
|
||||
var mcX = Math.round(data.min_mc_x + relX * (data.max_mc_x - data.min_mc_x));
|
||||
var mcZ = Math.round(data.min_mc_z + relZ * (data.max_mc_z - data.min_mc_z));
|
||||
|
||||
// Default Y coordinate (ground level, typically around 64-70)
|
||||
var mcY = 100;
|
||||
|
||||
return { x: mcX, y: mcY, z: mcZ };
|
||||
}
|
||||
|
||||
// Copy Minecraft coordinates to clipboard
|
||||
function copyMinecraftCoordinates() {
|
||||
if (!contextMenuElement) return;
|
||||
|
||||
var lat = parseFloat(contextMenuElement.dataset.lat);
|
||||
var lng = parseFloat(contextMenuElement.dataset.lng);
|
||||
|
||||
var coords = calculateMinecraftCoords(lat, lng);
|
||||
if (!coords) return;
|
||||
|
||||
var tpCommand = '/tp ' + coords.x + ' ' + coords.y + ' ' + coords.z;
|
||||
|
||||
// Copy to clipboard using modern API with fallback
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(tpCommand).catch(function(err) {
|
||||
// Fallback for clipboard API failure
|
||||
fallbackCopyToClipboard(tpCommand);
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
fallbackCopyToClipboard(tpCommand);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback clipboard copy method for older browsers
|
||||
function fallbackCopyToClipboard(text) {
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-9999px';
|
||||
textArea.style.top = '-9999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy coordinates:', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
// Check if Minecraft coordinate bounds are available
|
||||
function hasMinecraftCoords() {
|
||||
if (!worldOverlayData) return false;
|
||||
var data = worldOverlayData;
|
||||
return !(data.min_mc_x === 0 && data.max_mc_x === 0 &&
|
||||
data.min_mc_z === 0 && data.max_mc_z === 0);
|
||||
}
|
||||
|
||||
// Handle right-click on the map
|
||||
map.on('contextmenu', function(e) {
|
||||
// Only show context menu if world preview is available and has Minecraft coords
|
||||
if (worldPreviewAvailable && worldOverlayData && hasMinecraftCoords()) {
|
||||
// Check if the click is within the world bounds
|
||||
var data = worldOverlayData;
|
||||
var lat = e.latlng.lat;
|
||||
var lng = e.latlng.lng;
|
||||
|
||||
if (lat >= data.min_lat && lat <= data.max_lat &&
|
||||
lng >= data.min_lon && lng <= data.max_lon) {
|
||||
showContextMenu(e.originalEvent.clientX, e.originalEvent.clientY, e.latlng);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Hide context menu on any click or map interaction
|
||||
document.addEventListener('click', function(e) {
|
||||
if (contextMenuElement && !contextMenuElement.contains(e.target)) {
|
||||
hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
map.on('movestart', hideContextMenu);
|
||||
map.on('zoomstart', hideContextMenu);
|
||||
// ========== End Context Menu ==========
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data && event.data.type === 'changeTileTheme') {
|
||||
|
||||
197
src/gui/js/main.js
vendored
197
src/gui/js/main.js
vendored
@@ -34,11 +34,11 @@ async function setBboxSelectionInfo(bboxSelectionElement, localizationKey, color
|
||||
// Initialize elements and start the demo progress
|
||||
window.addEventListener("DOMContentLoaded", async () => {
|
||||
registerMessageEvent();
|
||||
window.createWorld = createWorld;
|
||||
window.selectWorld = selectWorld;
|
||||
window.startGeneration = startGeneration;
|
||||
setupProgressListener();
|
||||
await initSavePath();
|
||||
initSettings();
|
||||
initWorldPicker();
|
||||
initTelemetryConsent();
|
||||
handleBboxInput();
|
||||
const localization = await getLocalization();
|
||||
@@ -97,26 +97,27 @@ async function localizeElement(json, elementObject, localizedStringKey) {
|
||||
|
||||
async function applyLocalization(localization) {
|
||||
const localizationElements = {
|
||||
"span[id='choose_world']": "create_world",
|
||||
"span[id='choose_world']": "choose_world",
|
||||
"#selected-world": "no_world_selected",
|
||||
"#start-button": "start_generation",
|
||||
"h2[data-localize='choose_world_modal_title']": "choose_world_modal_title",
|
||||
"button[data-localize='select_existing_world']": "select_existing_world",
|
||||
"button[data-localize='generate_new_world']": "generate_new_world",
|
||||
"h2[data-localize='customization_settings']": "customization_settings",
|
||||
"span[data-localize='world_scale']": "world_scale",
|
||||
"span[data-localize='custom_bounding_box']": "custom_bounding_box",
|
||||
"label[data-localize='world_scale']": "world_scale",
|
||||
"label[data-localize='custom_bounding_box']": "custom_bounding_box",
|
||||
// DEPRECATED: Ground level localization removed
|
||||
// "label[data-localize='ground_level']": "ground_level",
|
||||
"span[data-localize='language']": "language",
|
||||
"span[data-localize='generation_mode']": "generation_mode",
|
||||
"label[data-localize='language']": "language",
|
||||
"label[data-localize='generation_mode']": "generation_mode",
|
||||
"option[data-localize='mode_geo_terrain']": "mode_geo_terrain",
|
||||
"option[data-localize='mode_geo_only']": "mode_geo_only",
|
||||
"option[data-localize='mode_terrain_only']": "mode_terrain_only",
|
||||
"span[data-localize='terrain']": "terrain",
|
||||
"span[data-localize='interior']": "interior",
|
||||
"span[data-localize='roof']": "roof",
|
||||
"span[data-localize='fillground']": "fillground",
|
||||
"span[data-localize='city_boundaries']": "city_boundaries",
|
||||
"span[data-localize='map_theme']": "map_theme",
|
||||
"span[data-localize='save_path']": "save_path",
|
||||
"label[data-localize='terrain']": "terrain",
|
||||
"label[data-localize='interior']": "interior",
|
||||
"label[data-localize='roof']": "roof",
|
||||
"label[data-localize='fillground']": "fillground",
|
||||
"label[data-localize='map_theme']": "map_theme",
|
||||
".footer-link": "footer_text",
|
||||
"button[data-localize='license_and_credits']": "license_and_credits",
|
||||
"h2[data-localize='license_and_credits']": "license_and_credits",
|
||||
@@ -296,9 +297,6 @@ function initSettings() {
|
||||
// World format toggle (Java/Bedrock)
|
||||
initWorldFormatToggle();
|
||||
|
||||
// Save path setting
|
||||
initSavePathSetting();
|
||||
|
||||
// Language selector
|
||||
const languageSelect = document.getElementById("language-select");
|
||||
const availableOptions = Array.from(languageSelect.options).map(opt => opt.value);
|
||||
@@ -337,14 +335,6 @@ function initSettings() {
|
||||
// Reload localization with the new language
|
||||
const localization = await fetchLanguage(selectedLanguage);
|
||||
await applyLocalization(localization);
|
||||
|
||||
// Restore correct #selected-world text after localization overwrites it
|
||||
updateFormatToggleUI(selectedWorldFormat);
|
||||
// If a world was already created, show its name
|
||||
if (worldPath) {
|
||||
const lastSegment = worldPath.split(/[\\/]/).pop();
|
||||
document.getElementById('selected-world').textContent = lastSegment;
|
||||
}
|
||||
});
|
||||
|
||||
// Tile theme selector
|
||||
@@ -440,22 +430,22 @@ function updateFormatToggleUI(format) {
|
||||
if (format === 'java') {
|
||||
javaBtn.classList.add('format-active');
|
||||
bedrockBtn.classList.remove('format-active');
|
||||
// Enable Create World button for Java
|
||||
// Enable Choose World button for Java
|
||||
if (chooseWorldBtn) {
|
||||
chooseWorldBtn.disabled = false;
|
||||
chooseWorldBtn.style.opacity = '1';
|
||||
chooseWorldBtn.style.cursor = 'pointer';
|
||||
}
|
||||
// Show appropriate text based on whether a world was already created
|
||||
if (selectedWorldText && !worldPath) {
|
||||
const noWorldText = window.localization?.no_world_selected || 'No world created';
|
||||
// Show default text (world was cleared when switching to Bedrock)
|
||||
if (selectedWorldText) {
|
||||
const noWorldText = window.localization?.no_world_selected || 'No world selected';
|
||||
selectedWorldText.textContent = noWorldText;
|
||||
selectedWorldText.style.color = '#fecc44';
|
||||
}
|
||||
} else {
|
||||
javaBtn.classList.remove('format-active');
|
||||
bedrockBtn.classList.add('format-active');
|
||||
// Disable Create World button for Bedrock
|
||||
// Disable Choose World button for Bedrock and clear any selected world
|
||||
if (chooseWorldBtn) {
|
||||
chooseWorldBtn.disabled = true;
|
||||
chooseWorldBtn.style.opacity = '0.5';
|
||||
@@ -463,8 +453,9 @@ function updateFormatToggleUI(format) {
|
||||
}
|
||||
// Clear world selection and show Bedrock info message
|
||||
worldPath = "";
|
||||
isNewWorld = false;
|
||||
if (selectedWorldText) {
|
||||
const bedrockText = window.localization?.bedrock_auto_generated || 'Bedrock world is auto-generated';
|
||||
const bedrockText = window.localization?.bedrock_use_java || 'Use Java to select worlds';
|
||||
selectedWorldText.textContent = bedrockText;
|
||||
selectedWorldText.style.color = '#fecc44';
|
||||
}
|
||||
@@ -517,86 +508,24 @@ function initTelemetryConsent() {
|
||||
};
|
||||
}
|
||||
|
||||
/// Save path management
|
||||
let savePath = "";
|
||||
function initWorldPicker() {
|
||||
// World Picker
|
||||
const worldPickerModal = document.getElementById("world-modal");
|
||||
|
||||
async function initSavePath() {
|
||||
// Check if user has a saved path in localStorage
|
||||
const saved = localStorage.getItem('arnis-save-path');
|
||||
if (saved) {
|
||||
// Validate the saved path still exists (handles upgrades / moved directories)
|
||||
try {
|
||||
const normalized = await invoke('gui_set_save_path', { path: saved });
|
||||
savePath = normalized;
|
||||
localStorage.setItem('arnis-save-path', savePath);
|
||||
} catch (_) {
|
||||
// Saved path is no longer valid – re-detect
|
||||
console.warn("Stored save path no longer valid, re-detecting...");
|
||||
localStorage.removeItem('arnis-save-path');
|
||||
try {
|
||||
savePath = await invoke('gui_get_default_save_path');
|
||||
localStorage.setItem('arnis-save-path', savePath);
|
||||
} catch (error) {
|
||||
console.error("Failed to detect save path:", error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Auto-detect on first run
|
||||
try {
|
||||
savePath = await invoke('gui_get_default_save_path');
|
||||
localStorage.setItem('arnis-save-path', savePath);
|
||||
} catch (error) {
|
||||
console.error("Failed to detect save path:", error);
|
||||
}
|
||||
// Open world picker modal
|
||||
function openWorldPicker() {
|
||||
worldPickerModal.style.display = "flex";
|
||||
worldPickerModal.style.justifyContent = "center";
|
||||
worldPickerModal.style.alignItems = "center";
|
||||
}
|
||||
|
||||
// Populate the save path input in settings
|
||||
const savePathInput = document.getElementById('save-path-input');
|
||||
if (savePathInput) {
|
||||
savePathInput.value = savePath;
|
||||
// Close world picker modal
|
||||
function closeWorldPicker() {
|
||||
worldPickerModal.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function initSavePathSetting() {
|
||||
const savePathInput = document.getElementById('save-path-input');
|
||||
if (!savePathInput) return;
|
||||
|
||||
savePathInput.value = savePath;
|
||||
|
||||
// Manual text input – validate on change, revert if invalid
|
||||
savePathInput.addEventListener('change', async () => {
|
||||
const newPath = savePathInput.value.trim();
|
||||
if (!newPath) {
|
||||
savePathInput.value = savePath;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const validated = await invoke('gui_set_save_path', { path: newPath });
|
||||
savePath = validated;
|
||||
localStorage.setItem('arnis-save-path', savePath);
|
||||
} catch (_) {
|
||||
// Invalid path – silently revert to previous value
|
||||
savePathInput.value = savePath;
|
||||
}
|
||||
});
|
||||
|
||||
// Folder picker button
|
||||
const browseBtn = document.getElementById('save-path-browse');
|
||||
if (browseBtn) {
|
||||
browseBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
const picked = await invoke('gui_pick_save_directory', { startPath: savePath });
|
||||
if (picked) {
|
||||
savePath = picked;
|
||||
savePathInput.value = savePath;
|
||||
localStorage.setItem('arnis-save-path', savePath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Folder picker failed:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
window.openWorldPicker = openWorldPicker;
|
||||
window.closeWorldPicker = closeWorldPicker;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -781,31 +710,57 @@ function displayBboxInfoText(bboxText) {
|
||||
}
|
||||
|
||||
let worldPath = "";
|
||||
let isNewWorld = false;
|
||||
|
||||
async function createWorld() {
|
||||
// Don't create if format is Bedrock (button should be disabled)
|
||||
if (selectedWorldFormat === 'bedrock') return;
|
||||
|
||||
// Don't create if save path hasn't been initialized
|
||||
if (!savePath) {
|
||||
console.warn("Cannot create world: save path not set");
|
||||
return;
|
||||
}
|
||||
|
||||
async function selectWorld(generate_new_world) {
|
||||
try {
|
||||
const worldName = await invoke('gui_create_world', { savePath: savePath });
|
||||
const worldName = await invoke('gui_select_world', { generateNew: generate_new_world });
|
||||
if (worldName) {
|
||||
worldPath = worldName;
|
||||
isNewWorld = generate_new_world;
|
||||
const lastSegment = worldName.split(/[\\/]/).pop();
|
||||
document.getElementById('selected-world').textContent = lastSegment;
|
||||
document.getElementById('selected-world').style.color = "#fecc44";
|
||||
|
||||
// Notify that world changed (reset preview)
|
||||
notifyWorldChanged();
|
||||
|
||||
// If selecting an existing world, check for existing map data
|
||||
if (!generate_new_world) {
|
||||
await loadExistingWorldMapData();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
handleWorldSelectionError(error);
|
||||
}
|
||||
|
||||
closeWorldPicker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads existing world map data if available (for existing worlds)
|
||||
* This will zoom to the location and auto-enable the preview
|
||||
*/
|
||||
async function loadExistingWorldMapData() {
|
||||
if (!worldPath) return;
|
||||
|
||||
try {
|
||||
const mapData = await invoke('gui_get_world_map_data', { worldPath: worldPath });
|
||||
if (mapData) {
|
||||
currentWorldMapData = mapData;
|
||||
|
||||
// Send data to the map iframe with instruction to zoom and auto-enable
|
||||
const mapFrame = document.querySelector('.map-container');
|
||||
if (mapFrame && mapFrame.contentWindow) {
|
||||
mapFrame.contentWindow.postMessage({
|
||||
type: 'loadExistingWorldMap',
|
||||
data: mapData
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("No existing world map data found:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -846,10 +801,10 @@ async function startGeneration() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only require world creation for Java format (Bedrock generates a new .mcworld file)
|
||||
// Only require world selection for Java format (Bedrock generates a new .mcworld file)
|
||||
if (selectedWorldFormat === 'java' && (!worldPath || worldPath === "")) {
|
||||
const selectedWorld = document.getElementById('selected-world');
|
||||
localizeElement(window.localization, { element: selectedWorld }, "create_world_first");
|
||||
localizeElement(window.localization, { element: selectedWorld }, "select_minecraft_world_first");
|
||||
selectedWorld.style.color = "#fa7878";
|
||||
return;
|
||||
}
|
||||
@@ -877,7 +832,6 @@ async function startGeneration() {
|
||||
var interior = document.getElementById("interior-toggle").checked;
|
||||
var roof = document.getElementById("roof-toggle").checked;
|
||||
var fill_ground = document.getElementById("fillground-toggle").checked;
|
||||
var city_boundaries = document.getElementById("city-boundaries-toggle").checked;
|
||||
var scale = parseFloat(document.getElementById("scale-value-slider").value);
|
||||
// var ground_level = parseInt(document.getElementById("ground-level").value, 10);
|
||||
// DEPRECATED: Ground level input removed from UI
|
||||
@@ -900,8 +854,7 @@ async function startGeneration() {
|
||||
interiorEnabled: interior,
|
||||
roofEnabled: roof,
|
||||
fillgroundEnabled: fill_ground,
|
||||
cityBoundariesEnabled: city_boundaries,
|
||||
isNewWorld: true,
|
||||
isNewWorld: isNewWorld,
|
||||
spawnPoint: spawnPoint,
|
||||
telemetryConsent: telemetryConsent || false,
|
||||
worldFormat: selectedWorldFormat
|
||||
|
||||
4
src/gui/js/maps/wkt.parser.js
vendored
4
src/gui/js/maps/wkt.parser.js
vendored
@@ -195,7 +195,11 @@ Wkt.Wkt.prototype.toObject = function (config) {
|
||||
* Absorbs the geometry of another Wkt.Wkt instance, merging it with its own,
|
||||
* creating a collection (MULTI-geometry) based on their types, which must agree.
|
||||
* For example, creates a MULTIPOLYGON from a POLYGON type merged with another
|
||||
<<<<<<< HEAD
|
||||
* POLYGON type.
|
||||
=======
|
||||
* POLYGON type, or adds a POLYGON instance to a MULTIPOLYGON instance.
|
||||
>>>>>>> dev
|
||||
* @memberof Wkt.Wkt
|
||||
* @method
|
||||
*/
|
||||
|
||||
13
src/gui/locales/ar.json
vendored
13
src/gui/locales/ar.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "إنشاء عالم",
|
||||
"no_world_selected": "لم يتم إنشاء عالم",
|
||||
"choose_world": "اختيار عالم",
|
||||
"no_world_selected": "لم يتم تحديد عالم",
|
||||
"start_generation": "بدء البناء",
|
||||
"custom_selection_confirmed": "تم تأكيد التحديد المخصص!",
|
||||
"error_coordinates_out_of_range": "خطأ: الإحداثيات خارج النطاق أو مرتبة بشكل غير صحيح (مطلوب خط العرض قبل خط الطول).",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "مربع الحدود المخصص",
|
||||
"floodfill_timeout": "مهلة ملء الفيضان (ثواني)",
|
||||
"ground_level": "مستوى الأرض",
|
||||
"choose_world_modal_title": "اختيار عالم",
|
||||
"select_existing_world": "اختيار عالم موجود مسبقًا",
|
||||
"generate_new_world": "إنشاء عالم جديد",
|
||||
"customization_settings": "إعدادات التخصيص",
|
||||
"footer_text": "© {year} Arnis v{version} من louis-e",
|
||||
"new_version_available": "هناك نسخة جديدة متاحة! انقر هنا لتنزيلها.",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "العالم المحدد قيد الاستخدام حاليًا",
|
||||
"failed_to_create_world": "حدث خطأ عند محاولة إنشاء عالم جديد",
|
||||
"no_world_selected_error": "لم يتم تحديد عالم",
|
||||
"create_world_first": "أنشئ عالمًا أولاً!",
|
||||
"select_minecraft_world_first": "يرجى تحديد عالم ماين كرافت أولاً!",
|
||||
"select_location_first": "يرجى اختيار موقع أولاً!",
|
||||
"area_too_large": "تُعتبر هذه المنطقة كبيرة جدًا وقد تتجاوز حدود الحوسبة النموذجية.",
|
||||
"area_extensive": "المنطقة واسعة جدًا وقد تتطلب الكثير من الوقت والموارد.",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "توليد الداخلية",
|
||||
"roof": "توليد السقف",
|
||||
"fillground": "ملء الأرض",
|
||||
"city_boundaries": "أرضية المدينة",
|
||||
"bedrock_auto_generated": "يتم إنشاء عالم Bedrock تلقائيًا",
|
||||
"save_path": "مسار الحفظ"
|
||||
"bedrock_use_java": "استخدم Java لاختيار العوالم"
|
||||
}
|
||||
|
||||
13
src/gui/locales/de.json
vendored
13
src/gui/locales/de.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "Welt erstellen",
|
||||
"no_world_selected": "Keine Welt erstellt",
|
||||
"choose_world": "Welt wählen",
|
||||
"no_world_selected": "Keine Welt ausgewählt",
|
||||
"start_generation": "Generierung starten",
|
||||
"custom_selection_confirmed": "Benutzerdefinierte Auswahl bestätigt!",
|
||||
"error_coordinates_out_of_range": "Fehler: Koordinaten sind außerhalb des Bereichs oder falsch geordnet (Lat vor Lng erforderlich).",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "Benutzerdefinierte BBOX",
|
||||
"floodfill_timeout": "Floodfill-Timeout (Sek)",
|
||||
"ground_level": "Bodenhöhe",
|
||||
"choose_world_modal_title": "Welt wählen",
|
||||
"select_existing_world": "Vorhandene Welt auswählen",
|
||||
"generate_new_world": "Neue Welt generieren",
|
||||
"customization_settings": "Einstellungen",
|
||||
"footer_text": "© {year} Arnis v{version} von louis-e",
|
||||
"new_version_available": "Eine neue Version ist verfügbar! Klicke hier, um sie herunterzuladen.",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "Die ausgewählte Welt ist gerade in Benutzung",
|
||||
"failed_to_create_world": "Neue Welt konnte nicht erstellt werden",
|
||||
"no_world_selected_error": "Keine Welt ausgewählt",
|
||||
"create_world_first": "Erstelle zuerst eine Welt!",
|
||||
"select_minecraft_world_first": "Wähle zuerst eine Minecraft Welt aus!",
|
||||
"select_location_first": "Wähle zuerst einen Standort aus!",
|
||||
"area_too_large": "Dieses Gebiet ist sehr groß und könnte das Berechnungslimit überschreiten.",
|
||||
"area_extensive": "Diese Gebietsgröße könnte längere Zeit für die Generierung benötigen.",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "Innenraum Generierung",
|
||||
"roof": "Dach Generierung",
|
||||
"fillground": "Boden füllen",
|
||||
"city_boundaries": "Stadtboden",
|
||||
"bedrock_auto_generated": "Bedrock-Welt wird automatisch generiert",
|
||||
"save_path": "Speicherpfad"
|
||||
"bedrock_use_java": "Java für Weltauswahl nutzen"
|
||||
}
|
||||
13
src/gui/locales/en-US.json
vendored
13
src/gui/locales/en-US.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "Create World",
|
||||
"no_world_selected": "No world created",
|
||||
"choose_world": "Choose World",
|
||||
"no_world_selected": "No world selected",
|
||||
"start_generation": "Start Generation",
|
||||
"custom_selection_confirmed": "Custom selection confirmed!",
|
||||
"error_coordinates_out_of_range": "Error: Coordinates are out of range or incorrectly ordered (Lat before Lng required).",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "Custom Bounding Box",
|
||||
"floodfill_timeout": "Floodfill Timeout (sec)",
|
||||
"ground_level": "Ground Level",
|
||||
"choose_world_modal_title": "Choose World",
|
||||
"select_existing_world": "Select existing world",
|
||||
"generate_new_world": "Generate new world",
|
||||
"customization_settings": "Customization Settings",
|
||||
"footer_text": "© {year} Arnis v{version} by louis-e",
|
||||
"new_version_available": "There's a new version available! Click here to download it.",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "The selected world is currently in use",
|
||||
"failed_to_create_world": "Failed to create new world",
|
||||
"no_world_selected_error": "No world selected",
|
||||
"create_world_first": "Create a world first!",
|
||||
"select_minecraft_world_first": "Select a Minecraft world first!",
|
||||
"select_location_first": "Select a location first!",
|
||||
"area_too_large": "This area is very large and could exceed typical computing limits.",
|
||||
"area_extensive": "The area is quite extensive and may take significant time and resources.",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "Interior Generation",
|
||||
"roof": "Roof Generation",
|
||||
"fillground": "Fill Ground",
|
||||
"city_boundaries": "City Ground",
|
||||
"bedrock_auto_generated": "Bedrock world is auto-generated",
|
||||
"save_path": "Save Path"
|
||||
"bedrock_use_java": "Use Java to select worlds"
|
||||
}
|
||||
13
src/gui/locales/es.json
vendored
13
src/gui/locales/es.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "Crear mundo",
|
||||
"no_world_selected": "Ningún mundo creado",
|
||||
"choose_world": "Elegir mundo",
|
||||
"no_world_selected": "Ningún mundo seleccionado",
|
||||
"start_generation": "Iniciar generación",
|
||||
"custom_selection_confirmed": "¡Selección personalizada confirmada!",
|
||||
"error_coordinates_out_of_range": "Error: Las coordenadas están fuera de rango o están ordenadas incorrectamente (Lat antes de Lng requerido).",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "Caja delimitadora personalizada",
|
||||
"floodfill_timeout": "Tiempo de espera de relleno (seg)",
|
||||
"ground_level": "Nivel del suelo",
|
||||
"choose_world_modal_title": "Elegir mundo",
|
||||
"select_existing_world": "Seleccionar mundo existente",
|
||||
"generate_new_world": "Generar nuevo mundo",
|
||||
"customization_settings": "Configuración de personalización",
|
||||
"footer_text": "© {year} Arnis v{version} por louis-e",
|
||||
"new_version_available": "¡Hay una nueva versión disponible! Haga clic aquí para descargarla.",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "El mundo seleccionado está en uso",
|
||||
"failed_to_create_world": "No se pudo crear un nuevo mundo",
|
||||
"no_world_selected_error": "Ningún mundo seleccionado",
|
||||
"create_world_first": "¡Crea un mundo primero!",
|
||||
"select_minecraft_world_first": "¡Seleccione un mundo de Minecraft primero!",
|
||||
"select_location_first": "¡Seleccione una ubicación primero!",
|
||||
"area_too_large": "Esta área es muy grande y podría exceder los límites típicos de computación.",
|
||||
"area_extensive": "El área es bastante extensa y puede requerir mucho tiempo y recursos.",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "Generación Interior",
|
||||
"roof": "Generación de Tejado",
|
||||
"fillground": "Rellenar Suelo",
|
||||
"city_boundaries": "Suelo Urbano",
|
||||
"bedrock_auto_generated": "El mundo Bedrock se genera automáticamente",
|
||||
"save_path": "Ruta de guardado"
|
||||
"bedrock_use_java": "Usa Java para elegir mundos"
|
||||
}
|
||||
13
src/gui/locales/fi.json
vendored
13
src/gui/locales/fi.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "Luo maailma",
|
||||
"no_world_selected": "Maailmaa ei luotu",
|
||||
"choose_world": "Valitse maailma",
|
||||
"no_world_selected": "Maailmaa ei valittu",
|
||||
"start_generation": "Aloita generointi",
|
||||
"custom_selection_confirmed": "Mukautettu valinta vahvistettu!",
|
||||
"error_coordinates_out_of_range": "Virhe: Koordinaatit ovat kantaman ulkopuolella tai vääriin aseteltu (Lat ennen Lng vaadittu).",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "Mukautettu rajoituslaatikko",
|
||||
"floodfill_timeout": "Täytön aikakatkaisu (sec)",
|
||||
"ground_level": "Maataso",
|
||||
"choose_world_modal_title": "Valitse maailma",
|
||||
"select_existing_world": "Valitse olemassa oleva maailma",
|
||||
"generate_new_world": "Luo uusi maailma",
|
||||
"customization_settings": "Kustomisaatio-asetukset",
|
||||
"footer_text": "© {year} Arnis v{version} tekijänä louis-e",
|
||||
"new_version_available": "Uusi versio on saatavilla! Paina tästä ladataksesi sen.",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "Valittu maailma käytössä.",
|
||||
"failed_to_create_world": "Uuden maailman luonti epäonnistui",
|
||||
"no_world_selected_error": "Maailmaa ei valittu",
|
||||
"create_world_first": "Luo ensin maailma!",
|
||||
"select_minecraft_world_first": "Valitse Minecraft-maailma ensin!",
|
||||
"select_location_first": "Valitse paikka ensin!",
|
||||
"area_too_large": "Tämä alue on todella iso ja voi ylittää tyypilliset laskentarajat.",
|
||||
"area_extensive": "Alue on aika laaja ja voi viedä pitkän ajan ja resursseja.",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "Sisätilan luonti",
|
||||
"roof": "Katon luonti",
|
||||
"fillground": "Täytä maa",
|
||||
"city_boundaries": "Kaupungin maa",
|
||||
"bedrock_auto_generated": "Bedrock-maailma luodaan automaattisesti",
|
||||
"save_path": "Tallennuspolku"
|
||||
"bedrock_use_java": "Käytä Javaa maailmojen valintaan"
|
||||
}
|
||||
|
||||
13
src/gui/locales/fr-FR.json
vendored
13
src/gui/locales/fr-FR.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "Créer un monde",
|
||||
"no_world_selected": "Aucun monde créé",
|
||||
"choose_world": "Choisir un monde",
|
||||
"no_world_selected": "Aucun monde sélectionné",
|
||||
"start_generation": "Commencer la génération",
|
||||
"custom_selection_confirmed": "Sélection personnalisée confirmée !",
|
||||
"error_coordinates_out_of_range": "Erreur: Coordonnées hors de portée ou dans un ordre incorrect (besoin de la latitude avant la longitude).",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "Cadre de délimitation personnalisé",
|
||||
"floodfill_timeout": "Expiration du délai de remplissage (en secondes)",
|
||||
"ground_level": "Niveau du sol",
|
||||
"choose_world_modal_title": "Choisir un monde",
|
||||
"select_existing_world": "Sélectionner un monde existant",
|
||||
"generate_new_world": "Générer un nouveau monde",
|
||||
"customization_settings": "Paramètres de personnalisation",
|
||||
"footer_text": "© {year} Arnis v{version} par louis-e",
|
||||
"new_version_available": "Une nouvelle version est disponible ! Cliquez ici pour la télécharger.",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "Le monde sélectionné est en cours d'utilisation",
|
||||
"failed_to_create_world": "Échec de la création du nouveau monde",
|
||||
"no_world_selected_error": "Aucun monde sélectionné",
|
||||
"create_world_first": "Créez d'abord un monde !",
|
||||
"select_minecraft_world_first": "Sélectionnez d'abord un monde Minecraft !",
|
||||
"select_location_first": "Sélectionnez d'abord une localisation !",
|
||||
"area_too_large": "Cette zone est très grande et pourrait dépasser les limites de calcul courantes.",
|
||||
"area_extensive": "Cette zone est très étendue et pourrait nécessiter beaucoup de ressources et de temps.",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "Génération d'intérieur",
|
||||
"roof": "Génération de toit",
|
||||
"fillground": "Remplir le sol",
|
||||
"city_boundaries": "Sol urbain",
|
||||
"bedrock_auto_generated": "Le monde Bedrock est généré automatiquement",
|
||||
"save_path": "Chemin de sauvegarde"
|
||||
"bedrock_use_java": "Utilisez Java pour les mondes"
|
||||
}
|
||||
|
||||
13
src/gui/locales/hu.json
vendored
13
src/gui/locales/hu.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "Világ létrehozása",
|
||||
"no_world_selected": "Nincs világ létrehozva",
|
||||
"choose_world": "Világ kiválasztása",
|
||||
"no_world_selected": "Nincs világ kiválasztva",
|
||||
"start_generation": "Generálás indítása",
|
||||
"custom_selection_confirmed": "Egyéni kiválasztás megerősítve",
|
||||
"error_coordinates_out_of_range": "Hiba: A koordináták tartományon kívül vannak vagy hibásan rendezettek (a szélességi foknak a hosszúsági fok előtt kell lennie)",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "Egyéni határoló keret",
|
||||
"floodfill_timeout": "Floodfill Timeout (sec)",
|
||||
"ground_level": "Földszint",
|
||||
"choose_world_modal_title": "Világ kiválasztása",
|
||||
"select_existing_world": "Már létező világ kiválasztása",
|
||||
"generate_new_world": "Új világ generálása",
|
||||
"customization_settings": "Testreszabási lehetőségek",
|
||||
"footer_text": "© {year} Arnis v{version} by louis-e",
|
||||
"new_version_available": "Egy új verzió elérhető kattints ide hogy letöltsd",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "A kiválasztott világ már használatban van",
|
||||
"failed_to_create_world": "Nem sikerült új világot létrehozni",
|
||||
"no_world_selected_error": "Nincs kiválasztott világ",
|
||||
"create_world_first": "Először hozz létre egy világot!",
|
||||
"select_minecraft_world_first": "Válassz ki egy Minecraft világot először!",
|
||||
"select_location_first": "Válassz egy helyet először!",
|
||||
"area_too_large": "Ez a terület nagyon nagy, és meghaladhatja a szokásos számítási korlátokat.",
|
||||
"area_extensive": "A terület meglehetősen kiterjedt, és jelentős időt és erőforrásokat igényelhet.",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "Belső generálás",
|
||||
"roof": "Tető generálás",
|
||||
"fillground": "Talaj feltöltése",
|
||||
"city_boundaries": "Városi talaj",
|
||||
"bedrock_auto_generated": "A Bedrock világ automatikusan generálódik",
|
||||
"save_path": "Mentési útvonal"
|
||||
"bedrock_use_java": "Java világválasztáshoz"
|
||||
}
|
||||
13
src/gui/locales/ko.json
vendored
13
src/gui/locales/ko.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "월드 만들기",
|
||||
"no_world_selected": "생성된 월드 없음",
|
||||
"choose_world": "세계 선택",
|
||||
"no_world_selected": "선택된 세계 없음",
|
||||
"start_generation": "생성 시작",
|
||||
"custom_selection_confirmed": "사용자 지정 선택이 확인되었습니다!",
|
||||
"error_coordinates_out_of_range": "오류: 좌표가 범위를 벗어나거나 잘못된 순서입니다 (Lat이 Lng보다 먼저 필요합니다).",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "사용자 지정 경계 상자",
|
||||
"floodfill_timeout": "채우기 시간 초과 (초)",
|
||||
"ground_level": "지면 레벨",
|
||||
"choose_world_modal_title": "세계 선택",
|
||||
"select_existing_world": "이미 존재하는 세계 선택",
|
||||
"generate_new_world": "새 세계 생성",
|
||||
"customization_settings": "사용자 지정 설정",
|
||||
"footer_text": "© {year} Arnis v{version} by louis-e",
|
||||
"new_version_available": "새로운 버전이 있습니다! 여기를 클릭하여 다운로드하세요.",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "선택한 세계가 현재 사용 중입니다",
|
||||
"failed_to_create_world": "새 세계 생성에 실패했습니다",
|
||||
"no_world_selected_error": "선택된 세계 없음 오류",
|
||||
"create_world_first": "먼저 월드를 만드세요!",
|
||||
"select_minecraft_world_first": "먼저 마인크래프트 세계를 선택하세요!",
|
||||
"select_location_first": "먼저 위치를 선택하세요!",
|
||||
"area_too_large": "이 지역은 매우 크고, 일반적인 계산 한계를 초과할 수 있습니다.",
|
||||
"area_extensive": "이 지역은 꽤 광범위하여 상당한 시간과 자원이 필요할 수 있습니다.",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "내부 생성",
|
||||
"roof": "지붕 생성",
|
||||
"fillground": "지면 채우기",
|
||||
"city_boundaries": "도시 지면",
|
||||
"bedrock_auto_generated": "Bedrock 월드는 자동 생성됩니다",
|
||||
"save_path": "저장 경로"
|
||||
"bedrock_use_java": "Java로 세계 선택"
|
||||
}
|
||||
13
src/gui/locales/lt.json
vendored
13
src/gui/locales/lt.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "Sukurti pasaulį",
|
||||
"no_world_selected": "Pasaulis nesukurtas",
|
||||
"choose_world": "Pasirinkti pasaulį",
|
||||
"no_world_selected": "Pasaulis nepasirinktas",
|
||||
"start_generation": "Pradėti generaciją",
|
||||
"custom_selection_confirmed": "Rėmo pasirinkimas patvirtintas!",
|
||||
"error_coordinates_out_of_range": "Klaida: Koordinatės yra už ribų arba neteisingai išdėstytos (plat turi būti prieš ilg).",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "Pasirinktinis ribos rėmas",
|
||||
"floodfill_timeout": "Užpildymo laiko limitas (sek.)",
|
||||
"ground_level": "Žemės lygis",
|
||||
"choose_world_modal_title": "Pasaulio pasirinkimas",
|
||||
"select_existing_world": "Pasirinkti esamą pasaulį",
|
||||
"generate_new_world": "Sugeneruoti naują pasaulį",
|
||||
"customization_settings": "Generacijos nustatymai",
|
||||
"footer_text": "© {year} „Arnis“ v{version} sukurta louis-e",
|
||||
"new_version_available": "Surasta nauja versija! Spauskite čia kad ją atsisiųstumėte.",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "Pasirinktas pasaulis dabar užimtas",
|
||||
"failed_to_create_world": "Klaida sukuriant naują pasaulį",
|
||||
"no_world_selected_error": "Nėra pasirinktas pasaulis",
|
||||
"create_world_first": "Pirmiausia sukurkite pasaulį!",
|
||||
"select_minecraft_world_first": "Pirma pasirinkite „Minecraft“ pasaulį!",
|
||||
"select_location_first": "Pirma pasirinkite vietą!",
|
||||
"area_too_large": "Šis plotas yra labai didelis ir gali viršyti tipinius resursų limitus.",
|
||||
"area_extensive": "Šis plotas yra pakankamai didelis kuriam reikėtų daug laiko ir resursų.",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "Interjero generavimas",
|
||||
"roof": "Stogo generavimas",
|
||||
"fillground": "Užpildyti pagrindą",
|
||||
"city_boundaries": "Miesto žemė",
|
||||
"bedrock_auto_generated": "Bedrock pasaulis generuojamas automatiškai",
|
||||
"save_path": "Išsaugojimo kelias"
|
||||
"bedrock_use_java": "Naudok Java pasauliams"
|
||||
}
|
||||
13
src/gui/locales/lv.json
vendored
13
src/gui/locales/lv.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "Izveidot pasauli",
|
||||
"no_world_selected": "Pasaule nav izveidota",
|
||||
"choose_world": "Izvēlēties pasauli",
|
||||
"no_world_selected": "Pasaulē nav izvēlēta",
|
||||
"start_generation": "Sākt ģenerēšanu",
|
||||
"custom_selection_confirmed": "Pielāgota izvēle apstiprināta!",
|
||||
"error_coordinates_out_of_range": "Kļūda: koordinātas ir ārpus darbības zonas vai norādītas nepareizā secībā (vispirms platums, tad garums)",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "Pielāgota ierobežojošā rāmja zona",
|
||||
"floodfill_timeout": "Aizpildes noildze (sek.)",
|
||||
"ground_level": "Zemes līmenis",
|
||||
"choose_world_modal_title": "Izvēlēties pasauli",
|
||||
"select_existing_world": "Izvēlēties esošu pasauli",
|
||||
"generate_new_world": "Izveidot jaunu pasauli",
|
||||
"customization_settings": "Personalizācijas iestatījumi",
|
||||
"footer_text": "© {year} Arnis v{version} no louis-e",
|
||||
"new_version_available": "Pieejama jauna versija! Noklikšķiniet šeit, lai lejupielādētu",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "Izvēlētā pasaule jau tiek izmantota",
|
||||
"failed_to_create_world": "Neizdevās izveidot jaunu pasauli",
|
||||
"no_world_selected_error": "Pasaulē nav izvēlēta",
|
||||
"create_world_first": "Vispirms izveidojiet pasauli!",
|
||||
"select_minecraft_world_first": "Vispirms izvēlieties Minecraft pasauli!",
|
||||
"select_location_first": "Vispirms izvēlieties atrašanās vietu!",
|
||||
"area_too_large": "Šis apgabals ir pārāk liels un var pārsniegt tipiskos aprēķina ierobežojumus",
|
||||
"area_extensive": "Apgabals ir diezgan plašs un var prasīt ievērojamu laiku un resursus",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "Interjera ģenerēšana",
|
||||
"roof": "Jumta ģenerēšana",
|
||||
"fillground": "Aizpildīt zemi",
|
||||
"city_boundaries": "Pilsētas zeme",
|
||||
"bedrock_auto_generated": "Bedrock pasaule tiek ģenerēta automātiski",
|
||||
"save_path": "Saglabāšanas ceļš"
|
||||
"bedrock_use_java": "Izmanto Java pasaulēm"
|
||||
}
|
||||
13
src/gui/locales/pl.json
vendored
13
src/gui/locales/pl.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "Utwórz świat",
|
||||
"no_world_selected": "Nie utworzono świata",
|
||||
"choose_world": "Wybierz świat",
|
||||
"no_world_selected": "Nie wybrano świata",
|
||||
"start_generation": "Rozpocznij generowanie",
|
||||
"custom_selection_confirmed": "Niestandardowy wybór potwierdzony!",
|
||||
"error_coordinates_out_of_range": "Błąd: Współrzędne są poza zakresem lub w złej kolejności (wymagana szerokość przed długością).",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "Niestandardowy obszar",
|
||||
"floodfill_timeout": "Limit czasu wypełniania (sek)",
|
||||
"ground_level": "Wysokość obszaru",
|
||||
"choose_world_modal_title": "Wybierz świat",
|
||||
"select_existing_world": "Wybierz istniejący świat",
|
||||
"generate_new_world": "Generuj nowy świat",
|
||||
"customization_settings": "Ustawienia personalizacji",
|
||||
"footer_text": "© {year} Arnis v{version} autorstwa louis-e",
|
||||
"new_version_available": "Dostępna jest nowa wersja! Kliknij tutaj, aby ją pobrać.",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "Wybrany świat jest obecnie używany",
|
||||
"failed_to_create_world": "Nie udało się utworzyć świata",
|
||||
"no_world_selected_error": "Nie wybrano świata",
|
||||
"create_world_first": "Najpierw utwórz świat!",
|
||||
"select_minecraft_world_first": "Najpierw wybierz świat Minecrafta!",
|
||||
"select_location_first": "Najpierw wybierz lokalizację!",
|
||||
"area_too_large": "Ten obszar jest bardzo duży i może przekroczyć limity obliczeniowe.",
|
||||
"area_extensive": "Ten obszar jest rozległy i może pochłonąć dużo czasu oraz zasobów.",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "Generowanie wnętrza",
|
||||
"roof": "Generowanie dachu",
|
||||
"fillground": "Wypełnij podłoże",
|
||||
"city_boundaries": "Podłoże miejskie",
|
||||
"bedrock_auto_generated": "Świat Bedrock jest generowany automatycznie",
|
||||
"save_path": "Ścieżka zapisu"
|
||||
"bedrock_use_java": "Użyj Java do wyboru światów"
|
||||
}
|
||||
13
src/gui/locales/ru.json
vendored
13
src/gui/locales/ru.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "Создать мир",
|
||||
"no_world_selected": "Мир не создан",
|
||||
"choose_world": "Выбрать мир",
|
||||
"no_world_selected": "Мир не выбран",
|
||||
"start_generation": "Начать генерацию",
|
||||
"custom_selection_confirmed": "Пользовательский выбор подтвержден!",
|
||||
"error_coordinates_out_of_range": "Ошибка: Координаты находятся вне зоны действия или указаны в неправильном порядке (сначала широта, затем долгота)",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "Пользовательская ограничивающая рамка",
|
||||
"floodfill_timeout": "Тайм-аут заливки (сек)",
|
||||
"ground_level": "Уровень земли",
|
||||
"choose_world_modal_title": "Выбрать мир",
|
||||
"select_existing_world": "Выбрать существующий мир",
|
||||
"generate_new_world": "Создать новый мир",
|
||||
"customization_settings": "Настройки персонализации",
|
||||
"footer_text": "© {year} Arnis v{version} от louis-e",
|
||||
"new_version_available": "Доступна новая версия! Нажмите здесь, чтобы скачать",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "Выбранный мир уже используется",
|
||||
"failed_to_create_world": "Не удалось создать новый мир",
|
||||
"no_world_selected_error": "Мир не выбран",
|
||||
"create_world_first": "Сначала создайте мир!",
|
||||
"select_minecraft_world_first": "Сначала выберите мир Minecraft!",
|
||||
"select_location_first": "Сначала выберите местоположение!",
|
||||
"area_too_large": "Эта область слишком велика и может превысить типичные вычислительные ограничения",
|
||||
"area_extensive": "Область довольно обширна и может потребовать значительного времени и ресурсов",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "Генерация Интерьера",
|
||||
"roof": "Генерация Крыши",
|
||||
"fillground": "Заполнить Землю",
|
||||
"city_boundaries": "Городской грунт",
|
||||
"bedrock_auto_generated": "Мир Bedrock генерируется автоматически",
|
||||
"save_path": "Путь сохранения"
|
||||
"bedrock_use_java": "Используйте Java для миров"
|
||||
}
|
||||
|
||||
13
src/gui/locales/sv.json
vendored
13
src/gui/locales/sv.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "Skapa värld",
|
||||
"no_world_selected": "Ingen värld skapad",
|
||||
"choose_world": "Välj värld",
|
||||
"no_world_selected": "Ingen värld vald",
|
||||
"start_generation": "Starta generering",
|
||||
"custom_selection_confirmed": "Anpassad markering bekräftad!",
|
||||
"error_coordinates_out_of_range": "Fel: Koordinater är utanför området eller felaktigt ordnade (Lat före Lng krävs).",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "Anpassad begränsningsram",
|
||||
"floodfill_timeout": "Floodfill-tidsgräns (sek)",
|
||||
"ground_level": "Marknivå",
|
||||
"choose_world_modal_title": "Välj värld",
|
||||
"select_existing_world": "Välj existerande värld",
|
||||
"generate_new_world": "Generera ny värld",
|
||||
"customization_settings": "Anpassningsinställningar",
|
||||
"footer_text": "© {year} Arnis v{version} by louis-e",
|
||||
"new_version_available": "Det finns en ny version tillgänglig! Klicka här för att ladda ner den.",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "Den valda världen används just nu",
|
||||
"failed_to_create_world": "Misslyckades att skapa ny värld",
|
||||
"no_world_selected_error": "Ingen värld vald fel",
|
||||
"create_world_first": "Skapa en värld först!",
|
||||
"select_minecraft_world_first": "Välj Minecraft-värld först!",
|
||||
"select_location_first": "Välj plats först!",
|
||||
"area_too_large": "Detta område är mycket stort och kan överskrida vanliga beräkningsgränser.",
|
||||
"area_extensive": "Området är ganska extensivt och kan ta betydande tid och resurser.",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "Interiörgenerering",
|
||||
"roof": "Takgenerering",
|
||||
"fillground": "Fyll mark",
|
||||
"city_boundaries": "Stadsmark",
|
||||
"bedrock_auto_generated": "Bedrock-världen genereras automatiskt",
|
||||
"save_path": "Sökväg"
|
||||
"bedrock_use_java": "Använd Java för världar"
|
||||
}
|
||||
13
src/gui/locales/ua.json
vendored
13
src/gui/locales/ua.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "Створити світ",
|
||||
"no_world_selected": "Світ не створено",
|
||||
"choose_world": "Обрати світ",
|
||||
"no_world_selected": "Світ не обрано",
|
||||
"start_generation": "Почати генерацію",
|
||||
"custom_selection_confirmed": "Користувацький вибір підтверджено!",
|
||||
"error_coordinates_out_of_range": "Помилка: Координати поза діапазоном або неправильно впорядковані (потрібно широта перед довгота)",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "Користувацька обмежувальна рамка",
|
||||
"floodfill_timeout": "Тайм-аут заливки (сек)",
|
||||
"ground_level": "Рівень землі",
|
||||
"choose_world_modal_title": "Обрати світ",
|
||||
"select_existing_world": "Обрати наявний світ",
|
||||
"generate_new_world": "Створити новий світ",
|
||||
"customization_settings": "Налаштування параметрів",
|
||||
"footer_text": "© {year} Arnis v{version} від louis-e",
|
||||
"new_version_available": "Доступна нова версія! Натисніть тут, щоб завантажити її",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "Вибраний світ зараз використовується",
|
||||
"failed_to_create_world": "Не вдалося створити новий світ",
|
||||
"no_world_selected_error": "Світ не обрано",
|
||||
"create_world_first": "Спочатку створіть світ!",
|
||||
"select_minecraft_world_first": "Спочатку виберіть світ Minecraft!",
|
||||
"select_location_first": "Спочатку виберіть місцезнаходження!",
|
||||
"area_too_large": "Ця область дуже велика і може перевищити типові обчислювальні межі",
|
||||
"area_extensive": "Область досить велика і може вимагати значного часу та ресурсів",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "Генерація інтер'єру",
|
||||
"roof": "Генерація даху",
|
||||
"fillground": "Заповнити землю",
|
||||
"city_boundaries": "Міська земля",
|
||||
"bedrock_auto_generated": "Bedrock світ генерується автоматично",
|
||||
"save_path": "Шлях збереження"
|
||||
"bedrock_use_java": "Використовуй Java для світів"
|
||||
}
|
||||
13
src/gui/locales/zh-CN.json
vendored
13
src/gui/locales/zh-CN.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"create_world": "创建世界",
|
||||
"no_world_selected": "未创建世界",
|
||||
"choose_world": "选择世界",
|
||||
"no_world_selected": "未选择世界",
|
||||
"start_generation": "开始生成",
|
||||
"custom_selection_confirmed": "自定义选择已确认!",
|
||||
"error_coordinates_out_of_range": "错误:坐标超出范围或顺序不正确(需要先纬度后经度)。",
|
||||
@@ -11,6 +11,9 @@
|
||||
"custom_bounding_box": "自定义边界框",
|
||||
"floodfill_timeout": "填充超时(秒)",
|
||||
"ground_level": "地面高度",
|
||||
"choose_world_modal_title": "选择世界",
|
||||
"select_existing_world": "选择现有世界",
|
||||
"generate_new_world": "生成新世界",
|
||||
"customization_settings": "自定义设置",
|
||||
"footer_text": "© {year} Arnis v{version} 由 louis-e 提供",
|
||||
"new_version_available": "有新版本可用!点击这里下载。",
|
||||
@@ -18,7 +21,7 @@
|
||||
"world_in_use": "所选世界正在使用中",
|
||||
"failed_to_create_world": "无法创建新世界",
|
||||
"no_world_selected_error": "未选择世界",
|
||||
"create_world_first": "请先创建一个世界!",
|
||||
"select_minecraft_world_first": "请先选择一个 Minecraft 世界!",
|
||||
"select_location_first": "请先选择一个位置!",
|
||||
"area_too_large": "该区域非常大,可能会超出典型的计算限制。",
|
||||
"area_extensive": "该区域相当广泛,可能需要大量时间和资源。",
|
||||
@@ -39,7 +42,5 @@
|
||||
"interior": "内部生成",
|
||||
"roof": "屋顶生成",
|
||||
"fillground": "填充地面",
|
||||
"city_boundaries": "城市地面",
|
||||
"bedrock_auto_generated": "Bedrock 世界自动生成",
|
||||
"save_path": "存档路径"
|
||||
"bedrock_use_java": "使用Java选择世界"
|
||||
}
|
||||
84
src/main.rs
84
src/main.rs
@@ -18,6 +18,7 @@ mod ground;
|
||||
mod map_renderer;
|
||||
mod map_transformation;
|
||||
mod osm_parser;
|
||||
mod parallel_processing;
|
||||
#[cfg(feature = "gui")]
|
||||
mod progress;
|
||||
mod retrieve_data;
|
||||
@@ -25,15 +26,13 @@ mod retrieve_data;
|
||||
mod telemetry;
|
||||
#[cfg(test)]
|
||||
mod test_utilities;
|
||||
mod urban_ground;
|
||||
mod unit_processing;
|
||||
mod version_check;
|
||||
mod world_editor;
|
||||
mod world_utils;
|
||||
|
||||
use args::Args;
|
||||
use clap::Parser;
|
||||
use colored::*;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs, io::Write};
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
@@ -93,54 +92,6 @@ fn run_cli() {
|
||||
// Parse input arguments
|
||||
let args: Args = Args::parse();
|
||||
|
||||
// Validate arguments (path requirements differ between Java and Bedrock)
|
||||
if let Err(e) = args::validate_args(&args) {
|
||||
eprintln!("{}: {}", "Error".red().bold(), e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Early guard: --bedrock requires the bedrock cargo feature
|
||||
if args.bedrock && !cfg!(feature = "bedrock") {
|
||||
eprintln!(
|
||||
"{}: The --bedrock flag requires the 'bedrock' feature. Rebuild with: cargo build --features bedrock",
|
||||
"Error".red().bold()
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Determine world format and output path
|
||||
let world_format = if args.bedrock {
|
||||
world_editor::WorldFormat::BedrockMcWorld
|
||||
} else {
|
||||
world_editor::WorldFormat::JavaAnvil
|
||||
};
|
||||
|
||||
// Build the generation output path and level name
|
||||
let (generation_path, level_name) = if args.bedrock {
|
||||
// Bedrock: generate .mcworld file in user-specified path or Desktop
|
||||
let output_dir = args
|
||||
.path
|
||||
.clone()
|
||||
.unwrap_or_else(world_utils::get_bedrock_output_directory);
|
||||
let (output_path, lvl_name) = world_utils::build_bedrock_output(&args.bbox, output_dir);
|
||||
(output_path, Some(lvl_name))
|
||||
} else {
|
||||
// Java: create a new world in the provided output directory
|
||||
let base_dir = args.path.clone().unwrap();
|
||||
let world_path = match world_utils::create_new_world(&base_dir) {
|
||||
Ok(path) => PathBuf::from(path),
|
||||
Err(e) => {
|
||||
eprintln!("{} {}", "Error:".red().bold(), e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
println!(
|
||||
"Created new world at: {}",
|
||||
world_path.display().to_string().bright_white().bold()
|
||||
);
|
||||
(world_path, None)
|
||||
};
|
||||
|
||||
// Fetch data
|
||||
let raw_data = match &args.file {
|
||||
Some(file) => retrieve_data::fetch_data_from_file(file),
|
||||
@@ -181,37 +132,8 @@ fn run_cli() {
|
||||
// Transform map (parsed_elements). Operations are defined in a json file
|
||||
map_transformation::transform_map(&mut parsed_elements, &mut xzbbox, &mut ground);
|
||||
|
||||
// Build generation options
|
||||
let generation_options = data_processing::GenerationOptions {
|
||||
path: generation_path.clone(),
|
||||
format: world_format,
|
||||
level_name,
|
||||
spawn_point: None,
|
||||
};
|
||||
|
||||
// Generate world
|
||||
match data_processing::generate_world_with_options(
|
||||
parsed_elements,
|
||||
xzbbox,
|
||||
args.bbox,
|
||||
ground,
|
||||
&args,
|
||||
generation_options,
|
||||
) {
|
||||
Ok(_) => {
|
||||
if args.bedrock {
|
||||
println!(
|
||||
"{} Bedrock world saved to: {}",
|
||||
"Done!".green().bold(),
|
||||
generation_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{} {}", "Error:".red().bold(), e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
let _ = data_processing::generate_world(parsed_elements, xzbbox, args.bbox, ground, &args);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -111,7 +111,6 @@ pub struct ProcessedWay {
|
||||
pub enum ProcessedMemberRole {
|
||||
Outer,
|
||||
Inner,
|
||||
Part,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -273,24 +272,13 @@ pub fn parse_osm_data(
|
||||
continue;
|
||||
};
|
||||
|
||||
// Process multipolygons and building relations
|
||||
let relation_type = tags.get("type").map(|x: &String| x.as_str());
|
||||
if relation_type != Some("multipolygon") && relation_type != Some("building") {
|
||||
// Only process multipolygons for now
|
||||
if tags.get("type").map(|x: &String| x.as_str()) != Some("multipolygon") {
|
||||
continue;
|
||||
};
|
||||
|
||||
let is_building_relation = relation_type == Some("building")
|
||||
|| tags.contains_key("building")
|
||||
|| tags.contains_key("building:part");
|
||||
|
||||
// Water relations require unclipped ways for ring merging in water_areas.rs
|
||||
// Building multipolygon relations also need unclipped ways so that
|
||||
// open outer-way segments can be merged into closed rings before clipping
|
||||
let is_water_relation = is_water_element(tags);
|
||||
let is_building_multipolygon = (tags.contains_key("building")
|
||||
|| tags.contains_key("building:part"))
|
||||
&& relation_type == Some("multipolygon");
|
||||
let keep_unclipped = is_water_relation || is_building_multipolygon;
|
||||
|
||||
let members: Vec<ProcessedMember> = element
|
||||
.members
|
||||
@@ -301,25 +289,10 @@ pub fn parse_osm_data(
|
||||
return None;
|
||||
}
|
||||
|
||||
let trimmed_role = mem.role.trim();
|
||||
let role = if trimmed_role.eq_ignore_ascii_case("outer")
|
||||
|| trimmed_role.eq_ignore_ascii_case("outline")
|
||||
{
|
||||
ProcessedMemberRole::Outer
|
||||
} else if trimmed_role.eq_ignore_ascii_case("inner") {
|
||||
ProcessedMemberRole::Inner
|
||||
} else if trimmed_role.eq_ignore_ascii_case("part") {
|
||||
if relation_type == Some("building") {
|
||||
// "part" role only applies to type=building relations.
|
||||
ProcessedMemberRole::Part
|
||||
} else {
|
||||
// For multipolygon relations, "part" is not a valid role, skip.
|
||||
return None;
|
||||
}
|
||||
} else if is_building_relation {
|
||||
ProcessedMemberRole::Outer
|
||||
} else {
|
||||
return None;
|
||||
let role = match mem.role.as_str() {
|
||||
"outer" => ProcessedMemberRole::Outer,
|
||||
"inner" => ProcessedMemberRole::Inner,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Check if the way exists in ways_map
|
||||
@@ -331,9 +304,9 @@ pub fn parse_osm_data(
|
||||
}
|
||||
};
|
||||
|
||||
// If keep_unclipped is true (e.g., certain water or building multipolygon
|
||||
// relations), keep member ways unclipped for ring merging; otherwise clip now.
|
||||
let final_way = if keep_unclipped {
|
||||
// Water relations: keep unclipped for ring merging
|
||||
// Non-water relations: clip member ways now
|
||||
let final_way = if is_water_relation {
|
||||
way
|
||||
} else {
|
||||
let clipped_nodes = clip_way_to_bbox(&way.nodes, &xzbbox);
|
||||
|
||||
476
src/parallel_processing.rs
Normal file
476
src/parallel_processing.rs
Normal file
@@ -0,0 +1,476 @@
|
||||
//! Parallel region processing for improved memory efficiency and CPU utilization.
|
||||
//!
|
||||
//! This module splits the world generation into processing units (1 Minecraft region each),
|
||||
//! processes them in parallel, and flushes each region to disk immediately after completion.
|
||||
//!
|
||||
//! Key benefits:
|
||||
//! - Memory usage reduced by ~90% (only active regions in memory)
|
||||
//! - Multi-core CPU utilization
|
||||
//! - Consistent results via deterministic RNG
|
||||
|
||||
use crate::coordinate_system::cartesian::xzbbox::rectangle::XZBBoxRect;
|
||||
use crate::coordinate_system::cartesian::{XZBBox, XZPoint};
|
||||
use crate::floodfill_cache::BuildingFootprintBitmap;
|
||||
use crate::ground::Ground;
|
||||
use crate::osm_parser::{ProcessedElement, ProcessedNode};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::element_processing::highways::HighwayConnectivityMap;
|
||||
|
||||
/// Size of a Minecraft region in blocks (32 chunks × 16 blocks per chunk)
|
||||
pub const REGION_BLOCKS: i32 = 512;
|
||||
|
||||
/// A processing unit representing a single Minecraft region to be generated.
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ProcessingUnit {
|
||||
/// Region X coordinate (in region space, not block space)
|
||||
pub region_x: i32,
|
||||
/// Region Z coordinate (in region space, not block space)
|
||||
pub region_z: i32,
|
||||
|
||||
/// Minecraft coordinate bounds for this unit (512×512 blocks)
|
||||
pub min_x: i32,
|
||||
pub max_x: i32,
|
||||
pub min_z: i32,
|
||||
pub max_z: i32,
|
||||
|
||||
/// Expanded bounds for element fetching (includes buffer for boundary elements)
|
||||
pub fetch_min_x: i32,
|
||||
pub fetch_max_x: i32,
|
||||
pub fetch_min_z: i32,
|
||||
pub fetch_max_z: i32,
|
||||
}
|
||||
|
||||
impl ProcessingUnit {
|
||||
/// Creates a new processing unit for a specific region.
|
||||
#[allow(dead_code)]
|
||||
pub fn new(region_x: i32, region_z: i32, global_bbox: &XZBBox, buffer_blocks: i32) -> Self {
|
||||
Self::new_batched(region_x, region_z, region_x, region_z, global_bbox, buffer_blocks)
|
||||
}
|
||||
|
||||
/// Creates a processing unit spanning multiple regions (for batching).
|
||||
pub fn new_batched(
|
||||
start_region_x: i32, start_region_z: i32,
|
||||
end_region_x: i32, end_region_z: i32,
|
||||
global_bbox: &XZBBox, buffer_blocks: i32
|
||||
) -> Self {
|
||||
// Calculate block bounds for these regions
|
||||
let min_x = start_region_x * REGION_BLOCKS;
|
||||
let max_x = (end_region_x + 1) * REGION_BLOCKS - 1;
|
||||
let min_z = start_region_z * REGION_BLOCKS;
|
||||
let max_z = (end_region_z + 1) * REGION_BLOCKS - 1;
|
||||
|
||||
// Add buffer for fetch bounds, clamped to global bbox
|
||||
let fetch_min_x = (min_x - buffer_blocks).max(global_bbox.min_x());
|
||||
let fetch_max_x = (max_x + buffer_blocks).min(global_bbox.max_x());
|
||||
let fetch_min_z = (min_z - buffer_blocks).max(global_bbox.min_z());
|
||||
let fetch_max_z = (max_z + buffer_blocks).min(global_bbox.max_z());
|
||||
|
||||
Self {
|
||||
region_x: start_region_x,
|
||||
region_z: start_region_z,
|
||||
min_x,
|
||||
max_x,
|
||||
min_z,
|
||||
max_z,
|
||||
fetch_min_x,
|
||||
fetch_max_x,
|
||||
fetch_min_z,
|
||||
fetch_max_z,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the XZBBox for this unit's actual processing bounds (not fetch bounds).
|
||||
pub fn bbox(&self) -> XZBBox {
|
||||
XZBBox::Rect(
|
||||
XZBBoxRect::new(
|
||||
XZPoint::new(self.min_x, self.min_z),
|
||||
XZPoint::new(self.max_x, self.max_z),
|
||||
)
|
||||
.expect("Invalid unit bbox bounds"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the XZBBox for this unit's fetch bounds (includes buffer).
|
||||
#[allow(dead_code)]
|
||||
pub fn fetch_bbox(&self) -> XZBBox {
|
||||
XZBBox::Rect(
|
||||
XZBBoxRect::new(
|
||||
XZPoint::new(self.fetch_min_x, self.fetch_min_z),
|
||||
XZPoint::new(self.fetch_max_x, self.fetch_max_z),
|
||||
)
|
||||
.expect("Invalid unit fetch bbox bounds"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Checks if a point is within this unit's fetch bounds.
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
pub fn contains_fetch(&self, x: i32, z: i32) -> bool {
|
||||
x >= self.fetch_min_x
|
||||
&& x <= self.fetch_max_x
|
||||
&& z >= self.fetch_min_z
|
||||
&& z <= self.fetch_max_z
|
||||
}
|
||||
|
||||
/// Checks if an element's bounding box intersects with this unit's fetch bounds.
|
||||
pub fn intersects_element(&self, element: &ProcessedElement) -> bool {
|
||||
let (min_x, max_x, min_z, max_z) = element_bbox(element);
|
||||
|
||||
// Check for intersection
|
||||
!(max_x < self.fetch_min_x
|
||||
|| min_x > self.fetch_max_x
|
||||
|| max_z < self.fetch_min_z
|
||||
|| min_z > self.fetch_max_z)
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the bounding box of an element.
|
||||
fn element_bbox(element: &ProcessedElement) -> (i32, i32, i32, i32) {
|
||||
match element {
|
||||
ProcessedElement::Node(node) => (node.x, node.x, node.z, node.z),
|
||||
ProcessedElement::Way(way) => way_bbox(&way.nodes),
|
||||
ProcessedElement::Relation(rel) => {
|
||||
let mut min_x = i32::MAX;
|
||||
let mut max_x = i32::MIN;
|
||||
let mut min_z = i32::MAX;
|
||||
let mut max_z = i32::MIN;
|
||||
|
||||
for member in &rel.members {
|
||||
let (mx, mxx, mz, mxz) = way_bbox(&member.way.nodes);
|
||||
min_x = min_x.min(mx);
|
||||
max_x = max_x.max(mxx);
|
||||
min_z = min_z.min(mz);
|
||||
max_z = max_z.max(mxz);
|
||||
}
|
||||
|
||||
(min_x, max_x, min_z, max_z)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the bounding box of a way's nodes.
|
||||
fn way_bbox(nodes: &[ProcessedNode]) -> (i32, i32, i32, i32) {
|
||||
if nodes.is_empty() {
|
||||
return (0, 0, 0, 0);
|
||||
}
|
||||
|
||||
let mut min_x = i32::MAX;
|
||||
let mut max_x = i32::MIN;
|
||||
let mut min_z = i32::MAX;
|
||||
let mut max_z = i32::MIN;
|
||||
|
||||
for node in nodes {
|
||||
min_x = min_x.min(node.x);
|
||||
max_x = max_x.max(node.x);
|
||||
min_z = min_z.min(node.z);
|
||||
max_z = max_z.max(node.z);
|
||||
}
|
||||
|
||||
(min_x, max_x, min_z, max_z)
|
||||
}
|
||||
|
||||
/// Computes all processing units for a given world bounding box.
|
||||
/// With batch_size=1, creates one unit per region.
|
||||
/// With batch_size=2, creates one unit per 2x2 = 4 regions, etc.
|
||||
pub fn compute_processing_units(
|
||||
global_bbox: &XZBBox,
|
||||
buffer_blocks: i32,
|
||||
batch_size: usize,
|
||||
) -> Vec<ProcessingUnit> {
|
||||
// Calculate which regions are covered by the bbox
|
||||
let min_region_x = global_bbox.min_x() >> 9; // divide by 512
|
||||
let max_region_x = global_bbox.max_x() >> 9;
|
||||
let min_region_z = global_bbox.min_z() >> 9;
|
||||
let max_region_z = global_bbox.max_z() >> 9;
|
||||
|
||||
let mut units = Vec::new();
|
||||
|
||||
// Batch size determines how many regions are grouped together
|
||||
// batch_size=1 -> 1 region per unit
|
||||
// batch_size=2 -> 2x2=4 regions per unit
|
||||
let batch = batch_size.max(1) as i32;
|
||||
|
||||
// Create units grouped by batch_size
|
||||
let mut rx = min_region_x;
|
||||
while rx <= max_region_x {
|
||||
let mut rz = min_region_z;
|
||||
while rz <= max_region_z {
|
||||
// Create a unit spanning batch_size regions in each direction
|
||||
units.push(ProcessingUnit::new_batched(
|
||||
rx, rz,
|
||||
(rx + batch - 1).min(max_region_x),
|
||||
(rz + batch - 1).min(max_region_z),
|
||||
global_bbox,
|
||||
buffer_blocks
|
||||
));
|
||||
rz += batch;
|
||||
}
|
||||
rx += batch;
|
||||
}
|
||||
|
||||
units
|
||||
}
|
||||
|
||||
/// Distributes elements to processing units based on spatial intersection.
|
||||
///
|
||||
/// Each element is assigned to all units whose fetch bounds it intersects.
|
||||
/// This ensures elements at boundaries are processed by all relevant units.
|
||||
#[allow(dead_code)]
|
||||
pub fn distribute_elements_to_units<'a>(
|
||||
elements: &'a [ProcessedElement],
|
||||
units: &[ProcessingUnit],
|
||||
) -> Vec<Vec<&'a ProcessedElement>> {
|
||||
let mut unit_elements: Vec<Vec<&ProcessedElement>> = vec![Vec::new(); units.len()];
|
||||
|
||||
for element in elements {
|
||||
for (i, unit) in units.iter().enumerate() {
|
||||
if unit.intersects_element(element) {
|
||||
unit_elements[i].push(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unit_elements
|
||||
}
|
||||
|
||||
/// Distributes elements to processing units, returning indices instead of references.
|
||||
///
|
||||
/// This is useful when elements need to be shared across threads via Arc.
|
||||
pub fn distribute_elements_to_units_indices(
|
||||
elements: &[ProcessedElement],
|
||||
units: &[ProcessingUnit],
|
||||
) -> Vec<Vec<usize>> {
|
||||
let mut unit_indices: Vec<Vec<usize>> = vec![Vec::new(); units.len()];
|
||||
|
||||
for (idx, element) in elements.iter().enumerate() {
|
||||
for (i, unit) in units.iter().enumerate() {
|
||||
if unit.intersects_element(element) {
|
||||
unit_indices[i].push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unit_indices
|
||||
}
|
||||
|
||||
/// Global shared data that must be computed once and shared across all processing units.
|
||||
#[allow(dead_code)]
|
||||
pub struct GlobalSharedData {
|
||||
/// Ground/elevation data (must be consistent across boundaries)
|
||||
pub ground: Arc<Ground>,
|
||||
/// Building footprints bitmap (prevents trees inside buildings at boundaries)
|
||||
pub building_footprints: Arc<BuildingFootprintBitmap>,
|
||||
/// Highway connectivity map (for intersection detection)
|
||||
pub highway_connectivity: Arc<HighwayConnectivityMap>,
|
||||
}
|
||||
|
||||
/// Statistics from parallel processing.
|
||||
#[derive(Default)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ProcessingStats {
|
||||
pub total_units: u64,
|
||||
pub completed_units: AtomicU64,
|
||||
pub total_elements: u64,
|
||||
}
|
||||
|
||||
impl ProcessingStats {
|
||||
pub fn new(total_units: usize, total_elements: usize) -> Self {
|
||||
Self {
|
||||
total_units: total_units as u64,
|
||||
completed_units: AtomicU64::new(0),
|
||||
total_elements: total_elements as u64,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn increment_completed(&self) -> u64 {
|
||||
self.completed_units.fetch_add(1, Ordering::SeqCst) + 1
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn progress_percentage(&self) -> f64 {
|
||||
let completed = self.completed_units.load(Ordering::SeqCst);
|
||||
if self.total_units == 0 {
|
||||
100.0
|
||||
} else {
|
||||
(completed as f64 / self.total_units as f64) * 100.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clips an element to a unit's actual processing bounds (not fetch bounds).
|
||||
///
|
||||
/// Returns Some(clipped_element) if the element has any part within the unit's bounds,
|
||||
/// or None if the element is completely outside.
|
||||
#[allow(dead_code)]
|
||||
pub fn clip_element_to_unit(
|
||||
element: &ProcessedElement,
|
||||
unit: &ProcessingUnit,
|
||||
) -> Option<ProcessedElement> {
|
||||
match element {
|
||||
ProcessedElement::Node(node) => {
|
||||
// Nodes are either fully inside or outside
|
||||
if node.x >= unit.min_x
|
||||
&& node.x <= unit.max_x
|
||||
&& node.z >= unit.min_z
|
||||
&& node.z <= unit.max_z
|
||||
{
|
||||
Some(element.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ProcessedElement::Way(way) => {
|
||||
// For ways, we keep the full way but let the WorldEditor handle bounds checking
|
||||
// This ensures deterministic RNG produces the same results regardless of clipping
|
||||
// The WorldEditor.set_block() already checks bounds via xzbbox.contains()
|
||||
let (min_x, max_x, min_z, max_z) = way_bbox(&way.nodes);
|
||||
|
||||
// Check if way intersects unit bounds
|
||||
if max_x < unit.min_x
|
||||
|| min_x > unit.max_x
|
||||
|| max_z < unit.min_z
|
||||
|| min_z > unit.max_z
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(element.clone())
|
||||
}
|
||||
ProcessedElement::Relation(_rel) => {
|
||||
// For relations, similar approach - keep full relation for consistent processing
|
||||
let (min_x, max_x, min_z, max_z) = element_bbox(element);
|
||||
|
||||
// Check if relation intersects unit bounds
|
||||
if max_x < unit.min_x
|
||||
|| min_x > unit.max_x
|
||||
|| max_z < unit.min_z
|
||||
|| min_z > unit.max_z
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(element.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clips a collection of elements to a unit's bounds.
|
||||
#[allow(dead_code)]
|
||||
pub fn clip_elements_to_unit(
|
||||
elements: &[&ProcessedElement],
|
||||
unit: &ProcessingUnit,
|
||||
) -> Vec<ProcessedElement> {
|
||||
elements
|
||||
.iter()
|
||||
.filter_map(|e| clip_element_to_unit(e, unit))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calculates the number of parallel threads to use.
|
||||
pub fn calculate_parallel_threads(requested: usize) -> usize {
|
||||
let available = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(4);
|
||||
|
||||
if requested == 0 {
|
||||
// Default: use all but one core
|
||||
available.saturating_sub(1).max(1)
|
||||
} else {
|
||||
// Use requested amount, capped at available
|
||||
requested.min(available).max(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for parallel processing
|
||||
pub struct ParallelConfig {
|
||||
/// Number of threads to use (0 = auto, uses available - 1)
|
||||
pub num_threads: usize,
|
||||
/// Buffer in blocks around each unit for element fetching
|
||||
pub buffer_blocks: i32,
|
||||
/// Whether to use parallel processing (false = sequential for debugging)
|
||||
pub enabled: bool,
|
||||
/// Number of regions to batch per unit (1 = single region, 2 = 2x2 = 4 regions)
|
||||
pub region_batch_size: usize,
|
||||
}
|
||||
|
||||
impl Default for ParallelConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
num_threads: 0, // Auto-detect
|
||||
buffer_blocks: 64, // Buffer for boundary elements
|
||||
enabled: true,
|
||||
region_batch_size: 2, // 2x2=4 regions per unit - optimal balance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParallelConfig {
|
||||
pub fn sequential() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::coordinate_system::cartesian::xzbbox::rectangle::XZBBoxRect;
|
||||
|
||||
fn make_test_bbox(min_x: i32, max_x: i32, min_z: i32, max_z: i32) -> XZBBox {
|
||||
XZBBox::Rect(
|
||||
XZBBoxRect::new(XZPoint::new(min_x, min_z), XZPoint::new(max_x, max_z)).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_processing_unit_creation() {
|
||||
let global_bbox = make_test_bbox(0, 1023, 0, 1023);
|
||||
let unit = ProcessingUnit::new(0, 0, &global_bbox, 64);
|
||||
|
||||
assert_eq!(unit.region_x, 0);
|
||||
assert_eq!(unit.region_z, 0);
|
||||
assert_eq!(unit.min_x, 0);
|
||||
assert_eq!(unit.max_x, 511);
|
||||
assert_eq!(unit.min_z, 0);
|
||||
assert_eq!(unit.max_z, 511);
|
||||
// Fetch bounds should be clamped to global bbox
|
||||
assert_eq!(unit.fetch_min_x, 0); // Can't go below 0
|
||||
assert_eq!(unit.fetch_max_x, 575); // 511 + 64
|
||||
assert_eq!(unit.fetch_min_z, 0);
|
||||
assert_eq!(unit.fetch_max_z, 575);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_processing_units() {
|
||||
// A 2x2 region area
|
||||
let global_bbox = make_test_bbox(0, 1023, 0, 1023);
|
||||
let units = compute_processing_units(&global_bbox, 64, 1);
|
||||
|
||||
assert_eq!(units.len(), 4); // 2x2 = 4 regions
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_processing_units_batched() {
|
||||
// A 2x2 region area with batch size 2 should create 1 unit
|
||||
let global_bbox = make_test_bbox(0, 1023, 0, 1023);
|
||||
let units = compute_processing_units(&global_bbox, 64, 2);
|
||||
|
||||
assert_eq!(units.len(), 1); // All 4 regions in one batch
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_parallel_threads() {
|
||||
// Test default (0 = available - 1)
|
||||
let threads = calculate_parallel_threads(0);
|
||||
assert!(threads >= 1);
|
||||
|
||||
// Test explicit request
|
||||
let threads = calculate_parallel_threads(2);
|
||||
assert!(threads >= 1 && threads <= 2);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use crate::progress::{emit_gui_error, emit_gui_progress_update, is_running_with_
|
||||
#[cfg(feature = "gui")]
|
||||
use crate::telemetry::{send_log, LogLevel};
|
||||
use colored::Colorize;
|
||||
use rand::prelude::IndexedRandom;
|
||||
use rand::seq::SliceRandom;
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::blocking::ClientBuilder;
|
||||
use serde::Deserialize;
|
||||
@@ -117,14 +117,13 @@ pub fn fetch_data_from_overpass(
|
||||
];
|
||||
let fallback_api_servers: Vec<&str> =
|
||||
vec!["https://maps.mail.ru/osm/tools/overpass/api/interpreter"];
|
||||
let mut url: &&str = api_servers.choose(&mut rand::rng()).unwrap();
|
||||
let mut url: &&str = api_servers.choose(&mut rand::thread_rng()).unwrap();
|
||||
|
||||
// Generate Overpass API query for bounding box
|
||||
let query: String = format!(
|
||||
r#"[out:json][timeout:360][bbox:{},{},{},{}];
|
||||
(
|
||||
nwr["building"];
|
||||
nwr["building:part"];
|
||||
nwr["highway"];
|
||||
nwr["landuse"];
|
||||
nwr["natural"];
|
||||
@@ -135,17 +134,9 @@ pub fn fetch_data_from_overpass(
|
||||
nwr["tourism"];
|
||||
nwr["bridge"];
|
||||
nwr["railway"];
|
||||
nwr["roller_coaster"];
|
||||
nwr["barrier"];
|
||||
nwr["entrance"];
|
||||
nwr["door"];
|
||||
nwr["power"];
|
||||
nwr["historic"];
|
||||
nwr["emergency"];
|
||||
nwr["advertising"];
|
||||
nwr["man_made"];
|
||||
nwr["aeroway"];
|
||||
way["place"];
|
||||
way;
|
||||
)->.relsinbbox;
|
||||
(
|
||||
@@ -185,7 +176,9 @@ pub fn fetch_data_from_overpass(
|
||||
}
|
||||
|
||||
println!("Request failed. Switching to fallback url...");
|
||||
url = fallback_api_servers.choose(&mut rand::rng()).unwrap();
|
||||
url = fallback_api_servers
|
||||
.choose(&mut rand::thread_rng())
|
||||
.unwrap();
|
||||
attempt += 1;
|
||||
}
|
||||
}
|
||||
|
||||
328
src/unit_processing.rs
Normal file
328
src/unit_processing.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
//! Per-unit processing logic for parallel world generation.
|
||||
//!
|
||||
//! This module contains the functions that process a single region unit,
|
||||
//! generating all the elements within that unit's bounds.
|
||||
|
||||
use crate::args::Args;
|
||||
use crate::block_definitions::{BEDROCK, DIRT, GRASS_BLOCK, STONE};
|
||||
use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::data_processing::MIN_Y;
|
||||
use crate::element_processing::*;
|
||||
use crate::floodfill_cache::{BuildingFootprintBitmap, FloodFillCache};
|
||||
use crate::ground::Ground;
|
||||
use crate::osm_parser::ProcessedElement;
|
||||
use crate::parallel_processing::ProcessingUnit;
|
||||
use crate::world_editor::{WorldEditor, WorldFormat};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::element_processing::highways::HighwayConnectivityMap;
|
||||
|
||||
/// Shared data for unit processing - passed by reference to each unit
|
||||
#[allow(dead_code)]
|
||||
pub struct SharedProcessingData {
|
||||
pub ground: Arc<Ground>,
|
||||
pub highway_connectivity: Arc<HighwayConnectivityMap>,
|
||||
pub building_footprints: Arc<BuildingFootprintBitmap>,
|
||||
pub floodfill_cache: Arc<FloodFillCache>,
|
||||
pub llbbox: LLBBox,
|
||||
pub world_dir: PathBuf,
|
||||
pub format: WorldFormat,
|
||||
pub level_name: Option<String>,
|
||||
pub terrain_enabled: bool,
|
||||
pub ground_level: i32,
|
||||
pub fill_ground: bool,
|
||||
pub interior: bool,
|
||||
pub roof: bool,
|
||||
pub debug: bool,
|
||||
pub timeout: Option<std::time::Duration>,
|
||||
}
|
||||
|
||||
/// Process a single unit with element references (no cloning).
|
||||
/// The caller is responsible for saving and dropping the editor to free memory.
|
||||
pub fn process_unit_refs<'a>(
|
||||
unit: &ProcessingUnit,
|
||||
elements: &[&ProcessedElement],
|
||||
shared: &SharedProcessingData,
|
||||
unit_bbox: &'a XZBBox,
|
||||
args: &Args,
|
||||
) -> WorldEditor<'a> {
|
||||
// Create a WorldEditor for just this unit's bounds
|
||||
let mut editor = WorldEditor::new_with_format_and_name(
|
||||
shared.world_dir.clone(),
|
||||
unit_bbox,
|
||||
shared.llbbox,
|
||||
shared.format,
|
||||
shared.level_name.clone(),
|
||||
None, // Spawn point not set per-unit
|
||||
);
|
||||
|
||||
// Set ground reference for elevation-aware block placement
|
||||
editor.set_ground(Arc::clone(&shared.ground));
|
||||
|
||||
// Process all elements for this unit
|
||||
for element in elements {
|
||||
process_element(
|
||||
&mut editor,
|
||||
element,
|
||||
&shared.highway_connectivity,
|
||||
&shared.floodfill_cache,
|
||||
&shared.building_footprints,
|
||||
unit_bbox,
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
||||
// Generate ground layer for this unit
|
||||
generate_ground_for_unit(&mut editor, unit, shared, args);
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
/// Process a single unit and return the WorldEditor with blocks placed.
|
||||
/// The caller is responsible for saving and dropping the editor to free memory.
|
||||
#[allow(dead_code)]
|
||||
pub fn process_unit<'a>(
|
||||
unit: &ProcessingUnit,
|
||||
elements: &[ProcessedElement],
|
||||
shared: &SharedProcessingData,
|
||||
unit_bbox: &'a XZBBox,
|
||||
args: &Args,
|
||||
) -> WorldEditor<'a> {
|
||||
// Create a WorldEditor for just this unit's bounds
|
||||
let mut editor = WorldEditor::new_with_format_and_name(
|
||||
shared.world_dir.clone(),
|
||||
unit_bbox,
|
||||
shared.llbbox,
|
||||
shared.format,
|
||||
shared.level_name.clone(),
|
||||
None, // Spawn point not set per-unit
|
||||
);
|
||||
|
||||
// Set ground reference for elevation-aware block placement
|
||||
editor.set_ground(Arc::clone(&shared.ground));
|
||||
|
||||
// Process all elements for this unit
|
||||
for element in elements {
|
||||
process_element(
|
||||
&mut editor,
|
||||
element,
|
||||
&shared.highway_connectivity,
|
||||
&shared.floodfill_cache,
|
||||
&shared.building_footprints,
|
||||
unit_bbox,
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
||||
// Generate ground layer for this unit
|
||||
generate_ground_for_unit(&mut editor, unit, shared, args);
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
/// Process a single element, dispatching to the appropriate generator
|
||||
fn process_element(
|
||||
editor: &mut WorldEditor,
|
||||
element: &ProcessedElement,
|
||||
highway_connectivity: &HighwayConnectivityMap,
|
||||
flood_fill_cache: &FloodFillCache,
|
||||
building_footprints: &BuildingFootprintBitmap,
|
||||
xzbbox: &XZBBox,
|
||||
args: &Args,
|
||||
) {
|
||||
match element {
|
||||
ProcessedElement::Way(way) => {
|
||||
if way.tags.contains_key("building") || way.tags.contains_key("building:part") {
|
||||
buildings::generate_buildings(editor, way, args, None, flood_fill_cache);
|
||||
} else if way.tags.contains_key("highway") {
|
||||
highways::generate_highways(
|
||||
editor,
|
||||
element,
|
||||
args,
|
||||
highway_connectivity,
|
||||
flood_fill_cache,
|
||||
);
|
||||
} else if way.tags.contains_key("landuse") {
|
||||
landuse::generate_landuse(
|
||||
editor,
|
||||
way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
} else if way.tags.contains_key("natural") {
|
||||
natural::generate_natural(
|
||||
editor,
|
||||
element,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
} else if way.tags.contains_key("amenity") {
|
||||
amenities::generate_amenities(editor, element, args, flood_fill_cache);
|
||||
} else if way.tags.contains_key("leisure") {
|
||||
leisure::generate_leisure(
|
||||
editor,
|
||||
way,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
} else if way.tags.contains_key("barrier") {
|
||||
barriers::generate_barriers(editor, element);
|
||||
} else if let Some(val) = way.tags.get("waterway") {
|
||||
if val == "dock" {
|
||||
water_areas::generate_water_area_from_way(editor, way, xzbbox);
|
||||
} else {
|
||||
waterways::generate_waterways(editor, way);
|
||||
}
|
||||
} else if way.tags.contains_key("bridge") {
|
||||
// bridges::generate_bridges(editor, way, ground_level); // TODO FIX
|
||||
} else if way.tags.contains_key("railway") {
|
||||
railways::generate_railways(editor, way);
|
||||
} else if way.tags.contains_key("roller_coaster") {
|
||||
railways::generate_roller_coaster(editor, way);
|
||||
} else if way.tags.contains_key("aeroway") || way.tags.contains_key("area:aeroway") {
|
||||
highways::generate_aeroway(editor, way, args);
|
||||
} else if way.tags.get("service") == Some(&"siding".to_string()) {
|
||||
highways::generate_siding(editor, way);
|
||||
} else if way.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made(editor, element, args);
|
||||
}
|
||||
}
|
||||
ProcessedElement::Node(node) => {
|
||||
if node.tags.contains_key("door") || node.tags.contains_key("entrance") {
|
||||
doors::generate_doors(editor, node);
|
||||
} else if node.tags.contains_key("natural")
|
||||
&& node.tags.get("natural") == Some(&"tree".to_string())
|
||||
{
|
||||
natural::generate_natural(
|
||||
editor,
|
||||
element,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
} else if node.tags.contains_key("amenity") {
|
||||
amenities::generate_amenities(editor, element, args, flood_fill_cache);
|
||||
} else if node.tags.contains_key("barrier") {
|
||||
barriers::generate_barrier_nodes(editor, node);
|
||||
} else if node.tags.contains_key("highway") {
|
||||
highways::generate_highways(
|
||||
editor,
|
||||
element,
|
||||
args,
|
||||
highway_connectivity,
|
||||
flood_fill_cache,
|
||||
);
|
||||
} else if node.tags.contains_key("tourism") {
|
||||
tourisms::generate_tourisms(editor, node);
|
||||
} else if node.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made_nodes(editor, node);
|
||||
}
|
||||
}
|
||||
ProcessedElement::Relation(rel) => {
|
||||
if rel.tags.contains_key("building") || rel.tags.contains_key("building:part") {
|
||||
buildings::generate_building_from_relation(editor, rel, args, flood_fill_cache);
|
||||
} else if rel.tags.contains_key("water")
|
||||
|| rel
|
||||
.tags
|
||||
.get("natural")
|
||||
.map(|val| val == "water" || val == "bay")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
water_areas::generate_water_areas_from_relation(editor, rel, xzbbox);
|
||||
} else if rel.tags.contains_key("natural") {
|
||||
natural::generate_natural_from_relation(
|
||||
editor,
|
||||
rel,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
} else if rel.tags.contains_key("landuse") {
|
||||
landuse::generate_landuse_from_relation(
|
||||
editor,
|
||||
rel,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
} else if rel.tags.get("leisure") == Some(&"park".to_string()) {
|
||||
leisure::generate_leisure_from_relation(
|
||||
editor,
|
||||
rel,
|
||||
args,
|
||||
flood_fill_cache,
|
||||
building_footprints,
|
||||
);
|
||||
} else if rel.tags.contains_key("man_made") {
|
||||
man_made::generate_man_made(editor, element, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate ground layer (grass, dirt, bedrock) for a unit
|
||||
fn generate_ground_for_unit(
|
||||
editor: &mut WorldEditor,
|
||||
unit: &ProcessingUnit,
|
||||
shared: &SharedProcessingData,
|
||||
_args: &Args,
|
||||
) {
|
||||
let terrain_enabled = shared.terrain_enabled;
|
||||
let ground_level = shared.ground_level;
|
||||
|
||||
// Process chunk by chunk within this unit for cache locality
|
||||
let min_chunk_x = unit.min_x >> 4;
|
||||
let max_chunk_x = unit.max_x >> 4;
|
||||
let min_chunk_z = unit.min_z >> 4;
|
||||
let max_chunk_z = unit.max_z >> 4;
|
||||
|
||||
for chunk_x in min_chunk_x..=max_chunk_x {
|
||||
for chunk_z in min_chunk_z..=max_chunk_z {
|
||||
// Calculate the block range for this chunk, clamped to unit bounds
|
||||
let chunk_min_x = (chunk_x << 4).max(unit.min_x);
|
||||
let chunk_max_x = ((chunk_x << 4) + 15).min(unit.max_x);
|
||||
let chunk_min_z = (chunk_z << 4).max(unit.min_z);
|
||||
let chunk_max_z = ((chunk_z << 4) + 15).min(unit.max_z);
|
||||
|
||||
for x in chunk_min_x..=chunk_max_x {
|
||||
for z in chunk_min_z..=chunk_max_z {
|
||||
let ground_y = if terrain_enabled {
|
||||
editor.get_ground_level(x, z)
|
||||
} else {
|
||||
ground_level
|
||||
};
|
||||
|
||||
// Add default dirt and grass layer if there isn't a stone layer already
|
||||
if !editor.check_for_block_absolute(x, ground_y, z, Some(&[STONE]), None) {
|
||||
editor.set_block_absolute(GRASS_BLOCK, x, ground_y, z, None, None);
|
||||
editor.set_block_absolute(DIRT, x, ground_y - 1, z, None, None);
|
||||
editor.set_block_absolute(DIRT, x, ground_y - 2, z, None, None);
|
||||
}
|
||||
|
||||
// Fill underground with stone if enabled
|
||||
if shared.fill_ground {
|
||||
editor.fill_blocks_absolute(
|
||||
STONE,
|
||||
x,
|
||||
MIN_Y + 1,
|
||||
z,
|
||||
x,
|
||||
ground_y - 3,
|
||||
z,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
// Generate bedrock at MIN_Y
|
||||
editor.set_block_absolute(BEDROCK, x, MIN_Y, z, None, Some(&[BEDROCK]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,848 +0,0 @@
|
||||
//! Urban ground detection and generation based on building clusters.
|
||||
//!
|
||||
//! This module computes urban areas by analyzing building density and clustering,
|
||||
//! then generates appropriate ground blocks (smooth stone) for those areas.
|
||||
//!
|
||||
//! # Algorithm Overview
|
||||
//!
|
||||
//! 1. **Grid-based density analysis**: Divide the world into cells and count buildings per cell
|
||||
//! 2. **Connected component detection**: Find clusters of dense cells using flood fill
|
||||
//! 3. **Cluster filtering**: Only keep clusters with enough buildings to be considered "urban"
|
||||
//! 4. **Concave hull computation**: Compute a tight-fitting boundary around each cluster
|
||||
//! 5. **Ground filling**: Fill the hull area with stone blocks
|
||||
//!
|
||||
//! This approach handles various scenarios:
|
||||
//! - Full city coverage: Large connected cluster
|
||||
//! - Multiple cities: Separate clusters, each gets its own hull
|
||||
//! - Rural areas: No clusters meet threshold, no stone placed
|
||||
//! - Isolated buildings: Don't meet cluster threshold, remain on grass
|
||||
|
||||
use crate::coordinate_system::cartesian::XZBBox;
|
||||
use crate::floodfill::flood_fill_area;
|
||||
use geo::{ConcaveHull, ConvexHull, MultiPoint, Point, Polygon, Simplify};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Configuration for urban ground detection.
|
||||
///
|
||||
/// These parameters control how building clusters are identified and
|
||||
/// how the urban ground boundary is computed.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UrbanGroundConfig {
|
||||
/// Grid cell size for density analysis (in blocks).
|
||||
/// Smaller = more precise but slower. Default: 64 blocks (4 chunks).
|
||||
pub cell_size: i32,
|
||||
|
||||
/// Minimum buildings per cell to consider it potentially urban.
|
||||
/// Cells below this threshold are ignored. Default: 1.
|
||||
pub min_buildings_per_cell: usize,
|
||||
|
||||
/// Minimum total buildings in a connected cluster to be considered urban.
|
||||
/// Small clusters (villages, isolated buildings) won't get stone ground. Default: 5.
|
||||
pub min_buildings_for_cluster: usize,
|
||||
|
||||
/// Concavity parameter for hull computation (used in legacy hull-based method).
|
||||
/// Lower = tighter fit to buildings (more concave), Higher = smoother (more convex).
|
||||
/// Range: 1.0 (very tight) to 10.0 (almost convex). Default: 2.0.
|
||||
pub concavity: f64,
|
||||
|
||||
/// Whether to expand the hull slightly beyond building boundaries (used in legacy method).
|
||||
/// This creates a small buffer zone around the urban area. Default: true.
|
||||
pub expand_hull: bool,
|
||||
|
||||
/// Base number of cells to expand the urban region.
|
||||
/// This helps fill small gaps between buildings. Adaptive expansion may increase this.
|
||||
/// Default: 2.
|
||||
pub cell_expansion: i32,
|
||||
}
|
||||
|
||||
impl Default for UrbanGroundConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cell_size: 64, // Smaller cells for better granularity (4 chunks instead of 6)
|
||||
min_buildings_per_cell: 1,
|
||||
min_buildings_for_cluster: 5,
|
||||
concavity: 2.0,
|
||||
expand_hull: true,
|
||||
cell_expansion: 2, // Larger expansion to connect spread-out buildings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a detected urban cluster with its buildings and computed boundary.
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct UrbanCluster {
|
||||
/// Grid cells that belong to this cluster
|
||||
cells: Vec<(i32, i32)>,
|
||||
/// Building centroids within this cluster
|
||||
building_centroids: Vec<(i32, i32)>,
|
||||
/// Total number of buildings in the cluster
|
||||
building_count: usize,
|
||||
}
|
||||
|
||||
/// A compact lookup structure for checking if a coordinate is in an urban area.
|
||||
///
|
||||
/// Instead of storing millions of individual coordinates, this stores only
|
||||
/// the cell indices (thousands) and performs O(1) lookups. This reduces
|
||||
/// memory usage by ~4000x compared to storing all coordinates.
|
||||
///
|
||||
/// # Memory Usage
|
||||
/// - 7.8 km² area: ~17K cells × 16 bytes = ~270 KB (vs ~560 MB for coordinates)
|
||||
/// - 100 km² area: ~220K cells × 16 bytes = ~3.5 MB (vs ~7 GB for coordinates)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UrbanGroundLookup {
|
||||
/// Set of cell indices (cx, cz) that are urban
|
||||
urban_cells: HashSet<(i32, i32)>,
|
||||
/// Cell size used for coordinate-to-cell conversion
|
||||
cell_size: i32,
|
||||
/// Bounding box origin for coordinate conversion
|
||||
bbox_min_x: i32,
|
||||
bbox_min_z: i32,
|
||||
}
|
||||
|
||||
impl UrbanGroundLookup {
|
||||
/// Creates an empty lookup (no urban areas).
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
urban_cells: HashSet::new(),
|
||||
cell_size: 64,
|
||||
bbox_min_x: 0,
|
||||
bbox_min_z: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the given world coordinate is in an urban area.
|
||||
#[inline]
|
||||
pub fn is_urban(&self, x: i32, z: i32) -> bool {
|
||||
if self.urban_cells.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let cx = (x - self.bbox_min_x) / self.cell_size;
|
||||
let cz = (z - self.bbox_min_z) / self.cell_size;
|
||||
self.urban_cells.contains(&(cx, cz))
|
||||
}
|
||||
|
||||
/// Returns the number of urban cells.
|
||||
#[allow(dead_code)]
|
||||
pub fn cell_count(&self) -> usize {
|
||||
self.urban_cells.len()
|
||||
}
|
||||
|
||||
/// Returns true if there are no urban areas.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.urban_cells.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes urban ground areas from building locations.
|
||||
pub struct UrbanGroundComputer {
|
||||
config: UrbanGroundConfig,
|
||||
building_centroids: Vec<(i32, i32)>,
|
||||
xzbbox: XZBBox,
|
||||
}
|
||||
|
||||
impl UrbanGroundComputer {
|
||||
/// Creates a new urban ground computer with the given world bounds and configuration.
|
||||
pub fn new(xzbbox: XZBBox, config: UrbanGroundConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
building_centroids: Vec::new(),
|
||||
xzbbox,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new urban ground computer with default configuration.
|
||||
pub fn with_defaults(xzbbox: XZBBox) -> Self {
|
||||
Self::new(xzbbox, UrbanGroundConfig::default())
|
||||
}
|
||||
|
||||
/// Adds a building centroid to be considered for urban area detection.
|
||||
#[inline]
|
||||
pub fn add_building_centroid(&mut self, x: i32, z: i32) {
|
||||
// Only add if within bounds
|
||||
if x >= self.xzbbox.min_x()
|
||||
&& x <= self.xzbbox.max_x()
|
||||
&& z >= self.xzbbox.min_z()
|
||||
&& z <= self.xzbbox.max_z()
|
||||
{
|
||||
self.building_centroids.push((x, z));
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds multiple building centroids from an iterator.
|
||||
pub fn add_building_centroids<I>(&mut self, centroids: I)
|
||||
where
|
||||
I: IntoIterator<Item = (i32, i32)>,
|
||||
{
|
||||
for (x, z) in centroids {
|
||||
self.add_building_centroid(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of buildings added.
|
||||
#[allow(dead_code)]
|
||||
pub fn building_count(&self) -> usize {
|
||||
self.building_centroids.len()
|
||||
}
|
||||
|
||||
/// Computes all urban ground coordinates.
|
||||
///
|
||||
/// Returns a list of (x, z) coordinates that should have stone ground.
|
||||
/// The coordinates are clipped to the world bounding box.
|
||||
///
|
||||
/// Performance: Uses cell-based filling for O(cells) complexity instead of
|
||||
/// flood-filling complex hulls which would be O(area). For a city with 1000
|
||||
/// buildings in 100 cells, this is ~100x faster than flood fill.
|
||||
///
|
||||
/// NOTE: For better performance and memory usage, prefer `compute_lookup()`.
|
||||
#[allow(dead_code)]
|
||||
pub fn compute(&self, _timeout: Option<&Duration>) -> Vec<(i32, i32)> {
|
||||
// Not enough buildings for any urban area
|
||||
if self.building_centroids.len() < self.config.min_buildings_for_cluster {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Step 1: Create density grid (cell -> buildings in that cell)
|
||||
let grid = self.create_density_grid();
|
||||
|
||||
// Step 2: Find connected urban regions and get their expanded cells
|
||||
let clusters = self.find_urban_clusters(&grid);
|
||||
|
||||
if clusters.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Step 3: Fill cells directly instead of using expensive flood fill on hulls
|
||||
// This is much faster: O(cells × cell_size²) vs O(hull_area) for flood fill
|
||||
let mut all_coords = Vec::new();
|
||||
for cluster in clusters {
|
||||
let coords = self.fill_cluster_cells(&cluster);
|
||||
all_coords.extend(coords);
|
||||
}
|
||||
|
||||
all_coords
|
||||
}
|
||||
|
||||
/// Computes urban ground and returns a compact lookup structure.
|
||||
///
|
||||
/// This is the preferred method for production use. Instead of returning
|
||||
/// millions of coordinates (high memory), it returns a lookup structure
|
||||
/// that stores only cell indices (~4000x less memory) and provides O(1)
|
||||
/// coordinate lookups.
|
||||
///
|
||||
/// # Memory Comparison
|
||||
/// - `compute()`: ~560 MB for 7.8 km² area
|
||||
/// - `compute_lookup()`: ~270 KB for same area
|
||||
pub fn compute_lookup(&self) -> UrbanGroundLookup {
|
||||
// Not enough buildings for any urban area
|
||||
if self.building_centroids.len() < self.config.min_buildings_for_cluster {
|
||||
return UrbanGroundLookup::empty();
|
||||
}
|
||||
|
||||
// Step 1: Create density grid (cell -> buildings in that cell)
|
||||
let grid = self.create_density_grid();
|
||||
|
||||
// Step 2: Find connected urban regions and get their expanded cells
|
||||
let clusters = self.find_urban_clusters(&grid);
|
||||
|
||||
if clusters.is_empty() {
|
||||
return UrbanGroundLookup::empty();
|
||||
}
|
||||
|
||||
// Step 3: Collect all expanded cells from all clusters into a HashSet
|
||||
let mut urban_cells = HashSet::new();
|
||||
for cluster in clusters {
|
||||
urban_cells.extend(cluster.cells.iter().copied());
|
||||
}
|
||||
|
||||
UrbanGroundLookup {
|
||||
urban_cells,
|
||||
cell_size: self.config.cell_size,
|
||||
bbox_min_x: self.xzbbox.min_x(),
|
||||
bbox_min_z: self.xzbbox.min_z(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills all cells in a cluster directly, returning coordinates.
|
||||
/// This is much faster than computing a hull and flood-filling it.
|
||||
fn fill_cluster_cells(&self, cluster: &UrbanCluster) -> Vec<(i32, i32)> {
|
||||
let mut coords = Vec::new();
|
||||
let cell_size = self.config.cell_size;
|
||||
|
||||
// Pre-calculate bounds once
|
||||
let bbox_min_x = self.xzbbox.min_x();
|
||||
let bbox_max_x = self.xzbbox.max_x();
|
||||
let bbox_min_z = self.xzbbox.min_z();
|
||||
let bbox_max_z = self.xzbbox.max_z();
|
||||
|
||||
for &(cx, cz) in &cluster.cells {
|
||||
// Calculate cell bounds in world coordinates
|
||||
let cell_min_x = (bbox_min_x + cx * cell_size).max(bbox_min_x);
|
||||
let cell_max_x = (bbox_min_x + (cx + 1) * cell_size - 1).min(bbox_max_x);
|
||||
let cell_min_z = (bbox_min_z + cz * cell_size).max(bbox_min_z);
|
||||
let cell_max_z = (bbox_min_z + (cz + 1) * cell_size - 1).min(bbox_max_z);
|
||||
|
||||
// Skip if cell is entirely outside bbox
|
||||
if cell_min_x > bbox_max_x
|
||||
|| cell_max_x < bbox_min_x
|
||||
|| cell_min_z > bbox_max_z
|
||||
|| cell_max_z < bbox_min_z
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fill all coordinates in this cell
|
||||
for x in cell_min_x..=cell_max_x {
|
||||
for z in cell_min_z..=cell_max_z {
|
||||
coords.push((x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coords
|
||||
}
|
||||
|
||||
/// Creates a density grid mapping cell coordinates to buildings in that cell.
|
||||
fn create_density_grid(&self) -> HashMap<(i32, i32), Vec<(i32, i32)>> {
|
||||
let mut grid: HashMap<(i32, i32), Vec<(i32, i32)>> = HashMap::new();
|
||||
|
||||
for &(x, z) in &self.building_centroids {
|
||||
let cell_x = (x - self.xzbbox.min_x()) / self.config.cell_size;
|
||||
let cell_z = (z - self.xzbbox.min_z()) / self.config.cell_size;
|
||||
grid.entry((cell_x, cell_z)).or_default().push((x, z));
|
||||
}
|
||||
|
||||
grid
|
||||
}
|
||||
|
||||
/// Finds connected clusters of urban cells.
|
||||
fn find_urban_clusters(
|
||||
&self,
|
||||
grid: &HashMap<(i32, i32), Vec<(i32, i32)>>,
|
||||
) -> Vec<UrbanCluster> {
|
||||
// Step 1: Identify cells that meet minimum density threshold
|
||||
let dense_cells: HashSet<(i32, i32)> = grid
|
||||
.iter()
|
||||
.filter(|(_, buildings)| buildings.len() >= self.config.min_buildings_per_cell)
|
||||
.map(|(&cell, _)| cell)
|
||||
.collect();
|
||||
|
||||
if dense_cells.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Step 2: Calculate adaptive expansion based on building density
|
||||
// For spread-out cities, we need more expansion to connect buildings
|
||||
let adaptive_expansion = self.calculate_adaptive_expansion(&dense_cells, grid);
|
||||
|
||||
// Step 3: Expand dense cells to connect nearby clusters
|
||||
let expanded_cells = self.expand_cells_adaptive(&dense_cells, adaptive_expansion);
|
||||
|
||||
// Step 4: Find connected components using flood fill
|
||||
let mut visited = HashSet::new();
|
||||
let mut clusters = Vec::new();
|
||||
|
||||
for &cell in &expanded_cells {
|
||||
if visited.contains(&cell) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// BFS to find connected component
|
||||
let mut component_cells = Vec::new();
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back(cell);
|
||||
visited.insert(cell);
|
||||
|
||||
while let Some(current) = queue.pop_front() {
|
||||
component_cells.push(current);
|
||||
|
||||
// Check 8-connected neighbors (including diagonals for better connectivity)
|
||||
for dz in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
if dx == 0 && dz == 0 {
|
||||
continue;
|
||||
}
|
||||
let neighbor = (current.0 + dx, current.1 + dz);
|
||||
if expanded_cells.contains(&neighbor) && !visited.contains(&neighbor) {
|
||||
visited.insert(neighbor);
|
||||
queue.push_back(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect buildings from the original dense cells only (not expanded empty cells)
|
||||
let mut cluster_buildings = Vec::new();
|
||||
for &cell in &component_cells {
|
||||
if let Some(buildings) = grid.get(&cell) {
|
||||
cluster_buildings.extend(buildings.iter().copied());
|
||||
}
|
||||
}
|
||||
|
||||
let building_count = cluster_buildings.len();
|
||||
|
||||
// Only keep clusters with enough buildings
|
||||
if building_count >= self.config.min_buildings_for_cluster {
|
||||
clusters.push(UrbanCluster {
|
||||
cells: component_cells,
|
||||
building_centroids: cluster_buildings,
|
||||
building_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clusters
|
||||
}
|
||||
|
||||
/// Calculates adaptive expansion based on building density.
|
||||
///
|
||||
/// For spread-out cities (low density), we need more expansion to connect
|
||||
/// buildings that are farther apart. For dense cities, less expansion is needed.
|
||||
fn calculate_adaptive_expansion(
|
||||
&self,
|
||||
dense_cells: &HashSet<(i32, i32)>,
|
||||
grid: &HashMap<(i32, i32), Vec<(i32, i32)>>,
|
||||
) -> i32 {
|
||||
if dense_cells.is_empty() {
|
||||
return self.config.cell_expansion;
|
||||
}
|
||||
|
||||
// Calculate total buildings and average per occupied cell
|
||||
let total_buildings: usize = dense_cells
|
||||
.iter()
|
||||
.filter_map(|cell| grid.get(cell))
|
||||
.map(|buildings| buildings.len())
|
||||
.sum();
|
||||
|
||||
let avg_buildings_per_cell = total_buildings as f64 / dense_cells.len() as f64;
|
||||
|
||||
// Calculate the "spread" of cells - how far apart are occupied cells?
|
||||
// Find bounding box of occupied cells
|
||||
if dense_cells.len() < 2 {
|
||||
return self.config.cell_expansion;
|
||||
}
|
||||
|
||||
let min_x = dense_cells.iter().map(|(x, _)| x).min().unwrap();
|
||||
let max_x = dense_cells.iter().map(|(x, _)| x).max().unwrap();
|
||||
let min_z = dense_cells.iter().map(|(_, z)| z).min().unwrap();
|
||||
let max_z = dense_cells.iter().map(|(_, z)| z).max().unwrap();
|
||||
|
||||
let grid_span_x = (max_x - min_x + 1) as f64;
|
||||
let grid_span_z = (max_z - min_z + 1) as f64;
|
||||
let total_possible_cells = grid_span_x * grid_span_z;
|
||||
|
||||
// Cell occupancy ratio: what fraction of the bounding box has buildings?
|
||||
let occupancy = dense_cells.len() as f64 / total_possible_cells;
|
||||
|
||||
// Adaptive expansion logic:
|
||||
// - High density (many buildings per cell) AND high occupancy = dense city, use base expansion
|
||||
// - Low density OR low occupancy = spread-out city, need more expansion
|
||||
|
||||
let base_expansion = self.config.cell_expansion;
|
||||
|
||||
// Scale factor: lower density = higher factor
|
||||
// avg_buildings_per_cell < 2 → spread out
|
||||
// occupancy < 0.3 → sparse grid with gaps
|
||||
let density_factor = if avg_buildings_per_cell < 3.0 {
|
||||
1.5
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let occupancy_factor = if occupancy < 0.4 {
|
||||
1.5
|
||||
} else if occupancy < 0.6 {
|
||||
1.25
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let adaptive = (base_expansion as f64 * density_factor * occupancy_factor).ceil() as i32;
|
||||
|
||||
// Cap at reasonable maximum (4 cells = 256 blocks with 64-block cells)
|
||||
adaptive.min(4).max(base_expansion)
|
||||
}
|
||||
|
||||
/// Expands the set of cells by adding neighbors within expansion distance.
|
||||
fn expand_cells_adaptive(
|
||||
&self,
|
||||
cells: &HashSet<(i32, i32)>,
|
||||
expansion: i32,
|
||||
) -> HashSet<(i32, i32)> {
|
||||
if expansion <= 0 {
|
||||
return cells.clone();
|
||||
}
|
||||
|
||||
let mut expanded = cells.clone();
|
||||
|
||||
for &(cx, cz) in cells {
|
||||
for dz in -expansion..=expansion {
|
||||
for dx in -expansion..=expansion {
|
||||
expanded.insert((cx + dx, cz + dz));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expanded
|
||||
}
|
||||
|
||||
/// Expands the set of cells by adding neighbors within expansion distance.
|
||||
#[allow(dead_code)]
|
||||
fn expand_cells(&self, cells: &HashSet<(i32, i32)>) -> HashSet<(i32, i32)> {
|
||||
self.expand_cells_adaptive(cells, self.config.cell_expansion)
|
||||
}
|
||||
|
||||
/// Computes ground coordinates for a single urban cluster.
|
||||
///
|
||||
/// NOTE: This hull-based method is kept for reference but not used in production.
|
||||
/// The cell-based `fill_cluster_cells` method is much faster.
|
||||
#[allow(dead_code)]
|
||||
fn compute_cluster_ground(
|
||||
&self,
|
||||
cluster: &UrbanCluster,
|
||||
grid: &HashMap<(i32, i32), Vec<(i32, i32)>>,
|
||||
timeout: Option<&Duration>,
|
||||
) -> Vec<(i32, i32)> {
|
||||
// Need at least 3 points for a hull
|
||||
if cluster.building_centroids.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Collect points for hull computation
|
||||
// Include building centroids plus cell corner points for better coverage
|
||||
let mut hull_points: Vec<(f64, f64)> = cluster
|
||||
.building_centroids
|
||||
.iter()
|
||||
.map(|&(x, z)| (x as f64, z as f64))
|
||||
.collect();
|
||||
|
||||
// Add cell boundary points if expand_hull is enabled
|
||||
// This ensures the hull extends slightly beyond buildings
|
||||
if self.config.expand_hull {
|
||||
for &(cx, cz) in &cluster.cells {
|
||||
// Only add corners for cells that actually have buildings
|
||||
if grid.get(&(cx, cz)).map(|b| !b.is_empty()).unwrap_or(false) {
|
||||
let base_x = (self.xzbbox.min_x() + cx * self.config.cell_size) as f64;
|
||||
let base_z = (self.xzbbox.min_z() + cz * self.config.cell_size) as f64;
|
||||
let size = self.config.cell_size as f64;
|
||||
|
||||
// Add cell corners with small padding
|
||||
let pad = size * 0.1; // 10% padding
|
||||
hull_points.push((base_x - pad, base_z - pad));
|
||||
hull_points.push((base_x + size + pad, base_z - pad));
|
||||
hull_points.push((base_x - pad, base_z + size + pad));
|
||||
hull_points.push((base_x + size + pad, base_z + size + pad));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to geo MultiPoint
|
||||
let multi_point: MultiPoint<f64> =
|
||||
hull_points.iter().map(|&(x, z)| Point::new(x, z)).collect();
|
||||
|
||||
// Compute hull based on point count
|
||||
let hull: Polygon<f64> = if hull_points.len() < 10 {
|
||||
// Too few points for concave hull, use convex
|
||||
multi_point.convex_hull()
|
||||
} else {
|
||||
// Use concave hull for better fit
|
||||
multi_point.concave_hull(self.config.concavity)
|
||||
};
|
||||
|
||||
// Simplify the hull to reduce vertex count (improves flood fill performance)
|
||||
let hull = hull.simplify(2.0);
|
||||
|
||||
// Convert hull to integer coordinates for flood fill
|
||||
self.fill_hull_polygon(&hull, timeout)
|
||||
}
|
||||
|
||||
/// Fills a hull polygon and returns all interior coordinates.
|
||||
///
|
||||
/// NOTE: This method is kept for reference but not used in production.
|
||||
/// The cell-based approach is much faster.
|
||||
#[allow(dead_code)]
|
||||
fn fill_hull_polygon(
|
||||
&self,
|
||||
polygon: &Polygon<f64>,
|
||||
timeout: Option<&Duration>,
|
||||
) -> Vec<(i32, i32)> {
|
||||
// Convert polygon exterior to integer coordinates
|
||||
let exterior: Vec<(i32, i32)> = polygon
|
||||
.exterior()
|
||||
.coords()
|
||||
.map(|c| (c.x.round() as i32, c.y.round() as i32))
|
||||
.collect();
|
||||
|
||||
if exterior.len() < 3 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Remove duplicate consecutive points (can cause flood fill issues)
|
||||
let mut clean_exterior = Vec::with_capacity(exterior.len());
|
||||
for point in exterior {
|
||||
if clean_exterior.last() != Some(&point) {
|
||||
clean_exterior.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the polygon is closed
|
||||
if clean_exterior.first() != clean_exterior.last() && !clean_exterior.is_empty() {
|
||||
clean_exterior.push(clean_exterior[0]);
|
||||
}
|
||||
|
||||
if clean_exterior.len() < 4 {
|
||||
// Need at least 3 unique points + closing point
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Use existing flood fill, clipping to bbox
|
||||
let filled = flood_fill_area(&clean_exterior, timeout);
|
||||
|
||||
// Filter to only include points within world bounds
|
||||
filled
|
||||
.into_iter()
|
||||
.filter(|&(x, z)| {
|
||||
x >= self.xzbbox.min_x()
|
||||
&& x <= self.xzbbox.max_x()
|
||||
&& z >= self.xzbbox.min_z()
|
||||
&& z <= self.xzbbox.max_z()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the centroid of a set of coordinates.
|
||||
///
|
||||
/// Returns None if the slice is empty.
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
pub fn compute_centroid(coords: &[(i32, i32)]) -> Option<(i32, i32)> {
|
||||
if coords.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let sum_x: i64 = coords.iter().map(|(x, _)| i64::from(*x)).sum();
|
||||
let sum_z: i64 = coords.iter().map(|(_, z)| i64::from(*z)).sum();
|
||||
let len = coords.len() as i64;
|
||||
Some(((sum_x / len) as i32, (sum_z / len) as i32))
|
||||
}
|
||||
|
||||
/// Convenience function to compute urban ground from building centroids.
|
||||
///
|
||||
/// NOTE: This function is kept for backward compatibility and tests.
|
||||
/// For production use, prefer `compute_urban_ground_lookup` which uses
|
||||
/// ~4000x less memory.
|
||||
#[allow(dead_code)]
|
||||
pub fn compute_urban_ground(
|
||||
building_centroids: Vec<(i32, i32)>,
|
||||
xzbbox: &XZBBox,
|
||||
timeout: Option<&Duration>,
|
||||
) -> Vec<(i32, i32)> {
|
||||
let mut computer = UrbanGroundComputer::with_defaults(xzbbox.clone());
|
||||
computer.add_building_centroids(building_centroids);
|
||||
computer.compute(timeout)
|
||||
}
|
||||
|
||||
/// Computes urban ground and returns a compact lookup structure.
|
||||
///
|
||||
/// This is the preferred entry point for production use. Returns a lookup
|
||||
/// structure that uses ~270 KB instead of ~560 MB for a typical city area.
|
||||
pub fn compute_urban_ground_lookup(
|
||||
building_centroids: Vec<(i32, i32)>,
|
||||
xzbbox: &XZBBox,
|
||||
) -> UrbanGroundLookup {
|
||||
let mut computer = UrbanGroundComputer::with_defaults(xzbbox.clone());
|
||||
computer.add_building_centroids(building_centroids);
|
||||
computer.compute_lookup()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_bbox() -> XZBBox {
|
||||
XZBBox::rect_from_xz_lengths(1000.0, 1000.0).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_buildings() {
|
||||
let computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||
let result = computer.compute(None);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_few_scattered_buildings() {
|
||||
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||
// Add a few scattered buildings (not enough for a cluster)
|
||||
computer.add_building_centroid(100, 100);
|
||||
computer.add_building_centroid(500, 500);
|
||||
computer.add_building_centroid(900, 900);
|
||||
|
||||
let result = computer.compute(None);
|
||||
assert!(
|
||||
result.is_empty(),
|
||||
"Scattered buildings should not form urban area"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dense_cluster() {
|
||||
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||
|
||||
// Add a dense cluster of buildings
|
||||
for i in 0..30 {
|
||||
for j in 0..30 {
|
||||
if (i + j) % 3 == 0 {
|
||||
// Add building every 3rd position
|
||||
computer.add_building_centroid(100 + i * 10, 100 + j * 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = computer.compute(None);
|
||||
assert!(
|
||||
!result.is_empty(),
|
||||
"Dense cluster should produce urban area"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_centroid() {
|
||||
let coords = vec![(0, 0), (10, 0), (10, 10), (0, 10)];
|
||||
let centroid = compute_centroid(&coords);
|
||||
assert_eq!(centroid, Some((5, 5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_centroid_empty() {
|
||||
let coords: Vec<(i32, i32)> = vec![];
|
||||
let centroid = compute_centroid(&coords);
|
||||
assert_eq!(centroid, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spread_out_buildings() {
|
||||
// Simulate a spread-out city like Erding where buildings are farther apart
|
||||
// This should still be detected as urban due to adaptive expansion
|
||||
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||
|
||||
// Add buildings spread across a larger area with gaps
|
||||
// Buildings are ~100-150 blocks apart (would fail with small expansion)
|
||||
let building_positions = [
|
||||
(100, 100),
|
||||
(250, 100),
|
||||
(400, 100),
|
||||
(100, 250),
|
||||
(250, 250),
|
||||
(400, 250),
|
||||
(100, 400),
|
||||
(250, 400),
|
||||
(400, 400),
|
||||
// Add a few more to ensure cluster threshold is met
|
||||
(175, 175),
|
||||
(325, 175),
|
||||
(175, 325),
|
||||
(325, 325),
|
||||
];
|
||||
|
||||
for (x, z) in building_positions {
|
||||
computer.add_building_centroid(x, z);
|
||||
}
|
||||
|
||||
let result = computer.compute(None);
|
||||
assert!(
|
||||
!result.is_empty(),
|
||||
"Spread-out buildings should still form urban area with adaptive expansion"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_expansion_calculated() {
|
||||
let bbox = create_test_bbox();
|
||||
let computer = UrbanGroundComputer::with_defaults(bbox);
|
||||
|
||||
// Create a sparse grid with low occupancy
|
||||
let mut dense_cells = HashSet::new();
|
||||
// Only 4 cells in a 10x10 potential grid = 4% occupancy
|
||||
dense_cells.insert((0, 0));
|
||||
dense_cells.insert((5, 0));
|
||||
dense_cells.insert((0, 5));
|
||||
dense_cells.insert((5, 5));
|
||||
|
||||
let mut grid = HashMap::new();
|
||||
// Only 1 building per cell (low density)
|
||||
grid.insert((0, 0), vec![(10, 10)]);
|
||||
grid.insert((5, 0), vec![(330, 10)]);
|
||||
grid.insert((0, 5), vec![(10, 330)]);
|
||||
grid.insert((5, 5), vec![(330, 330)]);
|
||||
|
||||
let expansion = computer.calculate_adaptive_expansion(&dense_cells, &grid);
|
||||
|
||||
// Should be higher than base (2) due to low occupancy and density
|
||||
assert!(
|
||||
expansion > 2,
|
||||
"Sparse grid should trigger higher expansion, got {}",
|
||||
expansion
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_empty() {
|
||||
let lookup = UrbanGroundLookup::empty();
|
||||
assert!(lookup.is_empty());
|
||||
assert!(!lookup.is_urban(100, 100));
|
||||
assert_eq!(lookup.cell_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_membership() {
|
||||
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||
|
||||
// Create a dense cluster of buildings
|
||||
for x in 0..10 {
|
||||
for z in 0..10 {
|
||||
computer.add_building_centroid(100 + x * 10, 100 + z * 10);
|
||||
}
|
||||
}
|
||||
|
||||
let lookup = computer.compute_lookup();
|
||||
assert!(!lookup.is_empty());
|
||||
|
||||
// Points inside the cluster should be urban
|
||||
assert!(
|
||||
lookup.is_urban(150, 150),
|
||||
"Center of cluster should be urban"
|
||||
);
|
||||
|
||||
// Points far outside the cluster should not be urban
|
||||
assert!(
|
||||
!lookup.is_urban(900, 900),
|
||||
"Point far from cluster should not be urban"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookup_vs_compute_consistency() {
|
||||
let mut computer = UrbanGroundComputer::with_defaults(create_test_bbox());
|
||||
|
||||
// Create a medium-sized cluster
|
||||
for x in 0..5 {
|
||||
for z in 0..5 {
|
||||
computer.add_building_centroid(200 + x * 20, 200 + z * 20);
|
||||
}
|
||||
}
|
||||
|
||||
let coords = computer.compute(None);
|
||||
let lookup = computer.compute_lookup();
|
||||
|
||||
// Every coordinate from compute() should be marked urban in lookup
|
||||
for (x, z) in &coords {
|
||||
assert!(
|
||||
lookup.is_urban(*x, *z),
|
||||
"Coordinate ({}, {}) should be urban in lookup",
|
||||
x,
|
||||
z
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,11 @@ use crate::ground::Ground;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
|
||||
use bedrockrs_level::level::db_interface::bedrock_key::ChunkKey;
|
||||
use bedrockrs_level::level::db_interface::key_level::KeyTypeTag;
|
||||
use bedrockrs_level::level::db_interface::rusty::{mcpe_options, RustyDBInterface};
|
||||
use bedrockrs_level::level::db_interface::rusty::RustyDBInterface;
|
||||
use bedrockrs_level::level::file_interface::RawWorldTrait;
|
||||
use bedrockrs_shared::world::dimension::Dimension;
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use fastnbt::Value;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use rusty_leveldb::DB;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap as StdHashMap;
|
||||
use std::fs::{self, File};
|
||||
@@ -85,8 +82,6 @@ impl From<serde_json::Error> for BedrockSaveError {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_BEDROCK_COMPRESSION_LEVEL: u8 = 6;
|
||||
|
||||
/// Metadata for Bedrock worlds
|
||||
#[derive(Serialize)]
|
||||
struct BedrockMetadata {
|
||||
@@ -407,7 +402,7 @@ impl BedrockWriter {
|
||||
// Open LevelDB with Bedrock-compatible options
|
||||
let mut state = ();
|
||||
let mut db: RustyDBInterface<()> =
|
||||
RustyDBInterface::new(db_path.clone().into_boxed_path(), true, &mut state)
|
||||
RustyDBInterface::new(db_path.into_boxed_path(), true, &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Count total chunks for progress
|
||||
@@ -421,128 +416,63 @@ impl BedrockWriter {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
{
|
||||
let progress_bar = ProgressBar::new(total_chunks as u64);
|
||||
progress_bar.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} chunks ({eta})")
|
||||
.unwrap()
|
||||
.progress_chars("█▓░"),
|
||||
);
|
||||
let progress_bar = ProgressBar::new(total_chunks as u64);
|
||||
progress_bar.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{elapsed_precise}] [{bar:45.white/black}] {pos}/{len} chunks ({eta})")
|
||||
.unwrap()
|
||||
.progress_chars("█▓░"),
|
||||
);
|
||||
|
||||
let mut chunks_processed: usize = 0;
|
||||
|
||||
// Process each region and chunk
|
||||
for ((region_x, region_z), region) in &world.regions {
|
||||
for ((local_chunk_x, local_chunk_z), chunk) in ®ion.chunks {
|
||||
// Calculate absolute chunk coordinates
|
||||
let abs_chunk_x = region_x * 32 + local_chunk_x;
|
||||
let abs_chunk_z = region_z * 32 + local_chunk_z;
|
||||
let chunk_pos = Vec2::new(abs_chunk_x, abs_chunk_z);
|
||||
|
||||
// Write chunk version marker (42 is current Bedrock version as of 1.21+)
|
||||
let version_key = ChunkKey::chunk_marker(chunk_pos, Dimension::Overworld);
|
||||
db.set_subchunk_raw(version_key, &[42], &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Write Data3D (heightmap + biomes) - required for chunk to be valid
|
||||
let data3d_key = ChunkKey::data3d(chunk_pos, Dimension::Overworld);
|
||||
let data3d = self.create_data3d(chunk);
|
||||
db.set_subchunk_raw(data3d_key, &data3d, &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Process each section (subchunk)
|
||||
for (§ion_y, section) in &chunk.sections {
|
||||
// Encode the subchunk
|
||||
let subchunk_bytes = self.encode_subchunk(section, section_y)?;
|
||||
|
||||
// Write to database
|
||||
let subchunk_key =
|
||||
ChunkKey::new_subchunk(chunk_pos, Dimension::Overworld, section_y);
|
||||
db.set_subchunk_raw(subchunk_key, &subchunk_bytes, &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
}
|
||||
|
||||
chunks_processed += 1;
|
||||
progress_bar.inc(1);
|
||||
|
||||
// Update GUI progress (92% to 97% range for chunk writing)
|
||||
if chunks_processed.is_multiple_of(10) || chunks_processed == total_chunks {
|
||||
let chunk_progress = chunks_processed as f64 / total_chunks as f64;
|
||||
let gui_progress = 92.0 + (chunk_progress * 5.0); // 92% to 97%
|
||||
emit_gui_progress_update(gui_progress, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progress_bar.finish_with_message("Chunks written to LevelDB");
|
||||
}
|
||||
|
||||
// Ensure the RustyDBInterface handle is dropped before opening another DB for the same path.
|
||||
drop(db);
|
||||
|
||||
self.write_chunk_entities(world, &db_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_chunk_entities(
|
||||
&self,
|
||||
world: &WorldToModify,
|
||||
db_path: &std::path::Path,
|
||||
) -> Result<(), BedrockSaveError> {
|
||||
let mut opts = mcpe_options(DEFAULT_BEDROCK_COMPRESSION_LEVEL);
|
||||
opts.create_if_missing = true;
|
||||
let mut db = DB::open(db_path.to_path_buf().into_boxed_path(), opts)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
let mut chunks_processed: usize = 0;
|
||||
|
||||
// Process each region and chunk
|
||||
for ((region_x, region_z), region) in &world.regions {
|
||||
for ((local_chunk_x, local_chunk_z), chunk) in ®ion.chunks {
|
||||
let chunk_pos =
|
||||
Vec2::new(region_x * 32 + local_chunk_x, region_z * 32 + local_chunk_z);
|
||||
// Calculate absolute chunk coordinates
|
||||
let abs_chunk_x = region_x * 32 + local_chunk_x;
|
||||
let abs_chunk_z = region_z * 32 + local_chunk_z;
|
||||
let chunk_pos = Vec2::new(abs_chunk_x, abs_chunk_z);
|
||||
|
||||
self.write_compound_list_record(
|
||||
&mut db,
|
||||
chunk_pos,
|
||||
KeyTypeTag::BlockEntity,
|
||||
chunk.other.get("block_entities"),
|
||||
)?;
|
||||
self.write_compound_list_record(
|
||||
&mut db,
|
||||
chunk_pos,
|
||||
KeyTypeTag::Entity,
|
||||
chunk.other.get("entities"),
|
||||
)?;
|
||||
// Write chunk version marker (42 is current Bedrock version as of 1.21+)
|
||||
let version_key = ChunkKey::chunk_marker(chunk_pos, Dimension::Overworld);
|
||||
db.set_subchunk_raw(version_key, &[42], &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Write Data3D (heightmap + biomes) - required for chunk to be valid
|
||||
let data3d_key = ChunkKey::data3d(chunk_pos, Dimension::Overworld);
|
||||
let data3d = self.create_data3d(chunk);
|
||||
db.set_subchunk_raw(data3d_key, &data3d, &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
|
||||
// Process each section (subchunk)
|
||||
for (§ion_y, section) in &chunk.sections {
|
||||
// Encode the subchunk
|
||||
let subchunk_bytes = self.encode_subchunk(section, section_y)?;
|
||||
|
||||
// Write to database
|
||||
let subchunk_key =
|
||||
ChunkKey::new_subchunk(chunk_pos, Dimension::Overworld, section_y);
|
||||
db.set_subchunk_raw(subchunk_key, &subchunk_bytes, &mut state)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
}
|
||||
|
||||
chunks_processed += 1;
|
||||
progress_bar.inc(1);
|
||||
|
||||
// Update GUI progress (92% to 97% range for chunk writing)
|
||||
if chunks_processed.is_multiple_of(10) || chunks_processed == total_chunks {
|
||||
let chunk_progress = chunks_processed as f64 / total_chunks as f64;
|
||||
let gui_progress = 92.0 + (chunk_progress * 5.0); // 92% to 97%
|
||||
emit_gui_progress_update(gui_progress, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
progress_bar.finish_with_message("Chunks written to LevelDB");
|
||||
|
||||
fn write_compound_list_record(
|
||||
&self,
|
||||
db: &mut DB,
|
||||
chunk_pos: Vec2<i32>,
|
||||
key_type: KeyTypeTag,
|
||||
value: Option<&Value>,
|
||||
) -> Result<(), BedrockSaveError> {
|
||||
let Some(Value::List(values)) = value else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if values.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let deduped = dedup_compound_list(values);
|
||||
if deduped.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let data = nbtx::to_le_bytes(&deduped).map_err(|e| BedrockSaveError::Nbt(e.to_string()))?;
|
||||
let key = build_chunk_key_bytes(chunk_pos, Dimension::Overworld, key_type, None);
|
||||
db.put(&key, &data)
|
||||
.map_err(|e| BedrockSaveError::Database(format!("{:?}", e)))?;
|
||||
// LevelDB writes are flushed when the database is dropped
|
||||
drop(db);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -663,7 +593,7 @@ impl BedrockWriter {
|
||||
for z in 0..16usize {
|
||||
for y in 0..16usize {
|
||||
let internal_idx = y * 256 + z * 16 + x;
|
||||
let block = section.get_block_at_index(internal_idx);
|
||||
let block = section.blocks[internal_idx];
|
||||
|
||||
// Get stored properties for this block position (if any)
|
||||
let properties = section.properties.get(&internal_idx);
|
||||
@@ -807,91 +737,6 @@ fn bedrock_bits_per_block(palette_count: u32) -> u8 {
|
||||
16 // Maximum
|
||||
}
|
||||
|
||||
fn build_chunk_key_bytes(
|
||||
chunk_pos: Vec2<i32>,
|
||||
dimension: Dimension,
|
||||
key_type: KeyTypeTag,
|
||||
y_index: Option<i8>,
|
||||
) -> Vec<u8> {
|
||||
let mut buffer = Vec::with_capacity(
|
||||
9 + if dimension != Dimension::Overworld {
|
||||
4
|
||||
} else {
|
||||
0
|
||||
} + 1,
|
||||
);
|
||||
buffer.extend_from_slice(&chunk_pos.x.to_le_bytes());
|
||||
buffer.extend_from_slice(&chunk_pos.y.to_le_bytes());
|
||||
|
||||
if dimension != Dimension::Overworld {
|
||||
buffer.extend_from_slice(&i32::from(dimension).to_le_bytes());
|
||||
}
|
||||
|
||||
buffer.push(key_type.to_byte());
|
||||
if let Some(y) = y_index {
|
||||
buffer.push(y as u8);
|
||||
}
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
fn dedup_compound_list(values: &[Value]) -> Vec<Value> {
|
||||
let mut coord_index: StdHashMap<(i32, i32, i32), usize> = StdHashMap::new();
|
||||
let mut deduped: Vec<Value> = Vec::with_capacity(values.len());
|
||||
|
||||
for value in values {
|
||||
if let Value::Compound(map) = value {
|
||||
if let Some(coords) = get_entity_coords(map) {
|
||||
if let Some(idx) = coord_index.get(&coords).copied() {
|
||||
deduped[idx] = value.clone();
|
||||
continue;
|
||||
} else {
|
||||
coord_index.insert(coords, deduped.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
deduped.push(value.clone());
|
||||
}
|
||||
|
||||
deduped
|
||||
}
|
||||
|
||||
fn get_entity_coords(entity: &StdHashMap<String, Value>) -> Option<(i32, i32, i32)> {
|
||||
if let Some(Value::List(pos)) = entity.get("Pos") {
|
||||
if pos.len() == 3 {
|
||||
if let (Some(x), Some(y), Some(z)) = (
|
||||
value_to_i32(&pos[0]),
|
||||
value_to_i32(&pos[1]),
|
||||
value_to_i32(&pos[2]),
|
||||
) {
|
||||
return Some((x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (Some(x), Some(y), Some(z)) = (
|
||||
entity.get("x").and_then(value_to_i32),
|
||||
entity.get("y").and_then(value_to_i32),
|
||||
entity.get("z").and_then(value_to_i32),
|
||||
) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((x, y, z))
|
||||
}
|
||||
|
||||
fn value_to_i32(value: &Value) -> Option<i32> {
|
||||
match value {
|
||||
Value::Byte(v) => Some(i32::from(*v)),
|
||||
Value::Short(v) => Some(i32::from(*v)),
|
||||
Value::Int(v) => Some(*v),
|
||||
Value::Long(v) => i32::try_from(*v).ok(),
|
||||
Value::Float(v) => Some(*v as i32),
|
||||
Value::Double(v) => Some(*v as i32),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Level.dat structure for Bedrock Edition
|
||||
/// This struct contains all required fields for a valid Bedrock world
|
||||
#[derive(serde::Serialize)]
|
||||
|
||||
@@ -54,118 +54,9 @@ pub(crate) struct PaletteItem {
|
||||
pub properties: Option<Value>,
|
||||
}
|
||||
|
||||
/// Block storage strategy for a 16×16×16 section.
|
||||
///
|
||||
/// **Memory optimisation**: instead of always allocating a 4 096-byte array,
|
||||
/// we distinguish two cases:
|
||||
///
|
||||
/// * `Uniform(block)` – every position holds the same block (1 byte).
|
||||
/// This covers freshly-created (all-AIR) sections, and sections that were
|
||||
/// entirely filled with one type (e.g. STONE underground with `--fillground`).
|
||||
///
|
||||
/// * `Full(Vec<Block>)` – the general case, equivalent to the old `[Block; 4096]`
|
||||
/// but heap-allocated via `Vec` so the *inline* size inside the parent
|
||||
/// `FnvHashMap` entry is only 24 bytes (pointer + length + capacity) instead
|
||||
/// of 4 096 bytes. This eliminates huge HashMap-slot waste from unused
|
||||
/// capacity slots.
|
||||
pub(crate) enum BlockStorage {
|
||||
/// Every position is the same block (commonly AIR).
|
||||
Uniform(Block),
|
||||
/// Mixed blocks – always exactly 4 096 entries.
|
||||
Full(Vec<Block>),
|
||||
}
|
||||
|
||||
impl BlockStorage {
|
||||
/// Read block at flat `index` (0..4095).
|
||||
#[inline(always)]
|
||||
pub fn get(&self, index: usize) -> Block {
|
||||
match self {
|
||||
BlockStorage::Uniform(b) => *b,
|
||||
BlockStorage::Full(v) => v[index],
|
||||
}
|
||||
}
|
||||
|
||||
/// Write block at flat `index`.
|
||||
/// Promotes `Uniform` → `Full` on the first differing write.
|
||||
#[inline]
|
||||
pub fn set(&mut self, index: usize, block: Block) {
|
||||
match self {
|
||||
BlockStorage::Uniform(b) if *b == block => {
|
||||
// No-op – writing the same value.
|
||||
}
|
||||
BlockStorage::Uniform(base) => {
|
||||
let base = *base;
|
||||
let mut v = vec![base; 4096];
|
||||
v[index] = block;
|
||||
*self = BlockStorage::Full(v);
|
||||
}
|
||||
BlockStorage::Full(v) => {
|
||||
v[index] = block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all 4 096 blocks.
|
||||
#[inline]
|
||||
pub fn iter(&self) -> BlockStorageIter<'_> {
|
||||
match self {
|
||||
BlockStorage::Uniform(b) => BlockStorageIter::Uniform(*b, 0),
|
||||
BlockStorage::Full(v) => BlockStorageIter::Full(v.iter()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to collapse a `Full` vec back to `Uniform` if every entry
|
||||
/// is the same block. Frees the 4 KiB heap allocation.
|
||||
pub fn try_compact(&mut self) {
|
||||
if let BlockStorage::Full(v) = self {
|
||||
if let Some(&first) = v.first() {
|
||||
if v.iter().all(|&b| b == first) {
|
||||
*self = BlockStorage::Uniform(first);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator returned by [`BlockStorage::iter`].
|
||||
pub(crate) enum BlockStorageIter<'a> {
|
||||
Uniform(Block, usize),
|
||||
Full(std::slice::Iter<'a, Block>),
|
||||
}
|
||||
|
||||
impl<'a> Iterator for BlockStorageIter<'a> {
|
||||
type Item = Block;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Block> {
|
||||
match self {
|
||||
BlockStorageIter::Uniform(b, count) => {
|
||||
if *count < 4096 {
|
||||
*count += 1;
|
||||
Some(*b)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
BlockStorageIter::Full(it) => it.next().copied(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let rem = match self {
|
||||
BlockStorageIter::Uniform(_, c) => 4096 - *c,
|
||||
BlockStorageIter::Full(it) => it.len(),
|
||||
};
|
||||
(rem, Some(rem))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for BlockStorageIter<'_> {}
|
||||
|
||||
/// A section being modified (16x16x16 blocks)
|
||||
pub(crate) struct SectionToModify {
|
||||
pub storage: BlockStorage,
|
||||
pub blocks: [Block; 4096],
|
||||
/// Store properties for blocks that have them, indexed by the same index as blocks array
|
||||
pub properties: FnvHashMap<usize, Value>,
|
||||
}
|
||||
@@ -173,7 +64,7 @@ pub(crate) struct SectionToModify {
|
||||
impl SectionToModify {
|
||||
#[inline]
|
||||
pub fn get_block(&self, x: u8, y: u8, z: u8) -> Option<Block> {
|
||||
let b = self.storage.get(Self::index(x, y, z));
|
||||
let b = self.blocks[Self::index(x, y, z)];
|
||||
if b == AIR {
|
||||
return None;
|
||||
}
|
||||
@@ -182,9 +73,7 @@ impl SectionToModify {
|
||||
|
||||
#[inline]
|
||||
pub fn set_block(&mut self, x: u8, y: u8, z: u8, block: Block) {
|
||||
let index = Self::index(x, y, z);
|
||||
self.storage.set(index, block);
|
||||
self.properties.remove(&index);
|
||||
self.blocks[Self::index(x, y, z)] = block;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -196,7 +85,7 @@ impl SectionToModify {
|
||||
block_with_props: BlockWithProperties,
|
||||
) {
|
||||
let index = Self::index(x, y, z);
|
||||
self.storage.set(index, block_with_props.block);
|
||||
self.blocks[index] = block_with_props.block;
|
||||
|
||||
// Store properties if they exist
|
||||
if let Some(props) = block_with_props.properties {
|
||||
@@ -207,56 +96,20 @@ impl SectionToModify {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read block at a raw flat index (used by Bedrock serialiser).
|
||||
#[inline(always)]
|
||||
pub fn get_block_at_index(&self, index: usize) -> Block {
|
||||
self.storage.get(index)
|
||||
}
|
||||
|
||||
/// Calculate index from coordinates (YZX order)
|
||||
#[inline(always)]
|
||||
pub fn index(x: u8, y: u8, z: u8) -> usize {
|
||||
usize::from(y) % 16 * 256 + usize::from(z) * 16 + usize::from(x)
|
||||
}
|
||||
|
||||
/// Try to collapse the block array back to `Uniform` if every entry
|
||||
/// is the same block and there are no properties.
|
||||
pub fn compact(&mut self) {
|
||||
if self.properties.is_empty() {
|
||||
self.storage.try_compact();
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to Java Edition section format
|
||||
pub fn to_section(&self, y: i8) -> Section {
|
||||
// Fast path: Uniform section → single palette entry, no data array needed.
|
||||
// Only valid when no per-index properties exist, otherwise we must
|
||||
// fall through to the general path so every index is checked.
|
||||
if self.properties.is_empty() {
|
||||
if let BlockStorage::Uniform(block) = &self.storage {
|
||||
let palette_item = PaletteItem {
|
||||
name: format!("{}:{}", block.namespace(), block.name()),
|
||||
properties: block.properties(),
|
||||
};
|
||||
return Section {
|
||||
block_states: Blockstates {
|
||||
palette: vec![palette_item],
|
||||
data: None,
|
||||
other: FnvHashMap::default(),
|
||||
},
|
||||
y,
|
||||
other: FnvHashMap::default(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// General path: mixed blocks.
|
||||
// Create a map of unique block+properties combinations to palette indices
|
||||
let mut unique_blocks: Vec<(Block, Option<Value>)> = Vec::new();
|
||||
let mut palette_lookup: FnvHashMap<(Block, Option<String>), usize> = FnvHashMap::default();
|
||||
|
||||
// Build unique block combinations and lookup table
|
||||
for (i, block) in self.storage.iter().enumerate() {
|
||||
for (i, &block) in self.blocks.iter().enumerate() {
|
||||
let properties = self.properties.get(&i).cloned();
|
||||
|
||||
// Create a key for the lookup (block + properties hash)
|
||||
@@ -279,7 +132,7 @@ impl SectionToModify {
|
||||
let mut cur = 0;
|
||||
let mut cur_idx = 0;
|
||||
|
||||
for (i, block) in self.storage.iter().enumerate() {
|
||||
for (i, &block) in self.blocks.iter().enumerate() {
|
||||
let properties = self.properties.get(&i).cloned();
|
||||
let props_key = properties.as_ref().map(|p| format!("{p:?}"));
|
||||
let lookup_key = (block, props_key);
|
||||
@@ -322,7 +175,7 @@ impl SectionToModify {
|
||||
impl Default for SectionToModify {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
storage: BlockStorage::Uniform(AIR),
|
||||
blocks: [AIR; 4096],
|
||||
properties: FnvHashMap::default(),
|
||||
}
|
||||
}
|
||||
@@ -392,7 +245,7 @@ impl RegionToModify {
|
||||
}
|
||||
}
|
||||
|
||||
/// The entire world being modified.
|
||||
/// The entire world being modified
|
||||
#[derive(Default)]
|
||||
pub(crate) struct WorldToModify {
|
||||
pub regions: FnvHashMap<(i32, i32), RegionToModify>,
|
||||
@@ -418,6 +271,7 @@ impl WorldToModify {
|
||||
|
||||
let region: &RegionToModify = self.get_region(region_x, region_z)?;
|
||||
let chunk: &ChunkToModify = region.get_chunk(chunk_x & 31, chunk_z & 31)?;
|
||||
|
||||
chunk.get_block(
|
||||
(x & 15).try_into().unwrap(),
|
||||
y,
|
||||
@@ -434,6 +288,7 @@ impl WorldToModify {
|
||||
|
||||
let region: &mut RegionToModify = self.get_or_create_region(region_x, region_z);
|
||||
let chunk: &mut ChunkToModify = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
chunk.set_block(
|
||||
(x & 15).try_into().unwrap(),
|
||||
y,
|
||||
@@ -457,6 +312,7 @@ impl WorldToModify {
|
||||
|
||||
let region: &mut RegionToModify = self.get_or_create_region(region_x, region_z);
|
||||
let chunk: &mut ChunkToModify = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
chunk.set_block_with_properties(
|
||||
(x & 15).try_into().unwrap(),
|
||||
y,
|
||||
@@ -464,100 +320,4 @@ impl WorldToModify {
|
||||
block_with_props,
|
||||
);
|
||||
}
|
||||
|
||||
/// Set a block only if the position is currently empty (AIR / absent).
|
||||
///
|
||||
/// This avoids the double HashMap traversal of `get_block()` + `set_block()`
|
||||
/// which is the hot path in ground generation and many element processors.
|
||||
#[inline]
|
||||
pub fn set_block_if_absent(&mut self, x: i32, y: i32, z: i32, block: Block) {
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let region = self.regions.entry((region_x, region_z)).or_default();
|
||||
let chunk = region
|
||||
.chunks
|
||||
.entry((chunk_x & 31, chunk_z & 31))
|
||||
.or_default();
|
||||
|
||||
// Clamp Y
|
||||
let y = y.clamp(MIN_Y, MAX_Y);
|
||||
let section_idx: i8 = (y >> 4) as i8;
|
||||
let section = chunk.sections.entry(section_idx).or_default();
|
||||
|
||||
let local_x = (x & 15) as u8;
|
||||
let local_y = (y & 15) as u8;
|
||||
let local_z = (z & 15) as u8;
|
||||
let idx = SectionToModify::index(local_x, local_y, local_z);
|
||||
|
||||
// Only write if the current block is AIR
|
||||
if section.storage.get(idx) == AIR {
|
||||
section.storage.set(idx, block);
|
||||
// Clear any stale properties from a previous block at this position
|
||||
section.properties.remove(&idx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill an entire column (single x, z) from y_min to y_max with the same block,
|
||||
/// resolving region/chunk only once. Used by ground generation.
|
||||
#[inline]
|
||||
pub fn fill_column(
|
||||
&mut self,
|
||||
x: i32,
|
||||
z: i32,
|
||||
y_min: i32,
|
||||
y_max: i32,
|
||||
block: Block,
|
||||
skip_existing: bool,
|
||||
) {
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let region = self.regions.entry((region_x, region_z)).or_default();
|
||||
let chunk = region
|
||||
.chunks
|
||||
.entry((chunk_x & 31, chunk_z & 31))
|
||||
.or_default();
|
||||
|
||||
let local_x = (x & 15) as u8;
|
||||
let local_z = (z & 15) as u8;
|
||||
|
||||
let y_min = y_min.clamp(MIN_Y, MAX_Y);
|
||||
let y_max = y_max.clamp(MIN_Y, MAX_Y);
|
||||
|
||||
for y in y_min..=y_max {
|
||||
let section_idx: i8 = (y >> 4) as i8;
|
||||
let section = chunk.sections.entry(section_idx).or_default();
|
||||
let local_y = (y & 15) as u8;
|
||||
let idx = SectionToModify::index(local_x, local_y, local_z);
|
||||
|
||||
if skip_existing {
|
||||
if section.storage.get(idx) == AIR {
|
||||
section.storage.set(idx, block);
|
||||
section.properties.remove(&idx);
|
||||
}
|
||||
} else {
|
||||
section.storage.set(idx, block);
|
||||
section.properties.remove(&idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan every section and collapse any that are entirely one block type
|
||||
/// from `Full(Vec)` back to `Uniform(Block)`, freeing the 4 KiB allocation.
|
||||
pub fn compact_sections(&mut self) {
|
||||
for region in self.regions.values_mut() {
|
||||
for chunk in region.chunks.values_mut() {
|
||||
for section in chunk.sections.values_mut() {
|
||||
if matches!(§ion.storage, BlockStorage::Full(_)) {
|
||||
section.compact();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,10 @@ use rayon::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::{atomic::{AtomicU64, Ordering}, OnceLock};
|
||||
|
||||
/// Cached base chunk sections (grass at Y=-62)
|
||||
/// Computed once on first use and reused for all empty chunks
|
||||
/// Only computed once and reused for all empty chunks
|
||||
static BASE_CHUNK_SECTIONS: OnceLock<Vec<Section>> = OnceLock::new();
|
||||
|
||||
/// Get or create the cached base chunk sections
|
||||
@@ -67,10 +66,9 @@ impl<'a> WorldEditor<'a> {
|
||||
/// Helper function to create a base chunk with grass blocks at Y -62
|
||||
/// Uses cached sections for efficiency - only serialization happens per chunk
|
||||
pub(super) fn create_base_chunk(abs_chunk_x: i32, abs_chunk_z: i32) -> (Vec<u8>, bool) {
|
||||
// Use cached sections (computed once on first call)
|
||||
// Use cached sections (computed once)
|
||||
let sections = get_base_chunk_sections();
|
||||
|
||||
// Prepare chunk data with cloned sections
|
||||
|
||||
let chunk_data = Chunk {
|
||||
sections: sections.to_vec(),
|
||||
x_pos: abs_chunk_x,
|
||||
@@ -79,13 +77,10 @@ impl<'a> WorldEditor<'a> {
|
||||
other: FnvHashMap::default(),
|
||||
};
|
||||
|
||||
// Create the Level wrapper
|
||||
let level_data = create_level_wrapper(&chunk_data);
|
||||
|
||||
// Serialize the chunk with Level wrapper
|
||||
let mut ser_buffer = Vec::with_capacity(8192);
|
||||
fastnbt::to_writer(&mut ser_buffer, &level_data).unwrap();
|
||||
|
||||
|
||||
(ser_buffer, true)
|
||||
}
|
||||
|
||||
@@ -139,10 +134,22 @@ impl<'a> WorldEditor<'a> {
|
||||
save_pb.finish();
|
||||
}
|
||||
|
||||
/// Saves the world silently without progress messages (for parallel unit processing).
|
||||
pub(super) fn save_java_silent(&mut self) {
|
||||
// Save all regions without progress output
|
||||
// With batch_size=2, each unit should have at most 4 regions (2x2)
|
||||
let region_count = self.world.regions.len();
|
||||
if region_count > 4 {
|
||||
eprintln!("BUG: Unit has {} regions (max expected: 4 for batch_size=2)", region_count);
|
||||
}
|
||||
for ((region_x, region_z), region_to_modify) in &self.world.regions {
|
||||
self.save_single_region(*region_x, *region_z, region_to_modify);
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves a single region to disk.
|
||||
///
|
||||
/// Optimized for new world creation, writes chunks directly without reading existing data.
|
||||
/// This assumes we're creating a fresh world, not modifying an existing one.
|
||||
/// This is extracted to allow streaming mode to save and release regions one at a time.
|
||||
fn save_single_region(
|
||||
&self,
|
||||
region_x: i32,
|
||||
@@ -152,10 +159,9 @@ impl<'a> WorldEditor<'a> {
|
||||
let mut region = self.create_region(region_x, region_z);
|
||||
let mut ser_buffer = Vec::with_capacity(8192);
|
||||
|
||||
// First pass: write all chunks that have content
|
||||
for (&(chunk_x, chunk_z), chunk_to_modify) in ®ion_to_modify.chunks {
|
||||
if !chunk_to_modify.sections.is_empty() || !chunk_to_modify.other.is_empty() {
|
||||
// Create chunk directly, we're writing to a fresh region file
|
||||
// Create new chunk directly - we're writing to a fresh region file
|
||||
// so there's no existing data to preserve
|
||||
let chunk = Chunk {
|
||||
sections: chunk_to_modify.sections().collect(),
|
||||
@@ -175,16 +181,28 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: ensure all chunks exist (fill with base layer if not)
|
||||
// Second pass: ensure all chunks within world bounds exist
|
||||
// Only write base chunks for chunks that are within the actual world bounding box
|
||||
let world_min_chunk_x = self.xzbbox.min_x() >> 4;
|
||||
let world_max_chunk_x = self.xzbbox.max_x() >> 4;
|
||||
let world_min_chunk_z = self.xzbbox.min_z() >> 4;
|
||||
let world_max_chunk_z = self.xzbbox.max_z() >> 4;
|
||||
|
||||
for chunk_x in 0..32 {
|
||||
for chunk_z in 0..32 {
|
||||
let abs_chunk_x = chunk_x + (region_x * 32);
|
||||
let abs_chunk_z = chunk_z + (region_z * 32);
|
||||
|
||||
// Skip chunks outside the world bounding box
|
||||
if abs_chunk_x < world_min_chunk_x || abs_chunk_x > world_max_chunk_x ||
|
||||
abs_chunk_z < world_min_chunk_z || abs_chunk_z > world_max_chunk_z {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if chunk exists in our modifications
|
||||
let chunk_exists = region_to_modify.chunks.contains_key(&(chunk_x, chunk_z));
|
||||
|
||||
// If chunk doesn't exist, create it with base layer
|
||||
// If chunk doesn't exist but is within bounds, create it with base layer
|
||||
if !chunk_exists {
|
||||
let (ser_buffer, _) = Self::create_base_chunk(abs_chunk_x, abs_chunk_z);
|
||||
region
|
||||
@@ -197,138 +215,89 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
|
||||
/// Helper function to get entity coordinates
|
||||
/// Note: Currently unused since we write directly without merging, but kept for potential future use
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
fn get_entity_coords(entity: &HashMap<String, Value>) -> Option<(i32, i32, i32)> {
|
||||
if let Some(Value::List(pos)) = entity.get("Pos") {
|
||||
if pos.len() == 3 {
|
||||
if let (Some(x), Some(y), Some(z)) = (
|
||||
value_to_i32(&pos[0]),
|
||||
value_to_i32(&pos[1]),
|
||||
value_to_i32(&pos[2]),
|
||||
) {
|
||||
return Some((x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (Some(x), Some(y), Some(z)) = (
|
||||
entity.get("x").and_then(value_to_i32),
|
||||
entity.get("y").and_then(value_to_i32),
|
||||
entity.get("z").and_then(value_to_i32),
|
||||
) else {
|
||||
return None;
|
||||
fn get_entity_coords(entity: &HashMap<String, Value>) -> (i32, i32, i32) {
|
||||
let x = if let Value::Int(x) = entity.get("x").unwrap_or(&Value::Int(0)) {
|
||||
*x
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Some((x, y, z))
|
||||
let y = if let Value::Int(y) = entity.get("y").unwrap_or(&Value::Int(0)) {
|
||||
*y
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let z = if let Value::Int(z) = entity.get("z").unwrap_or(&Value::Int(0)) {
|
||||
*z
|
||||
} else {
|
||||
0
|
||||
};
|
||||
(x, y, z)
|
||||
}
|
||||
|
||||
/// Creates a Level wrapper for chunk data (Java Edition format)
|
||||
#[inline]
|
||||
fn create_level_wrapper(chunk: &Chunk) -> HashMap<String, Value> {
|
||||
let mut level_map = HashMap::from([
|
||||
("xPos".to_string(), Value::Int(chunk.x_pos)),
|
||||
("zPos".to_string(), Value::Int(chunk.z_pos)),
|
||||
(
|
||||
"isLightOn".to_string(),
|
||||
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
|
||||
),
|
||||
(
|
||||
"sections".to_string(),
|
||||
Value::List(
|
||||
chunk
|
||||
.sections
|
||||
.iter()
|
||||
.map(|section| {
|
||||
let mut block_states = HashMap::from([(
|
||||
"palette".to_string(),
|
||||
Value::List(
|
||||
section
|
||||
.block_states
|
||||
.palette
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let mut palette_item = HashMap::from([(
|
||||
"Name".to_string(),
|
||||
Value::String(item.name.clone()),
|
||||
)]);
|
||||
if let Some(props) = &item.properties {
|
||||
palette_item
|
||||
.insert("Properties".to_string(), props.clone());
|
||||
}
|
||||
Value::Compound(palette_item)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
)]);
|
||||
|
||||
// Only add the `data` attribute if it's non-empty
|
||||
// to maintain compatibility with third-party tools like Dynmap
|
||||
if let Some(data) = §ion.block_states.data {
|
||||
if !data.is_empty() {
|
||||
block_states
|
||||
.insert("data".to_string(), Value::LongArray(data.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
Value::Compound(HashMap::from([
|
||||
("Y".to_string(), Value::Byte(section.y)),
|
||||
("block_states".to_string(), Value::Compound(block_states)),
|
||||
]))
|
||||
})
|
||||
.collect(),
|
||||
HashMap::from([(
|
||||
"Level".to_string(),
|
||||
Value::Compound(HashMap::from([
|
||||
("xPos".to_string(), Value::Int(chunk.x_pos)),
|
||||
("zPos".to_string(), Value::Int(chunk.z_pos)),
|
||||
(
|
||||
"isLightOn".to_string(),
|
||||
Value::Byte(i8::try_from(chunk.is_light_on).unwrap()),
|
||||
),
|
||||
),
|
||||
]);
|
||||
(
|
||||
"sections".to_string(),
|
||||
Value::List(
|
||||
chunk
|
||||
.sections
|
||||
.iter()
|
||||
.map(|section| {
|
||||
let mut block_states = HashMap::from([(
|
||||
"palette".to_string(),
|
||||
Value::List(
|
||||
section
|
||||
.block_states
|
||||
.palette
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let mut palette_item = HashMap::from([(
|
||||
"Name".to_string(),
|
||||
Value::String(item.name.clone()),
|
||||
)]);
|
||||
if let Some(props) = &item.properties {
|
||||
palette_item.insert(
|
||||
"Properties".to_string(),
|
||||
props.clone(),
|
||||
);
|
||||
}
|
||||
Value::Compound(palette_item)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
)]);
|
||||
|
||||
for (key, value) in &chunk.other {
|
||||
level_map.insert(key.clone(), value.clone());
|
||||
}
|
||||
|
||||
HashMap::from([("Level".to_string(), Value::Compound(level_map))])
|
||||
}
|
||||
|
||||
/// Merge compound lists (entities, block_entities) from chunk_to_modify into chunk
|
||||
/// Note: Currently unused since we write directly without merging, but kept for potential future use
|
||||
#[allow(dead_code)]
|
||||
fn merge_compound_list(chunk: &mut Chunk, chunk_to_modify: &ChunkToModify, key: &str) {
|
||||
if let Some(existing_entities) = chunk.other.get_mut(key) {
|
||||
if let Some(new_entities) = chunk_to_modify.other.get(key) {
|
||||
if let (Value::List(existing), Value::List(new)) = (existing_entities, new_entities) {
|
||||
existing.retain(|e| {
|
||||
if let Value::Compound(map) = e {
|
||||
if let Some((x, y, z)) = get_entity_coords(map) {
|
||||
return !new.iter().any(|new_e| {
|
||||
if let Value::Compound(new_map) = new_e {
|
||||
get_entity_coords(new_map) == Some((x, y, z))
|
||||
} else {
|
||||
false
|
||||
// Only add the `data` attribute if it's non-empty
|
||||
// to maintain compatibility with third-party tools like Dynmap
|
||||
if let Some(data) = §ion.block_states.data {
|
||||
if !data.is_empty() {
|
||||
block_states.insert(
|
||||
"data".to_string(),
|
||||
Value::LongArray(data.to_owned()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
existing.extend(new.clone());
|
||||
}
|
||||
}
|
||||
} else if let Some(new_entities) = chunk_to_modify.other.get(key) {
|
||||
chunk.other.insert(key.to_string(), new_entities.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert NBT Value to i32
|
||||
/// Note: Currently unused since we write directly without merging, but kept for potential future use
|
||||
#[allow(dead_code)]
|
||||
fn value_to_i32(value: &Value) -> Option<i32> {
|
||||
match value {
|
||||
Value::Byte(v) => Some(i32::from(*v)),
|
||||
Value::Short(v) => Some(i32::from(*v)),
|
||||
Value::Int(v) => Some(*v),
|
||||
Value::Long(v) => i32::try_from(*v).ok(),
|
||||
Value::Float(v) => Some(*v as i32),
|
||||
Value::Double(v) => Some(*v as i32),
|
||||
_ => None,
|
||||
}
|
||||
Value::Compound(HashMap::from([
|
||||
("Y".to_string(), Value::Byte(section.y)),
|
||||
("block_states".to_string(), Value::Compound(block_states)),
|
||||
]))
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
),
|
||||
])),
|
||||
)])
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::ground::Ground;
|
||||
use crate::progress::emit_gui_progress_update;
|
||||
use colored::Colorize;
|
||||
use fastnbt::{IntArray, Value};
|
||||
use fastnbt::Value;
|
||||
use serde::Serialize;
|
||||
use std::collections::{hash_map::Entry, HashMap};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
@@ -75,10 +75,8 @@ pub struct WorldEditor<'a> {
|
||||
ground: Option<Arc<Ground>>,
|
||||
format: WorldFormat,
|
||||
/// Optional level name for Bedrock worlds (e.g., "Arnis World: New York City")
|
||||
#[cfg(feature = "bedrock")]
|
||||
bedrock_level_name: Option<String>,
|
||||
/// Optional spawn point for Bedrock worlds (x, z coordinates)
|
||||
#[cfg(feature = "bedrock")]
|
||||
bedrock_spawn_point: Option<(i32, i32)>,
|
||||
}
|
||||
|
||||
@@ -95,9 +93,7 @@ impl<'a> WorldEditor<'a> {
|
||||
llbbox,
|
||||
ground: None,
|
||||
format: WorldFormat::JavaAnvil,
|
||||
#[cfg(feature = "bedrock")]
|
||||
bedrock_level_name: None,
|
||||
#[cfg(feature = "bedrock")]
|
||||
bedrock_spawn_point: None,
|
||||
}
|
||||
}
|
||||
@@ -111,12 +107,8 @@ impl<'a> WorldEditor<'a> {
|
||||
xzbbox: &'a XZBBox,
|
||||
llbbox: LLBBox,
|
||||
format: WorldFormat,
|
||||
#[cfg_attr(not(feature = "bedrock"), allow(unused_variables))] bedrock_level_name: Option<
|
||||
String,
|
||||
>,
|
||||
#[cfg_attr(not(feature = "bedrock"), allow(unused_variables))] bedrock_spawn_point: Option<
|
||||
(i32, i32),
|
||||
>,
|
||||
bedrock_level_name: Option<String>,
|
||||
bedrock_spawn_point: Option<(i32, i32)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
world_dir,
|
||||
@@ -125,9 +117,7 @@ impl<'a> WorldEditor<'a> {
|
||||
llbbox,
|
||||
ground: None,
|
||||
format,
|
||||
#[cfg(feature = "bedrock")]
|
||||
bedrock_level_name,
|
||||
#[cfg(feature = "bedrock")]
|
||||
bedrock_spawn_point,
|
||||
}
|
||||
}
|
||||
@@ -253,212 +243,6 @@ impl<'a> WorldEditor<'a> {
|
||||
self.set_block(SIGN, x, y, z, None, None);
|
||||
}
|
||||
|
||||
/// Adds an entity at the given coordinates (Y is ground-relative).
|
||||
#[allow(dead_code)]
|
||||
pub fn add_entity(
|
||||
&mut self,
|
||||
id: &str,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
extra_data: Option<HashMap<String, Value>>,
|
||||
) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
|
||||
let mut entity = HashMap::new();
|
||||
entity.insert("id".to_string(), Value::String(id.to_string()));
|
||||
entity.insert(
|
||||
"Pos".to_string(),
|
||||
Value::List(vec![
|
||||
Value::Double(x as f64 + 0.5),
|
||||
Value::Double(absolute_y as f64),
|
||||
Value::Double(z as f64 + 0.5),
|
||||
]),
|
||||
);
|
||||
entity.insert(
|
||||
"Motion".to_string(),
|
||||
Value::List(vec![
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
Value::Double(0.0),
|
||||
]),
|
||||
);
|
||||
entity.insert(
|
||||
"Rotation".to_string(),
|
||||
Value::List(vec![Value::Float(0.0), Value::Float(0.0)]),
|
||||
);
|
||||
entity.insert("OnGround".to_string(), Value::Byte(1));
|
||||
entity.insert("FallDistance".to_string(), Value::Float(0.0));
|
||||
entity.insert("Fire".to_string(), Value::Short(-20));
|
||||
entity.insert("Air".to_string(), Value::Short(300));
|
||||
entity.insert("PortalCooldown".to_string(), Value::Int(0));
|
||||
entity.insert(
|
||||
"UUID".to_string(),
|
||||
Value::IntArray(build_deterministic_uuid(id, x, absolute_y, z)),
|
||||
);
|
||||
|
||||
if let Some(extra) = extra_data {
|
||||
for (key, value) in extra {
|
||||
entity.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let region = self.world.get_or_create_region(region_x, region_z);
|
||||
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
match chunk.other.entry("entities".to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let Value::List(list) = entry.get_mut() {
|
||||
list.push(Value::Compound(entity));
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(Value::List(vec![Value::Compound(entity)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Places a chest with the provided items at the given coordinates (ground-relative Y).
|
||||
#[allow(dead_code)]
|
||||
pub fn set_chest_with_items(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
items: Vec<HashMap<String, Value>>,
|
||||
) {
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
self.set_chest_with_items_absolute(x, absolute_y, z, items);
|
||||
}
|
||||
|
||||
/// Places a chest with the provided items at the given coordinates (absolute Y).
|
||||
#[allow(dead_code)]
|
||||
pub fn set_chest_with_items_absolute(
|
||||
&mut self,
|
||||
x: i32,
|
||||
absolute_y: i32,
|
||||
z: i32,
|
||||
items: Vec<HashMap<String, Value>>,
|
||||
) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let mut chest_data = HashMap::new();
|
||||
chest_data.insert(
|
||||
"id".to_string(),
|
||||
Value::String("minecraft:chest".to_string()),
|
||||
);
|
||||
chest_data.insert("x".to_string(), Value::Int(x));
|
||||
chest_data.insert("y".to_string(), Value::Int(absolute_y));
|
||||
chest_data.insert("z".to_string(), Value::Int(z));
|
||||
chest_data.insert(
|
||||
"Items".to_string(),
|
||||
Value::List(items.into_iter().map(Value::Compound).collect()),
|
||||
);
|
||||
chest_data.insert("keepPacked".to_string(), Value::Byte(0));
|
||||
|
||||
let region = self.world.get_or_create_region(region_x, region_z);
|
||||
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
match chunk.other.entry("block_entities".to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let Value::List(list) = entry.get_mut() {
|
||||
list.push(Value::Compound(chest_data));
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(Value::List(vec![Value::Compound(chest_data)]));
|
||||
}
|
||||
}
|
||||
|
||||
self.set_block_absolute(CHEST, x, absolute_y, z, None, None);
|
||||
}
|
||||
|
||||
/// Places a block entity with items at the given coordinates (ground-relative Y).
|
||||
#[allow(dead_code)]
|
||||
pub fn set_block_entity_with_items(
|
||||
&mut self,
|
||||
block_with_props: BlockWithProperties,
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
block_entity_id: &str,
|
||||
items: Vec<HashMap<String, Value>>,
|
||||
) {
|
||||
let absolute_y = self.get_absolute_y(x, y, z);
|
||||
self.set_block_entity_with_items_absolute(
|
||||
block_with_props,
|
||||
x,
|
||||
absolute_y,
|
||||
z,
|
||||
block_entity_id,
|
||||
items,
|
||||
);
|
||||
}
|
||||
|
||||
/// Places a block entity with items at the given coordinates (absolute Y).
|
||||
#[allow(dead_code)]
|
||||
pub fn set_block_entity_with_items_absolute(
|
||||
&mut self,
|
||||
block_with_props: BlockWithProperties,
|
||||
x: i32,
|
||||
absolute_y: i32,
|
||||
z: i32,
|
||||
block_entity_id: &str,
|
||||
items: Vec<HashMap<String, Value>>,
|
||||
) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let chunk_x: i32 = x >> 4;
|
||||
let chunk_z: i32 = z >> 4;
|
||||
let region_x: i32 = chunk_x >> 5;
|
||||
let region_z: i32 = chunk_z >> 5;
|
||||
|
||||
let mut block_entity = HashMap::new();
|
||||
block_entity.insert("id".to_string(), Value::String(block_entity_id.to_string()));
|
||||
block_entity.insert("x".to_string(), Value::Int(x));
|
||||
block_entity.insert("y".to_string(), Value::Int(absolute_y));
|
||||
block_entity.insert("z".to_string(), Value::Int(z));
|
||||
block_entity.insert(
|
||||
"Items".to_string(),
|
||||
Value::List(items.into_iter().map(Value::Compound).collect()),
|
||||
);
|
||||
block_entity.insert("keepPacked".to_string(), Value::Byte(0));
|
||||
|
||||
let region = self.world.get_or_create_region(region_x, region_z);
|
||||
let chunk = region.get_or_create_chunk(chunk_x & 31, chunk_z & 31);
|
||||
|
||||
match chunk.other.entry("block_entities".to_string()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if let Value::List(list) = entry.get_mut() {
|
||||
list.push(Value::Compound(block_entity));
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(Value::List(vec![Value::Compound(block_entity)]));
|
||||
}
|
||||
}
|
||||
|
||||
self.set_block_with_properties_absolute(block_with_props, x, absolute_y, z, None, None);
|
||||
}
|
||||
|
||||
/// Sets a block of the specified type at the given coordinates.
|
||||
///
|
||||
/// Y value is interpreted as an offset from ground level.
|
||||
@@ -614,6 +398,45 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fills a cuboid area with the specified block between two coordinates using absolute Y values.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[inline]
|
||||
pub fn fill_blocks_absolute(
|
||||
&mut self,
|
||||
block: Block,
|
||||
x1: i32,
|
||||
y1_absolute: i32,
|
||||
z1: i32,
|
||||
x2: i32,
|
||||
y2_absolute: i32,
|
||||
z2: i32,
|
||||
override_whitelist: Option<&[Block]>,
|
||||
override_blacklist: Option<&[Block]>,
|
||||
) {
|
||||
let (min_x, max_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) };
|
||||
let (min_y, max_y) = if y1_absolute < y2_absolute {
|
||||
(y1_absolute, y2_absolute)
|
||||
} else {
|
||||
(y2_absolute, y1_absolute)
|
||||
};
|
||||
let (min_z, max_z) = if z1 < z2 { (z1, z2) } else { (z2, z1) };
|
||||
|
||||
for x in min_x..=max_x {
|
||||
for absolute_y in min_y..=max_y {
|
||||
for z in min_z..=max_z {
|
||||
self.set_block_absolute(
|
||||
block,
|
||||
x,
|
||||
absolute_y,
|
||||
z,
|
||||
override_whitelist,
|
||||
override_blacklist,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for a block at the given coordinates.
|
||||
#[inline]
|
||||
pub fn check_for_block(&self, x: i32, y: i32, z: i32, whitelist: Option<&[Block]>) -> bool {
|
||||
@@ -677,40 +500,6 @@ impl<'a> WorldEditor<'a> {
|
||||
self.world.get_block(x, absolute_y, z).is_some()
|
||||
}
|
||||
|
||||
/// Sets a block only if no modification has been recorded yet at this
|
||||
/// position (i.e. the in-memory overlay still holds AIR).
|
||||
///
|
||||
/// This is faster than `set_block_absolute` with `None` whitelists/blacklists
|
||||
/// because it avoids the double HashMap traversal.
|
||||
#[inline]
|
||||
pub fn set_block_if_absent_absolute(&mut self, block: Block, x: i32, absolute_y: i32, z: i32) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
self.world.set_block_if_absent(x, absolute_y, z, block);
|
||||
}
|
||||
|
||||
/// Fills an entire column from y_min to y_max with one block type.
|
||||
///
|
||||
/// Resolves region/chunk once instead of per-Y-level, making underground
|
||||
/// fill (`--fillground`) dramatically faster.
|
||||
#[inline]
|
||||
pub fn fill_column_absolute(
|
||||
&mut self,
|
||||
block: Block,
|
||||
x: i32,
|
||||
z: i32,
|
||||
y_min: i32,
|
||||
y_max: i32,
|
||||
skip_existing: bool,
|
||||
) {
|
||||
if !self.xzbbox.contains(&XZPoint::new(x, z)) {
|
||||
return;
|
||||
}
|
||||
self.world
|
||||
.fill_column(x, z, y_min, y_max, block, skip_existing);
|
||||
}
|
||||
|
||||
/// Saves all changes made to the world by writing to the appropriate format.
|
||||
pub fn save(&mut self) {
|
||||
println!(
|
||||
@@ -721,16 +510,24 @@ impl<'a> WorldEditor<'a> {
|
||||
}
|
||||
);
|
||||
|
||||
// Compact sections before saving: collapses uniform Full(Vec) sections
|
||||
// (e.g. all-STONE from --fillground) back to Uniform, freeing ~4 KiB each.
|
||||
self.world.compact_sections();
|
||||
|
||||
match self.format {
|
||||
WorldFormat::JavaAnvil => self.save_java(),
|
||||
WorldFormat::BedrockMcWorld => self.save_bedrock(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves regions silently without progress messages (for parallel unit processing).
|
||||
/// Does not save metadata - call save_metadata() separately if needed.
|
||||
pub fn save_silent(&mut self) {
|
||||
match self.format {
|
||||
WorldFormat::JavaAnvil => self.save_java_silent(),
|
||||
WorldFormat::BedrockMcWorld => {
|
||||
// For Bedrock, use normal save since it's not called per-unit
|
||||
self.save_bedrock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
fn save_bedrock(&mut self) {
|
||||
println!("{} Saving Bedrock world...", "[7/7]".bold());
|
||||
@@ -814,30 +611,3 @@ impl<'a> WorldEditor<'a> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn build_deterministic_uuid(id: &str, x: i32, y: i32, z: i32) -> IntArray {
|
||||
let mut hash: i64 = 17;
|
||||
for byte in id.bytes() {
|
||||
hash = hash.wrapping_mul(31).wrapping_add(byte as i64);
|
||||
}
|
||||
|
||||
let seed_a = hash ^ (x as i64).wrapping_shl(32) ^ (y as i64).wrapping_mul(17);
|
||||
let seed_b = hash.rotate_left(7) ^ (z as i64).wrapping_mul(31) ^ (x as i64).wrapping_mul(13);
|
||||
|
||||
IntArray::new(vec![
|
||||
(seed_a >> 32) as i32,
|
||||
seed_a as i32,
|
||||
(seed_b >> 32) as i32,
|
||||
seed_b as i32,
|
||||
])
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn single_item(id: &str, slot: i8, count: i8) -> HashMap<String, Value> {
|
||||
let mut item = HashMap::new();
|
||||
item.insert("id".to_string(), Value::String(id.to_string()));
|
||||
item.insert("Slot".to_string(), Value::Byte(slot));
|
||||
item.insert("Count".to_string(), Value::Byte(count));
|
||||
item
|
||||
}
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
use crate::coordinate_system::geographic::LLBBox;
|
||||
use crate::retrieve_data;
|
||||
use fastnbt::Value;
|
||||
use flate2::read::GzDecoder;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{fs, io::Write};
|
||||
|
||||
/// Returns the Desktop directory for Bedrock .mcworld file output.
|
||||
/// Falls back to home directory, then current directory.
|
||||
pub fn get_bedrock_output_directory() -> PathBuf {
|
||||
dirs::desktop_dir()
|
||||
.or_else(dirs::home_dir)
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
}
|
||||
|
||||
/// Gets the area name for a given bounding box using the center point.
|
||||
pub fn get_area_name_for_bedrock(bbox: &LLBBox) -> String {
|
||||
let center_lat = (bbox.min().lat() + bbox.max().lat()) / 2.0;
|
||||
let center_lon = (bbox.min().lng() + bbox.max().lng()) / 2.0;
|
||||
|
||||
match retrieve_data::fetch_area_name(center_lat, center_lon) {
|
||||
Ok(Some(name)) => name,
|
||||
_ => "Unknown Location".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitizes an area name for safe use in filesystem paths.
|
||||
/// Replaces characters that are invalid on Windows/macOS/Linux, trims whitespace,
|
||||
/// and limits length to prevent excessively long filenames.
|
||||
pub fn sanitize_for_filename(name: &str) -> String {
|
||||
let invalid_chars = ['<', '>', ':', '"', '/', '\\', '|', '?', '*'];
|
||||
let mut sanitized: String = name
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_control() || invalid_chars.contains(&c) {
|
||||
'_'
|
||||
} else {
|
||||
c
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
sanitized = sanitized.trim().to_string();
|
||||
|
||||
// Limit length to avoid excessively long filenames
|
||||
const MAX_LEN: usize = 64;
|
||||
if sanitized.len() > MAX_LEN {
|
||||
// Find a valid UTF-8 char boundary at or before MAX_LEN bytes
|
||||
let cutoff = sanitized
|
||||
.char_indices()
|
||||
.take_while(|(idx, _)| *idx < MAX_LEN)
|
||||
.last()
|
||||
.map(|(idx, ch)| idx + ch.len_utf8())
|
||||
.unwrap_or(0);
|
||||
sanitized.truncate(cutoff);
|
||||
sanitized = sanitized.trim_end().to_string();
|
||||
}
|
||||
|
||||
if sanitized.is_empty() {
|
||||
"Unknown Location".to_string()
|
||||
} else {
|
||||
sanitized
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the Bedrock output path and level name for a given bounding box.
|
||||
/// Combines area name lookup, sanitization, and path construction.
|
||||
pub fn build_bedrock_output(bbox: &LLBBox, output_dir: PathBuf) -> (PathBuf, String) {
|
||||
let area_name = get_area_name_for_bedrock(bbox);
|
||||
let safe_name = sanitize_for_filename(&area_name);
|
||||
let filename = format!("Arnis {safe_name}.mcworld");
|
||||
let lvl_name = format!("Arnis World: {safe_name}");
|
||||
(output_dir.join(&filename), lvl_name)
|
||||
}
|
||||
|
||||
/// Creates a new Java Edition world in the given base directory.
|
||||
///
|
||||
/// Generates a unique "Arnis World N" name, creates the directory structure
|
||||
/// (with a `region/` subdirectory), writes the region template, level.dat
|
||||
/// (with updated name, timestamp, and spawn position), and icon.png.
|
||||
///
|
||||
/// Returns the full path to the newly created world directory.
|
||||
pub fn create_new_world(base_path: &Path) -> Result<String, String> {
|
||||
// Generate a unique world name with proper counter
|
||||
// Check for both "Arnis World X" and "Arnis World X: Location" patterns
|
||||
let mut counter: i32 = 1;
|
||||
let unique_name: String = loop {
|
||||
let candidate_name: String = format!("Arnis World {counter}");
|
||||
let candidate_path: PathBuf = base_path.join(&candidate_name);
|
||||
|
||||
// Check for exact match (no location suffix)
|
||||
let exact_match_exists = candidate_path.exists();
|
||||
|
||||
// Check for worlds with location suffix (Arnis World X: Location)
|
||||
let location_pattern = format!("Arnis World {counter}: ");
|
||||
let location_match_exists = fs::read_dir(base_path)
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.any(|name| name.starts_with(&location_pattern))
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if !exact_match_exists && !location_match_exists {
|
||||
break candidate_name;
|
||||
}
|
||||
counter += 1;
|
||||
};
|
||||
|
||||
let new_world_path: PathBuf = base_path.join(&unique_name);
|
||||
|
||||
// Create the new world directory structure
|
||||
fs::create_dir_all(new_world_path.join("region"))
|
||||
.map_err(|e| format!("Failed to create world directory: {e}"))?;
|
||||
|
||||
// Copy the region template file
|
||||
const REGION_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/region.template");
|
||||
let region_path = new_world_path.join("region").join("r.0.0.mca");
|
||||
fs::write(®ion_path, REGION_TEMPLATE)
|
||||
.map_err(|e| format!("Failed to create region file: {e}"))?;
|
||||
|
||||
// Add the level.dat file
|
||||
const LEVEL_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/level.dat");
|
||||
|
||||
// Decompress the gzipped level.template
|
||||
let mut decoder = GzDecoder::new(LEVEL_TEMPLATE);
|
||||
let mut decompressed_data = Vec::new();
|
||||
decoder
|
||||
.read_to_end(&mut decompressed_data)
|
||||
.map_err(|e| format!("Failed to decompress level.template: {e}"))?;
|
||||
|
||||
// Parse the decompressed NBT data
|
||||
let mut level_data: Value = fastnbt::from_bytes(&decompressed_data)
|
||||
.map_err(|e| format!("Failed to parse level.dat template: {e}"))?;
|
||||
|
||||
// Modify the LevelName, LastPlayed and player position fields
|
||||
if let Value::Compound(ref mut root) = level_data {
|
||||
if let Some(Value::Compound(ref mut data)) = root.get_mut("Data") {
|
||||
// Update LevelName
|
||||
data.insert("LevelName".to_string(), Value::String(unique_name.clone()));
|
||||
|
||||
// Update LastPlayed to the current Unix time in milliseconds
|
||||
let current_time = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_err(|e| format!("Failed to get current time: {e}"))?;
|
||||
let current_time_millis = current_time.as_millis() as i64;
|
||||
data.insert("LastPlayed".to_string(), Value::Long(current_time_millis));
|
||||
|
||||
// Update player position and rotation
|
||||
if let Some(Value::Compound(ref mut player)) = data.get_mut("Player") {
|
||||
if let Some(Value::List(ref mut pos)) = player.get_mut("Pos") {
|
||||
if pos.len() < 3 {
|
||||
return Err(
|
||||
"Invalid level.dat template: Player Pos list has fewer than 3 elements"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if let Value::Double(ref mut x) = pos[0] {
|
||||
*x = -5.0;
|
||||
}
|
||||
if let Value::Double(ref mut y) = pos[1] {
|
||||
*y = -61.0;
|
||||
}
|
||||
if let Value::Double(ref mut z) = pos[2] {
|
||||
*z = -5.0;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Value::List(ref mut rot)) = player.get_mut("Rotation") {
|
||||
if rot.is_empty() {
|
||||
return Err(
|
||||
"Invalid level.dat template: Player Rotation list is empty".to_string()
|
||||
);
|
||||
}
|
||||
if let Value::Float(ref mut x) = rot[0] {
|
||||
*x = -45.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the updated NBT data back to bytes
|
||||
let serialized_level_data: Vec<u8> = fastnbt::to_bytes(&level_data)
|
||||
.map_err(|e| format!("Failed to serialize updated level.dat: {e}"))?;
|
||||
|
||||
// Compress the serialized data back to gzip
|
||||
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
encoder
|
||||
.write_all(&serialized_level_data)
|
||||
.map_err(|e| format!("Failed to compress updated level.dat: {e}"))?;
|
||||
let compressed_level_data = encoder
|
||||
.finish()
|
||||
.map_err(|e| format!("Failed to finalize compression for level.dat: {e}"))?;
|
||||
|
||||
// Write the level.dat file
|
||||
fs::write(new_world_path.join("level.dat"), compressed_level_data)
|
||||
.map_err(|e| format!("Failed to create level.dat file: {e}"))?;
|
||||
|
||||
// Add the icon.png file
|
||||
const ICON_TEMPLATE: &[u8] = include_bytes!("../assets/minecraft/icon.png");
|
||||
fs::write(new_world_path.join("icon.png"), ICON_TEMPLATE)
|
||||
.map_err(|e| format!("Failed to create icon.png file: {e}"))?;
|
||||
|
||||
Ok(new_world_path.display().to_string())
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Arnis",
|
||||
"version": "2.5.0",
|
||||
"version": "2.4.1",
|
||||
"identifier": "com.louisdev.arnis",
|
||||
"build": {
|
||||
"frontendDist": "src/gui"
|
||||
|
||||
Reference in New Issue
Block a user