mirror of
https://github.com/dpethes/rerogue.git
synced 2025-06-07 18:58:32 +02:00
model viewer: load models directly from DAT file
Displays a filelist with hob files, use it to select a mesh. Also, hob and hmt parsers now load data directly from memory streams.
This commit is contained in:
parent
a2b074d9e8
commit
1aff4888ab
@ -4,7 +4,7 @@ unit hob_mesh;
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, gl, GLext, math, gvector,
|
||||
Classes, SysUtils, gl, GLext, math, gvector, imgui,
|
||||
hob_parser, hmt_parser;
|
||||
|
||||
type
|
||||
@ -50,12 +50,12 @@ type
|
||||
_materials: array of TMaterial;
|
||||
_hmt: THmtFile;
|
||||
_hmt_loaded: boolean;
|
||||
procedure HmtRead(const filename: string);
|
||||
procedure HobRead(const filename: string);
|
||||
procedure HmtRead(stream: TMemoryStream);
|
||||
procedure HobRead(stream: TMemoryStream);
|
||||
procedure HobReadMesh(const mesh: THobObject);
|
||||
public
|
||||
destructor Destroy; override;
|
||||
procedure Load(const hob_filename, hmt_filename: string);
|
||||
procedure Load(hob, hmt: TMemoryStream);
|
||||
procedure InitGL;
|
||||
procedure DrawGL(opts: TRenderOpts);
|
||||
procedure ExportObj(const obj_name: string);
|
||||
@ -150,12 +150,12 @@ begin
|
||||
end;
|
||||
|
||||
|
||||
procedure TModel.HobRead(const filename: string);
|
||||
procedure TModel.HobRead(stream: TMemoryStream);
|
||||
var
|
||||
i: Integer;
|
||||
hob: THobFile;
|
||||
begin
|
||||
hob := ParseHobFile(filename);
|
||||
hob := ParseHobFile(stream);
|
||||
for i := 0 to 0 do
|
||||
HobReadMesh(hob.objects[i]);
|
||||
WriteLn('vertices: ', _vertices.Size);
|
||||
@ -163,7 +163,7 @@ begin
|
||||
end;
|
||||
|
||||
|
||||
procedure TModel.HmtRead(const filename: string);
|
||||
procedure TModel.HmtRead(stream: TMemoryStream);
|
||||
procedure SetTexByName (var mat: TMaterial; const name: string);
|
||||
var
|
||||
i: integer;
|
||||
@ -192,7 +192,7 @@ procedure TModel.HmtRead(const filename: string);
|
||||
var
|
||||
i: integer;
|
||||
begin
|
||||
_hmt := ParseHmtFile(filename);
|
||||
_hmt := ParseHmtFile(stream);
|
||||
SetLength(_materials, _hmt.material_count);
|
||||
for i := 0 to _hmt.material_count - 1 do
|
||||
SetTexByName(_materials[i], _hmt.materials[i].name_string);
|
||||
@ -205,19 +205,15 @@ begin
|
||||
// _triangles.Free;
|
||||
end;
|
||||
|
||||
procedure TModel.Load(const hob_filename, hmt_filename: string);
|
||||
procedure TModel.Load(hob, hmt: TMemoryStream);
|
||||
begin
|
||||
_vertices := TVertexList.Create;
|
||||
//_triangles := TTriangleList.Create;
|
||||
WriteLn('Loading mesh file ', hob_filename);
|
||||
HobRead(hob_filename);
|
||||
if FileExists(hmt_filename) then begin
|
||||
WriteLn('Loading material file ', hmt_filename);
|
||||
HmtRead(hmt_filename);
|
||||
_hmt_loaded := true;
|
||||
end else begin
|
||||
_hmt_loaded := false;
|
||||
end;
|
||||
WriteLn('Loading mesh file');
|
||||
HobRead(hob);
|
||||
WriteLn('Loading material file');
|
||||
HmtRead(hmt);
|
||||
_hmt_loaded := true;
|
||||
end;
|
||||
|
||||
procedure pnm_save(const fname: string; const p: pbyte; const w, h: integer);
|
||||
@ -286,6 +282,7 @@ procedure TModel.DrawGL(opts: TRenderOpts);
|
||||
var
|
||||
vert: TVertex;
|
||||
i, k: integer;
|
||||
triangle_count: integer = 0;
|
||||
|
||||
procedure DrawTri(tri: TTriangle);
|
||||
var
|
||||
@ -309,13 +306,12 @@ var
|
||||
glVertex3fv(@tri.vertices[k]);
|
||||
end;
|
||||
glEnd;
|
||||
triangle_count += 1;
|
||||
end;
|
||||
|
||||
begin
|
||||
if opts.wireframe then
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
||||
else
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
|
||||
|
||||
glDisable(GL_TEXTURE_2D);
|
||||
if opts.points then begin
|
||||
@ -333,6 +329,13 @@ begin
|
||||
//k := min(opts.fg_to_draw, Length(_triangles) - 1);
|
||||
for i := 0 to _triangles[k].Size - 1 do
|
||||
DrawTri(_triangles[k][i]);
|
||||
|
||||
if opts.wireframe then
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
|
||||
igBegin('Mesh');
|
||||
ImguiText(Format('triangles: %d (vertices: %d)', [triangle_count, _vertices.Size]));
|
||||
igEnd;
|
||||
end;
|
||||
|
||||
|
||||
|
@ -35,7 +35,6 @@ type
|
||||
PImGuiContext = Pointer;
|
||||
PImGuiSizeConstraintCallbackData = ^ImGuiSizeConstraintCallbackData;
|
||||
PImGuiStorage = ^ImGuiStorage;
|
||||
PImGuiStyle = ^ImGuiStyle;
|
||||
PImGuiTextEditCallbackData = ^ImGuiTextEditCallbackData;
|
||||
|
||||
ImVec2 = record
|
||||
@ -98,7 +97,83 @@ type
|
||||
//ImGuiKey_COUNT // this is unnecessary, if we declare KeyMap as array[ImGuiKey_]
|
||||
);
|
||||
|
||||
//structs
|
||||
ImGuiCol_ = (
|
||||
ImGuiCol_Text,
|
||||
ImGuiCol_TextDisabled,
|
||||
ImGuiCol_WindowBg,
|
||||
ImGuiCol_ChildWindowBg,
|
||||
ImGuiCol_PopupBg,
|
||||
ImGuiCol_Border,
|
||||
ImGuiCol_BorderShadow,
|
||||
ImGuiCol_FrameBg,
|
||||
ImGuiCol_FrameBgHovered,
|
||||
ImGuiCol_FrameBgActive,
|
||||
ImGuiCol_TitleBg,
|
||||
ImGuiCol_TitleBgCollapsed,
|
||||
ImGuiCol_TitleBgActive,
|
||||
ImGuiCol_MenuBarBg,
|
||||
ImGuiCol_ScrollbarBg,
|
||||
ImGuiCol_ScrollbarGrab,
|
||||
ImGuiCol_ScrollbarGrabHovered,
|
||||
ImGuiCol_ScrollbarGrabActive,
|
||||
ImGuiCol_ComboBg,
|
||||
ImGuiCol_CheckMark,
|
||||
ImGuiCol_SliderGrab,
|
||||
ImGuiCol_SliderGrabActive,
|
||||
ImGuiCol_Button,
|
||||
ImGuiCol_ButtonHovered,
|
||||
ImGuiCol_ButtonActive,
|
||||
ImGuiCol_Header,
|
||||
ImGuiCol_HeaderHovered,
|
||||
ImGuiCol_HeaderActive,
|
||||
ImGuiCol_Column,
|
||||
ImGuiCol_ColumnHovered,
|
||||
ImGuiCol_ColumnActive,
|
||||
ImGuiCol_ResizeGrip,
|
||||
ImGuiCol_ResizeGripHovered,
|
||||
ImGuiCol_ResizeGripActive,
|
||||
ImGuiCol_CloseButton,
|
||||
ImGuiCol_CloseButtonHovered,
|
||||
ImGuiCol_CloseButtonActive,
|
||||
ImGuiCol_PlotLines,
|
||||
ImGuiCol_PlotLinesHovered,
|
||||
ImGuiCol_PlotHistogram,
|
||||
ImGuiCol_PlotHistogramHovered,
|
||||
ImGuiCol_TextSelectedBg,
|
||||
ImGuiCol_ModalWindowDarkening
|
||||
//ImGuiCol_COUNT - unnecessary
|
||||
);
|
||||
|
||||
|
||||
{ structs }
|
||||
ImGuiStyle = record
|
||||
Alpha : single;
|
||||
WindowPadding : ImVec2;
|
||||
WindowMinSize : ImVec2;
|
||||
WindowRounding : single;
|
||||
WindowTitleAlign : ImVec2;
|
||||
ChildWindowRounding : single;
|
||||
FramePadding : ImVec2;
|
||||
FrameRounding : single;
|
||||
ItemSpacing : ImVec2;
|
||||
ItemInnerSpacing : ImVec2;
|
||||
TouchExtraPadding : ImVec2;
|
||||
IndentSpacing : single;
|
||||
ColumnsMinSpacing : single;
|
||||
ScrollbarSize : single;
|
||||
ScrollbarRounding : single;
|
||||
GrabMinSize : single;
|
||||
GrabRounding : single;
|
||||
ButtonTextAlign : ImVec2;
|
||||
DisplayWindowPadding : ImVec2;
|
||||
DisplaySafeAreaPadding : ImVec2;
|
||||
AntiAliasedLines : bool;
|
||||
AntiAliasedShapes : bool;
|
||||
CurveTessellationTol : single;
|
||||
Colors: array[ImGuiCol_] of ImVec4;
|
||||
end;
|
||||
PImGuiStyle = ^ImGuiStyle;
|
||||
|
||||
ImGuiIO = record
|
||||
DisplaySize : ImVec2;
|
||||
DeltaTime : single;
|
||||
@ -194,8 +269,6 @@ type
|
||||
TotalIdxCount: integer;
|
||||
end;
|
||||
|
||||
ImGuiStyle = record
|
||||
end;
|
||||
ImGuiTextEditCallbackData = record
|
||||
end;
|
||||
ImGuiSizeConstraintCallbackData = record
|
||||
@ -702,6 +775,9 @@ procedure ImDrawList_UpdateTextureID(list: PImDrawList); cdecl; external ImguiLi
|
||||
//binding helpers
|
||||
function ImVec2Init(const x, y: single): Imvec2; inline;
|
||||
|
||||
procedure ImguiText(const s: string); inline;
|
||||
function ImguiSelectable(const s: string; const selected: boolean): boolean; inline;
|
||||
|
||||
implementation
|
||||
|
||||
function ImVec2Init(const x, y: single): Imvec2;
|
||||
@ -710,4 +786,14 @@ begin
|
||||
result.y := y;
|
||||
end;
|
||||
|
||||
procedure ImguiText(const s: string);
|
||||
begin
|
||||
igText(pchar(s));
|
||||
end;
|
||||
|
||||
function ImguiSelectable(const s: string; const selected: boolean): boolean;
|
||||
begin
|
||||
result := igSelectable(pchar(s), selected, 0, ImVec2Init(0,0));
|
||||
end;
|
||||
|
||||
end.
|
||||
|
@ -34,7 +34,7 @@
|
||||
<PackageName Value="LCL"/>
|
||||
</Item1>
|
||||
</RequiredPackages>
|
||||
<Units Count="8">
|
||||
<Units Count="9">
|
||||
<Unit0>
|
||||
<Filename Value="model_viewer.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
@ -67,6 +67,10 @@
|
||||
<Filename Value="imgui\imgui_impl_sdlgl2.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit7>
|
||||
<Unit8>
|
||||
<Filename Value="..\rs_units\rs_dat.pas"/>
|
||||
<IsPartOfProject Value="True"/>
|
||||
</Unit8>
|
||||
</Units>
|
||||
</ProjectOptions>
|
||||
<CompilerOptions>
|
||||
|
@ -19,9 +19,9 @@ along with RS model viewer. If not, see <http://www.gnu.org/licenses/>.
|
||||
program model_viewer;
|
||||
|
||||
uses
|
||||
sysutils, math,
|
||||
sysutils, classes, math, strutils, gvector,
|
||||
gl, glu, glext, sdl2, imgui, imgui_impl_sdlgl2,
|
||||
hob_mesh;
|
||||
rs_dat, hob_mesh;
|
||||
|
||||
const
|
||||
SCR_W_fscrn = 1024;
|
||||
@ -35,11 +35,23 @@ const
|
||||
PitchIncrement = 0.5;
|
||||
MouseTranslateMultiply = 0.025;
|
||||
|
||||
type
|
||||
TFileListItem = record
|
||||
name: string;
|
||||
node_hob: PRsDatFileNode;
|
||||
node_hmt: PRsDatFileNode;
|
||||
end;
|
||||
TFileList = specialize TVector<TFileListItem>;
|
||||
|
||||
var
|
||||
g_window: PSDL_Window;
|
||||
g_ogl_context: TSDL_GLContext;
|
||||
|
||||
model: TModel;
|
||||
g_rsdata: TRSDatFile;
|
||||
g_filelist: TFileList;
|
||||
g_model: TModel;
|
||||
g_model_name: string;
|
||||
g_model_loading_failed: Boolean;
|
||||
|
||||
view: record
|
||||
rotation_angle: single;
|
||||
@ -132,9 +144,10 @@ begin
|
||||
if view.rotation_angle > 360 then
|
||||
view.rotation_angle -= 360;
|
||||
|
||||
model.DrawGL(view.opts);
|
||||
if g_model <> nil then begin
|
||||
g_model.DrawGL(view.opts);
|
||||
end;
|
||||
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
igRender;
|
||||
|
||||
SDL_GL_SwapWindow(g_window);
|
||||
@ -272,7 +285,7 @@ begin
|
||||
view.autorotate := not view.autorotate;
|
||||
key_pressed.autorotate := true;
|
||||
end;
|
||||
//model rendering opts
|
||||
//g_model rendering opts
|
||||
SDLK_w:
|
||||
if not key_pressed.wireframe then begin
|
||||
view.opts.wireframe := not view.opts.wireframe;
|
||||
@ -362,22 +375,137 @@ begin
|
||||
end; {case}
|
||||
end;
|
||||
|
||||
procedure LoadMesh(item: TFileListItem);
|
||||
var
|
||||
hob, hmt: TMemoryStream;
|
||||
begin
|
||||
if g_model <> nil then
|
||||
g_model.Free;
|
||||
hob := TMemoryStream.Create;
|
||||
hob.WriteBuffer(item.node_hob^.Data^, item.node_hob^.size);
|
||||
hob.Seek(0, soBeginning);
|
||||
|
||||
hmt := TMemoryStream.Create;
|
||||
hmt.WriteBuffer(item.node_hmt^.Data^, item.node_hmt^.size);
|
||||
hmt.Seek(0, soBeginning);
|
||||
|
||||
g_model := TModel.Create;
|
||||
g_model.Load(hob, hmt);
|
||||
g_model.InitGL;
|
||||
end;
|
||||
|
||||
|
||||
procedure DrawGui;
|
||||
var
|
||||
showtest: bool;
|
||||
style: PImGuiStyle;
|
||||
file_item: TFileListItem;
|
||||
fitem_selected: Boolean = false;
|
||||
selected_mesh_name: String;
|
||||
selected_item: TFileListItem;
|
||||
begin
|
||||
ImGui_ImplSdlGL2_NewFrame(g_window);
|
||||
|
||||
style := igGetStyle();
|
||||
style^.WindowRounding := 0;
|
||||
|
||||
igBegin('Mesh');
|
||||
if not g_model_loading_failed then begin
|
||||
ImguiText(g_model_name);
|
||||
end else
|
||||
ImguiText('mesh loading failed :(');
|
||||
igEnd;
|
||||
|
||||
igBegin('Rendering options');
|
||||
igCheckbox('points', @view.opts.points);
|
||||
igCheckbox('wireframe', @view.opts.wireframe);
|
||||
igCheckbox('textures', @view.opts.textures);
|
||||
igCheckbox('vertex colors', @view.opts.vcolors);
|
||||
if g_model <> nil then
|
||||
if igButton('Export to obj', ImVec2Init(0,0)) then
|
||||
g_model.ExportObj('rs_exported.obj');
|
||||
igEnd;
|
||||
|
||||
selected_mesh_name := EmptyStr;
|
||||
igBegin('File list');
|
||||
//todo filter
|
||||
for file_item in g_filelist do begin
|
||||
fitem_selected := file_item.name = g_model_name;
|
||||
if ImguiSelectable(file_item.name, fitem_selected) then begin
|
||||
selected_mesh_name := file_item.name;
|
||||
selected_item := file_item;
|
||||
end;
|
||||
end;
|
||||
igEnd;
|
||||
|
||||
//igShowTestWindow(@showtest);
|
||||
|
||||
if (selected_mesh_name <> EmptyStr) and (selected_mesh_name <> g_model_name) then begin
|
||||
try
|
||||
LoadMesh(selected_item);
|
||||
g_model_name := selected_mesh_name;
|
||||
g_model_loading_failed := false;
|
||||
except
|
||||
g_model_loading_failed := true;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
//we only care about HOB and HMT files
|
||||
procedure LoadMeshFilelist;
|
||||
procedure AddFile(const path: string; const fnode, fnode_next: PRsDatFileNode);
|
||||
var
|
||||
item: TFileListItem;
|
||||
name: String;
|
||||
i: integer;
|
||||
begin
|
||||
if fnode^.is_directory then begin
|
||||
for i := 0 to Length(fnode^.nodes)-2 do //hob/hmt always go in pairs
|
||||
AddFile(path + fnode^.Name + '/', fnode^.nodes[i], fnode^.nodes[i+1]);
|
||||
end
|
||||
else begin
|
||||
name := fnode^.name;
|
||||
if AnsiEndsText('hob', name) then begin
|
||||
Assert(AnsiEndsText('hmt', fnode_next^.name), 'no HMT file!');
|
||||
item.name := path + name;
|
||||
item.node_hob := fnode;
|
||||
item.node_hmt := fnode_next;
|
||||
g_filelist.PushBack(item);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
var
|
||||
rs_files: TRsDatFileNodeList;
|
||||
file_: TRsDatFileNode;
|
||||
begin
|
||||
rs_files := g_rsdata.GetStructure();
|
||||
for file_ in rs_files do begin
|
||||
AddFile('', @file_, nil);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
//******************************************************************************
|
||||
var
|
||||
sec, frames: integer;
|
||||
event: TSDL_Event;
|
||||
done: boolean;
|
||||
hob_file, hmt_file, obj_file: string;
|
||||
|
||||
begin
|
||||
if Paramcount < 1 then begin
|
||||
writeln('specify HOB file');
|
||||
if not (FileExists(RS_DATA_HDR) and FileExists(RS_DATA_DAT)) then begin
|
||||
writeln('RS data files not found!');
|
||||
exit;
|
||||
end;
|
||||
hob_file := ParamStr(1);
|
||||
hmt_file := StringReplace(hob_file, '.hob', '.hmt', [rfIgnoreCase]);
|
||||
model := TModel.Create;
|
||||
model.Load(hob_file, hmt_file);
|
||||
|
||||
writeln('loading data');
|
||||
g_rsdata := TRSDatFile.Create(RS_DATA_HDR, RS_DATA_DAT);
|
||||
g_rsdata.Parse();
|
||||
g_filelist := TFileList.Create;
|
||||
LoadMeshFilelist();
|
||||
|
||||
g_model := nil;
|
||||
g_model_name := '';
|
||||
g_model_loading_failed := false;
|
||||
|
||||
writeln('Init SDL...');
|
||||
SDL_Init(SDL_INIT_VIDEO or SDL_INIT_TIMER);
|
||||
@ -387,11 +515,6 @@ begin
|
||||
SetGLWindowSize(g_window^.w, g_window^.h);
|
||||
|
||||
InitView;
|
||||
model.InitGL;
|
||||
|
||||
//export
|
||||
//obj_file := StringReplace(hob_file, '.hob', '.obj', [rfIgnoreCase]);
|
||||
//model.ExportObj(obj_file);
|
||||
|
||||
sec := SDL_GetTicks;
|
||||
frames := 0;
|
||||
@ -399,15 +522,8 @@ begin
|
||||
key_pressed.wireframe := false;
|
||||
key_pressed.fullscreen := false;
|
||||
while not Done do begin
|
||||
ImGui_ImplSdlGL2_NewFrame(g_window);
|
||||
|
||||
igBegin('rendering options');
|
||||
igCheckbox('points', @view.opts.points);
|
||||
igCheckbox('wireframe', @view.opts.wireframe);
|
||||
igCheckbox('textures', @view.opts.textures);
|
||||
igCheckbox('vertex colors', @view.opts.vcolors);
|
||||
igEnd;
|
||||
|
||||
DrawGui;
|
||||
DrawGLScene;
|
||||
|
||||
while SDL_PollEvent(@event) > 0 do
|
||||
@ -419,13 +535,16 @@ begin
|
||||
frames := 0;
|
||||
sec := SDL_GetTicks;
|
||||
end;
|
||||
SDL_Delay(10);
|
||||
SDL_Delay(5);
|
||||
//WindowScreenshot( surface^.w, surface^.h );
|
||||
end;
|
||||
|
||||
model.Free;
|
||||
|
||||
WindowFree;
|
||||
SDL_Quit;
|
||||
|
||||
if g_model <> nil then
|
||||
g_model.Free;
|
||||
g_filelist.Free;
|
||||
g_rsdata.Free;
|
||||
end.
|
||||
|
||||
|
@ -36,7 +36,7 @@ type
|
||||
textures: array of THmtTexture;
|
||||
end;
|
||||
|
||||
function ParseHmtFile(const fname: string): THmtFile;
|
||||
function ParseHmtFile(f: TMemoryStream): THmtFile;
|
||||
|
||||
//**************************************************************************************************
|
||||
implementation
|
||||
@ -138,15 +138,11 @@ begin
|
||||
end;
|
||||
|
||||
|
||||
function ParseHmtFile(const fname: string): THmtFile;
|
||||
function ParseHmtFile(f: TMemoryStream): THmtFile;
|
||||
var
|
||||
f: TMemoryStream;
|
||||
hmt: THmtFile;
|
||||
i: Integer;
|
||||
begin
|
||||
f := TMemoryStream.Create;
|
||||
f.LoadFromFile(fname);
|
||||
|
||||
//read main info
|
||||
hmt.material_count := f.ReadDWord;
|
||||
hmt.texture_offset := f.ReadDWord;
|
||||
@ -175,7 +171,6 @@ begin
|
||||
ReadTexture(hmt.textures[i], f);
|
||||
end;
|
||||
|
||||
f.Free;
|
||||
result := hmt;
|
||||
end;
|
||||
|
||||
|
@ -64,7 +64,7 @@ type
|
||||
objects: array of THobObject;
|
||||
end;
|
||||
|
||||
function ParseHobFile(const fname: string): THobFile;
|
||||
function ParseHobFile(f: TMemoryStream): THobFile;
|
||||
|
||||
//**************************************************************************************************
|
||||
implementation
|
||||
@ -323,16 +323,12 @@ begin
|
||||
end;
|
||||
|
||||
|
||||
function ParseHobFile(const fname: string): THobFile;
|
||||
function ParseHobFile(f: TMemoryStream): THobFile;
|
||||
var
|
||||
f: TMemoryStream;
|
||||
hob: THobFile;
|
||||
i: integer;
|
||||
filepos: int64;
|
||||
begin
|
||||
f := TMemoryStream.Create;
|
||||
f.LoadFromFile(fname);
|
||||
|
||||
hob.obj_count := f.ReadDWord;
|
||||
f.ReadDWord; //sometimes face block start, but useless in general
|
||||
|
||||
@ -353,7 +349,6 @@ begin
|
||||
f.Seek(filepos + 116, fsFromBeginning);
|
||||
end;
|
||||
|
||||
f.Free;
|
||||
result := hob;
|
||||
end;
|
||||
|
||||
|
304
rs_units/rs_dat.pas
Normal file
304
rs_units/rs_dat.pas
Normal file
@ -0,0 +1,304 @@
|
||||
{
|
||||
Loads DATA.DAT/HDR files to memory and parses the file structure.
|
||||
RS most likely loads just the file structure and the actual file data gets loaded as needed.
|
||||
We have plenty of ram nowadays, so read files just once and then load from memory.
|
||||
}
|
||||
unit rs_dat;
|
||||
{$mode objfpc}{$H+}
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
Classes, SysUtils, gvector;
|
||||
|
||||
const
|
||||
RS_DATA_HDR = 'DATA.HDR';
|
||||
RS_DATA_DAT = 'DATA.DAT';
|
||||
RS_DATA_FEDirectoryFlag = %10000000;
|
||||
|
||||
type
|
||||
//data file entry
|
||||
TRsDatFileEntry = packed record
|
||||
offset: longword;
|
||||
length: longword;
|
||||
padding: longword;
|
||||
type_flag: word;
|
||||
sub_entry_size: word;
|
||||
filename: array[0..15] of char;
|
||||
end;
|
||||
PRsDatFileEntry = ^TRsDatFileEntry;
|
||||
|
||||
//file or directory node
|
||||
PRsDatFileNode = ^TRsDatFileNode;
|
||||
TRsDatFileNode = record
|
||||
Name: string;
|
||||
is_directory: boolean;
|
||||
subentries_count: integer;
|
||||
offset: longword;
|
||||
size: longword;
|
||||
entry: TRsDatFileEntry;
|
||||
Data: pbyte;
|
||||
nodes: array of PRsDatFileNode; //children if directory
|
||||
end;
|
||||
|
||||
//header file entry
|
||||
TRsHdrSection = record
|
||||
Name: string; //section name
|
||||
dat_offset: integer; //offset in dat file
|
||||
Data: pbyte; //section's data pointer
|
||||
nodes: array of TRsDatFileNode; //all file entries / nodes
|
||||
root: TRsDatFileNode; //tree structure of nodes
|
||||
end;
|
||||
TRsDatFileNodeList = specialize TVector<TRsDatFileNode>;
|
||||
|
||||
{ TRSDatFile }
|
||||
|
||||
TRSDatFile = class
|
||||
public
|
||||
constructor Create(const hdr_file, dat_file: string); //loads header & data files to memory
|
||||
destructor Destroy; override; //cleanup loaded memory
|
||||
procedure Parse(); //parse file structure
|
||||
function GetStructure(): TRsDatFileNodeList; //list of sections, list must be freed by user
|
||||
procedure WriteFilesToDirectory(const path: string);
|
||||
private
|
||||
m_sections: array of TRsHdrSection;
|
||||
m_data: pbyte;
|
||||
|
||||
procedure ReadHeaderFile(const fname: string);
|
||||
procedure ReadDatFile(const fname: string);
|
||||
function ReadEntry(const stream: TMemoryStream; const mem_data: pbyte): TRsDatFileNode;
|
||||
procedure ReadSectionEntries(var section: TRsHdrSection; const mem_data: pbyte);
|
||||
procedure ParseSectionStructure(var section: TRsHdrSection);
|
||||
end;
|
||||
|
||||
function CountSubNodes(node: PRsDatFileNode): integer;
|
||||
function CountSubNodeSizes(node: PRsDatFileNode): integer;
|
||||
|
||||
|
||||
//**************************************************************************************************
|
||||
implementation
|
||||
|
||||
function CountSubNodes(node: PRsDatFileNode): integer;
|
||||
var
|
||||
i: integer;
|
||||
begin
|
||||
Result := 1;
|
||||
if node^.is_directory then
|
||||
for i := 0 to Length(node^.nodes) - 1 do
|
||||
Result += CountSubNodes(node^.nodes[i]);
|
||||
end;
|
||||
|
||||
function CountSubNodeSizes(node: PRsDatFileNode): integer;
|
||||
var
|
||||
i: integer;
|
||||
begin
|
||||
Result := node^.size;
|
||||
if node^.is_directory then
|
||||
for i := 0 to Length(node^.nodes) - 1 do
|
||||
Result += CountSubNodeSizes(node^.nodes[i]);
|
||||
end;
|
||||
|
||||
procedure SaveFile(const name: string; const buffer: pbyte; const buf_size: integer);
|
||||
var
|
||||
f: file;
|
||||
fname: string;
|
||||
begin
|
||||
fname := name;
|
||||
AssignFile(f, fname);
|
||||
Rewrite(f, 1);
|
||||
BlockWrite(f, buffer^, buf_size);
|
||||
CloseFile(f);
|
||||
end;
|
||||
|
||||
{ TRSDatFile }
|
||||
|
||||
constructor TRSDatFile.Create(const hdr_file, dat_file: string);
|
||||
begin
|
||||
ReadHeaderFile(hdr_file);
|
||||
ReadDatFile(dat_file);
|
||||
end;
|
||||
|
||||
destructor TRSDatFile.Destroy;
|
||||
begin
|
||||
inherited Destroy;
|
||||
m_sections := nil;
|
||||
freemem(m_data);
|
||||
end;
|
||||
|
||||
procedure TRSDatFile.Parse;
|
||||
var
|
||||
i: integer;
|
||||
begin
|
||||
for i := 0 to length(m_sections) - 1 do begin
|
||||
Writeln('reading section ', m_sections[i].name);
|
||||
ReadSectionEntries(m_sections[i], m_data + m_sections[i].dat_offset);
|
||||
ParseSectionStructure(m_sections[i]);
|
||||
end;
|
||||
end;
|
||||
|
||||
function TRSDatFile.GetStructure: TRsDatFileNodeList;
|
||||
var
|
||||
i: Integer;
|
||||
begin
|
||||
Assert(Length(m_sections) > 0, 'data not parsed');
|
||||
result := TRsDatFileNodeList.Create;
|
||||
for i := 0 to length(m_sections) - 1 do
|
||||
result.PushBack(m_sections[i].root);
|
||||
end;
|
||||
|
||||
procedure TRSDatFile.ReadHeaderFile(const fname: string);
|
||||
var
|
||||
f: file;
|
||||
section: TRsHdrSection;
|
||||
section_n: integer;
|
||||
i: integer;
|
||||
buffer: array[0..15] of char;
|
||||
begin
|
||||
assignfile(f, fname);
|
||||
reset(f, 1);
|
||||
section_n := FileSize(f) div 32;
|
||||
SetLength(m_sections, section_n);
|
||||
|
||||
for i := 0 to section_n - 1 do begin
|
||||
blockread(f, buffer, 16); //name
|
||||
section.name := trim(buffer);
|
||||
blockread(f, buffer, 12); //empty
|
||||
blockread(f, section.dat_offset, 4); //offset in m_data.DAT
|
||||
m_sections[i] := section;
|
||||
end;
|
||||
|
||||
closefile(f);
|
||||
end;
|
||||
|
||||
procedure TRSDatFile.ReadDatFile(const fname: string);
|
||||
var
|
||||
f: file;
|
||||
fsize: integer;
|
||||
begin
|
||||
AssignFile(f, fname);
|
||||
reset(f, 1);
|
||||
fsize := FileSize(f);
|
||||
m_data := getmem(fsize);
|
||||
Blockread(f, m_data^, fsize);
|
||||
closefile(f);
|
||||
end;
|
||||
|
||||
function TRSDatFile.ReadEntry(const stream: TMemoryStream; const mem_data: pbyte): TRsDatFileNode;
|
||||
var
|
||||
entry: TRsDatFileEntry;
|
||||
begin
|
||||
stream.ReadBuffer(entry, 32);
|
||||
result.name := Trim(entry.filename);
|
||||
result.offset := entry.offset;
|
||||
result.size := entry.length;
|
||||
result.is_directory := (entry.type_flag and RS_DATA_FEDirectoryFlag) <> 0;
|
||||
result.subentries_count := entry.sub_entry_size div 32 - 1;
|
||||
result.entry := entry;
|
||||
result.data := nil;
|
||||
if not result.is_directory then
|
||||
result.Data := mem_data + entry.offset;
|
||||
|
||||
//if (result.offset mod 32) <> 0 then writeln('unaligned offset');
|
||||
writeln(stderr, format('name: %s size: %d dir: %s subsize: %d flags: %s',
|
||||
[result.Name, entry.length, BoolToStr(result.is_directory),
|
||||
entry.sub_entry_size, binStr(entry.type_flag, 16)]));
|
||||
end;
|
||||
|
||||
procedure TRSDatFile.ReadSectionEntries(var section: TRsHdrSection; const mem_data: pbyte);
|
||||
var
|
||||
entries_offset: integer;
|
||||
entries_size: integer;
|
||||
entry_count: integer;
|
||||
stream: TMemoryStream;
|
||||
i: integer;
|
||||
begin
|
||||
section.data := mem_data;
|
||||
entries_offset := (pinteger(mem_data))^; //offset relative to section beginning
|
||||
entries_size := (pinteger(mem_data + 4))^; //length in bytes
|
||||
entry_count := entries_size div 32; //actual count of entries
|
||||
writeln('entries: ', entry_count);
|
||||
|
||||
//load entries to stream for easier parsing
|
||||
stream := TMemoryStream.Create;
|
||||
stream.WriteBuffer((mem_data + entries_offset)^, entries_size);
|
||||
stream.Seek(0, soBeginning);
|
||||
|
||||
SetLength(section.nodes, entry_count);
|
||||
for i := 0 to entry_count - 1 do begin
|
||||
section.nodes[i] := ReadEntry(stream, mem_data);
|
||||
end;
|
||||
|
||||
stream.Free;
|
||||
end;
|
||||
|
||||
|
||||
procedure AddNode(const parent: PRsDatFileNode; var nodes: array of TRsDatFileNode; var node_index: integer);
|
||||
var
|
||||
i: integer;
|
||||
node: PRsDatFileNode;
|
||||
begin
|
||||
if node_index > length(nodes) - 1 then
|
||||
exit;
|
||||
|
||||
//add current to parent
|
||||
node := @nodes[node_index];
|
||||
i := length(parent^.nodes);
|
||||
Setlength(parent^.nodes, i + 1);
|
||||
parent^.nodes[i] := node;
|
||||
//writeln('added node: ', node^.name, ' to parent ', parent^.name);
|
||||
|
||||
//add all subentries if any
|
||||
if node^.is_directory then begin
|
||||
//writeln(' subentries: ', node^.subentries_count);
|
||||
while CountSubNodes(node) < node^.subentries_count + 1 do begin
|
||||
node_index += 1;
|
||||
AddNode(node, nodes, node_index);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TRSDatFile.ParseSectionStructure(var section: TRsHdrSection);
|
||||
var
|
||||
node_idx: integer = 0;
|
||||
begin
|
||||
section.root.name := section.name;
|
||||
section.root.is_directory := true;
|
||||
section.root.data := nil;
|
||||
while node_idx < length(section.nodes) do begin
|
||||
AddNode(@section.root, section.nodes, node_idx);
|
||||
node_idx += 1;
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
procedure WriteDirectory(const node: PRsDatFileNode; const path: string; const data: pbyte);
|
||||
var
|
||||
dir: string;
|
||||
subnode: PRsDatFileNode;
|
||||
i: Integer;
|
||||
begin
|
||||
dir := path + node^.name;
|
||||
if not DirectoryExists(dir) then
|
||||
MkDir(dir);
|
||||
for i := 0 to length(node^.nodes) - 1 do begin
|
||||
subnode := node^.nodes[i];
|
||||
if subnode^.is_directory then
|
||||
WriteDirectory(subnode, dir + DirectorySeparator, data)
|
||||
else
|
||||
SaveFile(dir + DirectorySeparator + subnode^.name, data + subnode^.offset, subnode^.size);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TRSDatFile.WriteFilesToDirectory(const path: string);
|
||||
var
|
||||
section: TRsHdrSection;
|
||||
begin
|
||||
for section in m_sections do begin
|
||||
WriteDirectory(@section.root, path, section.data);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
||||
|
||||
end.
|
||||
|
Loading…
x
Reference in New Issue
Block a user