utils/luzviewer.pyw

358 lines
14 KiB
Python
Raw Normal View History

import configparser
2016-06-21 19:24:11 +00:00
import enum
import os.path
import sqlite3
2015-11-19 20:25:16 +00:00
import sys
import tkinter.filedialog as filedialog
2015-11-19 20:25:16 +00:00
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
2016-06-21 19:24:11 +00:00
class PathType(enum.IntEnum):
2017-12-25 12:25:53 +00:00
Movement = 0
2016-06-21 19:24:11 +00:00
MovingPlatform = 1
Property = 2
Camera = 3
Spawner = 4
Showcase = 5
Race = 6
Rail = 7
2016-10-22 12:02:51 +00:00
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")
2015-11-19 20:25:16 +00:00
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)
2016-06-21 19:24:11 +00:00
assert version in (36, 38, 39, 40, 41), version
unknown1 = stream.read(c_uint)
world_id = stream.read(c_uint)
2016-06-21 19:24:11 +00:00
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")
2016-06-21 19:24:11 +00:00
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")
2016-06-21 19:24:11 +00:00
scene_id = stream.read(c_uint64)
scene_name = stream.read(bytes, length_type=c_ubyte).decode("latin1")
2016-06-21 19:24:11 +00:00
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
2016-06-21 19:24:11 +00:00
### 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))
2016-06-21 19:24:11 +00:00
2016-06-29 09:47:58 +00:00
### scene transitions
scene_transitions = self.tree.insert(zone, END, text="Scene Transitions")
for _ in range(stream.read(c_uint)):
2016-06-29 09:47:58 +00:00
scene_transition_values = ()
if version < 40:
scene_transition_values += stream.read(bytes, length_type=c_ubyte),
2016-06-29 09:47:58 +00:00
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))
2016-06-21 19:24:11 +00:00
remaining_length = stream.read(c_uint)
assert luz_len - stream.read_offset//8 == remaining_length
assert stream.read(c_uint) == 1
2016-06-21 19:24:11 +00:00
### 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)
2016-10-22 12:02:51 +00:00
behavior = PathBehavior(stream.read(c_uint))
values = path_version, name, unknown1, behavior
2016-06-21 19:24:11 +00:00
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:
2017-12-25 12:25:53 +00:00
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
2016-06-21 19:24:11 +00:00
elif path_type == PathType.Camera:
2017-12-25 12:25:53 +00:00
next_path = stream.read(str, length_type=c_ubyte)
values += next_path,
2016-06-21 19:24:11 +00:00
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")
2017-12-25 12:25:53 +00:00
respawn_time = stream.read(c_uint)
max_to_spawn = stream.read(c_int)
num_to_maintain = stream.read(c_uint)
2016-06-21 19:24:11 +00:00
object_id = stream.read(c_int64)
2017-12-25 12:25:53 +00:00
activate_on_load = stream.read(c_bool)
values += lot_name, respawn_time, max_to_spawn, num_to_maintain, object_id, activate_on_load
2016-06-21 19:24:11 +00:00
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)
2017-12-25 12:25:53 +00:00
speed = stream.read(c_float)
wait = stream.read(c_float)
waypoint_values += rotation, waypoint_unknown2, speed, wait
2016-06-21 19:24:11 +00:00
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:
2017-12-25 12:25:53 +00:00
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
2016-06-21 19:24:11 +00:00
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:
2017-09-17 09:34:52 +00:00
self.parse_old_lvl_header(stream)
self.lvl_parse_chunk_type_2001(stream, scene)
2017-09-17 09:34:52 +00:00
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":
2016-06-29 09:47:58 +00:00
cols = "Version", "unknown1", "World ID", "Spawnpoint Pos", "Spawnpoint Rot"
elif item_type == "Scene":
2016-06-29 09:47:58 +00:00
cols = "Filename", "Scene ID", "Scene Name"
elif item_type == "Terrain":
2016-06-29 09:47:58 +00:00
cols = "Filename", "Name", "Description"
elif item_type == "Transition Point":
cols = "Scene ID", "Position"
elif item_type == "Object":
2016-06-29 09:47:58 +00:00
cols = "Object ID", "LOT", "unknown1", "unknown2", "Position", "Rotation", "Scale"
2017-12-25 12:25:53 +00:00
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)
2017-12-25 12:25:53 +00:00
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))
2017-12-25 12:25:53 +00:00
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()