utils/luzviewer.pyw

358 lines
14 KiB
Python

import configparser
import enum
import os.path
import sqlite3
import sys
import tkinter.filedialog as filedialog
import tkinter.messagebox as messagebox
from tkinter import END, Menu
import viewer
from pyraknet.bitstream import c_bool, c_float, c_int, c_int64, c_ubyte, c_uint, c_uint64, c_ushort, ReadStream
class PathType(enum.IntEnum):
Movement = 0
MovingPlatform = 1
Property = 2
Camera = 3
Spawner = 4
Showcase = 5
Race = 6
Rail = 7
class PathBehavior(enum.IntEnum):
Loop = 0
Bounce = 1
Once = 2
class LUZViewer(viewer.Viewer):
def __init__(self):
super().__init__()
config = configparser.ConfigParser()
config.read("luzviewer.ini")
try:
self.db = sqlite3.connect(config["paths"]["db_path"])
except:
messagebox.showerror("Can not open database", "Make sure db_path in the INI is set correctly.")
sys.exit()
self.create_widgets()
def create_widgets(self):
super().create_widgets()
menubar = Menu()
menubar.add_command(label="Open", command=self.askopenfile)
self.master.config(menu=menubar)
def askopenfile(self):
path = filedialog.askopenfilename(filetypes=[("LEGO Universe Zone", "*.luz")])
if path:
self.load_luz(path)
def load_luz(self, luz_path):
self.tree.set_children("")
print("Loading", luz_path)
with open(luz_path, "rb") as file:
data = file.read()
luz_len = len(data)
stream = ReadStream(data, unlocked=True)
version = stream.read(c_uint)
assert version in (36, 38, 39, 40, 41), version
unknown1 = stream.read(c_uint)
world_id = stream.read(c_uint)
if version >= 38:
spawnpoint_pos = stream.read(c_float), stream.read(c_float), stream.read(c_float)
spawnpoint_rot = stream.read(c_float), stream.read(c_float), stream.read(c_float), stream.read(c_float)
zone = self.tree.insert("", END, text="Zone", values=(version, unknown1, world_id, spawnpoint_pos, spawnpoint_rot))
else:
zone = self.tree.insert("", END, text="Zone", values=(version, unknown1, world_id))
### scenes
scenes = self.tree.insert(zone, END, text="Scenes")
if version >= 37:
number_of_scenes = stream.read(c_uint)
else:
number_of_scenes = stream.read(c_ubyte)
for _ in range(number_of_scenes):
filename = stream.read(bytes, length_type=c_ubyte).decode("latin1")
scene_id = stream.read(c_uint64)
scene_name = stream.read(bytes, length_type=c_ubyte).decode("latin1")
scene = self.tree.insert(scenes, END, text="Scene", values=(filename, scene_id, scene_name))
assert stream.read(bytes, length=3)
lvl_path = os.path.join(os.path.dirname(luz_path), filename)
if os.path.exists(lvl_path):
with open(lvl_path, "rb") as lvl:
print("Loading lvl", filename)
try:
self.parse_lvl(ReadStream(lvl.read(), unlocked=True), scene)
except Exception:
import traceback
traceback.print_exc()
assert stream.read(c_ubyte) == 0
### terrain
filename = stream.read(bytes, length_type=c_ubyte).decode("latin1")
name = stream.read(bytes, length_type=c_ubyte).decode("latin1")
description = stream.read(bytes, length_type=c_ubyte).decode("latin1")
self.tree.insert(zone, END, text="Terrain", values=(filename, name, description))
### scene transitions
scene_transitions = self.tree.insert(zone, END, text="Scene Transitions")
for _ in range(stream.read(c_uint)):
scene_transition_values = ()
if version < 40:
scene_transition_values += stream.read(bytes, length_type=c_ubyte),
scene_transition_values += stream.read(c_float),
scene_transition = self.tree.insert(scene_transitions, END, text="Scene Transition", values=scene_transition_values)
if version < 39:
transition_point_count = 5
else:
transition_point_count = 2
for _ in range(transition_point_count):
transition_point_scene_id = stream.read(c_uint64),
transition_point_position = stream.read(c_float), stream.read(c_float), stream.read(c_float)
self.tree.insert(scene_transition, END, text="Transition Point", values=(transition_point_scene_id, transition_point_position))
remaining_length = stream.read(c_uint)
assert luz_len - stream.read_offset//8 == remaining_length
assert stream.read(c_uint) == 1
### paths
paths = self.tree.insert(zone, END, text="Paths")
for _ in range(stream.read(c_uint)):
path_version = stream.read(c_uint)
name = stream.read(str, length_type=c_ubyte)
path_type = stream.read(c_uint)
unknown1 = stream.read(c_uint)
behavior = PathBehavior(stream.read(c_uint))
values = path_version, name, unknown1, behavior
if path_type == PathType.MovingPlatform:
if path_version >= 18:
unknown3 = stream.read(c_ubyte)
values += unknown3,
elif path_version >= 13:
unknown_str = stream.read(str, length_type=c_ubyte)
values += unknown_str,
elif path_type == PathType.Property:
unknown3 = stream.read(c_int)
price = stream.read(c_int)
rental_time = stream.read(c_int)
associated_zone = stream.read(c_uint64)
display_name = stream.read(str, length_type=c_ubyte)
display_desc = stream.read(str, length_type=c_uint)
unknown4 = stream.read(c_int),
clone_limit = stream.read(c_int)
reputation_multiplier = stream.read(c_float)
time_unit = stream.read(c_int),
achievement_required = stream.read(c_int)
player_zone_coords = stream.read(c_float), stream.read(c_float), stream.read(c_float)
max_build_height = stream.read(c_float)
values += unknown3, price, rental_time, associated_zone, display_name, display_desc, unknown4, clone_limit, reputation_multiplier, time_unit, achievement_required, player_zone_coords, max_build_height
elif path_type == PathType.Camera:
next_path = stream.read(str, length_type=c_ubyte)
values += next_path,
if path_version >= 14:
unknown3 = stream.read(c_ubyte)
values += unknown3,
elif path_type == PathType.Spawner:
spawn_lot = stream.read(c_uint)
lot_name = str(spawn_lot)
try:
lot_name += " - "+self.db.execute("select name from Objects where id == "+str(spawn_lot)).fetchone()[0]
except TypeError:
print("Name for lot", spawn_lot, "not found")
respawn_time = stream.read(c_uint)
max_to_spawn = stream.read(c_int)
num_to_maintain = stream.read(c_uint)
object_id = stream.read(c_int64)
activate_on_load = stream.read(c_bool)
values += lot_name, respawn_time, max_to_spawn, num_to_maintain, object_id, activate_on_load
path = self.tree.insert(paths, END, text=PathType(path_type).name, values=values)
for _ in range(stream.read(c_uint)):
position = stream.read(c_float), stream.read(c_float), stream.read(c_float)
waypoint_values = position,
if path_type == PathType.MovingPlatform:
rotation = stream.read(c_float), stream.read(c_float), stream.read(c_float), stream.read(c_float)
waypoint_unknown2 = stream.read(c_ubyte)
speed = stream.read(c_float)
wait = stream.read(c_float)
waypoint_values += rotation, waypoint_unknown2, speed, wait
if path_version >= 13:
waypoint_audio_guid_1 = stream.read(str, length_type=c_ubyte)
waypoint_audio_guid_2 = stream.read(str, length_type=c_ubyte)
waypoint_values += waypoint_audio_guid_1, waypoint_audio_guid_2
elif path_type == PathType.Camera:
waypoint_unknown1 = stream.read(c_float), stream.read(c_float), stream.read(c_float), stream.read(c_float)
time = stream.read(c_float)
waypoint_unknown2 = stream.read(c_float)
tension = stream.read(c_float)
continuity = stream.read(c_float)
bias = stream.read(c_float)
waypoint_values += waypoint_unknown1, time, waypoint_unknown2, tension, continuity, bias
elif path_type == PathType.Spawner:
rotation = stream.read(c_float), stream.read(c_float), stream.read(c_float), stream.read(c_float)
waypoint_values += rotation,
elif path_type == PathType.Race:
waypoint_unknown1 = stream.read(c_float), stream.read(c_float), stream.read(c_float), stream.read(c_float)
waypoint_unknown2 = stream.read(c_ubyte), stream.read(c_ubyte)
waypoint_unknown3 = stream.read(c_float), stream.read(c_float), stream.read(c_float)
waypoint_values += waypoint_unknown1, waypoint_unknown2, waypoint_unknown3
elif path_type == PathType.Rail:
waypoint_unknown1 = stream.read(c_float), stream.read(c_float), stream.read(c_float), stream.read(c_float)
waypoint_values += waypoint_unknown1,
if path_version >= 17:
waypoint_unknown2 = stream.read(c_float)
waypoint_values += waypoint_unknown2,
waypoint = self.tree.insert(path, END, text="Waypoint", values=waypoint_values)
if path_type in (PathType.Movement, PathType.Spawner, PathType.Rail):
for _ in range(stream.read(c_uint)):
config_name = stream.read(str, length_type=c_ubyte)
config_type_and_value = stream.read(str, length_type=c_ubyte)
self.tree.insert(waypoint, END, text="Config", values=(config_name, config_type_and_value))
def parse_lvl(self, stream, scene):
header = stream.read(bytes, length=4)
stream.read_offset = 0
if header == b"CHNK":
# newer lvl file structure
# chunk based
while not stream.all_read():
assert stream.read_offset//8 % 16 == 0 # seems everything is aligned like this?
start_pos = stream.read_offset//8
assert stream.read(bytes, length=4) == b"CHNK"
chunk_type = stream.read(c_uint)
assert stream.read(c_ushort) == 1
assert stream.read(c_ushort) in (1, 2)
chunk_length = stream.read(c_uint)
data_pos = stream.read(c_uint)
stream.read_offset = data_pos * 8
assert stream.read_offset//8 % 16 == 0
if chunk_type == 1000:
pass
elif chunk_type == 2000:
pass
elif chunk_type == 2001:
self.lvl_parse_chunk_type_2001(stream, scene)
elif chunk_type == 2002:
pass
stream.read_offset = (start_pos + chunk_length) * 8 # go to the next CHNK
else:
self.parse_old_lvl_header(stream)
self.lvl_parse_chunk_type_2001(stream, scene)
def parse_old_lvl_header(self, stream):
version = stream.read(c_ushort)
assert stream.read(c_ushort) == version
stream.read(c_ubyte)
stream.read(c_uint)
if version >= 45:
stream.read(c_float)
for _ in range(4*3):
stream.read(c_float)
if version >= 31:
if version >= 39:
for _ in range(12):
stream.read(c_float)
if version >= 40:
for _ in range(stream.read(c_uint)):
stream.read(c_uint)
stream.read(c_float)
stream.read(c_float)
else:
stream.read(c_float)
stream.read(c_float)
for _ in range(3):
stream.read(c_float)
if version >= 36:
for _ in range(3):
stream.read(c_float)
if version < 42:
for _ in range(3):
stream.read(c_float)
if version >= 33:
for _ in range(4):
stream.read(c_float)
stream.read(bytes, length_type=c_uint)
for _ in range(5):
stream.read(bytes, length_type=c_uint)
stream.skip_read(4)
for _ in range(stream.read(c_uint)):
stream.read(c_float), stream.read(c_float), stream.read(c_float)
def lvl_parse_chunk_type_2001(self, stream, scene):
for _ in range(stream.read(c_uint)):
object_id = stream.read(c_int64) # seems like the object id, but without some bits
lot = stream.read(c_uint)
unknown1 = stream.read(c_uint)
unknown2 = stream.read(c_uint)
position = stream.read(c_float), stream.read(c_float), stream.read(c_float)
rotation = stream.read(c_float), stream.read(c_float), stream.read(c_float), stream.read(c_float)
scale = stream.read(c_float)
config_data = stream.read(str, length_type=c_uint)
config_data = config_data.replace("{", "<crlbrktopen>").replace("}", "<crlbrktclose>").replace("\\", "<backslash>") # for some reason these characters aren't properly escaped when sent to Tk
assert stream.read(c_uint) == 0
lot_name = ""
if lot == 176:
lot_name = "Spawner - "
lot = config_data[config_data.index("spawntemplate")+16:config_data.index("\n", config_data.index("spawntemplate")+16)]
try:
lot_name += self.db.execute("select name from Objects where id == "+str(lot)).fetchone()[0]
except TypeError:
print("Name for lot", lot, "not found")
lot_name += " - "+str(lot)
self.tree.insert(scene, END, text="Object", values=(object_id, lot_name, unknown1, unknown2, position, rotation, scale, config_data))
def on_item_select(self, _):
item = self.tree.selection()[0]
item_type = self.tree.item(item, "text")
if item_type == "Zone":
cols = "Version", "unknown1", "World ID", "Spawnpoint Pos", "Spawnpoint Rot"
elif item_type == "Scene":
cols = "Filename", "Scene ID", "Scene Name"
elif item_type == "Terrain":
cols = "Filename", "Name", "Description"
elif item_type == "Transition Point":
cols = "Scene ID", "Position"
elif item_type == "Object":
cols = "Object ID", "LOT", "unknown1", "unknown2", "Position", "Rotation", "Scale"
elif item_type == "Spawner":
cols = "Path Version", "Name", "unknown1", "Behavior", "Spawned LOT", "Respawn Time", "Max to Spawn", "Num to maintain", "Object ID", "Activate on load"
else:
cols = ()
if cols:
self.tree.configure(columns=cols)
colwidth = self.tree.winfo_width() // (len(cols)+1)
self.tree.column("#0", width=colwidth)
for i, col in enumerate(cols):
self.tree.heading(col, text=col, command=(lambda col: lambda: self.sort_column(col, False))(col))
self.tree.column(i, width=colwidth)
self.item_inspector.delete(1.0, END)
self.item_inspector.insert(END, "\n".join(self.tree.item(item, "values")))
if __name__ == "__main__":
app = LUZViewer()
app.mainloop()