boss contacts & many small fixes
This commit is contained in:
52
_Utils/Dungeon/dungeon_objects.hexpat
Normal file
52
_Utils/Dungeon/dungeon_objects.hexpat
Normal file
@@ -0,0 +1,52 @@
|
||||
struct addScr {
|
||||
u32 params[6];
|
||||
};
|
||||
|
||||
|
||||
struct vertexCoords {
|
||||
s16 X;
|
||||
s16 Z;
|
||||
s16 Y;
|
||||
u16 dummy;
|
||||
};
|
||||
|
||||
struct poly {
|
||||
u8 ulX;
|
||||
u8 ulY;
|
||||
u8 urX;
|
||||
u8 urY;
|
||||
u8 llX;
|
||||
u8 llY;
|
||||
u8 lrX;
|
||||
u8 lrY;
|
||||
|
||||
};
|
||||
|
||||
struct p2dungObj {
|
||||
u32 vertexDataOffset;
|
||||
u32 vertexCnt;
|
||||
u32 polyDataOffset;
|
||||
u32 polyCnt;
|
||||
u32 gpuCmdOffset;
|
||||
u64 gpuCmdCnt;
|
||||
|
||||
vertexCoords vertexData [vertexCnt] @ vertexDataOffset;
|
||||
poly polyData [polyCnt] @ polyDataOffset;
|
||||
u32 gpuData @ gpuCmdOffset;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
struct p2dungObjectsInfo {
|
||||
|
||||
u32 addPtr;
|
||||
u32 addCtr;
|
||||
u32 dummy;
|
||||
u32 objCtr;
|
||||
|
||||
p2dungObj objects[objCtr] @ 0x10;
|
||||
addScr add[objCtr] @ addPtr;
|
||||
};
|
||||
|
||||
p2dungObjectsInfo p2dObjects @ 0x00;
|
||||
81
_Utils/Dungeon/dungeon_struct.hexpat
Normal file
81
_Utils/Dungeon/dungeon_struct.hexpat
Normal file
@@ -0,0 +1,81 @@
|
||||
struct placeName {
|
||||
u16 status;
|
||||
char name[30];
|
||||
};
|
||||
|
||||
struct scriptParams {
|
||||
u32 cmd;
|
||||
u16 param1;
|
||||
u16 separ;
|
||||
u32 jumpType;
|
||||
u32 dungEventId;
|
||||
u32 param2;
|
||||
u8 endData[4];
|
||||
};
|
||||
|
||||
/* jump types
|
||||
0 - dungeon
|
||||
1 - event
|
||||
2 - city
|
||||
3 - ????
|
||||
|
||||
event loads from pointer table from
|
||||
Load_Event_from_Pointer:8002a5b0(R)
|
||||
@ 80072c70
|
||||
|
||||
|
||||
- координаты точек спавна описаны по указателю sTS
|
||||
- где описывается взаимодействие с блоками?
|
||||
- где привязываются и активируются текстовые окна?
|
||||
-
|
||||
*/
|
||||
|
||||
struct spawnEl {
|
||||
u16 id [[color("002277")]];
|
||||
u16 floorId [[color("002244")]];
|
||||
u8 x [[color("005555")]];
|
||||
u8 y [[color("775555")]];
|
||||
u16 rotation [[color("445555")]];
|
||||
};
|
||||
|
||||
struct floorInfo {
|
||||
u16 width;
|
||||
u16 height;
|
||||
u32 FFseparator;
|
||||
u32 floorId;
|
||||
u32 geometryPtr;
|
||||
u32 collisionsPtr;
|
||||
u32 mapsPtr;
|
||||
u32 vars1Ptr;
|
||||
u32 vars2Ptr;
|
||||
u32 vars3Ptr;
|
||||
u32 zonesPtr;
|
||||
u32 vars5Ptr;
|
||||
u32 aSeparate[4];
|
||||
|
||||
u16 geometryData[width*height] @ geometryPtr;
|
||||
u16 collisionsData[width*height*4] @ collisionsPtr;
|
||||
u16 mapsData[width*height*4] @ mapsPtr;
|
||||
u32 vars1Data @ vars1Ptr;
|
||||
scriptParams vars2Data[4] @ vars2Ptr;
|
||||
u32 some3Data @ vars3Ptr;
|
||||
placeName names[3] @ zonesPtr;
|
||||
u32 vars5Data @ vars5Ptr;
|
||||
|
||||
};
|
||||
|
||||
|
||||
struct p2dungeonInfo {
|
||||
char name[32];
|
||||
u32 floorCount;
|
||||
u8 floorIDs [0x10];
|
||||
u8 floorIDhz [8];
|
||||
u32 sTS;
|
||||
u32 sTE;
|
||||
floorInfo Info[floorCount];
|
||||
spawnEl eData[55] @ sTS;
|
||||
u32 uData @ sTE;
|
||||
};
|
||||
|
||||
|
||||
p2dungeonInfo dungeon @ 0x00;
|
||||
137
_Utils/Dungeon/texture_extract.py
Normal file
137
_Utils/Dungeon/texture_extract.py
Normal file
@@ -0,0 +1,137 @@
|
||||
import struct
|
||||
import os
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
def extract_textures_from_file(filename, output_dir="textures"):
|
||||
"""
|
||||
Извлекает текстуры из файла в отдельные PNG файлы с индексированными цветами
|
||||
"""
|
||||
|
||||
# Создаем директорию для выходных файлов
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Читаем файл
|
||||
with open(filename, 'rb') as f:
|
||||
fil = f.read()
|
||||
|
||||
# Получаем список смещений текстур
|
||||
tx_offsets = []
|
||||
first_addr = struct.unpack('<I', fil[0:4])[0]
|
||||
tx_offsets.append(first_addr)
|
||||
|
||||
reader = 4
|
||||
while reader < first_addr:
|
||||
addr = struct.unpack('<I', fil[reader:reader+4])[0]
|
||||
if addr == 0:
|
||||
break
|
||||
tx_offsets.append(addr)
|
||||
reader += 4
|
||||
|
||||
print(f"Найдено {len(tx_offsets)} текстурных блоков")
|
||||
|
||||
# Обрабатываем каждую текстуру
|
||||
for a in range(len(tx_offsets) - 1):
|
||||
try:
|
||||
reader = tx_offsets[a]
|
||||
|
||||
# Читаем CLUT и информацию об изображении
|
||||
clut_mode = struct.unpack('<I', fil[reader:reader+4])[0]
|
||||
image_mode = struct.unpack('<I', fil[reader+4:reader+8])[0]
|
||||
reader += 8
|
||||
|
||||
# Читаем CLUT (палитру цветов)
|
||||
clut_size = struct.unpack('<I', fil[reader:reader+4])[0]
|
||||
clut_x = struct.unpack('<H', fil[reader+4:reader+6])[0]
|
||||
clut_y = struct.unpack('<H', fil[reader+6:reader+8])[0]
|
||||
clut_w = struct.unpack('<H', fil[reader+8:reader+10])[0]
|
||||
clut_h = struct.unpack('<H', fil[reader+10:reader+12])[0]
|
||||
reader += 12
|
||||
|
||||
# Читаем данные палитры
|
||||
current_clut = []
|
||||
clut_data_remains = clut_size - 12
|
||||
while clut_data_remains > 0:
|
||||
color_code = struct.unpack('<H', fil[reader:reader+2])[0]
|
||||
current_clut.append(color_code)
|
||||
clut_data_remains -= 2
|
||||
reader += 2
|
||||
|
||||
# Читаем информацию об изображении
|
||||
image_size = struct.unpack('<I', fil[reader:reader+4])[0]
|
||||
image_x = struct.unpack('<H', fil[reader+4:reader+6])[0] * 2
|
||||
image_y = struct.unpack('<H', fil[reader+6:reader+8])[0] - 256
|
||||
image_w = struct.unpack('<H', fil[reader+8:reader+10])[0] * 2
|
||||
image_h = struct.unpack('<H', fil[reader+10:reader+12])[0]
|
||||
reader += 12
|
||||
|
||||
print(f"Текстура {a}: {image_w}x{image_h}, CLUT: {len(current_clut)} цветов")
|
||||
|
||||
# Создаем палитру для PIL
|
||||
palette = []
|
||||
for color_code in current_clut:
|
||||
# Конвертируем из формата PS1 (15-bit) в 24-bit RGB
|
||||
r = (color_code & 0x1F) * 8
|
||||
g = ((color_code & 0x3E0) >> 5) * 8
|
||||
b = ((color_code & 0x7C00) >> 10) * 8
|
||||
palette.extend([r, g, b])
|
||||
|
||||
# Дополняем палитру до 256 цветов (если нужно)
|
||||
while len(palette) < 768: # 256 цветов * 3 канала
|
||||
palette.append(0)
|
||||
|
||||
# Создаем изображение с индексированными цветами
|
||||
img_data = []
|
||||
image_data_remains = image_size - 12
|
||||
|
||||
for y in range(image_h):
|
||||
row = []
|
||||
for x in range(image_w):
|
||||
if image_data_remains > 0:
|
||||
color_index = fil[reader]
|
||||
row.append(color_index)
|
||||
reader += 1
|
||||
image_data_remains -= 1
|
||||
else:
|
||||
row.append(0) # Заполняем нулями если данные закончились
|
||||
img_data.append(row)
|
||||
|
||||
# Создаем изображение из данных
|
||||
img = Image.new('P', (image_w, image_h))
|
||||
|
||||
# Заполняем пиксели
|
||||
for y in range(image_h):
|
||||
for x in range(image_w):
|
||||
if y < len(img_data) and x < len(img_data[y]):
|
||||
img.putpixel((x, y), img_data[y][x])
|
||||
|
||||
# Устанавливаем палитру
|
||||
img.putpalette(palette)
|
||||
|
||||
# Сохраняем как PNG
|
||||
output_filename = os.path.join(output_dir, f"texture_{a:03d}.png")
|
||||
img.save(output_filename, 'PNG')
|
||||
|
||||
print(f"Сохранено: {output_filename}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при обработке текстуры {a}: {e}")
|
||||
continue
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("Использование: python extract.py <имя_файла>")
|
||||
sys.exit(1)
|
||||
|
||||
filename = sys.argv[1]
|
||||
|
||||
if not os.path.exists(filename):
|
||||
print(f"Файл {filename} не найден!")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Обрабатываю файл: {filename}")
|
||||
extract_textures_from_file(filename)
|
||||
print("Готово!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
220
_Utils/Dungeon/texture_import.py
Normal file
220
_Utils/Dungeon/texture_import.py
Normal file
@@ -0,0 +1,220 @@
|
||||
import struct
|
||||
import os
|
||||
import sys
|
||||
from PIL import Image
|
||||
|
||||
def import_texture_to_pack(pack_filename, texture_filename, output_pack_filename=None):
|
||||
"""
|
||||
Заменяет пиксельные данные текстуры в pack-файле на данные из PNG файла
|
||||
"""
|
||||
|
||||
if output_pack_filename is None:
|
||||
output_pack_filename = pack_filename
|
||||
|
||||
# Получаем ID текстуры из имени файла
|
||||
texture_id = extract_texture_id(texture_filename)
|
||||
if texture_id is None:
|
||||
print("Не удалось определить ID текстуры из имени файла!")
|
||||
return False
|
||||
|
||||
print(f"Импортируем текстуру ID: {texture_id} из {texture_filename}")
|
||||
|
||||
# Читаем pack-файл
|
||||
with open(pack_filename, 'rb') as f:
|
||||
pack_data = bytearray(f.read())
|
||||
|
||||
# Находим смещения текстур
|
||||
tx_offsets = find_texture_offsets(pack_data)
|
||||
if texture_id >= len(tx_offsets) - 1:
|
||||
print(f"Ошибка: текстура с ID {texture_id} не найдена в pack-файле!")
|
||||
return False
|
||||
|
||||
# Находим позицию пиксельных данных в pack-файле
|
||||
pixel_data_pos = find_pixel_data_position(pack_data, tx_offsets, texture_id)
|
||||
if pixel_data_pos is None:
|
||||
print(f"Не удалось найти позицию пиксельных данных для текстуры {texture_id}!")
|
||||
return False
|
||||
|
||||
# Получаем информацию о размерах текстуры из pack-файла
|
||||
texture_info = get_texture_info(pack_data, tx_offsets, texture_id)
|
||||
if texture_info is None:
|
||||
print(f"Не удалось получить информацию о текстуре {texture_id}!")
|
||||
return False
|
||||
|
||||
image_w, image_h, image_size = texture_info
|
||||
|
||||
# Загружаем PNG файл
|
||||
try:
|
||||
img = Image.open(texture_filename)
|
||||
except Exception as e:
|
||||
print(f"Ошибка загрузки PNG файла: {e}")
|
||||
return False
|
||||
|
||||
# Проверяем размеры
|
||||
if img.width != image_w or img.height != image_h:
|
||||
print(f"Ошибка: размеры не совпадают!")
|
||||
print(f"Ожидается: {image_w}x{image_h}")
|
||||
print(f"PNG файл: {img.width}x{img.height}")
|
||||
return False
|
||||
|
||||
# Конвертируем в индексированное изображение если нужно
|
||||
if img.mode != 'P':
|
||||
print("Предупреждение: PNG не в индексированном режиме. Конвертируем...")
|
||||
img = img.convert('P')
|
||||
|
||||
# Извлекаем пиксельные данные
|
||||
pixel_data = []
|
||||
for y in range(image_h):
|
||||
for x in range(image_w):
|
||||
try:
|
||||
pixel_index = img.getpixel((x, y))
|
||||
pixel_data.append(pixel_index & 0xFF) #确保是字节
|
||||
except:
|
||||
pixel_data.append(0)
|
||||
|
||||
# Проверяем размер данных
|
||||
expected_pixel_count = image_w * image_h
|
||||
actual_pixel_count = len(pixel_data)
|
||||
|
||||
if actual_pixel_count != expected_pixel_count:
|
||||
print(f"Ошибка: количество пикселей не совпадает!")
|
||||
print(f"Ожидается: {expected_pixel_count}")
|
||||
print(f"Получено: {actual_pixel_count}")
|
||||
return False
|
||||
|
||||
# Заменяем данные в pack-файле
|
||||
bytes_replaced = replace_pixel_data(pack_data, pixel_data_pos, pixel_data, image_size - 12)
|
||||
|
||||
# Сохраняем измененный pack-файл
|
||||
try:
|
||||
with open(output_pack_filename, 'wb') as f:
|
||||
f.write(pack_data)
|
||||
print(f"Успешно заменено {bytes_replaced} байт!")
|
||||
print(f"Сохранено в: {output_pack_filename}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Ошибка сохранения файла: {e}")
|
||||
return False
|
||||
|
||||
def extract_texture_id(filename):
|
||||
"""Извлекает ID текстуры из имени файла"""
|
||||
basename = os.path.basename(filename)
|
||||
# Ищем паттерн texture_XXX.png
|
||||
if basename.startswith('texture_') and basename.endswith('.png'):
|
||||
try:
|
||||
id_str = basename[8:-4] # Убираем 'texture_' и '.png'
|
||||
return int(id_str)
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def find_texture_offsets(pack_data):
|
||||
"""Находит смещения текстур в pack-файле"""
|
||||
tx_offsets = []
|
||||
first_addr = struct.unpack('<I', pack_data[0:4])[0]
|
||||
tx_offsets.append(first_addr)
|
||||
|
||||
reader = 4
|
||||
while reader < first_addr:
|
||||
addr = struct.unpack('<I', pack_data[reader:reader+4])[0]
|
||||
if addr == 0:
|
||||
break
|
||||
tx_offsets.append(addr)
|
||||
reader += 4
|
||||
|
||||
return tx_offsets
|
||||
|
||||
def find_pixel_data_position(pack_data, tx_offsets, texture_id):
|
||||
"""Находит позицию пиксельных данных для указанной текстуры"""
|
||||
try:
|
||||
reader = tx_offsets[texture_id]
|
||||
|
||||
# Пропускаем режимы CLUT и изображения
|
||||
reader += 8
|
||||
|
||||
# Читаем размер CLUT и пропускаем CLUT данные
|
||||
clut_size = struct.unpack('<I', pack_data[reader:reader+4])[0]
|
||||
reader += clut_size # Пропускаем весь CLUT блок
|
||||
|
||||
# Пропускаем заголовок изображения
|
||||
reader += 12
|
||||
|
||||
# Теперь reader указывает на начало пиксельных данных
|
||||
return reader
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка поиска позиции данных: {e}")
|
||||
return None
|
||||
|
||||
def get_texture_info(pack_data, tx_offsets, texture_id):
|
||||
"""Получает информацию о размерах текстуры"""
|
||||
try:
|
||||
reader = tx_offsets[texture_id]
|
||||
|
||||
# Пропускаем режимы
|
||||
reader += 8
|
||||
|
||||
# Пропускаем CLUT
|
||||
clut_size = struct.unpack('<I', pack_data[reader:reader+4])[0]
|
||||
reader += clut_size
|
||||
|
||||
# Читаем информацию об изображении
|
||||
image_size = struct.unpack('<I', pack_data[reader:reader+4])[0]
|
||||
image_x = struct.unpack('<H', pack_data[reader+4:reader+6])[0] * 2
|
||||
image_y = struct.unpack('<H', pack_data[reader+6:reader+8])[0] - 256
|
||||
image_w = struct.unpack('<H', pack_data[reader+8:reader+10])[0] * 2
|
||||
image_h = struct.unpack('<H', pack_data[reader+10:reader+12])[0]
|
||||
|
||||
return image_w, image_h, image_size
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка получения информации о текстуре: {e}")
|
||||
return None
|
||||
|
||||
def replace_pixel_data(pack_data, start_pos, new_pixel_data, expected_size):
|
||||
"""Заменяет пиксельные данные в pack-файле"""
|
||||
bytes_replaced = 0
|
||||
|
||||
for i, pixel_byte in enumerate(new_pixel_data):
|
||||
if start_pos + i < len(pack_data) and i < expected_size:
|
||||
pack_data[start_pos + i] = pixel_byte
|
||||
bytes_replaced += 1
|
||||
else:
|
||||
break
|
||||
|
||||
return bytes_replaced
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("Использование: python import.py <pack_file> <texture_file> [output_file]")
|
||||
print("Пример: python import.py game.pack textures/texture_005.png")
|
||||
print("Пример: python import.py game.pack textures/texture_010.png game_modified.pack")
|
||||
sys.exit(1)
|
||||
|
||||
pack_filename = sys.argv[1]
|
||||
texture_filename = sys.argv[2]
|
||||
output_filename = sys.argv[3] if len(sys.argv) > 3 else pack_filename
|
||||
|
||||
if not os.path.exists(pack_filename):
|
||||
print(f"Pack-файл не найден: {pack_filename}")
|
||||
sys.exit(1)
|
||||
|
||||
if not os.path.exists(texture_filename):
|
||||
print(f"Текстура не найдена: {texture_filename}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Pack файл: {pack_filename}")
|
||||
print(f"Текстура: {texture_filename}")
|
||||
print(f"Выходной файл: {output_filename}")
|
||||
print("-" * 50)
|
||||
|
||||
success = import_texture_to_pack(pack_filename, texture_filename, output_filename)
|
||||
|
||||
if success:
|
||||
print("Импорт завершен успешно!")
|
||||
else:
|
||||
print("Импорт завершен с ошибками!")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
36
_Utils/Event/0397_24_0_24_J.txt
Normal file
36
_Utils/Event/0397_24_0_24_J.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
START:
|
||||
000000A8
|
||||
000000B9 000000A0 00000078 00FFFFFF 00FFFFFF 00000002
|
||||
000000EA 00000019
|
||||
SoundPly 00003023 0000007F
|
||||
|
||||
00000043 00000004
|
||||
|
||||
//FADE IN
|
||||
000000A9
|
||||
000000BA 00000000 00006000 00000001
|
||||
000000BB 00000000 00006000 00000001
|
||||
0000004E
|
||||
//000000BC 00000000
|
||||
//000000BC 00000001
|
||||
//CtrlLock
|
||||
__GoSub># SHOW_TEXT
|
||||
__WaitTo #
|
||||
LoadDung 00002E06
|
||||
__Return--> START
|
||||
|
||||
NUNAH:
|
||||
collLink@ B 83 1 0 22
|
||||
__WaitTo @
|
||||
__Return--> NUNAH
|
||||
|
||||
SHOW_TEXT:
|
||||
WindShow! 0
|
||||
TextShow@ [2E12][0B00]Привет[2E12][0100][0111] Неужели эта херня работает?~[0611][0211][0311]
|
||||
__WaitTo @
|
||||
WinClose !
|
||||
__Return--> SHOW_TEXT
|
||||
|
||||
TEST_SUB:
|
||||
CSetAnim 1 1F 2D
|
||||
__Return--> TEST_SUB
|
||||
BIN
_Utils/Event/0397_24_0_24_J.txt.TRANSL
Normal file
BIN
_Utils/Event/0397_24_0_24_J.txt.TRANSL
Normal file
Binary file not shown.
421
_Utils/Event/Blender_plugins/event_scene_export.py
Normal file
421
_Utils/Event/Blender_plugins/event_scene_export.py
Normal file
@@ -0,0 +1,421 @@
|
||||
bl_info = {
|
||||
"name": "Persona2 PSX Scene Exporter",
|
||||
"author": "Sergey Shemet",
|
||||
"version": (1, 0),
|
||||
"blender": (4, 5, 0),
|
||||
"location": "File > Export",
|
||||
"description": "Export Persona 2 Eternal Punishment PSX 3D scenes (.p2e)",
|
||||
"category": "Import-Export",
|
||||
}
|
||||
|
||||
import bpy
|
||||
import struct
|
||||
import math
|
||||
import binascii
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
from bpy.props import StringProperty, BoolProperty
|
||||
|
||||
class ExportP2E(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "export_scene.p2e"
|
||||
bl_label = "Export Persona2 PSX Scene"
|
||||
bl_description = "Export Persona 2 Eternal Punishment PSX 3D scenes"
|
||||
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
|
||||
|
||||
filename_ext = ".p2e"
|
||||
|
||||
filter_glob: StringProperty(
|
||||
default="*.p2e",
|
||||
options={'HIDDEN'},
|
||||
maxlen=255,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
# 1. Сбор объектов из коллекции PSX_Export
|
||||
export_collection = bpy.data.collections.get("PSX_Export")
|
||||
if not export_collection:
|
||||
self.report({'ERROR'}, "Collection 'PSX_Export' not found")
|
||||
return {'CANCELLED'}
|
||||
|
||||
scene_data = self.prepare_scene_data(export_collection)
|
||||
if scene_data is None: # Если prepare_scene_data провалился
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.write_p2e_file(self.filepath, scene_data)
|
||||
self.report({'INFO'}, f"Exported {len(scene_data['objects'])} objects")
|
||||
return {'FINISHED'}
|
||||
|
||||
def prepare_scene_data(self, export_collection):
|
||||
"""Подготовка всех данных сцены"""
|
||||
if not export_collection:
|
||||
self.report({'ERROR'}, "Export collection is None")
|
||||
return None
|
||||
|
||||
objects_data = []
|
||||
global_vertices = []
|
||||
global_vertex_offset = 0
|
||||
|
||||
for obj in export_collection.objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
|
||||
# Данные объекта
|
||||
obj_data = {
|
||||
'id': len(objects_data),
|
||||
'custom_props': {k: v for k, v in obj.items() if k not in ['_RNA_UI']},
|
||||
'polygon_count': 0,
|
||||
'chunk_data': bytearray(),
|
||||
'local_vertices': [],
|
||||
'vertex_map': {}
|
||||
}
|
||||
|
||||
# Обработка вершин меша
|
||||
mesh = obj.data
|
||||
mesh.calc_loop_triangles() # Убедимся, что полигональные данные актуальны
|
||||
|
||||
local_vertices = []
|
||||
for vert in mesh.vertices:
|
||||
world_co = obj.matrix_world @ vert.co
|
||||
psx_vert = (
|
||||
int(world_co.x * 100),
|
||||
-int(world_co.z * 100),
|
||||
int(world_co.y * 100),
|
||||
0
|
||||
)
|
||||
local_vertices.append(psx_vert)
|
||||
|
||||
# Уникальные вершины
|
||||
unique_vertices = []
|
||||
vertex_map = {}
|
||||
for i, vert in enumerate(local_vertices):
|
||||
found = False
|
||||
for j, unique_vert in enumerate(unique_vertices):
|
||||
if vert == unique_vert:
|
||||
vertex_map[i] = j
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
vertex_map[i] = len(unique_vertices)
|
||||
unique_vertices.append(vert)
|
||||
|
||||
for vert in unique_vertices:
|
||||
global_vertices.append(vert)
|
||||
obj_data['local_vertices'].append(vert)
|
||||
obj_data['vertex_map'] = vertex_map
|
||||
|
||||
# === ПРОВЕРКА UV СЛОЯ ===
|
||||
if not mesh.uv_layers.active:
|
||||
self.report({'ERROR'}, f"Object '{obj.name}' has no active UV map. Please unwrap UVs.")
|
||||
return None
|
||||
|
||||
uv_layer = mesh.uv_layers.active.data
|
||||
if len(uv_layer) != len(mesh.loops):
|
||||
self.report({'WARNING'}, f"UV layer size mismatch for '{obj.name}': {len(uv_layer)} UVs vs {len(mesh.loops)} loops")
|
||||
|
||||
# === ОБРАБОТКА ПОЛИГОНОВ ===
|
||||
for poly in mesh.polygons:
|
||||
vertex_count = len(poly.vertices)
|
||||
if vertex_count not in (3, 4):
|
||||
self.report({'ERROR'}, f"Polygon in '{obj.name}' has {vertex_count} vertices. Only triangles (3) or quads (4) allowed.")
|
||||
return None
|
||||
|
||||
# Собираем UV координаты с проверкой индексов
|
||||
uvs = []
|
||||
for loop_idx in poly.loop_indices:
|
||||
if loop_idx >= len(uv_layer):
|
||||
self.report({'ERROR'}, f"Loop index {loop_idx} out of range in '{obj.name}'. UV data is broken or inconsistent.")
|
||||
return None
|
||||
uv = uv_layer[loop_idx].uv
|
||||
uvs.append((uv.x, uv.y))
|
||||
|
||||
# Получаем индексы вершин
|
||||
vertex_ids = [obj_data['vertex_map'][v] + global_vertex_offset for v in poly.vertices]
|
||||
|
||||
# Получаем цвет затенения
|
||||
shade = obj.get("shade", (128, 128, 128))
|
||||
|
||||
# Формируем чанк
|
||||
if vertex_count == 3:
|
||||
chunk = self.create_textured_tri_chunk(uvs, shade)
|
||||
else: # 4
|
||||
chunk = self.create_textured_quad_chunk(uvs, shade)
|
||||
|
||||
# тестовый разворот вершин
|
||||
vertex_ids = vertex_ids[::-1]
|
||||
|
||||
|
||||
# Меняем 3 и 4 вершины местами для квадов (если 4 вершины)
|
||||
if vertex_count == 4:
|
||||
vertex_ids[2], vertex_ids[3] = vertex_ids[3], vertex_ids[2]
|
||||
|
||||
# Добавляем индексы в чанк
|
||||
for vid in vertex_ids:
|
||||
chunk += struct.pack('<I', vid)
|
||||
|
||||
obj_data['chunk_data'] += chunk
|
||||
obj_data['polygon_count'] += 1
|
||||
|
||||
# Обновляем смещение
|
||||
global_vertex_offset += len(unique_vertices)
|
||||
objects_data.append(obj_data)
|
||||
|
||||
return {
|
||||
'objects': objects_data,
|
||||
'vertices': global_vertices,
|
||||
'vertex_count': len(global_vertices),
|
||||
'object_count': len(objects_data)
|
||||
}
|
||||
|
||||
|
||||
def calc_tex_params(self, uvs):
|
||||
"""Расчет CLUT и TexPage по UV координатам"""
|
||||
# Среднее U координат
|
||||
avg_u = sum(u for u, v in uvs) / len(uvs)
|
||||
|
||||
|
||||
# Расчет CLUT (0-7)
|
||||
# CLUT - это индекс в палитре текстур, нам нужно определить в каком из блоков 1/8 текстуры находится среднее U
|
||||
# Блоки CLUT расположены в VRAM линиями, начиная с 0x480. Поэтому нам нужно просто смещаться по Y по 1 для каждого блока
|
||||
clut_index = min(int(avg_u * 8), 7)
|
||||
clut = (clut_index + 480) << 6 # CLUT хранится как int16, YYYYYYYYYYXXXXX. Так как X всегда 0, то просто смещаем Y на 6 бит
|
||||
|
||||
|
||||
# Расчет TexPage
|
||||
tex_page_raw = int(avg_u * 4)
|
||||
tex_page = ((tex_page_raw & 15) + 10) | 16 | 128 #default p2 texpage shift (10) & enabling lower page (5th bit) & 8-bit mode (8th bit)
|
||||
|
||||
|
||||
# Расчет X координат - нам нужно сместить x относительно текстурной страницы
|
||||
tex_page_u_offset = (tex_page_raw & 3) * 0.25 # Коэффициент текстурной страницы в 0...1
|
||||
xs = []
|
||||
|
||||
for u, v in uvs:
|
||||
# X = (U - смещение страницы) * 512.
|
||||
# 512 - это ширина всей текстуры. Отнимаем коэффициент от X, умножая на 512,
|
||||
# чтобы получить положение X относительно начала страницы в координатах всей текстуры
|
||||
x = int((u - tex_page_u_offset) * 512) & 0xFF
|
||||
xs.append(x)
|
||||
|
||||
return {
|
||||
'clut': clut,
|
||||
'tex_page': tex_page,
|
||||
'xs': xs
|
||||
}
|
||||
|
||||
def create_textured_tri_chunk(self, uvs, shade):
|
||||
"""Создание чанка для текстурированного треугольника"""
|
||||
params = self.calc_tex_params(uvs)
|
||||
|
||||
chunk = bytearray()
|
||||
|
||||
# Заглушка (4 байта)
|
||||
chunk += b'\x5c\x64\x61\x07'
|
||||
|
||||
# Цвет и команда GPU
|
||||
chunk += struct.pack('<BBBB', shade[0], shade[1], shade[2], 0x24)
|
||||
|
||||
# Вершина 1
|
||||
chunk += b'\x70\x5c\x30\x30'
|
||||
chunk += struct.pack('<BBH',
|
||||
params['xs'][0], # X1
|
||||
int((1 - uvs[0][1]) * 255), # Y1 = V1 * 256
|
||||
params['clut'] # CLUT
|
||||
)
|
||||
|
||||
# Вершина 2
|
||||
chunk += b'\x30\x32\x34\x2e'
|
||||
chunk += struct.pack('<BBH',
|
||||
params['xs'][1], # X2
|
||||
int((1 - uvs[2][1]) * 255), # Y2
|
||||
params['tex_page'] # TexPage
|
||||
)
|
||||
|
||||
# Вершина 3
|
||||
chunk += b'\x1e\xc1\x00\x09'
|
||||
chunk += struct.pack('<BBH',
|
||||
params['xs'][2], # X3
|
||||
int((1 - uvs[3][1]) * 255), # Y3
|
||||
0 # Заглушка
|
||||
)
|
||||
|
||||
# Дублируем массив 2 раза (технический нюанс)
|
||||
full_chunk = chunk * 2
|
||||
|
||||
# Добавляем заголовок чанка
|
||||
header = struct.pack('<BBH', 0x2, 0x13, 0)
|
||||
return header + full_chunk
|
||||
|
||||
def create_textured_quad_chunk(self, uvs, shade):
|
||||
"""Создание чанка для текстурированного квадра"""
|
||||
params = self.calc_tex_params(uvs)
|
||||
|
||||
chunk = bytearray()
|
||||
|
||||
# Магические байты
|
||||
chunk += b'\x5c\x64\x61\x09'
|
||||
|
||||
# Цвет и команда
|
||||
chunk += struct.pack('<BBBB', shade[0], shade[1], shade[2], 0x2C)
|
||||
|
||||
# Вершина 1
|
||||
chunk += b'\x70\x5c\x30\x30'
|
||||
chunk += struct.pack('<BBH',
|
||||
params['xs'][3], # X1
|
||||
# params['xs'][0], # X1
|
||||
int((1 - uvs[3][1]) * 255), # Y1
|
||||
# int((1 - uvs[0][1]) * 255), # Y1
|
||||
params['clut'] # CLUT
|
||||
)
|
||||
|
||||
# Вершина 2
|
||||
chunk += b'\x30\x32\x34\x2e'
|
||||
chunk += struct.pack('<BBH',
|
||||
params['xs'][2], # X2
|
||||
# params['xs'][1], # X2
|
||||
int((1 - uvs[2][1]) * 255), # Y2
|
||||
# int((1 - uvs[1][1]) * 255), # Y2
|
||||
params['tex_page'] # TexPage
|
||||
)
|
||||
|
||||
# Вершина 4
|
||||
chunk += b'\x1e\xC1\x00\x09'
|
||||
chunk += struct.pack('<BBBB',
|
||||
params['xs'][0], # X4
|
||||
# params['xs'][3], # X4
|
||||
int((1 - uvs[0][1]) * 255), # Y4 = V4 * 256
|
||||
# int((1 - uvs[3][1]) * 255), # Y4 = V4 * 256
|
||||
84, 12 # Заглушка
|
||||
)
|
||||
|
||||
# Вершина 3
|
||||
chunk += b'\x5c\x64\x61\x07'
|
||||
chunk += struct.pack('<BBBB',
|
||||
params['xs'][1], # X3
|
||||
# params['xs'][2], # X3
|
||||
int((1 - uvs[1][1]) * 255), # Y3 = V3 * 256
|
||||
# int((1 - uvs[2][1]) * 255), # Y3 = V3 * 256
|
||||
128, 36 # Заглушка
|
||||
)
|
||||
|
||||
# Дублируем массив 2 раза
|
||||
full_chunk = chunk * 2
|
||||
|
||||
# Добавляем заголовок
|
||||
header = struct.pack('<BBH', 0x6, 0x18, 0)
|
||||
return header + full_chunk
|
||||
|
||||
def write_p2e_file(self, filepath, scene_data):
|
||||
"""Запись .p2e файла"""
|
||||
with open(filepath, 'wb') as f:
|
||||
# 1. sourceName (16 байт)
|
||||
f.write(b'Hello, blya.\x00\x00\x00\x00')
|
||||
|
||||
# 2. Заголовок сцены (пока заглушки)
|
||||
header_pos = f.tell() # Должно быть 0x10
|
||||
f.write(struct.pack('<HHII',
|
||||
0, # objectsCnt - заполним позже
|
||||
0, # someObjCtr
|
||||
0, # vertexArrayPtr - заполним позже
|
||||
0 # shiftInfoPtr - заполним позже
|
||||
))
|
||||
|
||||
# 3. Таблица указателей на объекты (начинается с 0x1C)
|
||||
# Каждый objectPtr - 4 байта
|
||||
object_ptr_table_pos = f.tell() # Должно быть 0x1C
|
||||
|
||||
# Резервируем места для указателей
|
||||
object_ptr_slots = []
|
||||
for i in range(scene_data['object_count']):
|
||||
slot_pos = f.tell()
|
||||
object_ptr_slots.append(slot_pos)
|
||||
f.write(b'\x00\x00\x00\x00') # Заполним позже
|
||||
|
||||
# 4. Запись gpuPacket объектов
|
||||
object_data_addresses = []
|
||||
|
||||
for obj_idx, obj_data in enumerate(scene_data['objects']):
|
||||
# Запоминаем адрес начала данных объекта
|
||||
object_data_pos = f.tell()
|
||||
object_data_addresses.append(object_data_pos)
|
||||
|
||||
# Записываем gpuPacket заголовок
|
||||
global_chunk_cnt = obj_data['polygon_count']
|
||||
f.write(struct.pack('<HHI',
|
||||
global_chunk_cnt, # globalChunkCnt
|
||||
0, # localChunkCnt (обычно 1)
|
||||
obj_data['id'] # objectId
|
||||
))
|
||||
|
||||
# Записываем чанки полигонов
|
||||
f.write(obj_data['chunk_data'])
|
||||
|
||||
# 5. Обновляем указатели в таблице
|
||||
current_pos = f.tell()
|
||||
for i, slot_pos in enumerate(object_ptr_slots):
|
||||
f.seek(slot_pos)
|
||||
f.write(struct.pack('<I', object_data_addresses[i]))
|
||||
f.seek(current_pos)
|
||||
|
||||
|
||||
# 6. Запись массива вершин
|
||||
vertex_ptr = f.tell()
|
||||
for x, y, z, d in scene_data['vertices']:
|
||||
f.write(struct.pack('<4h', x, y, z, d))
|
||||
|
||||
# 7. Запись таблицы смещений
|
||||
shift_ptr = f.tell()
|
||||
self.write_shift_table(f, scene_data['object_count'])
|
||||
|
||||
# 8. Обновляем заголовок сцены
|
||||
f.seek(header_pos)
|
||||
f.write(struct.pack('<HHII',
|
||||
scene_data['object_count'], # objectsCnt
|
||||
0, # someObjCtr
|
||||
vertex_ptr, # vertexArrayPtr
|
||||
shift_ptr # shiftInfoPtr
|
||||
))
|
||||
|
||||
def write_shift_table(self, f, object_count):
|
||||
"""Запись таблицы смещений (objShiftInfo)"""
|
||||
for i in range(object_count):
|
||||
|
||||
f.write(struct.pack('<B', 0)) # 0
|
||||
f.write(struct.pack('<B', 16)) # 0
|
||||
f.write(struct.pack('<B', 0)) # 16
|
||||
f.write(struct.pack('<B', 0)) # 0
|
||||
|
||||
f.write(struct.pack('<i', 0))
|
||||
|
||||
f.write(struct.pack('<B', 0)) # 0
|
||||
f.write(struct.pack('<B', 16)) # 16
|
||||
f.write(struct.pack('<B', 0)) # 0
|
||||
f.write(struct.pack('<B', 0)) # 0
|
||||
|
||||
f.write(struct.pack('<i', 0))
|
||||
|
||||
f.write(struct.pack('<B', 0)) # 0
|
||||
f.write(struct.pack('<B', 16)) # 16
|
||||
f.write(struct.pack('<B', 0)) # 0
|
||||
f.write(struct.pack('<B', 0)) # 0
|
||||
|
||||
# shiftX, shiftY, shiftZ (3 x int32)
|
||||
# (0,0,0,0) # комментарий о координатах инициализации
|
||||
f.write(struct.pack('<3i', 0, 0, 0))
|
||||
|
||||
# # Последние 5 x u32 hz2
|
||||
f.write(struct.pack('<i', 0) * 5)
|
||||
|
||||
# Регистрация
|
||||
# Регистрация плагина
|
||||
def menu_func_export(self, context):
|
||||
self.layout.operator(ExportP2E.bl_idname, text="Persona2 PSX Scene (.p2e)")
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(ExportP2E)
|
||||
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(ExportP2E)
|
||||
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
392
_Utils/Event/Blender_plugins/event_scene_import.py
Normal file
392
_Utils/Event/Blender_plugins/event_scene_import.py
Normal file
@@ -0,0 +1,392 @@
|
||||
bl_info = {
|
||||
"name": "Persona2 PSX Scene Importer",
|
||||
"author": "Sergey Shemet",
|
||||
"version": (1, 2),
|
||||
"blender": (4, 5, 0),
|
||||
"location": "File > Import",
|
||||
"description": "Import Persona 2 Eternal Punishment PSX 3D scenes (.p2e)",
|
||||
"category": "Import-Export",
|
||||
}
|
||||
|
||||
import bpy
|
||||
import struct
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from bpy.props import FloatProperty, EnumProperty, BoolProperty
|
||||
|
||||
class ImportP2E(bpy.types.Operator, ImportHelper):
|
||||
bl_idname = "import_scene.p2e"
|
||||
bl_label = "Import Persona2 PSX Scene"
|
||||
bl_description = "Import Persona 2 Eternal Punishment PSX 3D scenes"
|
||||
bl_options = {'REGISTER', 'UNDO', 'PRESET'}
|
||||
|
||||
filename_ext = ".p2e"
|
||||
|
||||
filter_glob: bpy.props.StringProperty(
|
||||
default="*.p2e",
|
||||
options={'HIDDEN'},
|
||||
maxlen=255,
|
||||
)
|
||||
|
||||
coord_scale: FloatProperty(
|
||||
name="Coordinate Scale",
|
||||
description="PSX fixed-point scale multiplier",
|
||||
default=0.01,
|
||||
min=0.00001,
|
||||
max=1.0,
|
||||
step=0.01,
|
||||
precision=5
|
||||
)
|
||||
|
||||
scale_preset: EnumProperty(
|
||||
name="PSX Scale Preset",
|
||||
description="Standard PSX fixed-point scales",
|
||||
items=[
|
||||
('CUSTOM', "Custom", "Custom scale"),
|
||||
('100', "1/100", "Life-like scale"),
|
||||
('4096', "1/4096 (12.4)", "High precision"),
|
||||
('256', "1/256 (8.8)", "Alternative scale"),
|
||||
('16', "1/16 (12.4)", "Typical PSX vertex scale"),
|
||||
('1', "1/1 (Raw)", "No scaling"),
|
||||
],
|
||||
default='100'
|
||||
)
|
||||
|
||||
show_vertices: BoolProperty(
|
||||
name="Show Vertices",
|
||||
description="Show vertex points for debugging",
|
||||
default=False
|
||||
)
|
||||
|
||||
vertex_size: FloatProperty(
|
||||
name="Vertex Size",
|
||||
description="Size of debug vertex markers",
|
||||
default=0.1,
|
||||
min=0.001,
|
||||
max=1.0
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
box = layout.box()
|
||||
box.label(text="PSX Import Settings", icon='SETTINGS')
|
||||
|
||||
box.prop(self, "scale_preset")
|
||||
|
||||
if self.scale_preset != 'CUSTOM':
|
||||
preset_scales = {
|
||||
'100': 1/100.0,
|
||||
'4096': 1/4096.0,
|
||||
'256': 1/256.0,
|
||||
'16': 1/16.0,
|
||||
'1': 1.0
|
||||
}
|
||||
self.coord_scale = preset_scales[self.scale_preset]
|
||||
|
||||
if self.scale_preset == 'CUSTOM':
|
||||
box.prop(self, "coord_scale")
|
||||
|
||||
box.separator()
|
||||
box.label(text="Debug Options", icon='MODIFIER')
|
||||
box.prop(self, "show_vertices")
|
||||
if self.show_vertices:
|
||||
box.prop(self, "vertex_size")
|
||||
|
||||
box.separator()
|
||||
box.label(text=f"Current scale: {self.coord_scale:.6f}", icon='INFO')
|
||||
|
||||
def execute(self, context):
|
||||
# Читаем файл
|
||||
vertices, objects_data = self.read_p2e_file(self.filepath)
|
||||
|
||||
if not vertices:
|
||||
self.report({'ERROR'}, "No vertices found")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Создаем меши для каждого объекта
|
||||
self.create_objects_from_data(vertices, objects_data)
|
||||
|
||||
# Опционально показываем вершины для отладки
|
||||
if self.show_vertices:
|
||||
self.create_debug_vertices(vertices)
|
||||
|
||||
self.report({'INFO'}, f"Imported {len(vertices)} vertices, {len(objects_data)} objects")
|
||||
return {'FINISHED'}
|
||||
|
||||
def read_p2e_file(self, filepath):
|
||||
"""Чтение .p2e файла: вершины и полигоны объектов"""
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
# Пропускаем заголовок (16 байт sourceName)
|
||||
f.seek(0x10)
|
||||
|
||||
# Читаем заголовок сцены
|
||||
data = f.read(12)
|
||||
if len(data) < 12:
|
||||
self.report({'ERROR'}, "File too small")
|
||||
return [], []
|
||||
|
||||
objects_cnt, some_obj_ctr, vertex_ptr, shift_ptr = struct.unpack('<HHII', data)
|
||||
|
||||
# Вычисляем количество вершин
|
||||
vertex_count = (shift_ptr - vertex_ptr) // 8
|
||||
|
||||
print(f"=== P2E File Info ===")
|
||||
print(f"Objects: {objects_cnt}")
|
||||
print(f"Vertices: {vertex_count}")
|
||||
print(f"Vertex pointer: 0x{vertex_ptr:08X}")
|
||||
print(f"Shift pointer: 0x{shift_ptr:08X}")
|
||||
|
||||
# 1. Читаем вершины
|
||||
vertices = []
|
||||
f.seek(vertex_ptr)
|
||||
|
||||
for i in range(vertex_count):
|
||||
vertex_data = f.read(8)
|
||||
if len(vertex_data) < 8:
|
||||
break
|
||||
|
||||
x, y, z, d = struct.unpack('<4h', vertex_data)
|
||||
# PSX координаты: X=X, Y=Z, Z=-Y
|
||||
vertex = (
|
||||
x * self.coord_scale,
|
||||
z * self.coord_scale,
|
||||
-y * self.coord_scale
|
||||
)
|
||||
vertices.append(vertex)
|
||||
|
||||
print(f"Read {len(vertices)} vertices")
|
||||
|
||||
# 2. Читаем объекты
|
||||
objects_data = []
|
||||
f.seek(0x10 + 12) # После заголовка сцены начинаются объекты
|
||||
|
||||
for obj_idx in range(objects_cnt):
|
||||
# Читаем указатель на объект
|
||||
obj_ptr_data = f.read(4)
|
||||
if len(obj_ptr_data) < 4:
|
||||
break
|
||||
|
||||
obj_ptr = struct.unpack('<I', obj_ptr_data)[0]
|
||||
|
||||
# Сохраняем текущую позицию
|
||||
current_pos = f.tell()
|
||||
|
||||
# Переходим к данным объекта
|
||||
f.seek(obj_ptr)
|
||||
|
||||
# Читаем gpuPacket заголовок
|
||||
packet_data = f.read(8) # u16 globalChunkCnt, u16 localChunkCnt, u32 objectId
|
||||
if len(packet_data) < 8:
|
||||
f.seek(current_pos)
|
||||
continue
|
||||
|
||||
global_chunk_cnt, local_chunk_cnt, object_id = struct.unpack('<HHI', packet_data)
|
||||
|
||||
print(f"\nObject {obj_idx}:")
|
||||
print(f" Pointer: 0x{obj_ptr:08X}")
|
||||
print(f" ID: {object_id}")
|
||||
print(f" Chunks: {global_chunk_cnt}")
|
||||
|
||||
# Собираем полигоны этого объекта
|
||||
obj_polygons = []
|
||||
|
||||
for chunk_idx in range(global_chunk_cnt):
|
||||
# Читаем заголовок chunk
|
||||
chunk_header = f.read(4)
|
||||
if len(chunk_header) < 4:
|
||||
break
|
||||
|
||||
cmd_cnt, chunk_size_words, header_fuck = struct.unpack('<BBH', chunk_header)
|
||||
|
||||
# print(f" Chunk {chunk_idx}: cmd={cmd_cnt}, size={chunk_size_words} words")
|
||||
|
||||
# Определяем тип полигона по cmd_cnt
|
||||
poly_type = self.get_polygon_type(cmd_cnt)
|
||||
|
||||
if poly_type is None:
|
||||
# Пропускаем неизвестный chunk
|
||||
bytes_to_skip = chunk_size_words * 4 # chunk_size в словах (4 байта)
|
||||
f.read(bytes_to_skip)
|
||||
continue
|
||||
|
||||
# Читаем ВЕСЬ chunk (включая индексы вершин)
|
||||
# chunk_size_words - количество 4-байтных слов данных ПОСЛЕ заголовка
|
||||
chunk_data_size = chunk_size_words * 4
|
||||
poly_data = f.read(chunk_data_size)
|
||||
if len(poly_data) < chunk_data_size:
|
||||
break
|
||||
|
||||
# Извлекаем индексы вершин из конца chunk
|
||||
vertex_indices = self.extract_vertex_indices(poly_type, poly_data)
|
||||
|
||||
if vertex_indices:
|
||||
obj_polygons.append({
|
||||
'type': poly_type,
|
||||
'cmd': cmd_cnt,
|
||||
'vertices': vertex_indices,
|
||||
'chunk_size': chunk_size_words
|
||||
})
|
||||
|
||||
# Сохраняем данные объекта
|
||||
if obj_polygons:
|
||||
objects_data.append({
|
||||
'id': object_id,
|
||||
'ptr': obj_ptr,
|
||||
'polygons': obj_polygons,
|
||||
'vertex_count': len(obj_polygons) * 4 # Примерно
|
||||
})
|
||||
|
||||
# Возвращаемся к списку объектов
|
||||
f.seek(current_pos)
|
||||
|
||||
print(f"\n=== Summary ===")
|
||||
print(f"Total objects with polygons: {len(objects_data)}")
|
||||
|
||||
return vertices, objects_data
|
||||
|
||||
except Exception as e:
|
||||
self.report({'ERROR'}, f"Error reading file: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return [], []
|
||||
|
||||
def get_polygon_type(self, cmd_cnt):
|
||||
"""Определяем тип полигона по команде"""
|
||||
poly_types = {
|
||||
7: 'shaded_textured_quad', # shadedTexturedPoly
|
||||
6: 'textured_quad', # texturedPoly
|
||||
5: 'shaded_quad', # shadedPoly
|
||||
3: 'shaded_textured_tri', # shadedTextured3Poly
|
||||
2: 'textured_tri', # textured3Poly
|
||||
}
|
||||
|
||||
return poly_types.get(cmd_cnt, None)
|
||||
|
||||
def extract_vertex_indices(self, poly_type, poly_data):
|
||||
"""Извлекаем индексы вершин из конца chunk данных"""
|
||||
try:
|
||||
if poly_type in ['shaded_textured_quad', 'textured_quad', 'shaded_quad']:
|
||||
# Квады: 4 индекса uint32 в конце (16 байт)
|
||||
if len(poly_data) >= 16:
|
||||
# Последние 16 байт: vertexId1-4
|
||||
v1, v2, v3, v4 = struct.unpack_from('<4I', poly_data, len(poly_data) - 16)
|
||||
return [v1, v2, v4, v3]
|
||||
|
||||
elif poly_type in ['shaded_textured_tri', 'textured_tri']:
|
||||
# Треугольники: 3 индекса uint32 в конце (12 байт)
|
||||
if len(poly_data) >= 12:
|
||||
# Последние 12 байт: vertexId1-3
|
||||
v1, v2, v3 = struct.unpack_from('<3I', poly_data, len(poly_data) - 12)
|
||||
return [v1, v2, v3]
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error extracting indices from {poly_type}: {e}")
|
||||
|
||||
return []
|
||||
|
||||
def create_objects_from_data(self, vertices, objects_data):
|
||||
"""Создание отдельных мешей для каждого объекта"""
|
||||
if not objects_data:
|
||||
return
|
||||
|
||||
# Создаем коллекцию для импортированных объектов
|
||||
collection_name = "PSX_Objects"
|
||||
if collection_name not in bpy.data.collections:
|
||||
collection = bpy.data.collections.new(collection_name)
|
||||
bpy.context.scene.collection.children.link(collection)
|
||||
else:
|
||||
collection = bpy.data.collections[collection_name]
|
||||
|
||||
total_polygons = 0
|
||||
successful_objects = 0
|
||||
|
||||
for obj_idx, obj_data in enumerate(objects_data):
|
||||
# Собираем все полигоны этого объекта
|
||||
all_faces = []
|
||||
|
||||
for poly in obj_data['polygons']:
|
||||
vertex_ids = poly['vertices']
|
||||
|
||||
# Проверяем, что индексы в пределах массива вершин
|
||||
valid_ids = []
|
||||
for vid in vertex_ids:
|
||||
if vid < len(vertices):
|
||||
valid_ids.append(vid)
|
||||
else:
|
||||
print(f"Warning: Vertex index {vid} out of range (max {len(vertices)-1})")
|
||||
|
||||
if len(valid_ids) >= 3: # Нужно минимум 3 вершины для полигона
|
||||
all_faces.append(valid_ids)
|
||||
total_polygons += 1
|
||||
|
||||
if not all_faces:
|
||||
print(f"Object {obj_idx} has no valid polygons")
|
||||
continue
|
||||
|
||||
# Создаем меш
|
||||
mesh_name = f"PSX_Obj_{obj_idx:03d}_ID{obj_data['id']}"
|
||||
mesh = bpy.data.meshes.new(mesh_name)
|
||||
|
||||
# Создаем вершины и полигоны
|
||||
mesh.from_pydata(vertices, [], all_faces)
|
||||
|
||||
# Обновляем меш
|
||||
mesh.update()
|
||||
|
||||
# Создаем объект
|
||||
obj = bpy.data.objects.new(mesh_name, mesh)
|
||||
|
||||
# Добавляем кастомные свойства
|
||||
obj["psx_object_id"] = obj_data['id']
|
||||
obj["psx_object_ptr"] = obj_data['ptr']
|
||||
obj["psx_poly_count"] = len(all_faces)
|
||||
obj["psx_poly_types"] = ','.join(str(p['cmd']) for p in obj_data['polygons'])
|
||||
|
||||
# Добавляем в коллекцию
|
||||
collection.objects.link(obj)
|
||||
successful_objects += 1
|
||||
|
||||
print(f" Created object {obj_idx} with {len(all_faces)} polygons")
|
||||
|
||||
print(f"\nCreated {successful_objects} objects with {total_polygons} total polygons")
|
||||
|
||||
def create_debug_vertices(self, vertices):
|
||||
"""Создание мета-шаров для отладки вершин"""
|
||||
if not vertices:
|
||||
return
|
||||
|
||||
meta = bpy.data.metaballs.new("Debug_Vertices")
|
||||
meta_obj = bpy.data.objects.new("PSX_Debug_Vertices", meta)
|
||||
|
||||
meta.resolution = 0.1
|
||||
meta.threshold = 0.05
|
||||
|
||||
for i, vertex in enumerate(vertices):
|
||||
elem = meta.elements.new()
|
||||
elem.co = vertex
|
||||
elem.radius = self.vertex_size
|
||||
|
||||
bpy.context.collection.objects.link(meta_obj)
|
||||
|
||||
print(f"Created {len(vertices)} debug vertices")
|
||||
|
||||
def menu_func_import(self, context):
|
||||
self.layout.operator(ImportP2E.bl_idname, text="Persona2 PSX Scene (.p2e)")
|
||||
|
||||
classes = (ImportP2E,)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
||||
|
||||
def unregister():
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
250
_Utils/Event/Blender_plugins/p2coll_importer.py
Normal file
250
_Utils/Event/Blender_plugins/p2coll_importer.py
Normal file
@@ -0,0 +1,250 @@
|
||||
import bpy
|
||||
import struct
|
||||
import math
|
||||
from bpy_extras.io_utils import ImportHelper
|
||||
from bpy.props import StringProperty
|
||||
from bpy.types import Operator
|
||||
|
||||
bl_info = {
|
||||
"name": "Persona2 PSX Scene Collisions Importer",
|
||||
"author": "Sergey Shemet",
|
||||
"version": (1, 0),
|
||||
"blender": (4, 5, 0),
|
||||
"location": "File > Import",
|
||||
"description": "Imports Persona 2 Eternal Punishment PSX 3D scenes Collisions (.p2coll)",
|
||||
"category": "Import-Export",
|
||||
}
|
||||
|
||||
class P2CollisionImporter(Operator, ImportHelper):
|
||||
"""Import Persona 2 PSX Collisions"""
|
||||
bl_idname = "import_scene.p2coll"
|
||||
bl_label = "Import P2Coll"
|
||||
bl_options = {'PRESET', 'UNDO'}
|
||||
|
||||
filename_ext = ".p2coll"
|
||||
filter_glob: StringProperty(
|
||||
default="*.p2coll",
|
||||
options={'HIDDEN'},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
return read_p2coll_file(context, self.filepath)
|
||||
|
||||
def create_cylinder_at_location(location, radius, height, vertices=16, name="cylinder"):
|
||||
"""Создает цилиндр с заданными параметрами в указанной позиции"""
|
||||
# Создаем цилиндр
|
||||
bpy.ops.mesh.primitive_cylinder_add(
|
||||
vertices=vertices,
|
||||
radius=radius,
|
||||
depth=height,
|
||||
location=location
|
||||
)
|
||||
|
||||
# Получаем созданный объект
|
||||
cylinder = context.active_object
|
||||
cylinder.name = name
|
||||
|
||||
return cylinder
|
||||
|
||||
def read_p2coll_file(context, filepath):
|
||||
"""Чтение файла .p2coll и создание объектов в Blender"""
|
||||
|
||||
with open(filepath, 'rb') as file:
|
||||
# Чтение всего файла
|
||||
data = file.read()
|
||||
|
||||
# Чтение блоков из заголовка
|
||||
# Первый блок: vertexMap
|
||||
vertex_map_size = struct.unpack_from('<I', data, 0)[0]
|
||||
vertex_map_block_offset = struct.unpack_from('<I', data, 4)[0]
|
||||
|
||||
# Второй блок: collisionLines
|
||||
collision_lines_size = struct.unpack_from('<I', data, 8)[0]
|
||||
collision_lines_block_offset = struct.unpack_from('<I', data, 12)[0]
|
||||
|
||||
# Третий блок: arrow (теперь cylCollision)
|
||||
cyl_collision_size = struct.unpack_from('<I', data, 16)[0]
|
||||
cyl_collision_block_offset = struct.unpack_from('<I', data, 20)[0]
|
||||
|
||||
print(f"Vertex count: {vertex_map_size}")
|
||||
print(f"Wall collisions count: {collision_lines_size}")
|
||||
print(f"Cylinder collisions count: {cyl_collision_size}")
|
||||
|
||||
# Чтение массива вершин
|
||||
vertices = []
|
||||
vertex_data_offset = vertex_map_block_offset
|
||||
|
||||
for i in range(vertex_map_size):
|
||||
offset = vertex_data_offset + i * 8 # 8 bytes per coord struct (4 x short)
|
||||
if offset + 8 <= len(data):
|
||||
x, y, z, d = struct.unpack_from('<4h', data, offset)
|
||||
# Преобразование координат: y=z, z=-y, деление на 100
|
||||
vertices.append((
|
||||
x / 100.0,
|
||||
z / 100.0,
|
||||
-y / 100.0
|
||||
))
|
||||
|
||||
# Создание коллекции
|
||||
coll_collection_name = "PSX_Collisions"
|
||||
|
||||
# Проверяем, существует ли коллекция
|
||||
if coll_collection_name in bpy.data.collections:
|
||||
coll_collection = bpy.data.collections[coll_collection_name]
|
||||
else:
|
||||
coll_collection = bpy.data.collections.new(coll_collection_name)
|
||||
context.scene.collection.children.link(coll_collection)
|
||||
|
||||
# Сохраняем активную коллекцию для возврата
|
||||
current_collection = context.collection
|
||||
|
||||
# Чтение и создание объектов коллизий (стен)
|
||||
collision_data_offset = collision_lines_block_offset
|
||||
|
||||
for i in range(collision_lines_size):
|
||||
offset = collision_data_offset + i * 8 # 8 bytes per collisionObj struct
|
||||
if offset + 8 <= len(data):
|
||||
# Чтение данных коллизии
|
||||
vert_id1 = struct.unpack_from('<H', data, offset)[0]
|
||||
vert_id2 = struct.unpack_from('<H', data, offset + 2)[0]
|
||||
col_id = struct.unpack_from('<H', data, offset + 4)[0]
|
||||
collis_height = struct.unpack_from('<B', data, offset + 6)[0]
|
||||
collis_flag = struct.unpack_from('<B', data, offset + 7)[0]
|
||||
|
||||
print(f"Wall Collision {i}: v1={vert_id1}, v2={vert_id2}, height={collis_height}, flag={collis_flag}")
|
||||
|
||||
# Проверка, что индексы вершин валидны
|
||||
if vert_id1 < len(vertices) and vert_id2 < len(vertices):
|
||||
# Создание меша для стены
|
||||
mesh = bpy.data.meshes.new(f"wall_collision_{i}")
|
||||
obj = bpy.data.objects.new(f"wall_collision_{i}", mesh)
|
||||
|
||||
# Получаем координаты вершин
|
||||
v1 = vertices[vert_id1]
|
||||
v2 = vertices[vert_id2]
|
||||
|
||||
# Высота стены
|
||||
height = collis_height / 100.0
|
||||
|
||||
# Создаём вершины для прямоугольника (стены)
|
||||
verts = [
|
||||
(v1[0], v1[1], v1[2]), # Нижняя левая
|
||||
(v2[0], v2[1], v2[2]), # Нижняя правая
|
||||
(v2[0], v2[1], v2[2] + height), # Верхняя правая
|
||||
(v1[0], v1[1], v1[2] + height), # Верхняя левая
|
||||
]
|
||||
|
||||
# Грани (два треугольника для прямоугольника)
|
||||
faces = [(0, 1, 2, 3)]
|
||||
|
||||
# Создание меша
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
|
||||
# Добавляем метаданные
|
||||
obj["collisHeight"] = collis_height
|
||||
obj["collisFlag"] = collis_flag
|
||||
obj["collisionId"] = col_id
|
||||
obj["vertexId1"] = vert_id1
|
||||
obj["vertexId2"] = vert_id2
|
||||
obj["collisionType"] = "wall"
|
||||
|
||||
# Добавляем объект в коллекцию
|
||||
coll_collection.objects.link(obj)
|
||||
|
||||
obj.update_tag()
|
||||
|
||||
# Отключаем отображение в основной сцене
|
||||
if obj.name in context.scene.collection.objects:
|
||||
context.scene.collection.objects.unlink(obj)
|
||||
else:
|
||||
print(f"Warning: Invalid vertex indices in wall collision {i}: {vert_id1}, {vert_id2}")
|
||||
|
||||
# Чтение и создание цилиндрических коллизий
|
||||
cyl_collision_data_offset = cyl_collision_block_offset
|
||||
|
||||
for i in range(cyl_collision_size):
|
||||
offset = cyl_collision_data_offset + i * 8 # 8 bytes per cylCollisionObj struct
|
||||
if offset + 8 <= len(data):
|
||||
# Чтение данных цилиндрической коллизии
|
||||
coords_vertex = struct.unpack_from('<H', data, offset)[0]
|
||||
radius = struct.unpack_from('<H', data, offset + 2)[0] # Радиус вместо вращения
|
||||
object_id = struct.unpack_from('<H', data, offset + 4)[0]
|
||||
collis_height = struct.unpack_from('<B', data, offset + 6)[0]
|
||||
collis_flag = struct.unpack_from('<B', data, offset + 7)[0]
|
||||
|
||||
print(f"Cylinder Collision {i}: vertex={coords_vertex}, radius={radius}, height={collis_height}, flag={collis_flag}")
|
||||
|
||||
# Проверка, что индекс вершины валиден
|
||||
if coords_vertex < len(vertices):
|
||||
# Устанавливаем активную коллекцию для создания объектов
|
||||
context.view_layer.active_layer_collection = context.view_layer.layer_collection.children[coll_collection.name]
|
||||
|
||||
# Получаем позицию для цилиндра
|
||||
pos = vertices[coords_vertex]
|
||||
|
||||
# Преобразуем радиус и высоту (делим на 100)
|
||||
cyl_radius = radius / 100.0
|
||||
cyl_height = collis_height / 100.0
|
||||
|
||||
# Создаем цилиндр
|
||||
bpy.ops.mesh.primitive_cylinder_add(
|
||||
vertices=16, # Количество вершин для гладкости
|
||||
radius=cyl_radius,
|
||||
depth=cyl_height,
|
||||
location=pos,
|
||||
rotation=(0, 0, 0) # Без вращения
|
||||
)
|
||||
|
||||
# Получаем созданный объект
|
||||
cyl_obj = context.active_object
|
||||
cyl_obj.name = f"cyl_collision_{i}"
|
||||
|
||||
# Смещаем цилиндр так, чтобы его основание было в позиции вершины
|
||||
cyl_obj.location.z += cyl_height / 2.0
|
||||
|
||||
# Добавляем метаданные
|
||||
cyl_obj["collisHeight"] = collis_height
|
||||
cyl_obj["collisFlag"] = collis_flag
|
||||
cyl_obj["objectId"] = object_id
|
||||
cyl_obj["vertexId"] = coords_vertex
|
||||
cyl_obj["radius"] = radius
|
||||
cyl_obj["collisionType"] = "cylinder"
|
||||
|
||||
# Обновляем объект
|
||||
cyl_obj.update_tag()
|
||||
|
||||
# Убеждаемся, что объект в правильной коллекции
|
||||
if coll_collection.name not in cyl_obj.users_collection:
|
||||
# Удаляем из всех текущих коллекций
|
||||
for coll in cyl_obj.users_collection:
|
||||
coll.objects.unlink(cyl_obj)
|
||||
# Добавляем в нашу коллекцию
|
||||
coll_collection.objects.link(cyl_obj)
|
||||
|
||||
else:
|
||||
print(f"Warning: Invalid vertex index in cylinder collision {i}: {coords_vertex}")
|
||||
|
||||
# Возвращаем активную коллекцию
|
||||
context.view_layer.active_layer_collection = context.view_layer.layer_collection
|
||||
|
||||
# Выводим информацию о результате
|
||||
print(f"Импортировано {len(vertices)} вершин")
|
||||
print(f"Создано {collision_lines_size} стен-коллизий")
|
||||
print(f"Создано {cyl_collision_size} цилиндрических коллизий")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def menu_func_import(self, context):
|
||||
self.layout.operator(P2CollisionImporter.bl_idname, text="Persona 2 Collisions (.p2coll)")
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(P2CollisionImporter)
|
||||
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(P2CollisionImporter)
|
||||
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
35
_Utils/Event/TRANSL.hexpat
Normal file
35
_Utils/Event/TRANSL.hexpat
Normal file
@@ -0,0 +1,35 @@
|
||||
#include <std/mem.pat>
|
||||
#include <std/io.pat>
|
||||
|
||||
struct text_str {
|
||||
u16 string[while(std::mem::read_unsigned($, 2) != 0x1103)];
|
||||
u32 closeCode [[color("FFFFFF")]];
|
||||
};
|
||||
|
||||
struct command {
|
||||
u32 cmdID [[color("2222EE")]];
|
||||
u32 *paramsOffset : u32 ;
|
||||
if (cmdID == 0x13) {
|
||||
text_str text @ paramsOffset + textOffset [[color("3333FF"), name("TEXT_ARR")]];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
struct routine {
|
||||
char routineName[64] [[color("006600")]];
|
||||
u32 cmdAddr [[color("FFaa")]];
|
||||
u32 a = cmdOffset + (cmdAddr * 8);
|
||||
std::print(a);
|
||||
command *cmdLink : u32 @ a;
|
||||
u32 huyznaet [[color("00")]];
|
||||
};
|
||||
|
||||
|
||||
u32 hz1 @ 0x00;
|
||||
u32 routineOffset @ 0x04;
|
||||
u32 routineCount @ 0x08;
|
||||
u32 cmdOffset @ 0x0C;
|
||||
u32 paramsOffset @ 0x10;
|
||||
u32 textOffset @ 0x14;
|
||||
routine subRoutines[routineCount] @ routineOffset ;
|
||||
command commands [(paramsOffset-cmdOffset) / 8] @ cmdOffset;
|
||||
73
_Utils/Event/ep_scr_cmd.json
Normal file
73
_Utils/Event/ep_scr_cmd.json
Normal file
@@ -0,0 +1,73 @@
|
||||
[
|
||||
{ "__GetVar": "0x60" },
|
||||
{ "_if1Go>>": "0x1" },
|
||||
{ "_if0Go>>": "0x2" },
|
||||
{ "_if!=Go>": "0x4" },
|
||||
{ "if>!=<Go": "0x6" },
|
||||
{ "_JumpTo>": "0x7" },
|
||||
{ "_SetTemp": "0x8" },
|
||||
{ "_AddVars": "0x9" },
|
||||
{ "__GoSub>": "0xD" },
|
||||
{ "__Return-->": "0xE" },
|
||||
{ "_RndmzTo": "0x10" },
|
||||
{ "__WaitTo": "0x12" },
|
||||
{ "TextShow": "0x13" },
|
||||
{ "GetInput": "0x1A" },
|
||||
{ "VarToTxt": "0x1B" },
|
||||
{ "AvtrLoad": "0x1E" },
|
||||
{ "AvtEmSet": "0x1F" },
|
||||
{ "AvUnload": "0x20" },
|
||||
{ "_AvaFade": "0x22" },
|
||||
{ "AvaFWait": "0x23" },
|
||||
{ "AvaSetXY": "0x22" },
|
||||
{ "WindShow": "0x26" },
|
||||
{ "WinClose": "0x27" },
|
||||
{ "CamrMove": "0x28" },
|
||||
{ "CamrZoom": "0x2A" },
|
||||
{ "CamSetXY": "0x2C" },
|
||||
{ "CharLoad": "0x33" },
|
||||
{ "CharSett": "0x34" },
|
||||
{ "CSetAnim": "0x39" },
|
||||
{ "CRotToPl": "0x41" },
|
||||
{ "CtrlLock": "0x45" },
|
||||
{ "CtrUnlck": "0x46" },
|
||||
{ "ChSubSet": "0x48" },
|
||||
{ "CRotaDef": "0x4A" },
|
||||
{ "DungLoad": "0x51" },
|
||||
{ "EvntLoad": "0x52" },
|
||||
{ "CityLoad": "0x53" },
|
||||
{ "VideoPly": "0x59" },
|
||||
{ "PausInit": "0x5C" },
|
||||
{ "PauseFr_": "0x5D" },
|
||||
{ "_VarTrue": "0x5E" },
|
||||
{ "_VarFlse": "0x5F" },
|
||||
{ "__VarDec": "0x73" },
|
||||
{ "__VarSet": "0x74" },
|
||||
{ "__GetVar": "0x7B" },
|
||||
{ "_isChar?": "0x83" },
|
||||
{ "_getMney": "0x87" },
|
||||
{ "IncMoney": "0x91" },
|
||||
{ "ScrnFade": "0x92" },
|
||||
{ "MusicPly": "0x99" },
|
||||
{ "MuscLoad": "0x9B" },
|
||||
{ "SundStop": "0x9E" },
|
||||
{ "MuscFade": "0x9F" },
|
||||
{ "ScreenNegative": "0xAC" },
|
||||
{ "SoundPly": "0xC3" },
|
||||
{ "collLink": "0xD0" },
|
||||
{ "meshMove": "0xD4" },
|
||||
{ "meshTurn": "0xD5" },
|
||||
{ "meshWait": "0xD6" },
|
||||
{ "meshHide": "0xD6" },
|
||||
{ "FXSpLoad": "0xDC" },
|
||||
{ "FXSprSet": "0xDD" },
|
||||
{ "FXSprRun": "0xE0" },
|
||||
{ "BattleLoad": "0xED" },
|
||||
{ "ResetAll": "0xF3" },
|
||||
{ "WavMusic": "0x100" },
|
||||
{ "ScreenClrFilter": "0x103" },
|
||||
{ "ShowTextInput": "0x10E" },
|
||||
{ "LoadInputIdToVar": "0x10F" },
|
||||
{ "LoadDung": "0x113" },
|
||||
{ "LoadEvnt": "0x12B" }
|
||||
]
|
||||
69
_Utils/Event/event_collisions_struct.hexpat
Normal file
69
_Utils/Event/event_collisions_struct.hexpat
Normal file
@@ -0,0 +1,69 @@
|
||||
struct coord {
|
||||
s16 X;
|
||||
s16 Y;
|
||||
s16 Z;
|
||||
s16 d;
|
||||
};
|
||||
|
||||
struct collisionObj {
|
||||
u16 vertId1;
|
||||
u16 vertId2;
|
||||
u16 colId;
|
||||
u8 collisHeigth;
|
||||
u8 collisFlag;
|
||||
};
|
||||
|
||||
struct cylCollObj {
|
||||
u16 coordsVertex;
|
||||
u16 radius;
|
||||
u16 objectId;
|
||||
u8 collisHeigth;
|
||||
u8 collisFlag;
|
||||
};
|
||||
|
||||
struct boxObj {
|
||||
u16 coordsVertex;
|
||||
u16 boxId;
|
||||
u16 xWidth;
|
||||
u16 yWidth;
|
||||
u8 Heigth;
|
||||
u8 someFlag1;
|
||||
u8 someFlag2;
|
||||
u8 someFlag3;
|
||||
};
|
||||
|
||||
struct floorObj {
|
||||
u16 vertId1;
|
||||
u16 vertId2;
|
||||
u16 vertId3;
|
||||
u16 vertId4;
|
||||
u16 objectId;
|
||||
u8 Heigth;
|
||||
u8 someFlag;
|
||||
};
|
||||
|
||||
struct block {
|
||||
u32 blockSize;
|
||||
u32 blockOffset;
|
||||
};
|
||||
|
||||
struct header {
|
||||
block vertexMap;
|
||||
block collisionLines;
|
||||
block cylinders;
|
||||
block boxes;
|
||||
block floors;
|
||||
|
||||
coord coords[vertexMap.blockSize]
|
||||
@ vertexMap.blockOffset;
|
||||
collisionObj coll[collisionLines.blockSize]
|
||||
@ collisionLines.blockOffset;
|
||||
cylCollObj cyl[cylinders.blockSize]
|
||||
@ cylinders.blockOffset;
|
||||
boxObj box[boxes.blockSize]
|
||||
@ boxes.blockOffset;
|
||||
floorObj floor[floors.blockSize]
|
||||
@ floors.blockOffset;
|
||||
};
|
||||
|
||||
header colls @ 0x00;
|
||||
275
_Utils/Event/event_compiler.py
Normal file
275
_Utils/Event/event_compiler.py
Normal file
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env python3
|
||||
# Persona2EPEventCompiler
|
||||
# Author: SergeyShemet
|
||||
# Version: 1.0
|
||||
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
|
||||
def main():
|
||||
# Check input parameters
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: event_compiler.py input_file [output_file]")
|
||||
return
|
||||
|
||||
input_file = sys.argv[1]
|
||||
|
||||
# Set output filename
|
||||
if len(sys.argv) >= 3:
|
||||
output_file = sys.argv[2]
|
||||
else:
|
||||
output_file = input_file + ".TRANSL"
|
||||
|
||||
# Read script lines
|
||||
with open(input_file, 'r', encoding='utf-8') as f:
|
||||
script_lines = f.readlines()
|
||||
|
||||
# Load command codes from JSON
|
||||
with open('ep_scr_cmd.json', 'r') as f:
|
||||
cmd_codes_list = json.load(f)
|
||||
|
||||
# Convert command codes to dictionary
|
||||
cmd_codes = {}
|
||||
for item in cmd_codes_list:
|
||||
for key, value in item.items():
|
||||
cmd_codes[key] = int(value, 16)
|
||||
|
||||
# Initialize arrays
|
||||
cmds = [] # Will store dict objects
|
||||
subs = [] # Subroutines {name: cpuAddr}
|
||||
branches = [] # Branches {name: cpuAddr}
|
||||
text = bytearray() # Text data
|
||||
outputparams = [] # Final parameters
|
||||
outputfile = bytearray() # Output file
|
||||
|
||||
tempBr1 = tempBr2 = tempBr3 = 0
|
||||
cpu = 0 # Command counter
|
||||
|
||||
# FIRST PASS - Parse script
|
||||
for line in script_lines:
|
||||
line = line.rstrip('\n')
|
||||
line_trimmed = line.strip()
|
||||
|
||||
# Skip empty lines and comments
|
||||
if not line_trimmed or line_trimmed.startswith('//'):
|
||||
continue
|
||||
|
||||
# Check for subroutine
|
||||
if line_trimmed.endswith(':'):
|
||||
name = line_trimmed[:-1].strip()
|
||||
subs.append({'name': name, 'addr': cpu})
|
||||
continue
|
||||
|
||||
# Check for branch
|
||||
if line_trimmed.startswith('&'):
|
||||
name = line_trimmed[1:].strip()
|
||||
branches.append({'name': name, 'addr': cpu})
|
||||
continue
|
||||
|
||||
# Parse command
|
||||
match = re.match(r'^(\S+)\s+(.*)$', line_trimmed)
|
||||
if match:
|
||||
cmd_name = match.group(1)
|
||||
cmd_data = match.group(2)
|
||||
|
||||
cmd_obj = {
|
||||
'cpu': cpu,
|
||||
'cpuCmd': cmd_name,
|
||||
'scriptLine': line_trimmed,
|
||||
'params': []
|
||||
}
|
||||
|
||||
# Check for TextShow command
|
||||
if 'TextShow' in cmd_name:
|
||||
cmd_obj['text'] = cmd_data
|
||||
else:
|
||||
# Split params by whitespace
|
||||
params = cmd_data.split()
|
||||
cmd_obj['params'] = params
|
||||
|
||||
cmds.append(cmd_obj)
|
||||
cpu += 1
|
||||
else:
|
||||
cmd_obj = {
|
||||
'cpu': cpu,
|
||||
'cpuCmd': line_trimmed,
|
||||
'scriptLine': line_trimmed,
|
||||
'params': []
|
||||
}
|
||||
cmds.append(cmd_obj)
|
||||
cpu += 1
|
||||
|
||||
# SECOND PASS - Link and parse command codes
|
||||
for cmd in cmds:
|
||||
# Handle special markers
|
||||
if '!' in cmd['cpuCmd']:
|
||||
tempBr1 = cmd['cpu']
|
||||
cmd['cpuCmd'] = cmd['cpuCmd'].replace('!', '')
|
||||
if '@' in cmd['cpuCmd']:
|
||||
tempBr2 = cmd['cpu']
|
||||
cmd['cpuCmd'] = cmd['cpuCmd'].replace('@', '')
|
||||
if '#' in cmd['cpuCmd']:
|
||||
tempBr3 = cmd['cpu']
|
||||
cmd['cpuCmd'] = cmd['cpuCmd'].replace('#', '')
|
||||
|
||||
# Parse command code
|
||||
if cmd['cpuCmd'] in cmd_codes:
|
||||
cmd['cpuCode'] = cmd_codes[cmd['cpuCmd']]
|
||||
else:
|
||||
try:
|
||||
cmd['cpuCode'] = int(cmd['cpuCmd'], 16)
|
||||
except:
|
||||
print(f"Command not recognized: {cmd['scriptLine']}")
|
||||
cmd['cpuCode'] = 0
|
||||
|
||||
if cmd['cpuCmd'] == 'TextShow':
|
||||
text_start = len(text)
|
||||
text_data = cmd['text']
|
||||
|
||||
i = 0
|
||||
text_len = len(text_data)
|
||||
|
||||
while i < text_len:
|
||||
if text_data[i] == '[':
|
||||
j = text_data.find(']', i + 1)
|
||||
if j != -1:
|
||||
hex_str = text_data[i+1:j].replace(' ', '')
|
||||
hex_bytes = bytes.fromhex(hex_str)
|
||||
# hex_bytes = hex_bytes[::-1]
|
||||
text.extend(hex_bytes)
|
||||
i = j + 1
|
||||
continue
|
||||
|
||||
if text_data[i] == '\t':
|
||||
text.extend([0x31, 0x11])
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Accumulate regular text (including Russian)
|
||||
accum = bytearray()
|
||||
while i < text_len and text_data[i] != '[' and text_data[i] != '\t':
|
||||
# Encode in cp1251
|
||||
accum.extend(text_data[i].encode('cp1251'))
|
||||
i += 1
|
||||
|
||||
if len(accum) > 0:
|
||||
text.append(len(accum))
|
||||
text.append(0x20)
|
||||
text.extend(accum)
|
||||
if len(text) & 1:
|
||||
text.append(0)
|
||||
|
||||
cmd['params'] = [text_start]
|
||||
|
||||
else:
|
||||
# Parse parameters
|
||||
for i, param in enumerate(cmd['params']):
|
||||
# Handle special markers
|
||||
if param == '!':
|
||||
cmd['params'][i] = tempBr1
|
||||
elif param == '@':
|
||||
cmd['params'][i] = tempBr2
|
||||
elif param == '#':
|
||||
cmd['params'][i] = tempBr3
|
||||
else:
|
||||
# Try to parse as hex
|
||||
try:
|
||||
# if param.startswith('0x'):
|
||||
# cmd['params'][i] = int(param, 16)
|
||||
# else:
|
||||
# # Try to parse as decimal
|
||||
cmd['params'][i] = int(param, 16)
|
||||
except:
|
||||
# Look in branches
|
||||
found = False
|
||||
for branch in branches:
|
||||
if branch['name'] == param:
|
||||
cmd['params'][i] = branch['addr']
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
# Look in subs (store index, not address)
|
||||
for sub_idx, sub in enumerate(subs):
|
||||
if sub['name'] == param:
|
||||
cmd['params'][i] = sub_idx
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
print(f"Could not parse parameter '{param}' in command: {cmd['scriptLine']}")
|
||||
cmd['params'][i] = 0
|
||||
|
||||
# THIRD PASS - Build output file
|
||||
# Header
|
||||
subroutines_offset = 0x18
|
||||
subs_count = len(subs)
|
||||
cmd_offset = subs_count * 72 + 0x18
|
||||
params_offset = cmd_offset + len(cmds) * 8
|
||||
|
||||
# Write header
|
||||
outputfile.extend(subroutines_offset.to_bytes(4, 'little')) # subroutines offset
|
||||
outputfile.extend(subroutines_offset.to_bytes(4, 'little')) # again
|
||||
outputfile.extend(subs_count.to_bytes(4, 'little')) # subs count
|
||||
outputfile.extend(cmd_offset.to_bytes(4, 'little')) # cmd offset
|
||||
outputfile.extend(params_offset.to_bytes(4, 'little')) # params offset
|
||||
outputfile.extend(b'\x00\x00\x00\x00') # text offset (placeholder)
|
||||
|
||||
# Write subroutines
|
||||
for sub in subs:
|
||||
# Name (64 bytes)
|
||||
name_bytes = sub['name'].encode('ascii')
|
||||
outputfile.extend(name_bytes)
|
||||
outputfile.extend(b'\x00' * (64 - len(name_bytes)))
|
||||
|
||||
# cpuAddr
|
||||
outputfile.extend(sub['addr'].to_bytes(4, 'little'))
|
||||
|
||||
# Padding 0
|
||||
outputfile.extend((0).to_bytes(4, 'little'))
|
||||
|
||||
# Write commands
|
||||
params_offset_current = params_offset
|
||||
for cmd in cmds:
|
||||
print(cmd)
|
||||
# Command code
|
||||
outputfile.extend(cmd['cpuCode'].to_bytes(4, 'little'))
|
||||
|
||||
# Params offset
|
||||
outputfile.extend(params_offset_current.to_bytes(4, 'little'))
|
||||
|
||||
# Add params to outputparams
|
||||
for param in cmd['params']:
|
||||
if isinstance(param, str):
|
||||
try:
|
||||
param_val = int(param)
|
||||
except:
|
||||
param_val = 0
|
||||
else:
|
||||
param_val = param
|
||||
|
||||
outputparams.append(param_val)
|
||||
params_offset_current += 4
|
||||
|
||||
# Write outputparams
|
||||
for param in outputparams:
|
||||
outputfile.extend(param.to_bytes(4, 'little'))
|
||||
|
||||
# Write text
|
||||
text_offset = len(outputfile)
|
||||
outputfile.extend(text)
|
||||
|
||||
# Update text offset in header
|
||||
outputfile[0x14:0x18] = text_offset.to_bytes(4, 'little')
|
||||
|
||||
# Write to file
|
||||
with open(output_file, 'wb') as f:
|
||||
f.write(outputfile)
|
||||
|
||||
print(f"Compilation completed successfully!")
|
||||
print(f"Output file: {output_file}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
214
_Utils/Event/event_graphic_struct.hexpat
Normal file
214
_Utils/Event/event_graphic_struct.hexpat
Normal file
@@ -0,0 +1,214 @@
|
||||
import type.base;
|
||||
|
||||
|
||||
struct vertex {
|
||||
s16 X;
|
||||
s16 Y;
|
||||
s16 Z;
|
||||
s16 d;
|
||||
};
|
||||
|
||||
struct objShiftInfo {
|
||||
u32 hz[5];
|
||||
s32 shiftX;
|
||||
s32 shiftY;
|
||||
s32 shiftZ;
|
||||
u32 hz2[5];
|
||||
|
||||
|
||||
};
|
||||
|
||||
struct shadedTexturedPolyInfo1E {
|
||||
type::Hex<u32> hz_cho;
|
||||
u8 color1[3];
|
||||
type::Hex<u8> gpuCmdId;
|
||||
type::Hex<u32> vertex1;
|
||||
type::Hex<u16> clut;
|
||||
type::Hex<u8> texCoord1Y;
|
||||
type::Hex<u8> texCoord1X;
|
||||
type::Hex<u32> color2;
|
||||
type::Hex<u32> vertex2;
|
||||
type::Hex<u16> texPage;
|
||||
type::Hex<u8> texCoord2Y;
|
||||
type::Hex<u8> texCoord2X;
|
||||
type::Hex<u32> color3;
|
||||
type::Hex<u32> vertex3;
|
||||
|
||||
type::Hex<u8> texCoord3Y;
|
||||
type::Hex<u8> texCoord3X;
|
||||
u16 dummy;
|
||||
type::Hex<u32> color4;
|
||||
type::Hex<u32> vertex4;
|
||||
|
||||
type::Hex<u8> texCoord4Y;
|
||||
type::Hex<u8> texCoord4X;
|
||||
u16 dummy2;
|
||||
};
|
||||
|
||||
struct shadedTexturedPoly {
|
||||
shadedTexturedPolyInfo1E poly1;
|
||||
shadedTexturedPolyInfo1E poly2;
|
||||
u32 vertexId1;
|
||||
u32 vertexId2;
|
||||
u32 vertexId3;
|
||||
u32 vertexId4;
|
||||
};
|
||||
|
||||
|
||||
struct texturedPolyInfo18 {
|
||||
type::Hex<u32> hz_cho;
|
||||
u8 color1[3];
|
||||
type::Hex<u8> gpuCmdId;
|
||||
type::Hex<u32> vertex1;
|
||||
type::Hex<u8> texCoord1Y;
|
||||
type::Hex<u8> texCoord1X;
|
||||
type::Hex<u16> clut;
|
||||
type::Hex<u32> vertex2;
|
||||
type::Hex<u8> texCoord2Y;
|
||||
type::Hex<u8> texCoord2X;
|
||||
type::Hex<u16> texPage;
|
||||
type::Hex<u32> vertex3;
|
||||
|
||||
type::Hex<u8> texCoord3Y;
|
||||
type::Hex<u8> texCoord3X;
|
||||
u16 dummy;
|
||||
type::Hex<u32> vertex4;
|
||||
|
||||
type::Hex<u8> texCoord4Y;
|
||||
type::Hex<u8> texCoord4X;
|
||||
u16 dummy2;
|
||||
};
|
||||
|
||||
struct texturedPoly {
|
||||
texturedPolyInfo18 poly1;
|
||||
texturedPolyInfo18 poly2;
|
||||
u32 vertexId1;
|
||||
u32 vertexId2;
|
||||
u32 vertexId3;
|
||||
u32 vertexId4;
|
||||
};
|
||||
|
||||
struct shadedTextured3PolyInfo17 {
|
||||
type::Hex<u32> hz_cho;
|
||||
u8 color1[3];
|
||||
type::Hex<u8> gpuCmdId;
|
||||
type::Hex<u32> vertex1;
|
||||
type::Hex<u16> clut;
|
||||
type::Hex<u8> texCoord1Y;
|
||||
type::Hex<u8> texCoord1X;
|
||||
type::Hex<u32> color2;
|
||||
type::Hex<u32> vertex2;
|
||||
type::Hex<u16> texPage;
|
||||
type::Hex<u8> texCoord2Y;
|
||||
type::Hex<u8> texCoord2X;
|
||||
type::Hex<u32> color3;
|
||||
type::Hex<u32> vertex3;
|
||||
|
||||
type::Hex<u8> texCoord3Y;
|
||||
type::Hex<u8> texCoord3X;
|
||||
u16 dummy;
|
||||
};
|
||||
|
||||
struct shadedTextured3Poly {
|
||||
shadedTextured3PolyInfo17 poly1;
|
||||
shadedTextured3PolyInfo17 poly2;
|
||||
u32 vertexId1;
|
||||
u32 vertexId2;
|
||||
u32 vertexId3;
|
||||
};
|
||||
|
||||
struct shadedPolyInfo16 {
|
||||
type::Hex<u32> hz_cho;
|
||||
u8 color1[3];
|
||||
type::Hex<u8> gpuCmdId;
|
||||
type::Hex<u32> vertex1;
|
||||
type::Hex<u32> color2;
|
||||
type::Hex<u32> vertex2;
|
||||
type::Hex<u32> color3;
|
||||
type::Hex<u32> vertex3;
|
||||
type::Hex<u32> color4;
|
||||
type::Hex<u32> vertex4;
|
||||
};
|
||||
|
||||
struct shadedPoly {
|
||||
shadedPolyInfo16 poly1;
|
||||
shadedPolyInfo16 poly2;
|
||||
u32 vertexId1;
|
||||
u32 vertexId2;
|
||||
u32 vertexId3;
|
||||
u32 vertexId4;
|
||||
};
|
||||
|
||||
struct textured3PolyInfo13 {
|
||||
type::Hex<u32> hz_cho;
|
||||
u8 color1[3];
|
||||
type::Hex<u8> gpuCmdId;
|
||||
type::Hex<u32> vertex1;
|
||||
type::Hex<u8> texCoord1Y;
|
||||
type::Hex<u8> texCoord1X;
|
||||
type::Hex<u16> clut;
|
||||
type::Hex<u32> vertex2;
|
||||
type::Hex<u8> texCoord2Y;
|
||||
type::Hex<u8> texCoord2X;
|
||||
type::Hex<u16> texPage;
|
||||
type::Hex<u32> vertex3;
|
||||
|
||||
type::Hex<u8> texCoord3Y;
|
||||
type::Hex<u8> texCoord3X;
|
||||
u16 dummy;
|
||||
};
|
||||
|
||||
struct textured3Poly {
|
||||
textured3PolyInfo13 poly1;
|
||||
textured3PolyInfo13 poly2;
|
||||
u32 vertexId1;
|
||||
u32 vertexId2;
|
||||
u32 vertexId3;
|
||||
};
|
||||
|
||||
struct cmdChunk {
|
||||
u8 cmdCnt;
|
||||
u8 chunkSize;
|
||||
u16 headerFuck;
|
||||
|
||||
if (cmdCnt == 7) {
|
||||
shadedTexturedPoly poly;
|
||||
} else if (cmdCnt == 6) {
|
||||
texturedPoly poly;
|
||||
} else if (cmdCnt == 5) {
|
||||
shadedPoly poly;
|
||||
} else if (cmdCnt == 3) {
|
||||
shadedTextured3Poly poly;
|
||||
} else if (cmdCnt == 2) {
|
||||
textured3Poly poly;
|
||||
}
|
||||
};
|
||||
|
||||
struct gpuPacket {
|
||||
u16 globalChunkCnt;
|
||||
u16 localChunkCnt;
|
||||
u32 objectId;
|
||||
cmdChunk chunks[globalChunkCnt];
|
||||
|
||||
};
|
||||
|
||||
struct sceneObject {
|
||||
u32 objectPtr;
|
||||
gpuPacket gpu @ objectPtr;
|
||||
};
|
||||
|
||||
|
||||
struct p2event3d {
|
||||
|
||||
u16 objectsCnt;
|
||||
u16 someObjCtr;
|
||||
u32 vertexArrayPtr;
|
||||
u32 shiftInfoPtr;
|
||||
|
||||
sceneObject objects[objectsCnt];
|
||||
vertex vertexes[(shiftInfoPtr - vertexArrayPtr)/8] @ vertexArrayPtr;
|
||||
objShiftInfo shiftArray[objectsCnt] @ shiftInfoPtr;
|
||||
};
|
||||
|
||||
char sourceName[16] @ 0x00;
|
||||
p2event3d eventScene @ 0x10;
|
||||
Reference in New Issue
Block a user