boss contacts & many small fixes

This commit is contained in:
sShemet
2026-03-19 17:13:28 +05:00
parent 5bb16aa4d5
commit 3b19924266
535 changed files with 1333 additions and 1321 deletions

View 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;

View 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;

View 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()

View 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()

View 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

Binary file not shown.

View 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()

View 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()

View 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()

View 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;

View 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" }
]

View 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;

View 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()

View 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;