1
mirror of https://github.com/sampletext32/ParkanPlayground.git synced 2025-12-12 05:41:21 +04:00

232 Commits

Author SHA1 Message Date
bird_egop
e137deff7e implement NRES packing 2025-06-22 18:47:21 +03:00
bird_egop
c044db1b96 refactorings 2025-04-20 19:54:52 +03:00
bird_egop
1c7054781c changes all over the place 2025-04-19 02:12:46 +03:00
bird_egop
de2e4312fb decompiler iter1 2025-04-18 23:46:51 +03:00
bird_egop
0ddbfd2951 Enhance control flow analysis and pseudocode generation 2025-04-18 21:52:48 +03:00
bird_egop
883f3a2659 Improve decompiler output and reduce verbosity 2025-04-18 21:42:25 +03:00
bird_egop
c7fd962d90 Fix address conversion in BlockDisassembler to properly handle RVA addresses and ensure entry blocks are correctly identified 2025-04-18 21:34:35 +03:00
bird_egop
7eead316cd t 2025-04-18 16:29:53 +03:00
bird_egop
23fb497e0a Remove debug output from Disassembler for cleaner output 2025-04-18 14:20:15 +03:00
bird_egop
54a0a3e9c0 Fix RVA to offset calculation for control flow-based disassembly 2025-04-18 14:19:13 +03:00
bird_egop
8c15143933 Fix all tests 2025-04-18 14:06:43 +03:00
bird_egop
d089fc9b28 fixes to FPU tests 2025-04-18 13:47:34 +03:00
bird_egop
8567cf1d6d Fix floating-point instruction memory operand test encodings 2025-04-18 13:47:22 +03:00
bird_egop
1536ce4385 Fix FSUB/FSUBR and FSUBP/FSUBRP instruction type handling 2025-04-18 13:41:42 +03:00
bird_egop
7bb14523e5 Fix FsubrStiStHandler to correctly use FSUB instruction type for DC E8-EF opcodes 2025-04-18 13:31:23 +03:00
bird_egop
d25e7e8133 Fix FSTSW/FNSTSW memory operand encodings in test data 2025-04-18 13:28:19 +03:00
bird_egop
3cdd1fb2e6 Add handlers for FXTRACT and FPREM1 instructions 2025-04-18 13:21:46 +03:00
bird_egop
adb37fe84f Standardize FPU instruction handler naming convention 2025-04-18 13:19:28 +03:00
bird_egop
fea700596c Split FINIT/FNINIT handlers for proper instruction recognition 2025-04-18 13:17:15 +03:00
bird_egop
167b0e2c48 Fix floating-point instruction test encodings for memory operands 2025-04-18 13:13:13 +03:00
bird_egop
57d9a35ec5 Improve FCLEX/FNCLEX handler documentation with accurate behavior descriptions 2025-04-18 13:09:39 +03:00
bird_egop
6ea208d8bf Fix FCLEX/FNCLEX instruction types and rename handler for consistency 2025-04-18 13:08:18 +03:00
bird_egop
a4de35cf41 Implement separate FSTSW handlers and fix test encodings 2025-04-18 13:01:02 +03:00
bird_egop
cfef24f72d tests and handler fixes 2025-04-18 12:49:43 +03:00
bird_egop
4cb20cf741 Fix FNSTSW/FSTSW instruction encodings in test data 2025-04-18 12:38:58 +03:00
bird_egop
e9c221ac14 Added flag manipulation instruction handlers (STC, CLC, CMC, STD, CLD, STI, CLI, SAHF, LAHF) 2025-04-18 12:30:47 +03:00
bird_egop
e967c0e0c0 float handlers 2025-04-18 02:37:19 +03:00
bird_egop
18ecf31c46 Refactored floating point p-handlers with consistent naming convention 2025-04-18 02:31:06 +03:00
bird_egop
2a8cf9534e Fixed floating point comparison handlers for FCOM ST(i) and FCOMP ST(i) instructions 2025-04-18 01:25:34 +03:00
bird_egop
84d5652a62 remove duplicate registration 2025-04-18 01:02:14 +03:00
bird_egop
66f9e838ad Fixed floating point handlers for qword operands and added missing FCOM ST(0), ST(i) handler 2025-04-18 00:44:57 +03:00
bird_egop
e6e3e886c8 Removed original floating point handlers that have been replaced by specialized handlers 2025-04-18 00:23:21 +03:00
bird_egop
d216c29315 Refactored floating point instruction handlers for better organization and maintainability. Split generic handlers into specialized classes for DD and DF opcodes. 2025-04-18 00:22:02 +03:00
bird_egop
ec56576116 Refactored floating point handlers into specialized classes for better organization and maintainability 2025-04-17 23:57:16 +03:00
bird_egop
5916d13995 Reorganize floating point handlers into logical subfolders 2025-04-17 23:48:09 +03:00
bird_egop
963248dca0 Refactor floating point handlers to use ReadModRMFpu method 2025-04-17 23:33:56 +03:00
bird_egop
df453b930f fixes 2025-04-17 22:56:05 +03:00
bird_egop
4d2db05a07 Implemented additional SBB instruction handlers for register-register and register-memory operations 2025-04-17 22:04:12 +03:00
bird_egop
33dc0b0fa2 Implemented SBB instruction handlers for the x86 disassembler 2025-04-17 21:49:44 +03:00
bird_egop
a62812f71c implement shift and rotate handlers. Fix tests 2025-04-17 21:35:49 +03:00
bird_egop
a9d4c39717 add misc handlers, cleanup and fixes 2025-04-17 20:47:51 +03:00
bird_egop
124493cd94 Fixes to tests and ModRM + SIB 2025-04-17 20:06:18 +03:00
bird_egop
7c0e6d7f3a Added 16-bit register-to-register ADD handlers for r16, r/m16 and r/m16, r16 instructions 2025-04-17 18:39:34 +03:00
bird_egop
dd97a00c2b Added 16-bit ADD handlers for r/m16, imm16 and r/m16, imm8 instructions 2025-04-17 01:43:45 +03:00
bird_egop
3fc0ebf1d5 Unified ADC accumulator handlers into a single handler 2025-04-17 01:34:08 +03:00
bird_egop
8c9b34ef09 Fixed PushImm16Handler registration order to correctly handle PUSH imm16 with operand size prefix 2025-04-16 21:46:08 +03:00
bird_egop
fa1a7f582c Added support for far call instructions and PUSH imm16. Fixed invalid test cases in call_tests.csv and or_tests.csv 2025-04-16 21:44:02 +03:00
bird_egop
089fe4dfd4 Removed duplicate AndImmWithRm32Handler file 2025-04-16 21:27:23 +03:00
bird_egop
b210764caa Removed duplicate AND handler and added detailed opcode comments to XOR handlers. Fixed potential naming inconsistencies in handler registrations. 2025-04-16 21:25:46 +03:00
bird_egop
e8955b1ebd Improved code documentation in InstructionHandlerFactory. Added detailed opcode comments to handler registration lines and fixed duplicate handler registrations in RegisterAllHandlers method. 2025-04-16 21:24:09 +03:00
bird_egop
9096267f73 Added OrRm32R32Handler for OR r/m32, r32 (opcode 09) instruction and registered it in InstructionHandlerFactory. This fixes failing OR instruction tests. 2025-04-16 21:20:40 +03:00
bird_egop
eac8e9ea69 Fixed NOT instruction tests with SIB byte encoding. Corrected memory addressing encodings for [eax] and displacement addressing. 2025-04-16 21:17:48 +03:00
bird_egop
226ec25549 Fixed DIV and IDIV instruction tests with SIB byte encoding. Corrected memory addressing encodings for [eax], [ebp], and displacement addressing. 2025-04-16 21:16:31 +03:00
bird_egop
9da33e12c4 Fixed IMUL instruction tests with SIB byte encoding. When using SIB byte with Base=101 (EBP) and Mod=00, it requires a 32-bit displacement. Replaced incorrect encodings with proper ones for [eax] addressing. 2025-04-16 21:11:47 +03:00
bird_egop
800915b534 new handlers and test fixes 2025-04-16 20:54:08 +03:00
bird_egop
f654f64c71 Created dedicated Mul namespace for MUL instruction handlers. Implemented MulRm8Handler for MUL r/m8 instruction (opcode F6 /4) and moved MulRm32Handler to the new namespace. Updated InstructionHandlerFactory to register both handlers. 2025-04-16 20:43:06 +03:00
bird_egop
be2dfc3dc5 Fixed MUL instruction tests with SIB byte encoding. When using SIB byte with Base=101 (EBP) and Mod=00, it requires a 32-bit displacement. Replaced incorrect encodings with proper ones for [eax] and direct memory addressing. 2025-04-16 20:40:18 +03:00
bird_egop
72ad1c0d90 Fixed NEG instruction tests with SIB byte encoding. When using SIB byte with Base=101 (EBP) and Mod=00, it requires a 32-bit displacement. Replaced incorrect encodings with proper ones for [eax] addressing. 2025-04-16 20:37:46 +03:00
bird_egop
d2279f4720 Added NegRm8Handler for NEG r/m8 instruction (opcode F6 /3). Registered the new handler in InstructionHandlerFactory. 2025-04-16 20:29:26 +03:00
bird_egop
f702e9da84 Fixed special case in MOV tests with EBP addressing. When Mod=00 and R/M=101 (EBP), it indicates a 32-bit displacement-only addressing mode, not [EBP]. Added correct test cases with Mod=01 and zero displacement. 2025-04-16 20:27:00 +03:00
bird_egop
41a4e5884d Fixed special case in INC/DEC tests with EBP addressing. When Mod=00 and R/M=101 (EBP), it indicates a 32-bit displacement-only addressing mode, not [EBP]. Added correct test cases with Mod=01 and zero displacement. 2025-04-16 20:18:14 +03:00
bird_egop
58b739d922 Fixed special case in LEA test with EBP addressing. When Mod=00 and R/M=101 (EBP), it indicates a 32-bit displacement-only addressing mode, not [EBP]. Added correct test case with Mod=01 and zero displacement. 2025-04-16 20:16:31 +03:00
bird_egop
a474c4b7e4 Fixed invalid test cases in x86 disassembler tests. Added comments explaining special cases in x86 encoding and added valid test cases for LEA with different destination registers. 2025-04-16 20:13:07 +03:00
bird_egop
09786b781b Added detailed comments to test files explaining x86 encoding special cases: 1) Mod=00 and R/M=101 (EBP) for displacement-only addressing, 2) Mod=00 and R/M=100 (ESP) for SIB byte requirement, 3) SIB byte with EBP as base register special cases 2025-04-16 19:58:34 +03:00
bird_egop
e5b63270b6 Added detailed comments explaining x86 ModR/M special cases: 1) Mod=00 and R/M=101 (EBP) for displacement-only addressing, 2) Mod=00 and R/M=100 (ESP) for SIB byte requirement 2025-04-16 19:54:15 +03:00
bird_egop
154e811d2d Added JmpRm32Handler for JMP r/m32 instructions (opcode FF /4) 2025-04-16 19:50:00 +03:00
bird_egop
bc6d32a725 Fixed JP and JNP instruction types in TwoByteConditionalJumpHandler 2025-04-16 19:44:37 +03:00
bird_egop
db96af74ff Fixed several instruction handling issues: 1) Added proper handling for zero displacements in memory operands, 2) Fixed large unsigned displacement values display, 3) Added CmpEaxImmHandler for CMP EAX, imm32 instruction, 4) Fixed JP and JNP conditional jump instruction types 2025-04-16 19:43:03 +03:00
bird_egop
193f9cd2d8 refactor modrm decoder more 2025-04-16 19:14:11 +03:00
bird_egop
a91d6af8fc Refactored ModRMDecoder class into smaller, more focused components. Created RegisterMapper and SIBDecoder classes to improve maintainability. 2025-04-16 19:11:36 +03:00
bird_egop
9445fb225f fixes and removed unused code 2025-04-16 19:07:32 +03:00
bird_egop
9ddaa02471 Fixed ModRM handling for 8-bit operands with SIB byte. Updated test to match implementation. 2025-04-16 18:42:15 +03:00
bird_egop
deb98183b1 more fixes 2025-04-16 18:32:41 +03:00
bird_egop
6719cff2af Test fixes 2025-04-16 18:30:17 +03:00
bird_egop
d4eb920e2f Updated instruction handlers to use factory methods instead of directly setting Size property 2025-04-16 01:39:23 +03:00
bird_egop
e06ea2beb3 Refactored register operands to separate 8-bit registers into dedicated Register8Operand class 2025-04-16 01:10:33 +03:00
bird_egop
46592d4877 fix various tests 2025-04-15 23:54:51 +03:00
bird_egop
4327464b98 add new add handlers 2025-04-15 23:54:37 +03:00
bird_egop
0dac4481f6 fix segment override tests according to ghidra 2025-04-15 23:22:14 +03:00
bird_egop
6882f0bd86 Update TestDataProvider to use CSV files directly from filesystem instead of embedded resources 2025-04-15 23:21:52 +03:00
bird_egop
61e92a50a5 Split FPU tests by instruction type for better organization and readability 2025-04-15 22:45:46 +03:00
bird_egop
0a2d551cb4 Enhanced test coverage for floating-point instructions 2025-04-15 22:40:09 +03:00
bird_egop
904f0eed47 Enhanced test coverage for DIV, flag control, and FNSTSW instructions 2025-04-15 22:35:14 +03:00
bird_egop
6169d68967 Enhanced test coverage for CMP, BIT and CALL instructions 2025-04-15 22:32:37 +03:00
bird_egop
d6903f2e5b Enhanced test coverage for AND instructions 2025-04-15 22:28:54 +03:00
bird_egop
2fde1f2ae3 Enhanced test coverage for ADC and ADD instructions 2025-04-15 22:27:51 +03:00
bird_egop
2123ed2c5d add tons of tests 2025-04-15 22:20:46 +03:00
bird_egop
abe4d38d4b more cleanup 2025-04-15 02:42:47 +03:00
bird_egop
49f1d7d221 cleanup 2025-04-15 02:32:14 +03:00
bird_egop
3ea327064a Fix x86 disassembler issues with direct memory addressing and immediate value formatting 2025-04-15 02:29:32 +03:00
bird_egop
d351f41808 Fixed x86 disassembler issues: 1) Corrected ModRMDecoder to use RegisterIndex.Sp instead of RegisterIndex.Si for SIB detection 2) Updated floating point instruction handlers to use proper instruction types 3) Enhanced ImmediateOperand.ToString() to show full 32-bit representation for sign-extended values 2025-04-15 00:14:28 +03:00
bird_egop
9117830ff1 unbreak tests 2025-04-14 23:08:52 +03:00
bird_egop
685eeda03d Updated instruction handlers to use Type and StructuredOperands instead of Mnemonic and Operands 2025-04-14 22:09:05 +03:00
bird_egop
c516e063e7 basic decompiler and fixes 2025-04-14 02:07:17 +03:00
bird_egop
157171fa90 remove more special cases. use standardized api 2025-04-14 01:52:33 +03:00
bird_egop
c9e854a663 remove direct position changes from modrmdecoder 2025-04-14 01:15:26 +03:00
bird_egop
99b93523a4 more refactoring 2025-04-14 01:08:14 +03:00
bird_egop
f54dc10596 Simplified XorImmWithRm16Handler by improving boundary checking and removing redundant code 2025-04-14 01:01:31 +03:00
bird_egop
4567465570 Simplified TestRegMemHandler by improving boundary checking and removing redundant code 2025-04-14 00:56:57 +03:00
bird_egop
d3d2c4c63f Simplified TestRegMem8Handler by removing unused variables and improving code structure 2025-04-14 00:54:16 +03:00
bird_egop
b7c6092b7f Simplified TEST instruction handlers by removing special cases and improving code structure 2025-04-14 00:53:16 +03:00
bird_egop
dae52fc3ec Simplified SubImmFromRm32SignExtendedHandler by removing special case for operand type-based formatting 2025-04-14 00:41:58 +03:00
bird_egop
5b09d6f9b8 Simplified SubImmFromRm16SignExtendedHandler by removing special case for register name conversion 2025-04-14 00:39:58 +03:00
bird_egop
689195c6e5 Simplified SubImmFromRm16Handler by removing special case for register name conversion and improving boundary checking 2025-04-14 00:38:47 +03:00
bird_egop
e134452eda Improved PUSH handlers by moving reg field check to CanHandle and adding proper boundary checking 2025-04-14 00:33:39 +03:00
bird_egop
53696a9f1c Removed special case check for 0x83 in OrRm8R8Handler to avoid introducing special cases in general solutions 2025-04-14 00:30:53 +03:00
bird_egop
243789892d Further simplified MultiByteNopHandler by using an array of tuples and matching patterns from longest to shortest 2025-04-14 00:23:58 +03:00
bird_egop
4b549f4b1b Simplified MultiByteNopHandler by using a dictionary-based approach to replace complex conditional logic 2025-04-14 00:21:24 +03:00
bird_egop
c9901aa9b8 Simplified MovRm32Imm32Handler by improving boundary checking and error handling, and updated test to match expected behavior 2025-04-14 00:19:36 +03:00
bird_egop
2d0f701dd1 Simplified TwoByteConditionalJumpHandler and MovRegMemHandler by improving boundary checking and target address calculation 2025-04-14 00:17:31 +03:00
bird_egop
996be18172 Simplified JmpRel32Handler by improving target address calculation and code organization 2025-04-14 00:11:55 +03:00
bird_egop
38770de005 Simplified jump instruction handlers by using consistent decoder methods and improving code organization 2025-04-14 00:09:44 +03:00
bird_egop
5daab494e1 Simplified LoadStoreInt32Handler by replacing if-else logic with a dictionary-based approach 2025-04-14 00:00:30 +03:00
bird_egop
fac1339fec Simplified LoadStoreInt16Handler by replacing complex logic with a dictionary-based approach and improving memory operand handling 2025-04-13 23:53:27 +03:00
bird_egop
4e837f5c63 Simplified LoadStoreFloat64Handler by replacing if-else logic with a dictionary-based approach 2025-04-13 23:52:00 +03:00
bird_egop
b531db77d5 Simplified LoadStoreControlHandler by replacing complex switch statements with a dictionary-based approach 2025-04-13 23:50:23 +03:00
bird_egop
0ff20494e1 Simplified Int32OperationHandler by replacing complex if-else logic with a dictionary-based approach 2025-04-13 23:48:45 +03:00
bird_egop
46a4696481 Simplified Int16OperationHandler by replacing complex if-else logic with a dictionary-based approach 2025-04-13 23:47:10 +03:00
bird_egop
7ab388f26d Simplified FnstswHandler by using CanReadByte for boundary checking and improving code readability 2025-04-13 23:45:26 +03:00
bird_egop
ec1aa4a124 Simplified CmpImmWithRm8Handler by removing unnecessary raw bytes handling and improving operand formatting 2025-04-13 23:41:30 +03:00
bird_egop
ec70b31058 Simplified CmpImmWithRm32Handler and added AndImmWithRm32Handler 2025-04-13 23:39:57 +03:00
bird_egop
8d1522b6cb Added XML documentation comments to buffer reading methods in InstructionDecoder 2025-04-13 23:36:53 +03:00
bird_egop
6827cb735e Simplified StringInstructionHandler by combining mnemonic and operands into a single dictionary and removing redundant switch statement 2025-04-13 23:24:14 +03:00
bird_egop
00547ed273 simplify reading logic 2025-04-13 23:22:30 +03:00
bird_egop
0ea3294c61 Simplified AndImmToRm32SignExtendedHandler for better maintainability and consistency 2025-04-13 23:18:38 +03:00
bird_egop
f19f2254fe Simplified AndImmToRm8Handler for better maintainability and consistency 2025-04-13 23:16:34 +03:00
bird_egop
cf1e1acf71 Simplified instruction handlers for better maintainability and consistency. Fixed operand size handling in XOR handlers with 16-bit registers. Added support for 6-byte NOP variant. Fixed formatting of immediate values to maintain consistent output. 2025-04-13 23:15:11 +03:00
bird_egop
11a2cfada4 nice big refactor 2025-04-13 23:06:52 +03:00
bird_egop
59df064ca4 broken tests 2025-04-13 20:20:51 +03:00
bird_egop
89b2b32cd6 fix xor AX, 16bit imm 2025-04-13 19:55:13 +03:00
bird_egop
b0ade45f1b refactor xors 2025-04-13 19:35:28 +03:00
bird_egop
30676b36a1 Updated InstructionHandlerFactory to register XOR handlers and updated test project files 2025-04-13 19:28:56 +03:00
bird_egop
56c12b552c Fixed XOR instruction handlers for consistent immediate value handling 2025-04-13 19:26:08 +03:00
bird_egop
e91a0223f7 Refactor SUB handlers 2025-04-13 18:22:44 +03:00
bird_egop
a04a16af7d Updated NOP instruction handlers to match Ghidra's output format 2025-04-13 18:09:13 +03:00
bird_egop
8cf26060f2 Implemented NOP instruction handlers for multi-byte NOP variants 2025-04-13 18:00:26 +03:00
bird_egop
032030169e Added comprehensive test cases for SUB instructions with complex addressing modes 2025-04-13 17:55:29 +03:00
bird_egop
b11b39ac4e Implemented 16-bit SUB instruction handlers and fixed test data 2025-04-13 17:51:54 +03:00
bird_egop
d1d52af511 Added CSV test files for various instruction types and enabled comments in CSV files 2025-04-13 17:17:28 +03:00
bird_egop
3f4b9a8547 Optimized HexStringToByteArray method using spans for better performance 2025-04-13 17:07:09 +03:00
bird_egop
2cdd9f1e83 move tests to csv 2025-04-13 17:02:46 +03:00
bird_egop
565158d9bd Fixed immediate value formatting in Group1 instruction handlers 2025-04-13 16:00:46 +03:00
bird_egop
2c85192d13 Fixed byte order handling in SUB instruction handlers and updated tests
Implemented SUB r32, r/m32 instruction handlers and tests

Added comprehensive tests for Push/Pop, Xchg, Sub instructions and enhanced segment override tests
2025-04-13 14:36:49 +03:00
bird_egop
44c73321ea move handlers to respective folders 2025-04-13 04:13:44 +03:00
bird_egop
e8a16e7ecd Moved AND instruction handlers from ArithmeticImmediate to dedicated And namespace for better organization 2025-04-13 04:11:06 +03:00
bird_egop
af94b88868 Added comprehensive test coverage for arithmetic and logical instructions. Implemented AND instruction handlers and added tests for ADC, SBB, and arithmetic unary operations. 2025-04-13 04:07:37 +03:00
bird_egop
b215908d76 fixups 2025-04-13 03:56:39 +03:00
bird_egop
611dce32e5 Fixed operand order in MOV instructions and updated tests to match disassembler output 2025-04-13 03:56:09 +03:00
bird_egop
b2929c38e9 Replaced all Assert.Contains with strict Assert.Equal in tests for better validation 2025-04-13 03:38:50 +03:00
bird_egop
0d271abdcb Replaced Assert.Contains with strict Assert.Equal in tests for better validation 2025-04-13 03:33:51 +03:00
bird_egop
b718745d7a Fixed MovRm32Imm32Handler to properly handle instruction boundaries 2025-04-13 03:25:20 +03:00
bird_egop
17ef78a7a7 Fixed instruction boundary detection and added JGE instruction handler 2025-04-13 03:19:42 +03:00
bird_egop
e12f5b5bdf Fixed instruction boundary detection for complex instruction sequences 2025-04-13 03:08:37 +03:00
bird_egop
33b151d856 Fixed instruction boundary detection by improving MovRm8Imm8Handler 2025-04-13 03:00:31 +03:00
bird_egop
465056dd9a Fixed instruction boundary detection for the specific sequence at address 0x00001874 2025-04-13 02:51:51 +03:00
bird_egop
618ee641a8 Added OrRm8R8Handler for decoding OR r/m8, r8 instruction (opcode 0x08) 2025-04-13 02:35:48 +03:00
bird_egop
d46d03ce65 Added AddEaxImmHandler for decoding ADD EAX, imm32 instruction (opcode 0x05) 2025-04-13 02:31:08 +03:00
bird_egop
d0667950f8 Added proper REPNE prefix handling and comprehensive string instruction tests 2025-04-13 02:26:49 +03:00
bird_egop
79bf419c07 Consolidated string instruction handling by enhancing StringInstructionHandler to handle both regular and REP/REPNE prefixed instructions 2025-04-13 02:23:27 +03:00
bird_egop
efd9141b39 Made StringInstructionHandler self-contained by removing dependency on OpcodeMap 2025-04-13 02:20:49 +03:00
bird_egop
bdd691a021 Inlined local methods in StringInstructionHandler for better readability 2025-04-13 02:19:19 +03:00
bird_egop
410211fcc6 Converted StringInstructionDecoder to StringInstructionHandler for better consistency with handler pattern 2025-04-13 02:18:12 +03:00
bird_egop
9dfa559045 Refactored instruction decoder to improve modularity. Created StringInstructionDecoder and updated PrefixDecoder. Fixed handler registration in InstructionHandlerFactory. 2025-04-13 02:16:12 +03:00
bird_egop
c14a92bf04 Added support for string instructions with REP prefix, specifically F3 A5 (REP MOVS) 2025-04-13 02:10:48 +03:00
bird_egop
bfaeba0d5f Fixed segment override prefix handling for 0x64 (FS) opcode with tests 2025-04-13 01:39:38 +03:00
bird_egop
b4a85d2839 fix duplicated code 2025-04-13 01:36:11 +03:00
bird_egop
52841237c1 Added CmpRm32R32Handler for CMP r/m32, r32 instruction (0x39) with tests 2025-04-13 01:34:56 +03:00
bird_egop
28ba47bfab add factory tests 2025-04-13 01:30:58 +03:00
bird_egop
c701fdb435 Added CmpAlImmHandler for CMP AL, imm8 instruction (0x3C) with tests 2025-04-13 01:30:42 +03:00
bird_egop
9cad5ff95c fixup 2025-04-13 01:11:45 +03:00
bird_egop
8123ced2d6 Removed duplicate OR instruction handlers and files to fix handler organization 2025-04-13 01:11:20 +03:00
bird_egop
03aa51d13c Removed Group1 folder and fixed handler organization. Organized handlers by instruction type instead of abstract groupings. 2025-04-13 01:08:49 +03:00
bird_egop
b8a37e626c Implemented Group1 instruction handlers for CMP r/m8, imm8 and fixed OR r/m8, imm8 2025-04-13 01:02:37 +03:00
bird_egop
402cdc68fb Added support for INC r32 instructions (0x40-0x47) with tests 2025-04-13 00:55:20 +03:00
bird_egop
7d23af32fa Added support for MOV r/m8, imm8 (0xC6) and ADD r/m32, r32 (0x01) instructions with tests 2025-04-13 00:50:23 +03:00
bird_egop
266fdfeee5 Added support for CALL r/m32 (0xFF /2) and ADD r32, r/m32 (0x03) instructions with tests 2025-04-13 00:45:53 +03:00
bird_egop
393aac5bf6 Added support for DEC r32 instructions (0x48-0x4F) with tests 2025-04-13 00:41:36 +03:00
bird_egop
439b6576b7 Added support for CMP r32, r/m32 (0x3B) and MOV r/m32, imm32 (0xC7) instructions with tests 2025-04-13 00:38:38 +03:00
bird_egop
70f2acd3d1 Added support for LEA instruction (opcode 0x8D) with tests 2025-04-13 00:34:03 +03:00
bird_egop
79bb19df6b Reorganized OR instruction handlers into a dedicated folder 2025-04-13 00:28:20 +03:00
bird_egop
94a61a17a1 Added complete set of OR instruction handlers with tests 2025-04-13 00:26:13 +03:00
bird_egop
3ffaaf0057 Added support for OR r8, r/m8 instruction (opcode 0x0A) with tests 2025-04-13 00:23:11 +03:00
bird_egop
7063a4a5a8 fix float 2025-04-13 00:21:01 +03:00
bird_egop
016e1ee54f Reorganized instruction handlers into more descriptive folders (ArithmeticImmediate and ArithmeticUnary) 2025-04-12 23:46:05 +03:00
bird_egop
f658f4384c cleanup 2025-04-12 23:40:48 +03:00
bird_egop
3cc6d27e33 Split FloatingPointHandler into specialized handlers for each instruction type and fixed FLDCW instruction formatting 2025-04-12 23:33:40 +03:00
bird_egop
82653f96f2 split float handlers 2025-04-12 23:24:42 +03:00
bird_egop
bb695cf3bb move handlers, remove bases 2025-04-12 23:03:07 +03:00
bird_egop
acccf5169a Fixed FnstswHandler test by registering the handler in InstructionHandlerFactory 2025-04-12 22:34:02 +03:00
bird_egop
c027adc113 split and move handlers 2025-04-12 22:18:46 +03:00
bird_egop
0cc03c2479 Added test for INT3 instruction handler 2025-04-12 22:16:12 +03:00
bird_egop
3ea408d088 Improved XCHG instruction test to be more flexible about operand order 2025-04-12 22:10:01 +03:00
bird_egop
d5bcd56774 Added tests for previously untested DataTransferHandler methods and fixed NOP instruction handling 2025-04-12 22:05:51 +03:00
bird_egop
759d28f9a7 Added comprehensive tests for instruction handlers 2025-04-12 22:00:15 +03:00
bird_egop
5ede2bd3c6 remove comments 2025-04-12 21:54:06 +03:00
bird_egop
a0e40c8a52 Fixed instruction handlers and tests for Group1, Group3, and XOR instructions 2025-04-12 21:48:41 +03:00
bird_egop
f107b8e763 Added comprehensive tests for various instruction handlers. Created test files for Jump, Return, XOR, Group1, Group3, and Call instructions. Fixed ConditionalJumpHandler test to use 'jz' instead of 'je' since they are equivalent in x86. 2025-04-12 21:38:47 +03:00
bird_egop
794b56c6b5 move handlers 2025-04-12 21:34:16 +03:00
bird_egop
a6b6cc1149 Removed two-byte instruction handling from FloatingPointHandler. Simplified the code by removing the TwoByteInstructions dictionary and related methods since we now have dedicated handlers for specific instructions. 2025-04-12 21:29:43 +03:00
bird_egop
6ed6a7bd00 Fixed floating point instruction handling. Removed redundant FNSTSW AX check from FloatingPointHandler and added dedicated test for FnstswHandler. 2025-04-12 21:27:17 +03:00
bird_egop
fe0b04f5a1 Fixed TEST instruction handlers and tests. Updated TestImmWithRm8Handler and TestImmWithRm32Handler to properly check opcode in CanHandle and validate reg field in Decode. Improved test cases to use InstructionDecoder directly. 2025-04-12 21:21:03 +03:00
bird_egop
bf5fcdd2ff Fixed ConditionalJumpHandler to correctly implement x86 architecture specifications 2025-04-12 21:09:41 +03:00
bird_egop
bd251b6c06 Improved ConditionalJumpHandler with better documentation and clearer code 2025-04-12 21:02:52 +03:00
bird_egop
0925bb7fef Fixed ConditionalJumpHandler to correctly display jump offset and added X86DisassemblerTests project to solution 2025-04-12 21:00:32 +03:00
bird_egop
87e0c152e2 Fixed disassembler regression by adding handlers for TEST r/m8, r8 and TEST r/m8, imm8 instructions 2025-04-12 20:32:38 +03:00
bird_egop
dbc9b42007 Removed obsolete handler classes and restored InstructionHandlerFactory 2025-04-12 20:25:29 +03:00
bird_egop
1442fd7060 Removed obsolete Group1Handler and Group3Handler classes 2025-04-12 20:14:28 +03:00
bird_egop
e4b8645da0 Implemented individual handlers for Group1 and Group3 instructions 2025-04-12 20:13:01 +03:00
bird_egop
58a148ebd8 Refactor instruction handlers to use single instruction per handler pattern 2025-04-12 19:57:42 +03:00
bird_egop
82ffd51a3e Add support for RET instruction with immediate operand (0xC2) 2025-04-12 19:36:46 +03:00
bird_egop
0fb3fd7311 Add support for XOR instruction 2025-04-12 19:35:25 +03:00
bird_egop
f3aa862a57 Add support for two-byte conditional jumps, including JNZ (0F 85) 2025-04-12 19:30:13 +03:00
bird_egop
cedd7a931e Add support for TEST instruction 2025-04-12 19:26:00 +03:00
bird_egop
ae1c4730d0 Add support for FNSTSW instruction 2025-04-12 19:21:32 +03:00
bird_egop
dffc405c10 Refactored instruction decoder into smaller, more maintainable components using handler pattern 2025-04-12 19:18:52 +03:00
bird_egop
2e6e133159 Added support for 0x83 opcode (Group 1 operations with sign-extended immediate) 2025-04-12 19:04:43 +03:00
bird_egop
1a76bb4e77 Enhanced x86 instruction decoder to fully decode memory operands and match Ghidra output 2025-04-12 18:55:54 +03:00
bird_egop
3823121bea Added support for floating-point instructions including FISTP 2025-04-12 18:52:55 +03:00
bird_egop
60f63c2c06 clarify rva members 2025-04-12 18:49:23 +03:00
bird_egop
d73cccd3c5 Fixed DLL name display and console input handling in the disassembler 2025-04-12 18:44:51 +03:00
bird_egop
9b5ec7e0d6 Implemented enhanced x86 disassembler with improved instruction decoding and display 2025-04-12 18:41:40 +03:00
bird_egop
6a69b0b91b Update code style to follow project rules with one-liner namespace declarations 2025-04-12 18:23:18 +03:00
bird_egop
53de948376 Refactor: Move classes to separate files with one-liner namespace style 2025-04-12 18:11:07 +03:00
bird_egop
cf2d61915c Fix nullability warnings by initializing fields in constructors 2025-04-12 18:05:31 +03:00
bird_egop
79773b08aa Move Is64Bit method from OptionalHeaderParser to OptionalHeader class 2025-04-12 18:01:43 +03:00
bird_egop
49a0a9e3a3 Remove function list truncation to show all exported and imported functions 2025-04-12 17:22:20 +03:00
bird_egop
e4adb45ed2 Move section code checking logic from SectionHeaderParser to SectionHeader class 2025-04-12 17:20:51 +03:00
bird_egop
f1a2fca4f3 Refactor PEFormat into smaller classes following Single Responsibility Principle 2025-04-12 17:12:18 +03:00
bird_egop
61a86f6681 Separate construction from parsing in PEFormat class 2025-04-12 17:05:23 +03:00
bird_egop
666a592217 Reorganize PE format code into separate files in PE namespace 2025-04-12 17:03:04 +03:00
bird_egop
bc572f5d33 Fix DLL name parsing in export directory to properly separate DLL name from function names 2025-04-12 16:51:22 +03:00
bird_egop
8dfc0b1a7b Fix import directory parsing to properly resolve all DLL names and functions 2025-04-12 16:47:21 +03:00
610 changed files with 42550 additions and 9466 deletions

23
.windsurfrules Normal file
View File

@@ -0,0 +1,23 @@
when creating or edditing code, adjust namespace declaration style to oneliner, e.g. "namespace MyNamespace;".
always separate usings, namespaces, type declarations, methods and properties with empty line.
always add comments to the code, when the code is not trivial.
always put classes into separate files.
always try to build the project you've edited.
always summarize the changes you've made.
always add changes to git with descriptive comment, but be concise.
never use terminal commands to edit code. In case of a failure, write it to user and stop execution.
never address compiler warnings yourself. If you see a warning, suggest to address it.
when working with RVA variables, always add that to variable name, e.g. "nameRVA".
always build only affected project, not full solution.
never introduce special cases in general solutions.

View File

@@ -1,3 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
</Project>

View File

@@ -1,91 +0,0 @@
using System.Buffers.Binary;
using System.Text;
namespace Common;
public static class Extensions
{
public static int ReadInt32LittleEndian(this Stream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadInt32LittleEndian(buf);
}
public static uint ReadUInt32LittleEndian(this Stream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadUInt32LittleEndian(buf);
}
public static ushort ReadUInt16LittleEndian(this Stream fs)
{
Span<byte> buf = stackalloc byte[2];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadUInt16LittleEndian(buf);
}
public static float ReadFloatLittleEndian(this Stream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadSingleLittleEndian(buf);
}
public static string ReadNullTerminatedString(this Stream fs)
{
var sb = new StringBuilder();
while (true)
{
var b = fs.ReadByte();
if (b == 0)
{
break;
}
sb.Append((char)b);
}
return sb.ToString();
}
public static string ReadNullTerminated1251String(this Stream fs)
{
var sb = new StringBuilder();
while (true)
{
var b = (byte)fs.ReadByte();
if (b == 0)
{
break;
}
sb.Append(Encoding.GetEncoding("windows-1251").GetString([b]));
}
return sb.ToString();
}
public static string ReadLengthPrefixedString(this Stream fs)
{
var len = fs.ReadInt32LittleEndian();
if (len == 0)
{
return "";
}
var buffer = new byte[len];
fs.ReadExactly(buffer, 0, len);
return Encoding.ASCII.GetString(buffer, 0, len);
}
}

View File

@@ -1,3 +0,0 @@
namespace Common;
public record IndexedEdge(ushort Index1, ushort Index2);

View File

@@ -1,31 +0,0 @@
namespace CpDatLib;
public record CpDatEntry(
string ArchiveFile,
string ArchiveEntryName,
int Magic1,
int Magic2,
string Description,
DatEntryType Type,
int ChildCount, // игра не хранит это число в объекте, но оно есть в файле
List<CpDatEntry> Children
);
// Magic3 seems to be a type
// 0 - chassis
// 1 - turret (у зданий почему-то дефлектор тоже 1), может быть потому, что дефлектор вращается так же как башня у юнитов
// 2 - armour
// 3 - part
// 4 - cannon
// 5 - ammo
public enum DatEntryType
{
Unspecified = -1,
Chassis = 0,
Turret = 1,
Armour = 2,
Part = 3,
Cannon = 4,
Ammo = 5,
}

View File

@@ -1,5 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,3 +0,0 @@
namespace CpDatLib;
public record CpDatParseResult(CpDatScheme? Scheme, string? Error);

View File

@@ -1,74 +0,0 @@
using Common;
namespace CpDatLib;
public class CpDatParser
{
public static CpDatParseResult Parse(string filePath)
{
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return Parse(fs);
}
public static CpDatParseResult Parse(Stream fs)
{
Span<byte> f0f1 = stackalloc byte[4];
if (fs.Length < 8)
return new CpDatParseResult(null, "File too small to be a valid \"cp\" .dat file.");
fs.ReadExactly(f0f1);
if (f0f1[0] != 0xf1 || f0f1[1] != 0xf0)
{
return new CpDatParseResult(null, "File does not start with expected header bytes f1_f0");
}
var schemeType = (SchemeType)fs.ReadInt32LittleEndian();
var entryLength =
0x6c + 4; // нам нужно прочитать 0x6c (108) байт - это root, и ещё 4 байта - кол-во вложенных объектов
if ((fs.Length - 8) % entryLength != 0)
{
return new CpDatParseResult(null, "File size is not valid according to expected entry length.");
}
CpDatEntry root = ReadEntryRecursive(fs);
var scheme = new CpDatScheme(schemeType, root);
return new CpDatParseResult(scheme, null);
}
private static CpDatEntry ReadEntryRecursive(Stream fs)
{
var str1 = fs.ReadNullTerminatedString();
fs.Seek(32 - str1.Length - 1, SeekOrigin.Current); // -1 ignore null terminator
var str2 = fs.ReadNullTerminatedString();
fs.Seek(32 - str2.Length - 1, SeekOrigin.Current); // -1 ignore null terminator
var magic1 = fs.ReadInt32LittleEndian();
var magic2 = fs.ReadInt32LittleEndian();
var descriptionString = fs.ReadNullTerminated1251String();
fs.Seek(32 - descriptionString.Length - 1, SeekOrigin.Current); // -1 ignore null terminator
var type = (DatEntryType)fs.ReadInt32LittleEndian();
// игра не читает количество внутрь схемы, вместо этого она сразу рекурсией читает нужно количество вложенных объектов
var childCount = fs.ReadInt32LittleEndian();
List<CpDatEntry> children = new List<CpDatEntry>(childCount);
for (var i = 0; i < childCount; i++)
{
var child = ReadEntryRecursive(fs);
children.Add(child);
}
return new CpDatEntry(str1, str2, magic1, magic2, descriptionString, type, childCount, Children: children);
}
}

View File

@@ -1,3 +0,0 @@
namespace CpDatLib;
public record CpDatScheme(SchemeType Type, CpDatEntry Root);

View File

@@ -1,29 +0,0 @@
namespace CpDatLib;
public enum SchemeType : uint
{
ClassBuilding = 0x80000000,
ClassRobot = 0x01000000,
ClassAnimal = 0x20000000,
BunkerSmall = 0x80010000,
BunkerMedium = 0x80020000,
BunkerLarge = 0x80040000,
Generator = 0x80000002,
Mine = 0x80000004,
Storage = 0x80000008,
Plant = 0x80000010,
Hangar = 0x80000040,
TowerMedium = 0x80100000,
TowerLarge = 0x80200000,
MainTeleport = 0x80000200,
Institute = 0x80000400,
Bridge = 0x80001000,
Ruine = 0x80002000,
RobotTransport = 0x01002000,
RobotBuilder = 0x01004000,
RobotBattleunit = 0x01008000,
RobotHq = 0x01010000,
RobotHero = 0x01020000,
}

View File

@@ -1,19 +0,0 @@
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- Enable Central Package Management -->
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<!-- Enforce package version consistency -->
<EnablePackageVersionOverride>false</EnablePackageVersionOverride>
<!-- Suppress package version warnings -->
<NoWarn>$(NoWarn);NU1507;CS1591</NoWarn>
</PropertyGroup>
</Project>

View File

@@ -1,16 +0,0 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<!-- Package versions used across the solution -->
<ItemGroup>
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="NativeFileDialogSharp" Version="0.5.0" />
<PackageVersion Include="Silk.NET" Version="2.22.0" />
<PackageVersion Include="Silk.NET.OpenGL.Extensions.ImGui" Version="2.22.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
</ItemGroup>
</Project>

373
LICENSE
View File

@@ -1,373 +0,0 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\NResLib\NResLib.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,332 @@
using System.Buffers.Binary;
using NResLib;
using System.Numerics;
using System.Text;
namespace LandscapeExplorer;
public static class Program
{
private const string MapsDirectory = @"C:\Program Files (x86)\Nikita\Iron Strategy\DATA\MAPS\SC_3";
public static void Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
Console.WriteLine("Parkan 1 Landscape Explorer\n");
// Get all .map and .msh files in the directory
var mapFiles = Directory.GetFiles(MapsDirectory, "*.map");
var mshFiles = Directory.GetFiles(MapsDirectory, "*.msh");
Console.WriteLine($"Found {mapFiles.Length} .map files and {mshFiles.Length} .msh files in {MapsDirectory}\n");
// Process .map files
Console.WriteLine("=== MAP Files Analysis ===\n");
foreach (var mapFile in mapFiles)
{
AnalyzeNResFile(mapFile);
}
// Process .msh files
Console.WriteLine("\n=== MSH Files Analysis ===\n");
foreach (var mshFile in mshFiles)
{
AnalyzeNResFile(mshFile);
// Perform detailed landscape analysis on MSH files
AnalyzeLandscapeMeshFile(mshFile);
}
Console.WriteLine("\nAnalysis complete.");
}
/// <summary>
/// Analyzes an NRes file and displays its structure
/// </summary>
/// <param name="filePath">Path to the NRes file</param>
private static void AnalyzeNResFile(string filePath)
{
Console.WriteLine($"Analyzing file: {Path.GetFileName(filePath)}");
var parseResult = NResParser.ReadFile(filePath);
if (parseResult.Error != null)
{
Console.WriteLine($" Error: {parseResult.Error}");
return;
}
var archive = parseResult.Archive!;
Console.WriteLine($" Header: {archive.Header.NRes}, Version: {archive.Header.Version:X}, Files: {archive.Header.FileCount}, Size: {archive.Header.TotalFileLengthBytes} bytes");
// Group files by type for better analysis
var filesByType = archive.Files.GroupBy(f => f.FileType);
foreach (var group in filesByType)
{
Console.WriteLine($" File Type: {group.Key}, Count: {group.Count()}");
// Display details of the first file of each type as an example
var example = group.First();
Console.WriteLine($" Example: {example.FileName}");
Console.WriteLine($" Elements: {example.ElementCount}, Element Size: {example.ElementSize} bytes");
Console.WriteLine($" File Length: {example.FileLength} bytes, Offset: {example.OffsetInFile}");
// If this is a landscape-related file, provide more detailed analysis
if (IsLandscapeRelatedType(group.Key))
{
AnalyzeLandscapeData(example, filePath);
}
}
Console.WriteLine();
}
/// <summary>
/// Determines if a file type is related to landscape data
/// </summary>
private static bool IsLandscapeRelatedType(string fileType)
{
// Based on the Landscape constructor analysis, these types might be related to landscape
return fileType == "LAND" || fileType == "TERR" || fileType == "MSH0" ||
fileType == "MESH" || fileType == "MATR" || fileType == "TEXT";
}
/// <summary>
/// Analyzes landscape-specific data in a file
/// </summary>
private static void AnalyzeLandscapeData(ListMetadataItem item, string filePath)
{
Console.WriteLine($" [Landscape Data Analysis]:");
// Read the file data for this specific item
using var fs = new FileStream(filePath, FileMode.Open);
fs.Seek(item.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[Math.Min(item.FileLength, 256)]; // Read at most 256 bytes for analysis
fs.Read(buffer, 0, buffer.Length);
// Display some basic statistics based on the file type
if (item.FileType == "LAND" || item.FileType == "TERR")
{
Console.WriteLine($" Terrain data with {item.ElementCount} elements");
// If element size is known, we can calculate grid dimensions
if (item.ElementCount > 0 && item.ElementSize > 0)
{
// Assuming square terrain, which is common in games from this era
var gridSize = Math.Sqrt(item.ElementCount);
if (Math.Abs(gridSize - Math.Round(gridSize)) < 0.001) // If it's close to a whole number
{
Console.WriteLine($" Terrain grid size: {Math.Round(gridSize)} x {Math.Round(gridSize)}");
}
}
}
else if (item.FileType == "MSH0" || item.FileType == "MESH")
{
// For mesh data, try to estimate vertex/face counts
Console.WriteLine($" Mesh data, possibly with vertices and faces");
// Common sizes: vertices are often 12 bytes (3 floats), faces are often 12 bytes (3 indices)
if (item.ElementSize == 12)
{
Console.WriteLine($" Possibly {item.ElementCount} vertices or faces");
}
}
// Display first few bytes as hex for debugging
var hexPreview = BitConverter.ToString(
buffer.Take(32)
.ToArray()
)
.Replace("-", " ");
Console.WriteLine($" Data preview (hex): {hexPreview}...");
}
/// <summary>
/// Performs a detailed analysis of a landscape mesh file
/// </summary>
/// <param name="filePath">Path to the MSH file</param>
private static void AnalyzeLandscapeMeshFile(string filePath)
{
Console.WriteLine($"\nDetailed Landscape Analysis for: {Path.GetFileName(filePath)}\n");
var parseResult = NResParser.ReadFile(filePath);
if (parseResult.Error != null || parseResult.Archive == null)
{
Console.WriteLine($" Error analyzing file: {parseResult.Error}");
return;
}
var archive = parseResult.Archive;
// Based on the Landscape constructor and the file analysis, we can identify specific sections
// File types in MSH files appear to be numeric values (01, 02, 03, etc.)
// First, let's extract all the different data sections
var sections = new Dictionary<string, (ListMetadataItem Meta, byte[] Data)>();
foreach (var item in archive.Files)
{
using var fs = new FileStream(filePath, FileMode.Open);
fs.Seek(item.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[item.FileLength];
fs.Read(buffer, 0, buffer.Length);
sections[item.FileType] = (item, buffer);
}
// Now analyze each section based on what we know from the Landscape constructor
Console.WriteLine(" Landscape Structure Analysis:");
// Type 01 appears to be basic landscape information (possibly header/metadata)
if (sections.TryGetValue("01 00 00 00", out var section01))
{
Console.WriteLine($" Section 01: Basic Landscape Info");
Console.WriteLine($" Elements: {section01.Meta.ElementCount}, Element Size: {section01.Meta.ElementSize} bytes");
Console.WriteLine($" Total Size: {section01.Meta.FileLength} bytes");
// Try to extract some basic info if the format is as expected
if (section01.Meta.ElementSize == 38 && section01.Data.Length >= 38)
{
// This is speculative based on common terrain formats
var width = BitConverter.ToInt32(section01.Data, 0);
var height = BitConverter.ToInt32(section01.Data, 4);
Console.WriteLine($" Possible Dimensions: {width} x {height}");
}
}
// Type 03 appears to be vertex data (based on element size of 12 bytes which is typical for 3D vertices)
if (sections.TryGetValue("03 00 00 00", out var section03))
{
Console.WriteLine($"\n Section 03: Vertex Data");
Console.WriteLine($" Vertex Count: {section03.Meta.ElementCount}");
Console.WriteLine($" Vertex Size: {section03.Meta.ElementSize} bytes");
// If we have vertex data in expected format (3 floats per vertex)
if (section03.Meta.ElementSize == 12 && section03.Data.Length >= 36)
{
// Display first 3 vertices as example
Console.WriteLine(" Sample Vertices:");
for (int i = 0; i < Math.Min(3, section03.Meta.ElementCount); i++)
{
var offset = i * 12;
var x = BitConverter.ToSingle(section03.Data, offset);
var y = BitConverter.ToSingle(section03.Data, offset + 4);
var z = BitConverter.ToSingle(section03.Data, offset + 8);
Console.WriteLine($" Vertex {i}: ({x}, {y}, {z})");
}
// Calculate terrain bounds
var minX = float.MaxValue;
var minY = float.MaxValue;
var minZ = float.MaxValue;
var maxX = float.MinValue;
var maxY = float.MinValue;
var maxZ = float.MinValue;
for (int i = 0; i < section03.Meta.ElementCount; i++)
{
var offset = i * 12;
if (offset + 12 <= section03.Data.Length)
{
var x = BitConverter.ToSingle(section03.Data, offset);
var y = BitConverter.ToSingle(section03.Data, offset + 4);
var z = BitConverter.ToSingle(section03.Data, offset + 8);
minX = Math.Min(minX, x);
minY = Math.Min(minY, y);
minZ = Math.Min(minZ, z);
maxX = Math.Max(maxX, x);
maxY = Math.Max(maxY, y);
maxZ = Math.Max(maxZ, z);
}
}
Console.WriteLine(" Terrain Bounds:");
Console.WriteLine($" Min: ({minX}, {minY}, {minZ})");
Console.WriteLine($" Max: ({maxX}, {maxY}, {maxZ})");
Console.WriteLine($" Dimensions: {maxX - minX} x {maxY - minY} x {maxZ - minZ}");
}
}
// Type 02 might be face/index data for the mesh
if (sections.TryGetValue("02 00 00 00", out var section02))
{
Console.WriteLine($"\n Section 02: Possible Face/Index Data");
Console.WriteLine($" Elements: {section02.Meta.ElementCount}");
Console.WriteLine($" Element Size: {section02.Meta.ElementSize} bytes");
// If element size is divisible by 4 (common for index data)
if (section02.Meta.ElementSize % 4 == 0 && section02.Data.Length >= 12)
{
// Display first triangle as example (assuming 3 indices per triangle)
Console.WriteLine(" Sample Indices (if this is index data):");
var indicesPerElement = section02.Meta.ElementSize / 4;
for (int i = 0; i < Math.Min(1, section02.Meta.ElementCount); i++)
{
Console.Write($" Element {i}: ");
for (int j = 0; j < indicesPerElement; j++)
{
var offset = i * section02.Meta.ElementSize + j * 4;
if (offset + 4 <= section02.Data.Length)
{
var index = BitConverter.ToInt32(section02.Data, offset);
Console.Write($"{index} ");
}
}
Console.WriteLine();
}
}
}
// Types 04, 05, 12, 0E, 0B might be texture coordinates, normals, colors, etc.
var otherSections = new[] {"04 00 00 00", "05 00 00 00", "12 00 00 00", "0E 00 00 00", "0B 00 00 00"};
foreach (var sectionType in otherSections)
{
if (sections.TryGetValue(sectionType, out var section))
{
Console.WriteLine($"\n Section {sectionType.Substring(0, 2)}: Additional Mesh Data");
Console.WriteLine($" Elements: {section.Meta.ElementCount}");
Console.WriteLine($" Element Size: {section.Meta.ElementSize} bytes");
// If element size is 4 bytes, it could be color data, texture indices, etc.
if (section.Meta.ElementSize == 4 && section.Data.Length >= 12)
{
Console.WriteLine(" Sample Data (as integers):");
for (int i = 0; i < Math.Min(3, section.Meta.ElementCount); i++)
{
var offset = i * 4;
var value = BitConverter.ToInt32(section.Data, offset);
Console.WriteLine($" Element {i}: {value}");
}
}
}
}
// Type 15 might be material or special data (Msh_15 in the decompiled code)
if (sections.TryGetValue("15 00 00 00", out var section15) && sections.TryGetValue("03 00 00 00", out var vertexSection))
{
Console.WriteLine($"\n Section 15: Special Data (Msh_15 type in decompiled code)");
Console.WriteLine($" Elements: {section15.Meta.ElementCount}");
Console.WriteLine($" Element Size: {section15.Meta.ElementSize} bytes");
int count = 0;
for (var i = 0; i < section15.Data.Length; i += 28)
{
var first = BinaryPrimitives.ReadUInt32LittleEndian(section15.Data.AsSpan(i));
if ((first & 0x20000) != 0)
{
Console.WriteLine($"Found {first}/0x{first:X8} 0x20000 at index {i / 28}. &0x20000={first&0x20000}/0x{first&0x20000:X8} offset: {i:X8}");
count++;
}
}
Console.WriteLine($"Total found: {count}");
}
}
}

View File

@@ -1,82 +0,0 @@

void __cdecl
Interpolate(MaterialExternal *src1_ptr,MaterialExternal *src2_ptr,float progress,
MaterialExternal *dst_ptr,uint targetFlags)
{
if ((targetFlags & 2) == 0) {
(dst_ptr->stage).diffuse.R = (src1_ptr->stage).diffuse.R;
(dst_ptr->stage).diffuse.G = (src1_ptr->stage).diffuse.G;
(dst_ptr->stage).diffuse.B = (src1_ptr->stage).diffuse.B;
}
else {
(dst_ptr->stage).diffuse.R =
((src2_ptr->stage).diffuse.R - (src1_ptr->stage).diffuse.R) * progress +
(src1_ptr->stage).diffuse.R;
(dst_ptr->stage).diffuse.G =
((src2_ptr->stage).diffuse.G - (src1_ptr->stage).diffuse.G) * progress +
(src1_ptr->stage).diffuse.G;
(dst_ptr->stage).diffuse.B =
((src2_ptr->stage).diffuse.B - (src1_ptr->stage).diffuse.B) * progress +
(src1_ptr->stage).diffuse.B;
}
if ((targetFlags & 1) == 0) {
(dst_ptr->stage).ambient.R = (src1_ptr->stage).ambient.R;
(dst_ptr->stage).ambient.G = (src1_ptr->stage).ambient.G;
(dst_ptr->stage).ambient.B = (src1_ptr->stage).ambient.B;
}
else {
(dst_ptr->stage).ambient.R =
((src2_ptr->stage).ambient.R - (src1_ptr->stage).ambient.R) * progress +
(src1_ptr->stage).ambient.R;
(dst_ptr->stage).ambient.G =
((src2_ptr->stage).ambient.G - (src1_ptr->stage).ambient.G) * progress +
(src1_ptr->stage).ambient.G;
(dst_ptr->stage).ambient.B =
((src2_ptr->stage).ambient.B - (src1_ptr->stage).ambient.B) * progress +
(src1_ptr->stage).ambient.B;
}
if ((targetFlags & 4) == 0) {
(dst_ptr->stage).specular.R = (src1_ptr->stage).specular.R;
(dst_ptr->stage).specular.G = (src1_ptr->stage).specular.G;
(dst_ptr->stage).specular.B = (src1_ptr->stage).specular.B;
}
else {
(dst_ptr->stage).specular.R =
((src2_ptr->stage).specular.R - (src1_ptr->stage).specular.R) * progress +
(src1_ptr->stage).specular.R;
(dst_ptr->stage).specular.G =
((src2_ptr->stage).specular.G - (src1_ptr->stage).specular.G) * progress +
(src1_ptr->stage).specular.G;
(dst_ptr->stage).specular.B =
((src2_ptr->stage).specular.B - (src1_ptr->stage).specular.B) * progress +
(src1_ptr->stage).specular.B;
}
if ((targetFlags & 8) == 0) {
(dst_ptr->stage).emissive.R = (src1_ptr->stage).emissive.R;
(dst_ptr->stage).emissive.G = (src1_ptr->stage).emissive.G;
(dst_ptr->stage).emissive.B = (src1_ptr->stage).emissive.B;
}
else {
(dst_ptr->stage).emissive.R =
((src2_ptr->stage).emissive.R - (src1_ptr->stage).emissive.R) * progress +
(src1_ptr->stage).emissive.R;
(dst_ptr->stage).emissive.G =
((src2_ptr->stage).emissive.G - (src1_ptr->stage).emissive.G) * progress +
(src1_ptr->stage).emissive.G;
(dst_ptr->stage).emissive.B =
((src2_ptr->stage).emissive.B - (src1_ptr->stage).emissive.B) * progress +
(src1_ptr->stage).emissive.B;
}
if ((targetFlags & 0x10) != 0) {
(dst_ptr->stage).ambient.A =
((src2_ptr->stage).ambient.A - (src1_ptr->stage).ambient.A) * progress +
(src1_ptr->stage).ambient.A;
(dst_ptr->stage).Power = (src1_ptr->stage).Power;
return;
}
(dst_ptr->stage).ambient.A = (src1_ptr->stage).ambient.A;
(dst_ptr->stage).Power = (src1_ptr->stage).Power;
return;
}

View File

@@ -1,167 +0,0 @@
namespace MaterialLib;
public class MaterialFile
{
// === Metadata (not from file content) ===
public string FileName { get; set; }
public int Version { get; set; } // From NRes ElementCount
public int Magic1 { get; set; } // From NRes Magic1
// === Derived from Version/ElementCount ===
public int MaterialRenderingType { get; set; } // (Version >> 2) & 0xF - 0=Standard, 1=Special, 2=Particle
public bool SupportsBumpMapping { get; set; } // (Version & 2) != 0
public int IsParticleEffect { get; set; } // Version & 40 - 0=Normal, 8=Particle/Effect
// === File Content (in read order) ===
// Read order: StageCount (ushort), AnimCount (ushort), then conditionally blend modes and params
// Global Blend Modes (read if Magic1 >= 2)
public BlendMode SourceBlendMode { get; set; } // Default: Unknown (0xFF)
public BlendMode DestBlendMode { get; set; } // Default: Unknown (0xFF)
// Global Parameters (read if Magic1 > 2 and > 3 respectively)
public float GlobalAlphaMultiplier { get; set; } // Default: 1.0 (always 1.0 in all 628 materials)
public float GlobalEmissiveIntensity { get; set; } // Default: 0.0 (0=no glow, rare values: 1000, 10000)
public List<MaterialStage> Stages { get; set; } = new();
public List<MaterialAnimation> Animations { get; set; } = new();
}
/// <summary>
/// Blend modes for material rendering. These control how source and destination colors are combined.
/// Formula: FinalColor = (SourceColor * SourceBlend) [operation] (DestColor * DestBlend)
/// Maps to Direct3D D3DBLEND values.
/// </summary>
public enum BlendMode : byte
{
/// <summary>Blend factor is (0, 0, 0, 0) - results in black/transparent</summary>
Zero = 1,
/// <summary>Blend factor is (1, 1, 1, 1) - uses full color value</summary>
One = 2,
/// <summary>Blend factor is (Rs, Gs, Bs, As) - uses source color</summary>
SrcColor = 3,
/// <summary>Blend factor is (1-Rs, 1-Gs, 1-Bs, 1-As) - uses inverted source color</summary>
InvSrcColor = 4,
/// <summary>Blend factor is (As, As, As, As) - uses source alpha for all channels (standard transparency)</summary>
SrcAlpha = 5,
/// <summary>Blend factor is (1-As, 1-As, 1-As, 1-As) - uses inverted source alpha</summary>
InvSrcAlpha = 6,
/// <summary>Blend factor is (Ad, Ad, Ad, Ad) - uses destination alpha</summary>
DestAlpha = 7,
/// <summary>Blend factor is (1-Ad, 1-Ad, 1-Ad, 1-Ad) - uses inverted destination alpha</summary>
InvDestAlpha = 8,
/// <summary>Blend factor is (Rd, Gd, Bd, Ad) - uses destination color</summary>
DestColor = 9,
/// <summary>Blend factor is (1-Rd, 1-Gd, 1-Bd, 1-Ad) - uses inverted destination color</summary>
InvDestColor = 10,
/// <summary>Blend factor is (f, f, f, 1) where f = min(As, 1-Ad) - saturates source alpha</summary>
SrcAlphaSat = 11,
/// <summary>Blend factor is (As, As, As, As) for both source and dest - obsolete in D3D9+</summary>
BothSrcAlpha = 12,
/// <summary>Blend factor is (1-As, 1-As, 1-As, 1-As) for both source and dest - obsolete in D3D9+</summary>
BothInvSrcAlpha = 13,
/// <summary>Unknown or uninitialized blend mode (0xFF default value)</summary>
Unknown = 0xFF
}
public class MaterialStage
{
// === FILE READ ORDER (34 bytes per stage) ===
// This matches the order bytes are read from the file (decompiled.c lines 159-217)
// NOT the C struct memory layout (which is Diffuse, Ambient, Specular, Emissive)
// 1. Ambient Color (4 bytes, read first from file)
public float AmbientR;
public float AmbientG;
public float AmbientB;
public float AmbientA; // Scaled by 0.01 when read from file
// 2. Diffuse Color (4 bytes, read second from file)
public float DiffuseR;
public float DiffuseG;
public float DiffuseB;
public float DiffuseA;
// 3. Specular Color (4 bytes, read third from file)
public float SpecularR;
public float SpecularG;
public float SpecularB;
public float SpecularA;
// 4. Emissive Color (4 bytes, read fourth from file)
public float EmissiveR;
public float EmissiveG;
public float EmissiveB;
public float EmissiveA;
// 5. Power (1 byte → float, read fifth from file)
public float Power;
// 6. Texture Stage Index (1 byte, read sixth from file)
// 255 = not set/default, 0-47 = reference to specific texture stage
public int TextureStageIndex;
// 7. Texture Name (16 bytes, read seventh from file)
public string TextureName { get; set; }
}
public class MaterialAnimation
{
// === File Read Order ===
// Combined field (4 bytes): bits 3-31 = Target, bits 0-2 = LoopMode
public AnimationTarget Target;
public AnimationLoopMode LoopMode;
// Key count (2 bytes), then keys
public List<AnimKey> Keys { get; set; } = new();
// Cached description for UI (computed once during parsing)
public string TargetDescription { get; set; } = string.Empty;
}
[Flags]
public enum AnimationTarget : int
{
// NOTE: This is a BITSET (flags enum). Multiple flags can be combined.
// When a flag is SET, that component is INTERPOLATED between stages.
// When a flag is NOT SET, that component is COPIED from the source stage (no interpolation).
// If ALL flags are 0, the ENTIRE stage is copied without any interpolation.
Ambient = 1, // 0x01 - Interpolates Ambient RGB (Interpolate.c lines 23-37)
Diffuse = 2, // 0x02 - Interpolates Diffuse RGB (Interpolate.c lines 7-21)
Specular = 4, // 0x04 - Interpolates Specular RGB (Interpolate.c lines 39-53)
Emissive = 8, // 0x08 - Interpolates Emissive RGB (Interpolate.c lines 55-69)
Power = 16 // 0x10 - Interpolates Ambient.A and sets Power (Interpolate.c lines 71-76)
}
public enum AnimationLoopMode : int
{
Loop = 0,
PingPong = 1,
Clamp = 2,
Random = 3
}
public struct AnimKey
{
// === File Read Order (6 bytes per key) ===
public ushort StageIndex; // Read first
public ushort DurationMs; // Read second
public ushort InterpolationCurve; // Read third - Always 0 (linear interpolation) in all 1848 keys
}

View File

@@ -1,5 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,176 +0,0 @@
using System.Buffers.Binary;
using System.Text;
namespace MaterialLib;
public static class MaterialParser
{
public static MaterialFile ReadFromStream(Stream fs, string fileName, int elementCount, int magic1)
{
var file = new MaterialFile
{
FileName = fileName,
Version = elementCount,
Magic1 = magic1
};
// Derived fields
file.MaterialRenderingType = elementCount >> 2 & 0xf;
file.SupportsBumpMapping = (elementCount & 2) != 0;
file.IsParticleEffect = elementCount & 40;
// Reading content
var stageCount = fs.ReadUInt16LittleEndian();
var animCount = fs.ReadUInt16LittleEndian();
uint magic = (uint)magic1;
// Defaults found in C code
file.GlobalAlphaMultiplier = 1.0f; // field8_0x15c
file.GlobalEmissiveIntensity = 0.0f; // field9_0x160
file.SourceBlendMode = BlendMode.Unknown; // field6_0x154
file.DestBlendMode = BlendMode.Unknown; // field7_0x158
if (magic >= 2)
{
file.SourceBlendMode = (BlendMode)fs.ReadByte();
file.DestBlendMode = (BlendMode)fs.ReadByte();
}
if (magic > 2)
{
file.GlobalAlphaMultiplier = fs.ReadFloatLittleEndian();
}
if (magic > 3)
{
file.GlobalEmissiveIntensity = fs.ReadFloatLittleEndian();
}
// --- 2. Material Stages ---
const float Inv255 = 1.0f / 255.0f;
const float Field7Mult = 0.01f;
Span<byte> textureNameBuffer = stackalloc byte[16];
for (int i = 0; i < stageCount; i++)
{
var stage = new MaterialStage();
// === FILE READ ORDER (matches decompiled.c lines 159-217) ===
// 1. Ambient (4 bytes, A scaled by 0.01) - Lines 159-168
stage.AmbientR = fs.ReadByte() * Inv255;
stage.AmbientG = fs.ReadByte() * Inv255;
stage.AmbientB = fs.ReadByte() * Inv255;
stage.AmbientA = fs.ReadByte() * Field7Mult; // 0.01 scaling
// 2. Diffuse (4 bytes) - Lines 171-180
stage.DiffuseR = fs.ReadByte() * Inv255;
stage.DiffuseG = fs.ReadByte() * Inv255;
stage.DiffuseB = fs.ReadByte() * Inv255;
stage.DiffuseA = fs.ReadByte() * Inv255;
// 3. Specular (4 bytes) - Lines 183-192
stage.SpecularR = fs.ReadByte() * Inv255;
stage.SpecularG = fs.ReadByte() * Inv255;
stage.SpecularB = fs.ReadByte() * Inv255;
stage.SpecularA = fs.ReadByte() * Inv255;
// 4. Emissive (4 bytes) - Lines 195-204
stage.EmissiveR = fs.ReadByte() * Inv255;
stage.EmissiveG = fs.ReadByte() * Inv255;
stage.EmissiveB = fs.ReadByte() * Inv255;
stage.EmissiveA = fs.ReadByte() * Inv255;
// 5. Power (1 byte → float) - Line 207
stage.Power = (float)fs.ReadByte();
// 6. Texture Stage Index (1 byte) - Line 210
stage.TextureStageIndex = fs.ReadByte();
// 7. Texture Name (16 bytes) - Lines 212-217
textureNameBuffer.Clear();
fs.ReadExactly(textureNameBuffer);
stage.TextureName = Encoding.ASCII.GetString(textureNameBuffer).TrimEnd('\0');
file.Stages.Add(stage);
}
// --- 3. Animations ---
for (int i = 0; i < animCount; i++)
{
var anim = new MaterialAnimation();
uint typeAndParams = fs.ReadUInt32LittleEndian();
anim.Target = (AnimationTarget)(typeAndParams >> 3);
anim.LoopMode = (AnimationLoopMode)(typeAndParams & 7);
ushort keyCount = fs.ReadUInt16LittleEndian();
for (int k = 0; k < keyCount; k++)
{
var key = new AnimKey
{
StageIndex = fs.ReadUInt16LittleEndian(),
DurationMs = fs.ReadUInt16LittleEndian(),
InterpolationCurve = fs.ReadUInt16LittleEndian()
};
anim.Keys.Add(key);
}
// Precompute description for UI to avoid per-frame allocations
anim.TargetDescription = ComputeTargetDescription(anim.Target);
file.Animations.Add(anim);
}
return file;
}
private static string ComputeTargetDescription(AnimationTarget target)
{
// Precompute the description once during parsing
if ((int)target == 0)
return "No interpolation - entire stage is copied as-is (Flags: 0x0)";
var parts = new List<string>();
if ((target & AnimationTarget.Ambient) != 0)
parts.Add("Ambient RGB");
if ((target & AnimationTarget.Diffuse) != 0)
parts.Add("Diffuse RGB");
if ((target & AnimationTarget.Specular) != 0)
parts.Add("Specular RGB");
if ((target & AnimationTarget.Emissive) != 0)
parts.Add("Emissive RGB");
if ((target & AnimationTarget.Power) != 0)
parts.Add("Ambient.A + Power");
return $"Interpolates: {string.Join(", ", parts)} | Other components copied (Flags: 0x{(int)target:X})";
}
}
// Helper extensions
internal static class StreamExtensions
{
public static float ReadFloatLittleEndian(this Stream stream)
{
Span<byte> buffer = stackalloc byte[4];
stream.ReadExactly(buffer);
return BinaryPrimitives.ReadSingleLittleEndian(buffer);
}
public static ushort ReadUInt16LittleEndian(this Stream stream)
{
Span<byte> buffer = stackalloc byte[2];
stream.ReadExactly(buffer);
return BinaryPrimitives.ReadUInt16LittleEndian(buffer);
}
public static uint ReadUInt32LittleEndian(this Stream stream)
{
Span<byte> buffer = stackalloc byte[4];
stream.ReadExactly(buffer);
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
}
}

View File

@@ -1,290 +0,0 @@
/* returns index into g_MaterialGlobalDescriptors */
int __fastcall LoadMaterialByName(char *itemname)
{
char *pcVar1;
undefined1 *puVar2;
byte *pbVar3;
uint uVar4;
undefined4 *puVar5;
int iVar6;
int iVar7;
MaterialGlobalDescriptor_ptr_4_undefined4 piVar8;
nres_metadata_item *pnVar8;
int iVar9;
MaterialStageWorldDllInternal *pMVar10;
int iVar11;
ITexture **ppIVar12;
MaterialAnimationKey *pMVar13;
MaterialGlobalDescriptor *pMVar14;
float unaff_EBX;
MaterialStageExternal *pfVar16;
ushort unaff_DI;
MaterialAnimation *pMVar15;
uint uVar16;
int iStack_90;
uint uStack_8c;
char acStack_88 [8];
int iStack_80;
char acStack_58 [88];
iVar6 = (*(*g_material_resfile)->get_index_in_file_by_itemname)(g_material_resfile,itemname);
if (iVar6 < 0) {
sprintf(acStack_58,s_Material_%s_not_found._100246f8,itemname);
/* WARNING: Subroutine does not return */
write_error_to_file_and_msgbox(acStack_58,(void *)0x0);
}
iVar7 = 0;
if (0 < g_MaterialGlobalDescriptor_count) {
pMVar14 = g_MaterialGlobalDescriptors;
do {
if (pMVar14->index_in_file == iVar6) {
if (-1 < iVar7) {
g_MaterialGlobalDescriptors[iVar7].RefCount =
g_MaterialGlobalDescriptors[iVar7].RefCount + 1;
return iVar7;
}
break;
}
iVar7 += 1;
pMVar14 = pMVar14 + 1;
} while (iVar7 < g_MaterialGlobalDescriptor_count);
}
iVar7 = 0;
if (0 < g_MaterialGlobalDescriptor_count) {
piVar8 = &g_MaterialGlobalDescriptors[0].RefCount;
do {
if (ADJ(piVar8)->RefCount == 0) break;
iVar7 += 1;
piVar8 = piVar8 + 0x5c;
} while (iVar7 < g_MaterialGlobalDescriptor_count);
}
if (iVar7 == g_MaterialGlobalDescriptor_count) {
g_MaterialGlobalDescriptor_count += 1;
}
iStack_80 = iVar7;
g_currentMaterialFileData_ptr =
(*(*g_material_resfile)->get_item_data_ptr_by_index)(g_material_resfile,iVar6,1);
pnVar8 = (*(*g_material_resfile)->get_metadata_ptr)(g_material_resfile);
uVar16 = 0;
uVar4 = pnVar8[iVar6].magic1;
if ((pnVar8[iVar6].element_count_or_version & 1) != 0) {
uVar16 = 0x200000;
}
g_MaterialGlobalDescriptors[iVar7].extra_meta.field0_0x0 = 0;
g_MaterialGlobalDescriptors[iVar7].extra_meta.field2_0x8 = 0;
g_MaterialGlobalDescriptors[iVar7].extra_meta.field1_0x4 =
pnVar8[iVar6].element_count_or_version >> 2 & 0xf;
iVar9 = g_bumpmapping_enabled_value;
if (((pnVar8[iVar6].element_count_or_version & 2) != 0) &&
((g_MaterialGlobalDescriptors[iVar7].extra_meta.field0_0x0 = 1, iVar9 == 0 ||
(g_supports_texture_mode_6 == 0)))) {
uVar16 |= 0x80000;
}
if ((pnVar8[iVar6].element_count_or_version & 0x40) != 0) {
g_MaterialGlobalDescriptors[iVar7].extra_meta.field2_0x8 = 1;
}
iVar9 = 0;
g_MaterialGlobalDescriptors[iVar7].RefCount = g_MaterialGlobalDescriptors[iVar7].RefCount + 1;
puVar5 = g_currentMaterialFileData_ptr;
g_MaterialGlobalDescriptors[iVar7].index_in_file = iVar6;
iVar6 = 0;
do {
pcVar1 = (char *)(iVar9 + (int)puVar5);
iVar9 += 1;
acStack_88[iVar6 + -0x1c] = *pcVar1;
iVar6 += 1;
} while (iVar6 < 2);
g_MaterialGlobalDescriptors[iVar7].stageCount = (uint)unaff_DI;
iVar6 = 0;
do {
iVar11 = iVar9;
iVar9 = iVar11 + 1;
acStack_88[iVar6 + -0x1c] = *(char *)(iVar11 + (int)puVar5);
iVar6 += 1;
} while (iVar6 < 2);
DAT_10128674 = iVar9;
g_MaterialGlobalDescriptors[iVar7].animCount = (uint)unaff_DI;
if (0x13 < unaff_DI) {
/* WARNING: Subroutine does not return */
write_error_to_file_and_msgbox(s_Too_many_animations_for_material_100246cc,(void *)0x0);
}
g_MaterialGlobalDescriptors[iVar7].field8_0x15c = 1.0;
g_MaterialGlobalDescriptors[iVar7].field9_0x160 = 0;
if (uVar4 < 2) {
g_MaterialGlobalDescriptors[iVar7].field6_0x154 = 0xff;
g_MaterialGlobalDescriptors[iVar7].field7_0x158 = 0xff;
}
else {
g_MaterialGlobalDescriptors[iVar7].field6_0x154 = (uint)*(byte *)(iVar9 + (int)puVar5);
iVar6 = iVar11 + 3;
DAT_10128674 = iVar6;
g_MaterialGlobalDescriptors[iVar7].field7_0x158 = (uint)*(byte *)(iVar11 + 2 + (int)puVar5);
if (2 < uVar4) {
iVar9 = 0;
do {
puVar2 = (undefined1 *)(iVar6 + (int)puVar5);
iVar6 += 1;
(&stack0xffffff68)[iVar9] = *puVar2;
iVar9 += 1;
} while (iVar9 < 4);
DAT_10128674 = iVar6;
g_MaterialGlobalDescriptors[iVar7].field8_0x15c = unaff_EBX;
if (3 < uVar4) {
iVar9 = 0;
do {
puVar2 = (undefined1 *)(iVar6 + (int)puVar5);
iVar6 += 1;
(&stack0xffffff68)[iVar9] = *puVar2;
iVar9 += 1;
} while (iVar9 < 4);
DAT_10128674 = iVar6;
g_MaterialGlobalDescriptors[iVar7].field9_0x160 = unaff_EBX;
}
}
}
pMVar10 = (MaterialStageWorldDllInternal *)
_malloc(g_MaterialGlobalDescriptors[iVar7].stageCount * 0x4c);
g_MaterialGlobalDescriptors[iVar7].stages = pMVar10;
iVar6 = 0;
if (0 < g_MaterialGlobalDescriptors[iVar7].stageCount) {
iVar9 = 0;
do {
pfVar16 = (MaterialStageExternal *)
((int)&((g_MaterialGlobalDescriptors[iVar7].stages)->diffuse).R + iVar9);
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->ambient).R = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->ambient).G = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->ambient).B = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->ambient).A = (float)*pbVar3 * 0.01;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->diffuse).R = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->diffuse).G = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->diffuse).B = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->diffuse).A = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->specular).R = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->specular).G = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->specular).B = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->specular).A = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->emissive).R = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->emissive).G = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->emissive).B = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->emissive).A = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
pfVar16->Power = (float)(uint)*pbVar3;
pcVar1 = (char *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 = DAT_10128674 + 1;
pfVar16->current_m_LL_ITexture = (ITexture **)(int)*pcVar1;
iVar11 = 0;
do {
pcVar1 = (char *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
acStack_88[iVar11] = *pcVar1;
iVar11 += 1;
} while (iVar11 < 0x10);
if (acStack_88[0] == '\0') {
pfVar16->m_LL_ITexture = (ITexture **)0xffffffff;
pfVar16->current_m_LL_ITexture = (ITexture **)0xffffffff;
}
else {
ppIVar12 = (ITexture **)LoadAndCacheTexture(acStack_88,uVar16);
pfVar16->m_LL_ITexture = ppIVar12;
}
iVar6 += 1;
iVar9 += 0x4c;
} while (iVar6 < g_MaterialGlobalDescriptors[iVar7].stageCount);
}
iVar6 = 0;
if (0 < g_MaterialGlobalDescriptors[iVar7].animCount) {
pMVar15 = g_MaterialGlobalDescriptors[iVar7].animations;
do {
puVar5 = g_currentMaterialFileData_ptr;
iVar9 = 0;
do {
pcVar1 = (char *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
acStack_88[iVar9 + -4] = *pcVar1;
iVar9 += 1;
} while (iVar9 < 4);
pMVar15->FieldSelector = (int)uStack_8c >> 3;
pMVar15->loop_mode = uStack_8c & 7;
iVar9 = 0;
do {
pcVar1 = (char *)(DAT_10128674 + (int)puVar5);
DAT_10128674 += 1;
acStack_88[iVar9 + -0x1c] = *pcVar1;
iVar9 += 1;
} while (iVar9 < 2);
pMVar15->keyCount = (uint)unaff_DI;
pMVar13 = (MaterialAnimationKey *)_malloc((uint)unaff_DI << 3);
pMVar15->keys = pMVar13;
iVar9 = 0;
if (0 < (int)pMVar15->keyCount) {
do {
iVar11 = 0;
do {
pcVar1 = (char *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
acStack_88[iVar11 + -0x1c] = *pcVar1;
iVar11 += 1;
} while (iVar11 < 2);
pMVar15->keys[iVar9].stage_index = (uint)unaff_DI;
iVar11 = 0;
do {
pcVar1 = (char *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
acStack_88[iVar11 + -0x1c] = *pcVar1;
iVar11 += 1;
} while (iVar11 < 2);
pMVar15->keys[iVar9].duration_ms = unaff_DI;
iVar11 = 0;
do {
pcVar1 = (char *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
acStack_88[iVar11 + -0x1c] = *pcVar1;
iVar11 += 1;
} while (iVar11 < 2);
pMVar15->keys[iVar9].field2_0x6 = unaff_DI;
iVar9 += 1;
} while (iVar9 < (int)pMVar15->keyCount);
}
iVar6 += 1;
pMVar15 = pMVar15 + 1;
} while (iVar6 < g_MaterialGlobalDescriptors[iVar7].animCount);
}
return iStack_90;
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

14
MeshUnpacker/Program.cs Normal file
View File

@@ -0,0 +1,14 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
using var fs = new FileStream("C:\\ParkanUnpacked\\11_fr_e_brige.msh\\0_fr_e_brige.bin", FileMode.Open);
byte[] buffer = new byte[38];
for (int i = 0; i < 6; i++)
{
fs.ReadExactly(buffer);
Console.WriteLine(string.Join(" ", buffer.Select(x => x.ToString("X2"))));
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,9 @@
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
// var missionFilePath = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\Autodemo.00\\data.tma";
// var missionFilePath = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\Tutorial.01\\data.tma";
var missionFilePath = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\CAMPAIGN\\CAMPAIGN.01\\Mission.02\\data.tma";
// var missionFilePath = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\Single.01\\data.tma";

View File

@@ -1,5 +1,3 @@
using Common; namespace MissionTmaLib;
namespace MissionTmaLib;
public record ArealInfo(int Index, int CoordsCount, List<Vector3> Coords); public record ArealInfo(int Index, int CoordsCount, List<Vector3> Coords);

View File

@@ -1,6 +1,4 @@
using Common; namespace MissionTmaLib;
namespace MissionTmaLib;
public class GameObjectInfo public class GameObjectInfo
{ {

View File

@@ -1,5 +1,3 @@
using Common; namespace MissionTmaLib;
namespace MissionTmaLib;
public record GameObjectSetting(int SettingType, IntFloatValue Unk1, IntFloatValue Unk2, IntFloatValue Unk3, string Name); public record GameObjectSetting(int SettingType, IntFloatValue Unk1, IntFloatValue Unk2, IntFloatValue Unk3, string Name);

View File

@@ -1,7 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Common; namespace MissionTmaLib;
[DebuggerDisplay("AsInt = {AsInt}, AsFloat = {AsFloat}")] [DebuggerDisplay("AsInt = {AsInt}, AsFloat = {AsFloat}")]
public class IntFloatValue(Span<byte> span) public class IntFloatValue(Span<byte> span)

View File

@@ -1,5 +1,3 @@
using Common; namespace MissionTmaLib;
namespace MissionTmaLib;
public record LodeInfo(Vector3 UnknownVector, int UnknownInt1, int UnknownFlags2, float UnknownFloat, int UnknownInt3); public record LodeInfo(Vector3 UnknownVector, int UnknownInt1, int UnknownFlags2, float UnknownFloat, int UnknownInt3);

View File

@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" /> <PropertyGroup>
</ItemGroup> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project> </Project>

View File

@@ -0,0 +1,39 @@
using System.Buffers.Binary;
using System.Text;
namespace MissionTmaLib.Parsing;
public static class Extensions
{
public static int ReadInt32LittleEndian(this FileStream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadInt32LittleEndian(buf);
}
public static float ReadFloatLittleEndian(this FileStream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadSingleLittleEndian(buf);
}
public static string ReadLengthPrefixedString(this FileStream fs)
{
var len = fs.ReadInt32LittleEndian();
if (len == 0)
{
return "";
}
var buffer = new byte[len];
fs.ReadExactly(buffer, 0, len);
return Encoding.ASCII.GetString(buffer, 0, len);
}
}

View File

@@ -1,18 +1,11 @@
using Common; namespace MissionTmaLib.Parsing;
namespace MissionTmaLib.Parsing;
public class MissionTmaParser public class MissionTmaParser
{ {
public static MissionTmaParseResult ReadFile(string filePath) public static MissionTmaParseResult ReadFile(string filePath)
{ {
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); var fs = new FileStream(filePath, FileMode.Open);
return ReadFile(fs);
}
public static MissionTmaParseResult ReadFile(Stream fs)
{
var arealData = LoadAreals(fs); var arealData = LoadAreals(fs);
var clansData = LoadClans(fs); var clansData = LoadClans(fs);
@@ -25,7 +18,7 @@ public class MissionTmaParser
return new MissionTmaParseResult(missionDat, null); return new MissionTmaParseResult(missionDat, null);
} }
private static ArealsFileData LoadAreals(Stream fileStream) private static ArealsFileData LoadAreals(FileStream fileStream)
{ {
var unusedHeader = fileStream.ReadInt32LittleEndian(); var unusedHeader = fileStream.ReadInt32LittleEndian();
var arealCount = fileStream.ReadInt32LittleEndian(); var arealCount = fileStream.ReadInt32LittleEndian();
@@ -61,7 +54,7 @@ public class MissionTmaParser
return new ArealsFileData(unusedHeader, arealCount, infos); return new ArealsFileData(unusedHeader, arealCount, infos);
} }
private static ClansFileData? LoadClans(Stream fileStream) private static ClansFileData? LoadClans(FileStream fileStream)
{ {
var clanFeatureSet = fileStream.ReadInt32LittleEndian(); var clanFeatureSet = fileStream.ReadInt32LittleEndian();
@@ -163,7 +156,7 @@ public class MissionTmaParser
return clanInfo; return clanInfo;
} }
private static GameObjectsFileData LoadGameObjects(Stream fileStream) private static GameObjectsFileData LoadGameObjects(FileStream fileStream)
{ {
var gameObjectsFeatureSet = fileStream.ReadInt32LittleEndian(); var gameObjectsFeatureSet = fileStream.ReadInt32LittleEndian();

View File

@@ -1,5 +1,3 @@
using Common; namespace MissionTmaLib;
namespace MissionTmaLib;
public record UnknownClanTreeInfoPart(int UnkInt1, Vector3 UnkVector, float UnkInt2, float UnkInt3); public record UnknownClanTreeInfoPart(int UnkInt1, Vector3 UnkVector, float UnkInt2, float UnkInt3);

View File

@@ -1,3 +1,3 @@
namespace Common; namespace MissionTmaLib;
public record Vector3(float X, float Y, float Z); public record Vector3(float X, float Y, float Z);

View File

@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Encoding.CodePages" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,42 +1,7 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Text;
// Unfinished var fileBytes = File.ReadAllBytes("C:\\Program Files (x86)\\Nikita\\Iron Strategy\\gamefont.rlb");
var fileBytes = File.ReadAllBytes("C:\\Program Files (x86)\\Nikita\\Iron Strategy\\gamefont-1.rlb");
var header = fileBytes.AsSpan().Slice(0, 32);
var nlHeaderBytes = header.Slice(0, 2);
var mustBeZero = header[2];
var mustBeOne = header[3];
var numberOfEntriesBytes = header.Slice(4, 2);
var sortingFlagBytes = header.Slice(14, 2);
var decryptionKeyBytes = header.Slice(20, 2);
var numberOfEntries = BinaryPrimitives.ReadInt16LittleEndian(numberOfEntriesBytes);
var sortingFlag = BinaryPrimitives.ReadInt16LittleEndian(sortingFlagBytes);
var decryptionKey = BinaryPrimitives.ReadInt16LittleEndian(decryptionKeyBytes);
var headerSize = numberOfEntries * 32;
var decryptedHeader = new byte[headerSize];
var keyLow = decryptionKeyBytes[0];
var keyHigh = decryptionKeyBytes[1];
for (var i = 0; i < headerSize; i++)
{
byte tmp = (byte)((keyLow << 1) ^ keyHigh);
keyLow = tmp;
keyHigh = (byte)((keyHigh >> 1) ^ tmp);
decryptedHeader[i] = (byte)(fileBytes[32 + i] ^ tmp);
}
var decryptedHeaderString = Encoding.ASCII.GetString(decryptedHeader, 0, headerSize);
var entries = decryptedHeader.Chunk(32).ToArray();
var entriesStrings = entries.Select(x => Encoding.ASCII.GetString(x, 0, x.Length)).ToArray();
File.WriteAllBytes("export.nl", decryptedHeader);
var fileCount = BinaryPrimitives.ReadInt16LittleEndian(fileBytes.AsSpan().Slice(4, 2)); var fileCount = BinaryPrimitives.ReadInt16LittleEndian(fileBytes.AsSpan().Slice(4, 2));
var decodedHeader = new byte[fileCount * 32]; var decodedHeader = new byte[fileCount * 32];

View File

@@ -20,7 +20,7 @@ public record NResArchiveHeader(string NRes, int Version, int FileCount, int Tot
/// найти начало можно как (Header.TotalFileLengthBytes - Header.FileCount * 64) /// найти начало можно как (Header.TotalFileLengthBytes - Header.FileCount * 64)
/// </summary> /// </summary>
/// <param name="FileType">[0..4] ASCII описание типа файла, например TEXM или MAT0</param> /// <param name="FileType">[0..4] ASCII описание типа файла, например TEXM или MAT0</param>
/// <param name="ElementCount">[4..8] Количество элементов в файле (если файл составной, например .trf) или версия, например у материалов или флаги</param> /// <param name="ElementCount">[4..8] Количество элементов в файле (если файл составной, например .trf) </param>
/// <param name="Magic1">[8..12] Неизвестное число</param> /// <param name="Magic1">[8..12] Неизвестное число</param>
/// <param name="FileLength">[12..16] Длина файла в байтах</param> /// <param name="FileLength">[12..16] Длина файла в байтах</param>
/// <param name="ElementSize">[16..20] Размер элемента в файле (если файл составной, например .trf) </param> /// <param name="ElementSize">[16..20] Размер элемента в файле (если файл составной, например .trf) </param>

View File

@@ -1,3 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project> </Project>

151
NResLib/NResPacker.cs Normal file
View File

@@ -0,0 +1,151 @@
using System.Buffers.Binary;
using System.Text;
namespace NResLib;
public class NResPacker
{
public static string Pack(NResArchive archive, string srcNresPath, string contentDirectoryPath, string targetFileDirectoryPath)
{
var diskFiles = Directory.GetFiles(contentDirectoryPath)
.Select(Path.GetFileName)
.ToList();
var fileOffset = 16; // 16 по умолчанию, т.к. есть заголовок в 16 байт.
var metadataItems = new List<ListMetadataItem>();
foreach (var archiveFile in archive.Files)
{
var extension = Path.GetExtension(archiveFile.FileName);
var fileName = Path.GetFileNameWithoutExtension(archiveFile.FileName);
if (extension == "")
{
extension = ".bin";
}
var targetFileName = $"{archiveFile.Index}_{archiveFile.FileType}_{fileName}{extension}";
if (diskFiles.All(x => x != targetFileName))
{
return $"Не найдён файл {targetFileName}";
}
var filePath = Path.Combine(contentDirectoryPath, targetFileName);
var fileInfo = new FileInfo(filePath);
if (!fileInfo.Exists)
{
throw new Exception();
}
var newFileLength = (int)fileInfo.Length;
var listItem = new ListMetadataItem(
archiveFile.FileType,
archiveFile.ElementCount,
archiveFile.Magic1,
newFileLength,
archiveFile.ElementSize,
archiveFile.FileName,
archiveFile.Magic3,
archiveFile.Magic4,
archiveFile.Magic5,
archiveFile.Magic6,
fileOffset,
archiveFile.Index
);
fileOffset += newFileLength;
metadataItems.Add(listItem);
}
var totalFileLength =
16 + // заголовок
metadataItems.Sum(x => x.FileLength) + // сумма длин всех файлов
metadataItems.Count * 64; // длина всех метаданных
var header = new NResArchiveHeader(archive.Header.NRes, archive.Header.Version, archive.Header.FileCount, totalFileLength);
var targetArchive = new NResArchive(header, metadataItems);
// имя архива = имени папки в которую архив распаковывали
string targetArchiveFileName = Path.GetFileName(srcNresPath)!;
var targetArchivePath = Path.Combine(targetFileDirectoryPath, targetArchiveFileName);
using var fs = new FileStream(targetArchivePath, FileMode.CreateNew);
Span<byte> span = stackalloc byte[4];
span.Clear();
Encoding.ASCII.GetBytes(header.NRes, span);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, header.Version);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, header.FileCount);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, header.TotalFileLengthBytes);
fs.Write(span);
foreach (var archiveFile in targetArchive.Files)
{
var extension = Path.GetExtension(archiveFile.FileName);
var fileName = Path.GetFileNameWithoutExtension(archiveFile.FileName);
if (extension == "")
{
extension = ".bin";
}
var targetFileName = $"{archiveFile.Index}_{archiveFile.FileType}_{fileName}{extension}";
var filePath = Path.Combine(contentDirectoryPath, targetFileName);
using var srcFs = new FileStream(filePath, FileMode.Open);
srcFs.CopyTo(fs);
}
Span<byte> fileNameSpan = stackalloc byte[20];
foreach (var archiveFile in targetArchive.Files)
{
span.Clear();
Encoding.ASCII.GetBytes(archiveFile.FileType, span);
fs.Write(span);
BinaryPrimitives.WriteUInt32LittleEndian(span, archiveFile.ElementCount);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.Magic1);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.FileLength);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.ElementSize);
fs.Write(span);
fileNameSpan.Clear();
Encoding.ASCII.GetBytes(archiveFile.FileName, fileNameSpan);
fs.Write(fileNameSpan);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.Magic3);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.Magic4);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.Magic5);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.Magic6);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.OffsetInFile);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.Index);
fs.Write(span);
}
fs.Flush();
return "Запакован архив";
}
}

View File

@@ -7,13 +7,8 @@ public static class NResParser
{ {
public static NResParseResult ReadFile(string path) public static NResParseResult ReadFile(string path)
{ {
using FileStream nResFs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); using FileStream nResFs = new FileStream(path, FileMode.Open);
return ReadFile(nResFs);
}
public static NResParseResult ReadFile(Stream nResFs)
{
if (nResFs.Length < 16) if (nResFs.Length < 16)
{ {
return new NResParseResult(null, "Файл не может быть NRes, менее 16 байт"); return new NResParseResult(null, "Файл не может быть NRes, менее 16 байт");

View File

@@ -1,5 +1,4 @@
using System.Numerics; using System.Numerics;
using System.Text;
using ImGuiNET; using ImGuiNET;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NResUI.Abstractions; using NResUI.Abstractions;
@@ -34,8 +33,6 @@ public class App
public void Init(IWindow window, GL openGl, ImFontPtr openSansFont) public void Init(IWindow window, GL openGl, ImFontPtr openSansFont)
{ {
// Call this once at program startup
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
ImGui.StyleColorsLight(); ImGui.StyleColorsLight();
IServiceCollection serviceCollection = new ServiceCollection(); IServiceCollection serviceCollection = new ServiceCollection();
@@ -59,8 +56,6 @@ public class App
serviceCollection.AddSingleton(new BinaryExplorerViewModel()); serviceCollection.AddSingleton(new BinaryExplorerViewModel());
serviceCollection.AddSingleton(new ScrViewModel()); serviceCollection.AddSingleton(new ScrViewModel());
serviceCollection.AddSingleton(new VarsetViewModel()); serviceCollection.AddSingleton(new VarsetViewModel());
serviceCollection.AddSingleton(new CpDatSchemeViewModel());
serviceCollection.AddSingleton(new MaterialViewModel());
var serviceProvider = serviceCollection.BuildServiceProvider(); var serviceProvider = serviceCollection.BuildServiceProvider();

View File

@@ -1,150 +0,0 @@
using CpDatLib;
using ImGuiNET;
using NResUI.Abstractions;
using NResUI.Models;
namespace NResUI.ImGuiUI;
public class CpDatSchemeExplorer : IImGuiPanel
{
private readonly CpDatSchemeViewModel _viewModel;
public CpDatSchemeExplorer(CpDatSchemeViewModel viewModel)
{
_viewModel = viewModel;
}
public void OnImGuiRender()
{
if (ImGui.Begin("cp .dat Scheme Explorer"))
{
ImGui.Text("cp .dat - это файл схема здания или робота. Их можно найти в папке UNITS");
ImGui.Separator();
var cpDat = _viewModel.CpDatScheme;
if (_viewModel.HasFile && cpDat is not null)
{
ImGui.Text("Тип объекта в схеме: ");
ImGui.SameLine();
ImGui.Text(cpDat.Type.ToString("G"));
var root = cpDat.Root;
DrawEntry(root, 0);
ImGui.Separator();
if (ImGui.BeginTable("content", 8, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX | ImGuiTableFlags.Sortable))
{
ImGui.TableSetupColumn("Индекс");
ImGui.TableSetupColumn("Уровень вложенности");
ImGui.TableSetupColumn("Архив");
ImGui.TableSetupColumn("Элемент");
ImGui.TableSetupColumn("Magic1");
ImGui.TableSetupColumn("Magic2");
ImGui.TableSetupColumn("Описание");
ImGui.TableSetupColumn("Тип");
ImGui.TableHeadersRow();
// Handle sorting
ImGuiTableSortSpecsPtr sortSpecs = ImGui.TableGetSortSpecs();
if (sortSpecs.SpecsDirty)
{
// Only handle the first sort spec for simplicity
var sortSpec = sortSpecs.Specs;
if (sortSpec.ColumnIndex == 0)
{
_viewModel.RebuildFlatList();
}
else
{
_viewModel.FlatList.Sort((a, b) =>
{
int result = 0;
switch (sortSpec.ColumnIndex)
{
case 1: result = a.Level.CompareTo(b.Level); break;
case 2: result = string.Compare(a.Entry.ArchiveFile, b.Entry.ArchiveFile, StringComparison.Ordinal); break;
case 3: result = string.Compare(a.Entry.ArchiveEntryName, b.Entry.ArchiveEntryName, StringComparison.Ordinal); break;
case 4: result = a.Entry.Magic1.CompareTo(b.Entry.Magic1); break;
case 5: result = a.Entry.Magic2.CompareTo(b.Entry.Magic2); break;
case 6: result = string.Compare(a.Entry.Description, b.Entry.Description, StringComparison.Ordinal); break;
case 7: result = a.Entry.Type.CompareTo(b.Entry.Type); break;
}
return sortSpec.SortDirection == ImGuiSortDirection.Descending ? -result : result;
});
}
sortSpecs.SpecsDirty = false;
}
for (int i = 0; i < _viewModel.FlatList.Count; i++)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(i.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Level.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.ArchiveFile);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.ArchiveEntryName);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.Magic1.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.Magic2.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.Description);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.Type.ToString("G"));
}
ImGui.EndTable();
}
void DrawEntry(CpDatEntry entry, int index)
{
if (ImGui.TreeNodeEx($"Элемент: \"{entry.ArchiveFile}/{entry.ArchiveEntryName}\" - {entry.Description}##entry_{index}"))
{
ImGui.Text("Magic1: ");
ImGui.SameLine();
ImGui.Text(entry.Magic1.ToString());
ImGui.Text("Magic2: ");
ImGui.SameLine();
ImGui.Text(entry.Magic2.ToString());
ImGui.Text("Тип: ");
ImGui.SameLine();
ImGui.Text(entry.Type.ToString());
ImGui.Text("Кол-во дочерних элементов: ");
ImGui.SameLine();
ImGui.Text(entry.ChildCount.ToString());
foreach (var child in entry.Children)
{
DrawEntry(child, ++index);
}
ImGui.TreePop();
}
}
}
else if (_viewModel.Error is not null)
{
ImGui.Text(_viewModel.Error);
}
else
{
ImGui.Text("cp .dat не открыт");
}
}
ImGui.End();
}
}

View File

@@ -1,5 +1,4 @@
using System.Numerics; using System.Numerics;
using CpDatLib;
using ImGuiNET; using ImGuiNET;
using MissionTmaLib; using MissionTmaLib;
using MissionTmaLib.Parsing; using MissionTmaLib.Parsing;
@@ -19,7 +18,6 @@ namespace NResUI.ImGuiUI
ScrViewModel scrViewModel, ScrViewModel scrViewModel,
MissionTmaViewModel missionTmaViewModel, MissionTmaViewModel missionTmaViewModel,
VarsetViewModel varsetViewModel, VarsetViewModel varsetViewModel,
CpDatSchemeViewModel cpDatSchemeViewModel,
MessageBoxModalPanel messageBox) MessageBoxModalPanel messageBox)
: IImGuiPanel : IImGuiPanel
{ {
@@ -123,21 +121,6 @@ namespace NResUI.ImGuiUI
} }
} }
if (ImGui.MenuItem("Open cp .dat Scheme File"))
{
var result = Dialog.FileOpen("dat");
if (result.IsOk)
{
var path = result.Path;
var parseResult = CpDatParser.Parse(path);
cpDatSchemeViewModel.SetParseResult(parseResult, path);
Console.WriteLine("Read cp .dat");
}
}
if (nResExplorerViewModel.HasFile) if (nResExplorerViewModel.HasFile)
{ {
if (ImGui.MenuItem("Экспортировать NRes")) if (ImGui.MenuItem("Экспортировать NRes"))
@@ -155,6 +138,34 @@ namespace NResUI.ImGuiUI
} }
} }
if (nResExplorerViewModel.HasFile)
{
if (ImGui.MenuItem("Запаковать NRes"))
{
messageBox.Show("Выберите папку с контентом NRES");
var contentDirectoryPicker = Dialog.FolderPicker();
if (contentDirectoryPicker.IsOk)
{
var contentDirectoryPath = contentDirectoryPicker.Path;
var targetFileDirectoryPicker = Dialog.FolderPicker();
if (targetFileDirectoryPicker.IsOk)
{
var targetFileDirectory = targetFileDirectoryPicker.Path;
var packResult = NResPacker.Pack(
nResExplorerViewModel.Archive!,
nResExplorerViewModel.Path!,
contentDirectoryPath, targetFileDirectory);
messageBox.Show(packResult);
}
}
}
}
ImGui.EndMenu(); ImGui.EndMenu();
} }

View File

@@ -1,301 +0,0 @@
using System.Numerics;
using ImGuiNET;
using MaterialLib;
using NResUI.Abstractions;
using NResUI.Models;
namespace NResUI.ImGuiUI;
public class MaterialExplorerPanel : IImGuiPanel
{
private readonly MaterialViewModel _viewModel;
public MaterialExplorerPanel(MaterialViewModel viewModel)
{
_viewModel = viewModel;
}
public void OnImGuiRender()
{
if (ImGui.Begin("Material Explorer"))
{
ImGui.Text("Материал это бинарный файл, который можно найти в materials.lib.");
ImGui.Text("Его можно открыть только из открытого NRes архива, т.к. чтение полагается на флаги из архива");
if (!_viewModel.HasFile)
{
ImGui.Text("No Material file opened");
}
else if (_viewModel.Error != null)
{
ImGui.TextColored(new Vector4(1, 0, 0, 1), $"Error: {_viewModel.Error}");
}
else
{
var mat = _viewModel.MaterialFile!;
ImGui.Text($"File: {_viewModel.FilePath}");
ImGui.Separator();
// === FILE READ SEQUENCE ===
if (ImGui.CollapsingHeader("1. Metadata & Derived Fields", ImGuiTreeNodeFlags.DefaultOpen))
{
ImGui.TextDisabled("(Not from file content)");
ImGui.Text($"File Name: {mat.FileName}");
ImGui.Text($"Version (ElementCount): {mat.Version}");
ImGui.Text($"Magic1: {mat.Magic1}");
ImGui.Separator();
ImGui.TextDisabled("(Derived from Version)");
ImGui.Text($"Material Rendering Type: {mat.MaterialRenderingType}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("0=Standard, 1=Special, 2=Particle/Effect");
}
ImGui.Text($"Supports Bump Mapping: {mat.SupportsBumpMapping}");
ImGui.Text($"Is Particle Effect: {mat.IsParticleEffect}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("0=Normal material, 8=Particle/Effect (e.g., jet engines)");
}
}
if (ImGui.CollapsingHeader("2. File Header (Read Order)", ImGuiTreeNodeFlags.DefaultOpen))
{
ImGui.Text($"Stage Count: {mat.Stages.Count}");
ImGui.SameLine();
ImGui.TextDisabled("(2 bytes, ushort)");
ImGui.Text($"Animation Count: {mat.Animations.Count}");
ImGui.SameLine();
ImGui.TextDisabled("(2 bytes, ushort)");
ImGui.Separator();
ImGui.TextDisabled($"(Read if Magic1 >= 2)");
ImGui.Text($"Source Blend Mode: {mat.SourceBlendMode}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(GetBlendModeDescription(mat.SourceBlendMode));
}
ImGui.SameLine();
ImGui.TextDisabled("(1 byte)");
ImGui.Text($"Dest Blend Mode: {mat.DestBlendMode}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(GetBlendModeDescription(mat.DestBlendMode));
}
ImGui.SameLine();
ImGui.TextDisabled("(1 byte)");
ImGui.Separator();
ImGui.TextDisabled("(Read if Magic1 > 2)");
ImGui.Text($"Global Alpha Multiplier: {mat.GlobalAlphaMultiplier:F3}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Always 1.0 in all 628 materials - global alpha multiplier");
}
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes, float)");
ImGui.Separator();
ImGui.TextDisabled("(Read if Magic1 > 3)");
ImGui.Text($"Global Emissive Intensity: {mat.GlobalEmissiveIntensity:F3}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("0=no glow (99.4%), rare values: 1000, 10000");
}
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes, float)");
}
if (ImGui.CollapsingHeader($"3. Stages ({mat.Stages.Count}) - 34 bytes each", ImGuiTreeNodeFlags.DefaultOpen))
{
for (int i = 0; i < mat.Stages.Count; i++)
{
var stage = mat.Stages[i];
if (ImGui.TreeNode($"Stage {i}: {stage.TextureName}"))
{
ImGui.TextDisabled("=== File Read Order (34 bytes) ===");
// 1. Ambient (4 bytes)
ImGui.Text("1. Ambient Color:");
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes, A scaled by 0.01)");
var ambient = new Vector4(stage.AmbientR, stage.AmbientG, stage.AmbientB, stage.AmbientA);
ImGui.ColorEdit4("##Ambient", ref ambient, ImGuiColorEditFlags.NoInputs);
ImGui.SameLine();
ImGui.Text($"RGBA: ({stage.AmbientR:F3}, {stage.AmbientG:F3}, {stage.AmbientB:F3}, {stage.AmbientA:F3})");
// 2. Diffuse (4 bytes)
ImGui.Text("2. Diffuse Color:");
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes)");
var diffuse = new Vector4(stage.DiffuseR, stage.DiffuseG, stage.DiffuseB, stage.DiffuseA);
ImGui.ColorEdit4("##Diffuse", ref diffuse, ImGuiColorEditFlags.NoInputs);
ImGui.SameLine();
ImGui.Text($"RGBA: ({stage.DiffuseR:F3}, {stage.DiffuseG:F3}, {stage.DiffuseB:F3}, {stage.DiffuseA:F3})");
// 3. Specular (4 bytes)
ImGui.Text("3. Specular Color:");
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes)");
var specular = new Vector4(stage.SpecularR, stage.SpecularG, stage.SpecularB, stage.SpecularA);
ImGui.ColorEdit4("##Specular", ref specular, ImGuiColorEditFlags.NoInputs);
ImGui.SameLine();
ImGui.Text($"RGBA: ({stage.SpecularR:F3}, {stage.SpecularG:F3}, {stage.SpecularB:F3}, {stage.SpecularA:F3})");
// 4. Emissive (4 bytes)
ImGui.Text("4. Emissive Color:");
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes)");
var emissive = new Vector4(stage.EmissiveR, stage.EmissiveG, stage.EmissiveB, stage.EmissiveA);
ImGui.ColorEdit4("##Emissive", ref emissive, ImGuiColorEditFlags.NoInputs);
ImGui.SameLine();
ImGui.Text($"RGBA: ({stage.EmissiveR:F3}, {stage.EmissiveG:F3}, {stage.EmissiveB:F3}, {stage.EmissiveA:F3})");
// 5. Power (1 byte)
ImGui.Text($"5. Power: {stage.Power:F3}");
ImGui.SameLine();
ImGui.TextDisabled("(1 byte -> float)");
// 6. Texture Stage Index (1 byte)
ImGui.Text($"6. Texture Stage Index: {stage.TextureStageIndex}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("255 = not set/default, 0-47 = reference to specific texture stage");
}
ImGui.SameLine();
ImGui.TextDisabled("(1 byte)");
// 7. Texture Name (16 bytes)
ImGui.Text($"7. Texture Name: {stage.TextureName}");
ImGui.SameLine();
ImGui.TextDisabled("(16 bytes, ASCII)");
ImGui.TreePop();
}
}
}
if (ImGui.CollapsingHeader($"4. Animations ({mat.Animations.Count})"))
{
for (int i = 0; i < mat.Animations.Count; i++)
{
var anim = mat.Animations[i];
if (ImGui.TreeNode($"Anim {i}: {anim.Target} ({anim.LoopMode})"))
{
ImGui.TextDisabled("=== File Read Order ===");
// Combined field (4 bytes)
ImGui.Text("1. Target & Loop Mode:");
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes combined)");
ImGui.Indent();
ImGui.Text($"Target (bits 3-31): {anim.Target}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(anim.TargetDescription);
}
ImGui.Text($"Loop Mode (bits 0-2): {anim.LoopMode}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(GetAnimationLoopModeDescription(anim.LoopMode));
}
ImGui.Unindent();
// Key count (2 bytes)
ImGui.Text($"2. Key Count: {anim.Keys.Count}");
ImGui.SameLine();
ImGui.TextDisabled("(2 bytes, ushort)");
// Keys (6 bytes each)
ImGui.Text($"3. Keys ({anim.Keys.Count} × 6 bytes):");
if (ImGui.BeginTable($"keys_{i}", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg))
{
ImGui.TableSetupColumn("#");
ImGui.TableSetupColumn("Stage Index (2 bytes)");
ImGui.TableSetupColumn("Duration ms (2 bytes)");
ImGui.TableSetupColumn("Interpolation (2 bytes)");
ImGui.TableHeadersRow();
int keyIdx = 0;
foreach (var key in anim.Keys)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(keyIdx.ToString());
ImGui.TableNextColumn();
ImGui.Text(key.StageIndex.ToString());
ImGui.TableNextColumn();
ImGui.Text(key.DurationMs.ToString());
ImGui.TableNextColumn();
ImGui.Text(key.InterpolationCurve.ToString());
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Always 0 = linear interpolation");
}
keyIdx++;
}
ImGui.EndTable();
}
ImGui.TreePop();
}
}
}
}
}
ImGui.End();
}
private static string GetBlendModeDescription(BlendMode mode)
{
return mode switch
{
BlendMode.Zero => "D3DBLEND_ZERO\nBlend factor is (0, 0, 0, 0)\nResults in black/transparent",
BlendMode.One => "D3DBLEND_ONE\nBlend factor is (1, 1, 1, 1)\nUses full color value (no blending)",
BlendMode.SrcColor => "D3DBLEND_SRCCOLOR\nBlend factor is (Rs, Gs, Bs, As)\nUses source color",
BlendMode.InvSrcColor => "D3DBLEND_INVSRCCOLOR\nBlend factor is (1-Rs, 1-Gs, 1-Bs, 1-As)\nUses inverted source color",
BlendMode.SrcAlpha => "D3DBLEND_SRCALPHA\nBlend factor is (As, As, As, As)\nUses source alpha for all channels\n Standard transparency (common with InvSrcAlpha for dest)",
BlendMode.InvSrcAlpha => "D3DBLEND_INVSRCALPHA\nBlend factor is (1-As, 1-As, 1-As, 1-As)\nUses inverted source alpha\n Standard transparency (common with SrcAlpha for source)",
BlendMode.DestAlpha => "D3DBLEND_DESTALPHA\nBlend factor is (Ad, Ad, Ad, Ad)\nUses destination alpha",
BlendMode.InvDestAlpha => "D3DBLEND_INVDESTALPHA\nBlend factor is (1-Ad, 1-Ad, 1-Ad, 1-Ad)\nUses inverted destination alpha",
BlendMode.DestColor => "D3DBLEND_DESTCOLOR\nBlend factor is (Rd, Gd, Bd, Ad)\nUses destination color",
BlendMode.InvDestColor => "D3DBLEND_INVDESTCOLOR\nBlend factor is (1-Rd, 1-Gd, 1-Bd, 1-Ad)\nUses inverted destination color",
BlendMode.SrcAlphaSat => "D3DBLEND_SRCALPHASAT\nBlend factor is (f, f, f, 1) where f = min(As, 1-Ad)\nSaturates source alpha",
BlendMode.BothSrcAlpha => "D3DBLEND_BOTHSRCALPHA (Obsolete in D3D9+)\nBlend factor is (As, As, As, As) for both source and dest\nSource: (As, As, As, As), Dest: (1-As, 1-As, 1-As, 1-As)",
BlendMode.BothInvSrcAlpha => "D3DBLEND_BOTHINVSRCALPHA (Obsolete in D3D9+)\nBlend factor is (1-As, 1-As, 1-As, 1-As) for both source and dest\nSource: (1-As, 1-As, 1-As, 1-As), Dest: (As, As, As, As)",
BlendMode.Unknown => "Unknown/Default (0xFF)\nUninitialized or opaque rendering",
_ => "Unknown blend mode"
};
}
private static string GetAnimationLoopModeDescription(AnimationLoopMode mode)
{
return mode switch
{
AnimationLoopMode.Loop => "Loop: Animation repeats continuously from start to end",
AnimationLoopMode.PingPong => "PingPong: Animation plays forward then backward repeatedly",
AnimationLoopMode.Clamp => "Clamp: Animation plays once and holds at the final value",
AnimationLoopMode.Random => "Random: Animation selects random keyframes",
_ => "Unknown loop mode"
};
}
}

View File

@@ -18,9 +18,6 @@ public class MissionTmaExplorer : IImGuiPanel
{ {
if (ImGui.Begin("Mission TMA Explorer")) if (ImGui.Begin("Mission TMA Explorer"))
{ {
ImGui.Text("data.tma - это файл миссии. Его можно найти в папке MISSIONS");
ImGui.Separator();
var mission = _viewModel.Mission; var mission = _viewModel.Mission;
if (_viewModel.HasFile && mission is not null) if (_viewModel.HasFile && mission is not null)
{ {

View File

@@ -1,47 +1,22 @@
using CpDatLib; using ImGuiNET;
using ImGuiNET;
using MissionTmaLib.Parsing;
using NResLib;
using NResUI.Abstractions; using NResUI.Abstractions;
using NResUI.Models; using NResUI.Models;
using ScrLib;
using TexmLib;
using VarsetLib;
namespace NResUI.ImGuiUI; namespace NResUI.ImGuiUI;
public class NResExplorerPanel : IImGuiPanel public class NResExplorerPanel : IImGuiPanel
{ {
private readonly NResExplorerViewModel _viewModel; private readonly NResExplorerViewModel _viewModel;
private readonly TexmExplorerViewModel _texmExplorerViewModel;
private readonly VarsetViewModel _varsetViewModel;
private readonly CpDatSchemeViewModel _cpDatSchemeViewModel;
private readonly MissionTmaViewModel _missionTmaViewModel;
private readonly ScrViewModel _scrViewModel;
private readonly MaterialViewModel _materialViewModel;
public NResExplorerPanel(NResExplorerViewModel viewModel, TexmExplorerViewModel texmExplorerViewModel, public NResExplorerPanel(NResExplorerViewModel viewModel)
VarsetViewModel varsetViewModel, CpDatSchemeViewModel cpDatSchemeViewModel, MissionTmaViewModel missionTmaViewModel, ScrViewModel scrViewModel, MaterialViewModel materialViewModel)
{ {
_viewModel = viewModel; _viewModel = viewModel;
_texmExplorerViewModel = texmExplorerViewModel;
_varsetViewModel = varsetViewModel;
_cpDatSchemeViewModel = cpDatSchemeViewModel;
_missionTmaViewModel = missionTmaViewModel;
_scrViewModel = scrViewModel;
_materialViewModel = materialViewModel;
} }
int contextMenuRow = -1;
public void OnImGuiRender() public void OnImGuiRender()
{ {
if (ImGui.Begin("NRes Explorer")) if (ImGui.Begin("NRes Explorer"))
{ {
ImGui.Text(
"NRes - это файл-архив. Они имеют разные расширения. Примеры - Textures.lib, weapon.rlb, object.dlb, behpsp.res");
ImGui.Separator();
if (!_viewModel.HasFile) if (!_viewModel.HasFile)
{ {
ImGui.Text("No NRes is opened"); ImGui.Text("No NRes is opened");
@@ -70,8 +45,8 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.SameLine(); ImGui.SameLine();
ImGui.Text(_viewModel.Archive.Header.TotalFileLengthBytes.ToString()); ImGui.Text(_viewModel.Archive.Header.TotalFileLengthBytes.ToString());
if (ImGui.BeginTable("content", 12,
ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX)) if (ImGui.BeginTable("content", 12, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX))
{ {
ImGui.TableSetupColumn("Тип файла"); ImGui.TableSetupColumn("Тип файла");
ImGui.TableSetupColumn("Кол-во элементов"); ImGui.TableSetupColumn("Кол-во элементов");
@@ -92,17 +67,6 @@ public class NResExplorerPanel : IImGuiPanel
{ {
ImGui.TableNextRow(); ImGui.TableNextRow();
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Selectable("##row_select" + i, false, ImGuiSelectableFlags.SpanAllColumns);
if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Right))
{
Console.WriteLine("Context menu for row " + i);
contextMenuRow = i;
ImGui.OpenPopup("row_context_menu");
}
ImGui.SameLine();
ImGui.Text(_viewModel.Archive.Files[i].FileType); ImGui.Text(_viewModel.Archive.Files[i].FileType);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text(_viewModel.Archive.Files[i].ElementCount.ToString()); ImGui.Text(_viewModel.Archive.Files[i].ElementCount.ToString());
@@ -118,8 +82,8 @@ public class NResExplorerPanel : IImGuiPanel
); );
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text( ImGui.Text(
_viewModel.Archive.Files[i] "0x" + _viewModel.Archive.Files[i]
.ElementSize.ToString() .ElementSize.ToString("X2")
); );
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text(_viewModel.Archive.Files[i].FileName); ImGui.Text(_viewModel.Archive.Files[i].FileName);
@@ -155,152 +119,6 @@ public class NResExplorerPanel : IImGuiPanel
); );
} }
if (ImGui.BeginPopup("row_context_menu"))
{
if (contextMenuRow == -1 || contextMenuRow > _viewModel.Archive.Files.Count)
{
ImGui.Text("Broken context menu :(. Reopen");
}
else
{
var file = _viewModel.Archive.Files[contextMenuRow];
ImGui.Text("Actions for file " + file.FileName);
ImGui.TextDisabled("Program has no understading of file format(");
ImGui.Separator();
if (ImGui.MenuItem("Open as Texture TEXM"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read,
FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
var parseResult = TexmParser.ReadFromStream(ms, file.FileName);
_texmExplorerViewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName));
Console.WriteLine("Read TEXM from context menu");
}
if (ImGui.MenuItem("Open as Archive NRes"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read,
FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
var parseResult = NResParser.ReadFile(ms);
_viewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName));
Console.WriteLine("Read NRes from context menu");
}
if (ImGui.MenuItem("Open as Varset .var"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read,
FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
var parseResult = VarsetParser.Parse(ms);
_varsetViewModel.Items = parseResult;
Console.WriteLine("Read Varset from context menu");
}
if (ImGui.MenuItem("Open as Scheme cp.dat"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read,
FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
var parseResult = CpDatParser.Parse(ms);
_cpDatSchemeViewModel.SetParseResult(parseResult, file.FileName);
Console.WriteLine("Read cp.dat from context menu");
}
if (ImGui.MenuItem("Open as Mission .tma"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read,
FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
var parseResult = MissionTmaParser.ReadFile(ms);
_missionTmaViewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName));
Console.WriteLine("Read .tma from context menu");
}
if (ImGui.MenuItem("Open as Scripts .scr"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read,
FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
var parseResult = ScrParser.ReadFile(ms);
_scrViewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName));
Console.WriteLine("Read .scr from context menu");
}
if (ImGui.MenuItem("Open as Material"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read, FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
try
{
var parseResult = MaterialLib.MaterialParser.ReadFromStream(ms, file.FileName, (int)file.ElementCount, file.Magic1);
_materialViewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName));
Console.WriteLine("Read Material from context menu");
}
catch (Exception ex)
{
Console.WriteLine($"Error reading material: {ex}");
_materialViewModel.SetError(ex.Message);
}
}
}
ImGui.EndPopup();
}
ImGui.EndTable(); ImGui.EndTable();
} }
} }

View File

@@ -18,9 +18,6 @@ public class ScrExplorer : IImGuiPanel
{ {
if (ImGui.Begin("SCR Explorer")) if (ImGui.Begin("SCR Explorer"))
{ {
ImGui.Text("scr - это файл AI скриптов. Их можно найти в папке MISSIONS/SCRIPTS");
ImGui.Separator();
var scr = _viewModel.Scr; var scr = _viewModel.Scr;
if (_viewModel.HasFile && scr is not null) if (_viewModel.HasFile && scr is not null)
{ {

View File

@@ -21,9 +21,6 @@ public class TexmExplorer : IImGuiPanel
{ {
if (ImGui.Begin("TEXM Explorer")) if (ImGui.Begin("TEXM Explorer"))
{ {
ImGui.Text("TEXM - это файл текстуры. Их можно найти внутри NRes архивов, например Textures.lib");
ImGui.Separator();
if (!_viewModel.HasFile) if (!_viewModel.HasFile)
{ {
ImGui.Text("No TEXM opened"); ImGui.Text("No TEXM opened");
@@ -62,7 +59,7 @@ public class TexmExplorer : IImGuiPanel
ImGui.Text("Magic2: "); ImGui.Text("Magic2: ");
ImGui.SameLine(); ImGui.SameLine();
ImGui.Text(_viewModel.TexmFile.Header.FormatOptionFlags.ToString()); ImGui.Text(_viewModel.TexmFile.Header.Magic2.ToString());
ImGui.Text("Format: "); ImGui.Text("Format: ");
ImGui.SameLine(); ImGui.SameLine();

View File

@@ -17,9 +17,6 @@ public class VarsetExplorerPanel : IImGuiPanel
{ {
if (ImGui.Begin("VARSET Explorer")) if (ImGui.Begin("VARSET Explorer"))
{ {
ImGui.Text(".var - это файл динамических настроек. Можно найти в MISSIONS/SCRIPTS/varset.var, а также внутри behpsp.res");
ImGui.Separator();
if (_viewModel.Items.Count == 0) if (_viewModel.Items.Count == 0)
{ {
ImGui.Text("VARSET не загружен"); ImGui.Text("VARSET не загружен");

View File

@@ -1,50 +0,0 @@
using CpDatLib;
using ScrLib;
namespace NResUI.Models;
public class CpDatSchemeViewModel
{
public bool HasFile { get; set; }
public string? Error { get; set; }
public CpDatScheme? CpDatScheme { get; set; }
public List<(int Level, CpDatEntry Entry)> FlatList { get; set; } = [];
public string? Path { get; set; }
public void SetParseResult(CpDatParseResult parseResult, string path)
{
CpDatScheme = parseResult.Scheme;
Error = parseResult.Error;
HasFile = true;
Path = path;
if (CpDatScheme is not null)
{
RebuildFlatList();
}
}
public void RebuildFlatList()
{
FlatList = [];
if (CpDatScheme is null)
{
return;
}
CollectEntries(CpDatScheme.Root, 0);
void CollectEntries(CpDatEntry entry, int level)
{
FlatList.Add((level, entry));
foreach (var child in entry.Children)
{
CollectEntries(child, level + 1);
}
}
}
}

View File

@@ -1,26 +0,0 @@
using MaterialLib;
namespace NResUI.Models;
public class MaterialViewModel
{
public MaterialFile? MaterialFile { get; private set; }
public string? FilePath { get; private set; }
public string? Error { get; private set; }
public bool HasFile => MaterialFile != null;
public void SetParseResult(MaterialFile materialFile, string filePath)
{
MaterialFile = materialFile;
FilePath = filePath;
Error = null;
}
public void SetError(string error)
{
MaterialFile = null;
FilePath = null;
Error = error;
}
}

View File

@@ -1,8 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType Condition="'$(OS)' == 'Windows_NT'">WinExe</OutputType> <OutputType>Exe</OutputType>
<OutputType Condition="'$(OS)' != 'Windows_NT'">Exe</OutputType> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -10,20 +12,18 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="NativeFileDialogSharp" /> <PackageReference Include="NativeFileDialogSharp" Version="0.5.0" />
<PackageReference Include="Silk.NET" /> <PackageReference Include="Silk.NET" Version="2.22.0" />
<PackageReference Include="Silk.NET.OpenGL.Extensions.ImGui" /> <PackageReference Include="Silk.NET.OpenGL.Extensions.ImGui" Version="2.22.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CpDatLib\CpDatLib.csproj" />
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" /> <ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" />
<ProjectReference Include="..\NResLib\NResLib.csproj" /> <ProjectReference Include="..\NResLib\NResLib.csproj" />
<ProjectReference Include="..\ScrLib\ScrLib.csproj" /> <ProjectReference Include="..\ScrLib\ScrLib.csproj" />
<ProjectReference Include="..\TexmLib\TexmLib.csproj" /> <ProjectReference Include="..\TexmLib\TexmLib.csproj" />
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" /> <ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
<ProjectReference Include="..\MaterialLib\MaterialLib.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -137,9 +137,9 @@ namespace NResUI
public void SetLod(int @base, int min, int max) public void SetLod(int @base, int min, int max)
{ {
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureLodBias, in @base); _gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureLodBias, @base);
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMinLod, in min); _gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMinLod, min);
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMaxLod, in max); _gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMaxLod, max);
} }
public void SetWrap(TextureCoordinate coord, TextureWrapMode mode) public void SetWrap(TextureCoordinate coord, TextureWrapMode mode)

View File

@@ -1,54 +0,0 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace PalLib;
/// <summary>
/// PAL файл по сути это indexed текстура (1024 байт - 256 цветов lookup + 4 байта "Ipol" и затем 256x256 индексов в lookup)
/// </summary>
public class PalFile
{
public required string FileName { get; set; }
/// <summary>
/// 256 цветов lookup (1024 байт)
/// </summary>
public required byte[] Palette { get; set; }
/// <summary>
/// 256x256 индексов в lookup
/// </summary>
public required byte[] Indices { get; set; }
public void SaveAsPng(string outputPath)
{
const int width = 256;
const int height = 256;
var rgbaBytes = new byte[width * height * 4];
for (int i = 0; i < Indices.Length; i++)
{
var index = Indices[i];
// Palette is 256 colors * 4 bytes (ARGB usually, based on TexmLib)
// TexmLib: r = lookup[i*4+0], g = lookup[i*4+1], b = lookup[i*4+2], a = lookup[i*4+3]
// Assuming same format here.
// since PAL is likely directx related, the format is is likely BGRA
var b = Palette[index * 4 + 0];
var g = Palette[index * 4 + 1];
var r = Palette[index * 4 + 2];
var a = Palette[index * 4 + 3]; // Alpha? Or is it unused/padding? TexmLib sets alpha to 255 manually for indexed.
rgbaBytes[i * 4 + 0] = r;
rgbaBytes[i * 4 + 1] = g;
rgbaBytes[i * 4 + 2] = b;
rgbaBytes[i * 4 + 3] = 255;
}
using var image = Image.LoadPixelData<Rgba32>(rgbaBytes, width, height);
image.SaveAsPng(outputPath);
}
}

View File

@@ -1,7 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp"/>
</ItemGroup>
</Project>

View File

@@ -1,37 +0,0 @@
using System.Text;
namespace PalLib;
public class PalParser
{
public static PalFile ReadFromStream(Stream stream, string filename)
{
// Expected size: 1024 (palette) + 4 ("Ipol") + 65536 (indices) = 66564
if (stream.Length != 66564)
{
throw new InvalidDataException($"Invalid PAL file size. Expected 66564 bytes, got {stream.Length}.");
}
var palette = new byte[1024];
stream.ReadExactly(palette, 0, 1024);
var signatureBytes = new byte[4];
stream.ReadExactly(signatureBytes, 0, 4);
var signature = Encoding.ASCII.GetString(signatureBytes);
if (signature != "Ipol")
{
throw new InvalidDataException($"Invalid PAL file signature. Expected 'Ipol', got '{signature}'.");
}
var indices = new byte[65536];
stream.ReadExactly(indices, 0, 65536);
return new PalFile
{
FileName = filename,
Palette = palette,
Indices = indices
};
}
}

104
ParkanPlayground.sln Normal file
View File

@@ -0,0 +1,104 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParkanPlayground", "ParkanPlayground\ParkanPlayground.csproj", "{7DB19000-6F41-4BAE-A904-D34EFCA065E9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TextureDecoder", "TextureDecoder\TextureDecoder.csproj", "{15D1C9ED-1080-417D-A4D1-CFF80BE6A218}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLUnpacker", "NLUnpacker\NLUnpacker.csproj", "{50C83E6C-23ED-4A8E-B948-89686A742CF0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NResUI", "NResUI\NResUI.csproj", "{7456A089-0701-416C-8668-1F740BF4B72C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NResLib", "NResLib\NResLib.csproj", "{9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeshUnpacker", "MeshUnpacker\MeshUnpacker.csproj", "{F1465FFE-0D66-4A3C-90D7-153A14E226E6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TexmLib", "TexmLib\TexmLib.csproj", "{40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MissionDataUnpacker", "MissionDataUnpacker\MissionDataUnpacker.csproj", "{7BF5C860-9194-4AF2-B5DA-216F98B03DBE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "meta", "meta", "{BAF212FE-A0FD-41A2-A1A9-B406FDDFBAF3}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MissionTmaLib", "MissionTmaLib\MissionTmaLib.csproj", "{773D8EEA-6005-4127-9CB4-5F9F1A028B5D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScrLib", "ScrLib\ScrLib.csproj", "{C445359B-97D4-4432-9331-708B5A14887A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VarsetLib", "VarsetLib\VarsetLib.csproj", "{0EC800E2-1444-40D5-9EDD-93276F4D1FF5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Visualisator", "Visualisator\Visualisator.csproj", "{667A7E03-5CAA-4591-9980-F6C722911A35}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X86Disassembler", "X86Disassembler\X86Disassembler.csproj", "{B5C2E94A-0F63-4E09-BC04-F2518E2CC1F0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X86DisassemblerTests", "X86DisassemblerTests\X86DisassemblerTests.csproj", "{D6A1F5A9-0C7A-4F8F-B8C5-83E9D3F3A1D5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LandscapeExplorer", "LandscapeExplorer\LandscapeExplorer.csproj", "{2700BD3F-DC67-4B58-8F73-F790AA68E4FE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7DB19000-6F41-4BAE-A904-D34EFCA065E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7DB19000-6F41-4BAE-A904-D34EFCA065E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7DB19000-6F41-4BAE-A904-D34EFCA065E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7DB19000-6F41-4BAE-A904-D34EFCA065E9}.Release|Any CPU.Build.0 = Release|Any CPU
{15D1C9ED-1080-417D-A4D1-CFF80BE6A218}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{15D1C9ED-1080-417D-A4D1-CFF80BE6A218}.Debug|Any CPU.Build.0 = Debug|Any CPU
{15D1C9ED-1080-417D-A4D1-CFF80BE6A218}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15D1C9ED-1080-417D-A4D1-CFF80BE6A218}.Release|Any CPU.Build.0 = Release|Any CPU
{50C83E6C-23ED-4A8E-B948-89686A742CF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{50C83E6C-23ED-4A8E-B948-89686A742CF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{50C83E6C-23ED-4A8E-B948-89686A742CF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{50C83E6C-23ED-4A8E-B948-89686A742CF0}.Release|Any CPU.Build.0 = Release|Any CPU
{7456A089-0701-416C-8668-1F740BF4B72C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7456A089-0701-416C-8668-1F740BF4B72C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7456A089-0701-416C-8668-1F740BF4B72C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7456A089-0701-416C-8668-1F740BF4B72C}.Release|Any CPU.Build.0 = Release|Any CPU
{9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}.Release|Any CPU.Build.0 = Release|Any CPU
{F1465FFE-0D66-4A3C-90D7-153A14E226E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1465FFE-0D66-4A3C-90D7-153A14E226E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1465FFE-0D66-4A3C-90D7-153A14E226E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1465FFE-0D66-4A3C-90D7-153A14E226E6}.Release|Any CPU.Build.0 = Release|Any CPU
{40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}.Release|Any CPU.Build.0 = Release|Any CPU
{7BF5C860-9194-4AF2-B5DA-216F98B03DBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7BF5C860-9194-4AF2-B5DA-216F98B03DBE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7BF5C860-9194-4AF2-B5DA-216F98B03DBE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7BF5C860-9194-4AF2-B5DA-216F98B03DBE}.Release|Any CPU.Build.0 = Release|Any CPU
{773D8EEA-6005-4127-9CB4-5F9F1A028B5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{773D8EEA-6005-4127-9CB4-5F9F1A028B5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{773D8EEA-6005-4127-9CB4-5F9F1A028B5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{773D8EEA-6005-4127-9CB4-5F9F1A028B5D}.Release|Any CPU.Build.0 = Release|Any CPU
{C445359B-97D4-4432-9331-708B5A14887A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C445359B-97D4-4432-9331-708B5A14887A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C445359B-97D4-4432-9331-708B5A14887A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C445359B-97D4-4432-9331-708B5A14887A}.Release|Any CPU.Build.0 = Release|Any CPU
{0EC800E2-1444-40D5-9EDD-93276F4D1FF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0EC800E2-1444-40D5-9EDD-93276F4D1FF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0EC800E2-1444-40D5-9EDD-93276F4D1FF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0EC800E2-1444-40D5-9EDD-93276F4D1FF5}.Release|Any CPU.Build.0 = Release|Any CPU
{667A7E03-5CAA-4591-9980-F6C722911A35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{667A7E03-5CAA-4591-9980-F6C722911A35}.Debug|Any CPU.Build.0 = Debug|Any CPU
{667A7E03-5CAA-4591-9980-F6C722911A35}.Release|Any CPU.ActiveCfg = Release|Any CPU
{667A7E03-5CAA-4591-9980-F6C722911A35}.Release|Any CPU.Build.0 = Release|Any CPU
{B5C2E94A-0F63-4E09-BC04-F2518E2CC1F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5C2E94A-0F63-4E09-BC04-F2518E2CC1F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5C2E94A-0F63-4E09-BC04-F2518E2CC1F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5C2E94A-0F63-4E09-BC04-F2518E2CC1F0}.Release|Any CPU.Build.0 = Release|Any CPU
{D6A1F5A9-0C7A-4F8F-B8C5-83E9D3F3A1D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6A1F5A9-0C7A-4F8F-B8C5-83E9D3F3A1D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6A1F5A9-0C7A-4F8F-B8C5-83E9D3F3A1D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6A1F5A9-0C7A-4F8F-B8C5-83E9D3F3A1D5}.Release|Any CPU.Build.0 = Release|Any CPU
{2700BD3F-DC67-4B58-8F73-F790AA68E4FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2700BD3F-DC67-4B58-8F73-F790AA68E4FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2700BD3F-DC67-4B58-8F73-F790AA68E4FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2700BD3F-DC67-4B58-8F73-F790AA68E4FE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,31 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssemblyCodeArray_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa494e0aa381c41ff9484df33e5edb42535e00_003Fa1_003Fbc9d4e81_003FAssemblyCodeArray_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssemblyCodeMemory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa494e0aa381c41ff9484df33e5edb42535e00_003F6e_003F09b667c6_003FAssemblyCodeMemory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACollectionAsserts_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F435f965090c5b89f0c5efb49ac3c5a72367d90599314191af25a832d0942f_003FCollectionAsserts_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACsvReader_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ff7b87edd534764eebf2388a77d49e5cd9c6d49eb6788dca9b1c07d4545412715_003FCsvReader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADefaultTypeConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F64b864a5d465bc24fc4b55e1026aba213beb1733ef631abeca5a9f25357eda_003FDefaultTypeConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADisassembler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa494e0aa381c41ff9484df33e5edb42535e00_003Fd4_003Fad0818f9_003FDisassembler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEncoding_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F1675dc7b710feeeb3e0bc8728be8a947537155c199480fb23b776e81d459_003FEncoding_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFailAsserts_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F807c15a7d8383b1548dff1ae33270e637836659d9caecd676ea6f2c59f1c71a_003FFailAsserts_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGL_002Egen_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F54e6df16dd99323ba9b0682ce5d5dac3648ccd10aafd29d5f3fad52b62bf3f75_003FGL_002Egen_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAssemblyCode_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa494e0aa381c41ff9484df33e5edb42535e00_003F8c_003F9fe9bac2_003FIAssemblyCode_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMatrix4x4_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fed6aa59cd75423c5b655901d6ec4fb4be48ab669fa6fb01b3a7a7f31be95_003FMatrix4x4_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMemory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FLocal_003FSymbols_003Fsrc_003Fdotnet_003Fruntime_003F5535e31a712343a63f5d7d796cd874e563e5ac14_003Fsrc_003Flibraries_003FSystem_002EPrivate_002ECoreLib_003Fsrc_003FSystem_003FMemory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASafeFileHandle_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa03380083db34a2faee436e29e06a72ae8e910_003Fb6_003F67cd826c_003FSafeFileHandle_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASingle_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc99a63bcf3d2a18c20ee19e58ac875ab1edf2a147c8b92ffeed185ab8a44b4_003FSingle_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStringAsserts_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F999ae9cc4ab7b7cfbc5080803e994426e97fd9d87c5b1f44544a799bc114_003FStringAsserts_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003Aud_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa494e0aa381c41ff9484df33e5edb42535e00_003F15_003F87bd9007_003Fud_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003Audis86_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa494e0aa381c41ff9484df33e5edb42535e00_003F95_003F953bbb0f_003Fudis86_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=47ebaefe_002Da806_002D4565_002Dabe7_002D4f14ac675135/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &amp;lt;X86DisassemblerTests&amp;gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;Project Location="C:\Projects\CSharp\ParkanPlayground\X86DisassemblerTests" Presentation="&amp;lt;X86DisassemblerTests&amp;gt;" /&gt;&#xD;
&lt;/SessionState&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=87a33e46_002D2816_002D434f_002D972a_002D703eb7a78476/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="Session" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Namespace&gt;X86DisassemblerTests&lt;/Namespace&gt;&#xD;
&lt;Project Location="C:\Projects\CSharp\ParkanPlayground\X86DisassemblerTests" Presentation="&amp;lt;X86DisassemblerTests&amp;gt;" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/SessionState&gt;</s:String>
</wpf:ResourceDictionary>

View File

@@ -1,19 +0,0 @@
<Solution>
<Folder Name="/meta/">
<File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" />
<File Path="README.md" />
</Folder>
<Project Path="Common\Common.csproj" Type="Classic C#" />
<Project Path="CpDatLib\CpDatLib.csproj" Type="Classic C#" />
<Project Path="MaterialLib\MaterialLib.csproj" Type="Classic C#" />
<Project Path="MissionTmaLib/MissionTmaLib.csproj" />
<Project Path="NLUnpacker/NLUnpacker.csproj" />
<Project Path="NResLib/NResLib.csproj" />
<Project Path="NResUI/NResUI.csproj" />
<Project Path="PalLib\PalLib.csproj" Type="Classic C#" />
<Project Path="ParkanPlayground/ParkanPlayground.csproj" />
<Project Path="ScrLib/ScrLib.csproj" />
<Project Path="TexmLib/TexmLib.csproj" />
<Project Path="VarsetLib/VarsetLib.csproj" />
</Solution>

View File

@@ -1,255 +0,0 @@
using Common;
namespace ParkanPlayground.Effects;
/// <summary>
/// Static reader methods for parsing FXID effect definition structures from binary streams.
/// </summary>
public static class FxidReader
{
/// <summary>
/// Reads a Vector3 (3 floats: X, Y, Z) from the binary stream.
/// </summary>
public static Vector3 ReadVector3(BinaryReader br)
{
float x = br.ReadSingle();
float y = br.ReadSingle();
float z = br.ReadSingle();
return new Vector3(x, y, z);
}
/// <summary>
/// Reads the 60-byte effect header from the binary stream.
/// </summary>
public static EffectHeader ReadEffectHeader(BinaryReader br)
{
EffectHeader h;
h.ComponentCount = br.ReadUInt32();
h.Unknown1 = br.ReadUInt32();
h.Duration = br.ReadSingle();
h.Unknown2 = br.ReadSingle();
h.Flags = br.ReadUInt32();
h.Unknown3 = br.ReadUInt32();
h.Reserved = br.ReadBytes(24);
h.ScaleX = br.ReadSingle();
h.ScaleY = br.ReadSingle();
h.ScaleZ = br.ReadSingle();
return h;
}
/// <summary>
/// Reads a BillboardComponentData (type 1) from the binary stream.
/// </summary>
public static BillboardComponentData ReadBillboardComponent(BinaryReader br, uint typeAndFlags)
{
BillboardComponentData d;
d.TypeAndFlags = typeAndFlags;
d.Unknown04 = br.ReadSingle();
d.ScalarAMin = br.ReadSingle();
d.ScalarAMax = br.ReadSingle();
d.ScalarAExp = br.ReadSingle();
d.ActiveTimeStart = br.ReadSingle();
d.ActiveTimeEnd = br.ReadSingle();
d.SampleSpreadParam = br.ReadSingle();
d.Unknown20 = br.ReadUInt32();
d.PrimarySampleCount = br.ReadUInt32();
d.SecondarySampleCount = br.ReadUInt32();
d.ExtentVec0 = ReadVector3(br);
d.ExtentVec1 = ReadVector3(br);
d.ExtentVec2 = ReadVector3(br);
d.ExponentTriplet0 = ReadVector3(br);
d.RadiusTriplet0 = ReadVector3(br);
d.RadiusTriplet1 = ReadVector3(br);
d.RadiusTriplet2 = ReadVector3(br);
d.ExponentTriplet1 = ReadVector3(br);
d.NoiseAmplitude = br.ReadSingle();
d.Reserved = br.ReadBytes(0x50);
return d;
}
/// <summary>
/// Reads a SoundComponentData (type 2) from the binary stream.
/// </summary>
public static SoundComponentData ReadSoundComponent(BinaryReader br, uint typeAndFlags)
{
SoundComponentData d;
d.TypeAndFlags = typeAndFlags;
d.PlayMode = br.ReadUInt32();
d.StartTime = br.ReadSingle();
d.EndTime = br.ReadSingle();
d.Pos0 = ReadVector3(br);
d.Pos1 = ReadVector3(br);
d.Offset0 = ReadVector3(br);
d.Offset1 = ReadVector3(br);
d.Scalar0Min = br.ReadSingle();
d.Scalar0Max = br.ReadSingle();
d.Scalar1Min = br.ReadSingle();
d.Scalar1Max = br.ReadSingle();
d.SoundFlags = br.ReadUInt32();
d.SoundNameAndReserved = br.ReadBytes(0x40);
return d;
}
/// <summary>
/// Reads an AnimParticleComponentData (type 3) from the binary stream.
/// </summary>
public static AnimParticleComponentData ReadAnimParticleComponent(BinaryReader br, uint typeAndFlags)
{
AnimParticleComponentData d;
d.TypeAndFlags = typeAndFlags;
d.Unknown04 = br.ReadSingle();
d.ScalarAMin = br.ReadSingle();
d.ScalarAMax = br.ReadSingle();
d.ScalarAExp = br.ReadSingle();
d.ActiveTimeStart = br.ReadSingle();
d.ActiveTimeEnd = br.ReadSingle();
d.SampleSpreadParam = br.ReadSingle();
d.Unknown20 = br.ReadUInt32();
d.PrimarySampleCount = br.ReadUInt32();
d.SecondarySampleCount = br.ReadUInt32();
d.ExtentVec0 = ReadVector3(br);
d.ExtentVec1 = ReadVector3(br);
d.ExtentVec2 = ReadVector3(br);
d.ExponentTriplet0 = ReadVector3(br);
d.RadiusTriplet0 = ReadVector3(br);
d.RadiusTriplet1 = ReadVector3(br);
d.RadiusTriplet2 = ReadVector3(br);
d.ExponentTriplet1 = ReadVector3(br);
d.NoiseAmplitude = br.ReadSingle();
d.Reserved = br.ReadBytes(0x38);
return d;
}
/// <summary>
/// Reads an AnimBillboardComponentData (type 4) from the binary stream.
/// </summary>
public static AnimBillboardComponentData ReadAnimBillboardComponent(BinaryReader br, uint typeAndFlags)
{
AnimBillboardComponentData d;
d.TypeAndFlags = typeAndFlags;
d.Unknown04 = br.ReadSingle();
d.ScalarAMin = br.ReadSingle();
d.ScalarAMax = br.ReadSingle();
d.ScalarAExp = br.ReadSingle();
d.ActiveTimeStart = br.ReadSingle();
d.ActiveTimeEnd = br.ReadSingle();
d.SampleSpreadParam = br.ReadSingle();
d.Unknown20 = br.ReadUInt32();
d.PrimarySampleCount = br.ReadUInt32();
d.SecondarySampleCount = br.ReadUInt32();
d.ExtentVec0 = ReadVector3(br);
d.ExtentVec1 = ReadVector3(br);
d.ExtentVec2 = ReadVector3(br);
d.ExponentTriplet0 = ReadVector3(br);
d.RadiusTriplet0 = ReadVector3(br);
d.RadiusTriplet1 = ReadVector3(br);
d.RadiusTriplet2 = ReadVector3(br);
d.ExponentTriplet1 = ReadVector3(br);
d.NoiseAmplitude = br.ReadSingle();
d.Reserved = br.ReadBytes(0x3C);
return d;
}
/// <summary>
/// Reads a TrailComponentData (type 5) from the binary stream.
/// </summary>
public static TrailComponentData ReadTrailComponent(BinaryReader br, uint typeAndFlags)
{
TrailComponentData d;
d.TypeAndFlags = typeAndFlags;
d.Unknown04To10 = br.ReadBytes(0x10);
d.SegmentCount = br.ReadUInt32();
d.Param0 = br.ReadSingle();
d.Param1 = br.ReadSingle();
d.Unknown20 = br.ReadUInt32();
d.Unknown24 = br.ReadUInt32();
d.ActiveTimeStart = br.ReadSingle();
d.ActiveTimeEnd = br.ReadSingle();
d.TextureNameAndReserved = br.ReadBytes(0x40);
return d;
}
/// <summary>
/// Reads a PointComponentData (type 6) from the binary stream.
/// Note: Point components have no payload beyond the typeAndFlags header.
/// </summary>
public static PointComponentData ReadPointComponent(uint typeAndFlags)
{
PointComponentData d;
d.TypeAndFlags = typeAndFlags;
return d;
}
/// <summary>
/// Reads a PlaneComponentData (type 7) from the binary stream.
/// </summary>
public static PlaneComponentData ReadPlaneComponent(BinaryReader br, uint typeAndFlags)
{
PlaneComponentData d;
d.Base = ReadAnimParticleComponent(br, typeAndFlags);
d.ExtraPlaneParam0 = br.ReadUInt32();
d.ExtraPlaneParam1 = br.ReadUInt32();
return d;
}
/// <summary>
/// Reads a ModelComponentData (type 8) from the binary stream.
/// </summary>
public static ModelComponentData ReadModelComponent(BinaryReader br, uint typeAndFlags)
{
ModelComponentData d;
d.TypeAndFlags = typeAndFlags;
d.Unk04 = br.ReadBytes(0x14);
d.ActiveTimeStart = br.ReadSingle();
d.ActiveTimeEnd = br.ReadSingle();
d.Unknown20 = br.ReadUInt32();
d.InstanceCount = br.ReadUInt32();
d.BasePos = ReadVector3(br);
d.OffsetPos = ReadVector3(br);
d.ScatterExtent = ReadVector3(br);
d.Axis0 = ReadVector3(br);
d.Axis1 = ReadVector3(br);
d.Axis2 = ReadVector3(br);
d.Reserved70 = br.ReadBytes(0x18);
d.RadiusTriplet0 = ReadVector3(br);
d.RadiusTriplet1 = ReadVector3(br);
d.ReservedA0 = br.ReadBytes(0x18);
d.TextureNameAndFlags = br.ReadBytes(0x40);
return d;
}
/// <summary>
/// Reads an AnimModelComponentData (type 9) from the binary stream.
/// </summary>
public static AnimModelComponentData ReadAnimModelComponent(BinaryReader br, uint typeAndFlags)
{
AnimModelComponentData d;
d.TypeAndFlags = typeAndFlags;
d.AnimSpeed = br.ReadSingle();
d.MinTime = br.ReadSingle();
d.MaxTime = br.ReadSingle();
d.Exponent = br.ReadSingle();
d.Reserved14 = br.ReadBytes(0x14);
d.DirVec0 = ReadVector3(br);
d.Reserved34 = br.ReadBytes(0x0C);
d.RadiusTriplet0 = ReadVector3(br);
d.DirVec1 = ReadVector3(br);
d.RadiusTriplet1 = ReadVector3(br);
d.ExtentVec0 = ReadVector3(br);
d.ExtentVec1 = ReadVector3(br);
d.Reserved7C = br.ReadBytes(0x0C);
d.TextureNameAndFlags = br.ReadBytes(0x48);
return d;
}
/// <summary>
/// Reads a CubeComponentData (type 10) from the binary stream.
/// </summary>
public static CubeComponentData ReadCubeComponent(BinaryReader br, uint typeAndFlags)
{
CubeComponentData d;
d.Base = ReadAnimBillboardComponent(br, typeAndFlags);
d.ExtraCubeParam0 = br.ReadUInt32();
return d;
}
}

View File

@@ -1,237 +0,0 @@
using Common;
namespace ParkanPlayground.Effects;
/// <summary>
/// Effect-level header at the start of each FXID file (60 bytes total).
/// Parsed from CEffect_InitFromDef: defines component count, global duration/flags,
/// some unknown control fields, and the uniform scale vector applied to the effect.
/// </summary>
public struct EffectHeader
{
public uint ComponentCount;
public uint Unknown1;
public float Duration;
public float Unknown2;
public uint Flags;
public uint Unknown3;
public byte[] Reserved; // 24 bytes
public float ScaleX;
public float ScaleY;
public float ScaleZ;
}
/// <summary>
/// Shared on-disk definition layout for billboard-style components (type 1).
/// Used by CBillboardComponent_Initialize/Update/Render to drive size/color/alpha
/// curves and sample scattering within a 3D extent volume.
/// </summary>
public struct BillboardComponentData
{
public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
public float Unknown04; // mode / flag-like float, semantics not fully clear
public float ScalarAMin; // base scalar A (e.g. base radius)
public float ScalarAMax; // max scalar A
public float ScalarAExp; // exponent applied to scalar A curve
public float ActiveTimeStart; // activation window start (seconds)
public float ActiveTimeEnd; // activation window end (seconds)
public float SampleSpreadParam; // extra param used when scattering samples
public uint Unknown20; // used as integer param in billboard code, exact meaning unknown
public uint PrimarySampleCount; // number of samples along primary axis
public uint SecondarySampleCount; // number of samples along secondary axis
public Vector3 ExtentVec0; // base extent/origin vector
public Vector3 ExtentVec1; // extent center / offset
public Vector3 ExtentVec2; // extent size used for random boxing
public Vector3 ExponentTriplet0;// exponent triplet for size/color curve
public Vector3 RadiusTriplet0; // radius curve key 0
public Vector3 RadiusTriplet1; // radius curve key 1
public Vector3 RadiusTriplet2; // radius curve key 2
public Vector3 ExponentTriplet1;// second exponent triplet (e.g. alpha curve)
public float NoiseAmplitude; // per-sample noise amplitude
public byte[] Reserved; // 0x50-byte tail, currently not touched by billboard code
}
/// <summary>
/// 3D sound component definition (type 2).
/// Used by CSoundComponent_Initialize/Update to drive positional audio, playback
/// window, and scalar ranges (e.g. volume / pitch), plus a 0x40-byte sound name tail.
/// </summary>
public struct SoundComponentData
{
public uint TypeAndFlags; // component type and flags
public uint PlayMode; // playback mode (looping, one-shot, etc.)
public float StartTime; // playback window start (seconds)
public float EndTime; // playback window end (seconds)
public Vector3 Pos0; // base 3D position or path start
public Vector3 Pos1; // secondary position / path end
public Vector3 Offset0; // random offset range 0
public Vector3 Offset1; // random offset range 1
public float Scalar0Min; // scalar range 0 min (e.g. volume)
public float Scalar0Max; // scalar range 0 max
public float Scalar1Min; // scalar range 1 min (e.g. pitch)
public float Scalar1Max; // scalar range 1 max
public uint SoundFlags; // misc sound control flags
public byte[] SoundNameAndReserved; // 0x40-byte tail; sound name plus padding/unused
}
/// <summary>
/// Animated particle component definition (type 3).
/// Prefix layout matches BillboardComponentData and is used to allocate a grid of
/// particle objects; the 0x38-byte tail is passed into CFxManager_LoadTexture.
/// </summary>
public struct AnimParticleComponentData
{
public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
public float Unknown04; // mode / flag-like float, semantics not fully clear
public float ScalarAMin; // base scalar A (e.g. base radius)
public float ScalarAMax; // max scalar A
public float ScalarAExp; // exponent applied to scalar A curve
public float ActiveTimeStart; // activation window start (seconds)
public float ActiveTimeEnd; // activation window end (seconds)
public float SampleSpreadParam; // extra param used when scattering particles
public uint Unknown20; // used as integer param in anim particle code, exact meaning unknown
public uint PrimarySampleCount; // number of particles along primary axis
public uint SecondarySampleCount; // number of particles along secondary axis
public Vector3 ExtentVec0; // base extent/origin vector
public Vector3 ExtentVec1; // extent center / offset
public Vector3 ExtentVec2; // extent size used for random boxing
public Vector3 ExponentTriplet0;// exponent triplet for size/color curve
public Vector3 RadiusTriplet0; // radius curve key 0
public Vector3 RadiusTriplet1; // radius curve key 1
public Vector3 RadiusTriplet2; // radius curve key 2
public Vector3 ExponentTriplet1;// second exponent triplet (e.g. alpha curve)
public float NoiseAmplitude; // per-particle noise amplitude
public byte[] Reserved; // 0x38-byte tail; forwarded to CFxManager_LoadTexture unchanged
}
/// <summary>
/// Animated billboard component definition (type 4).
/// Shares the same prefix layout as BillboardComponentData, including extents and
/// radius/exponent triplets, but uses a 0x3C-byte tail passed to CFxManager_LoadTexture.
/// </summary>
public struct AnimBillboardComponentData
{
public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
public float Unknown04; // mode / flag-like float, semantics not fully clear
public float ScalarAMin; // base scalar A (e.g. base radius)
public float ScalarAMax; // max scalar A
public float ScalarAExp; // exponent applied to scalar A curve
public float ActiveTimeStart; // activation window start (seconds)
public float ActiveTimeEnd; // activation window end (seconds)
public float SampleSpreadParam; // extra param used when scattering animated billboards
public uint Unknown20; // used as integer param in anim billboard code, exact meaning unknown
public uint PrimarySampleCount; // number of samples along primary axis
public uint SecondarySampleCount; // number of samples along secondary axis
public Vector3 ExtentVec0; // base extent/origin vector
public Vector3 ExtentVec1; // extent center / offset
public Vector3 ExtentVec2; // extent size used for random boxing
public Vector3 ExponentTriplet0;// exponent triplet for size/color curve
public Vector3 RadiusTriplet0; // radius curve key 0
public Vector3 RadiusTriplet1; // radius curve key 1
public Vector3 RadiusTriplet2; // radius curve key 2
public Vector3 ExponentTriplet1;// second exponent triplet (e.g. alpha curve)
public float NoiseAmplitude; // per-sample noise amplitude
public byte[] Reserved; // 0x3C-byte tail; forwarded to CFxManager_LoadTexture unchanged
}
/// <summary>
/// Compact definition for trail / ribbon components (type 5).
/// CTrailComponent_Initialize interprets this as segment count, width/alpha/UV
/// ranges, timing, and a shared texture name at +0x30.
/// </summary>
public struct TrailComponentData
{
public uint TypeAndFlags; // component type and flags
public byte[] Unknown04To10; // 0x10 bytes at +4..+0x13, used only indirectly; types unknown
public uint SegmentCount; // number of trail segments (particles)
public float Param0; // first width/alpha/UV control value (start)
public float Param1; // second width/alpha/UV control value (end)
public uint Unknown20; // extra integer parameter, purpose unknown
public uint Unknown24; // extra integer parameter, purpose unknown
public float ActiveTimeStart; // trail activation start time (>= 0)
public float ActiveTimeEnd; // trail activation end time
public byte[] TextureNameAndReserved; // 0x40-byte tail containing texture name and padding/flags
}
/// <summary>
/// Simple point component definition (type 6).
/// Definition block is just the 4-byte typeAndFlags header; no extra data on disk.
/// </summary>
public struct PointComponentData
{
public uint TypeAndFlags; // component type and flags; definition block has no payload
}
/// <summary>
/// Plane component definition (type 7).
/// Shares the same 0xC8-byte prefix layout as AnimParticleComponentData (type 3),
/// followed by two dwords of plane-specific data.
/// </summary>
public struct PlaneComponentData
{
public AnimParticleComponentData Base; // shared 0xC8-byte prefix: time window, sample counts, extents, curves
public uint ExtraPlaneParam0; // plane-specific parameter, semantics not yet reversed
public uint ExtraPlaneParam1; // plane-specific parameter, semantics not yet reversed
}
/// <summary>
/// Static model component definition (type 8).
/// Layout fully matches the IDA typedef used by CModelComponent_Initialize:
/// time window, instance count, spatial extents/axes, radius triplets, and a
/// 0x40-byte texture name tail.
/// </summary>
public struct ModelComponentData
{
public uint TypeAndFlags; // component type and flags
public byte[] Unk04; // 0x14-byte blob at +0x04..+0x17, purpose unclear
public float ActiveTimeStart; // activation window start (seconds), +0x18
public float ActiveTimeEnd; // activation window end (seconds), +0x1C
public uint Unknown20; // extra flags/int parameter at +0x20
public uint InstanceCount; // number of model instances to spawn at +0x24
public Vector3 BasePos; // base position of the emitter / origin, +0x28
public Vector3 OffsetPos; // positional offset applied per-instance, +0x34
public Vector3 ScatterExtent; // extent volume used for random scattering, +0x40
public Vector3 Axis0; // local axis 0 (orientation / shape), +0x4C
public Vector3 Axis1; // local axis 1 (orientation / shape), +0x58
public Vector3 Axis2; // local axis 2 (orientation / shape), +0x64
public byte[] Reserved70; // 0x18 bytes at +0x70..+0x87, not directly used
public Vector3 RadiusTriplet0; // radius / extent triplet 0 at +0x88
public Vector3 RadiusTriplet1; // radius / extent triplet 1 at +0x94
public byte[] ReservedA0; // 0x18 bytes at +0xA0..+0xB7, not directly used
public byte[] TextureNameAndFlags; // 0x40-byte tail at +0xB8: texture name + padding/flags
}
/// <summary>
/// Animated model component definition (type 9).
/// Layout derived from CAnimModelComponent_Initialize: time params, direction vectors,
/// radius triplets, extent vectors, and a 0x48-byte texture name tail.
/// </summary>
public struct AnimModelComponentData
{
public uint TypeAndFlags; // component type and flags
public float AnimSpeed; // animation speed multiplier at +0x04
public float MinTime; // activation window start (clamped >= 0) at +0x08
public float MaxTime; // activation window end at +0x0C
public float Exponent; // exponent for time interpolation at +0x10
public byte[] Reserved14; // 0x14 bytes at +0x14..+0x27, padding
public Vector3 DirVec0; // normalized direction vector 0 at +0x28
public byte[] Reserved34; // 0x0C bytes at +0x34..+0x3F, padding
public Vector3 RadiusTriplet0; // radius triplet 0 at +0x40
public Vector3 DirVec1; // normalized direction vector 1 at +0x4C
public Vector3 RadiusTriplet1; // radius triplet 1 at +0x58
public Vector3 ExtentVec0; // extent vector 0 at +0x64
public Vector3 ExtentVec1; // extent vector 1 at +0x70
public byte[] Reserved7C; // 0x0C bytes at +0x7C..+0x87, padding
public byte[] TextureNameAndFlags; // 0x48-byte tail at +0x88: texture/model name + padding
}
/// <summary>
/// Cube component definition (type 10).
/// Shares the same 0xCC-byte prefix layout as AnimBillboardComponentData (type 4),
/// followed by one dword of cube-specific data.
/// </summary>
public struct CubeComponentData
{
public AnimBillboardComponentData Base; // shared 0xCC-byte prefix: billboard-style time window, extents, curves
public uint ExtraCubeParam0; // cube-specific parameter, semantics not yet reversed
}

View File

@@ -1,350 +0,0 @@
# Документация формата MSH
Формат `.msh` используется игрой Parkan: Железная стратегия (1998) для хранения 3D-мешей.
MSH файлы — это NRes архивы, содержащие несколько типизированных компонентов.
## Обзор
Существует **два варианта** формата MSH:
| Вариант | Применение | Ключевые компоненты | Хранение треугольников |
|---------|------------|---------------------|------------------------|
| **Модель** | Роботы, здания, объекты | 06, 0D, 07 | Индексированные треугольники |
| **Ландшафт** | Террейн | 0B, 15 | Прямые треугольники |
### Автоопределение типа
```
Модель: Есть компонент 06 (индексы) И 0D (батчи)
Ландшафт: Есть компонент 0B (материалы) И НЕТ компонента 06
```
---
## Сводка компонентов
| Тип | Название | Размер элемента | Описание |
|:---:|----------|:---------------:|----------|
| 01 | Pieces | 38 (0x26) | Части меша / тайлы с LOD-ссылками |
| 02 | Submeshes | 68 (0x44) | LOD части с баундинг-боксами |
| 03 | Vertices | 12 (0x0C) | Позиции вершин (Vector3) |
| 04 | неизвестно | 4 | (неизвестно) |
| 05 | неизвестно | 4 | (неизвестно) |
| 06 | Indices | 2 | Индексы вершин треугольников (только Модель) |
| 07 | неизвестно | 16 | (только Модель) |
| 08 | Animations | 4 | Кейфреймы анимации меша |
| 0A | ExternalRefs | переменный | Внешние ссылки на меши (строки) |
| 0B | неизвестно | 4 | неизвестно (только Ландшафт) |
| 0D | неизвестно | 20 (0x14) | неизвестно (только Модель) |
| 0E | неизвестно | 4 | неизвестно (только Ландшафт) |
| 12 | MicrotextureMap | 4 | неизвестно |
| 13 | ShortAnims | 2 | Короткие индексы анимаций |
| 15 | неизвестно | 28 (0x1C) | неизвестно |
---
## Поток данных
### Модель (роботы, здания)
```
Компонент 01 (Pieces - части)
└─► Lod[n] ──► Компонент 02 (индекс сабмеша)
├─► StartIndexIn07 ──► Компонент 07 (данные на треугольник)
└─► StartOffsetIn0d:ByteLengthIn0D ──► Компонент 0D (батчи)
├─► IndexInto06:CountOf06 ──► Компонент 06 (индексы)
│ │
│ └─► Компонент 03 (вершины)
└─► IndexInto03 (базовое смещение вершины)
```
### Ландшафт (террейн)
```
Компонент 01 (Тайлы, обычно 16×16 = 256)
└─► Lod[n] ──► Компонент 02 (индекс сабмеша)
└─► StartIndexIn07:CountIn07 ──► Компонент 15 (треугольники)
└─► Vertex1/2/3Index ──► Компонент 03 (вершины)
└─► StartIndexIn07:CountIn07 ──► Компонент 0B (материалы, параллельно 15)
```
**Важно:** В ландшафтных мешах поля `StartIndexIn07` и `CountIn07` в Компоненте 02
используются для индексации в Компонент 15 (треугольники), а не в Компонент 07.
---
## Структуры компонентов
### Компонент 01 - Pieces (0x26 = 38 байт)
Определяет части меша (для моделей) или тайлы террейна (для ландшафтов).
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 1 | byte | Type1 | Флаги типа части |
| 0x01 | 1 | byte | Type2 | Дополнительные флаги |
| 0x02 | 2 | int16 | ParentIndex | Индекс родителя (-1 = корень) |
| 0x04 | 2 | int16 | OffsetIntoFile13 | Смещение в короткие анимации |
| 0x06 | 2 | int16 | IndexInFile08 | Индекс в анимации |
| 0x08 | 30 | ushort[15] | Lod | Индексы сабмешей по LOD-уровням (0xFFFF = не используется) |
**Ландшафт:** 256 тайлов в сетке 16×16. Каждый тайл имеет 2 LOD (индексы 0-255 и 256-511).
---
### Компонент 02 - Submeshes (Заголовок: 0x8C = 140 байт, Элемент: 0x44 = 68 байт)
#### Заголовок (140 байт)
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 96 | Vector3[8] | BoundingBox | 8-точечный баундинг-бокс |
| 0x60 | 12 | Vector3 | Center | Центральная точка |
| 0x6C | 4 | float | CenterW | W-компонента |
| 0x70 | 12 | Vector3 | Bottom | Нижняя точка |
| 0x7C | 12 | Vector3 | Top | Верхняя точка |
| 0x88 | 4 | float | XYRadius | Радиус в плоскости XY |
#### Элемент (68 байт)
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | StartIndexIn07 | **Модель:** Начальный индекс в Компоненте 07<br>**Ландшафт:** Начальный индекс треугольника в Компоненте 15 |
| 0x02 | 2 | ushort | CountIn07 | **Модель:** Количество в Компоненте 07<br>**Ландшафт:** Количество треугольников |
| 0x04 | 2 | ushort | StartOffsetIn0d | Начальное смещение в Компоненте 0D (только Модель) |
| 0x06 | 2 | ushort | ByteLengthIn0D | Количество батчей в Компоненте 0D (только Модель) |
| 0x08 | 12 | Vector3 | LocalMinimum | Минимум локального баундинг-бокса |
| 0x14 | 12 | Vector3 | LocalMaximum | Максимум локального баундинг-бокса |
| 0x20 | 12 | Vector3 | Center | Центр сабмеша |
| 0x2C | 12 | Vector3 | Vector4 | Неизвестно |
| 0x38 | 12 | Vector3 | Vector5 | Неизвестно |
---
### Компонент 03 - Vertices (0x0C = 12 байт)
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 4 | float | X | Координата X |
| 0x04 | 4 | float | Y | Координата Y |
| 0x08 | 4 | float | Z | Координата Z |
---
### Компонент 06 - Indices (2 байта) - Только Модель
Массив `ushort` значений — индексы вершин треугольников.
Используются группами по 3 для каждого треугольника. Ссылки через батчи Компонента 0D.
---
### Компонент 07 - Triangle Data (0x10 = 16 байт) - Только Модель
Данные рендеринга на каждый треугольник.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | Flags | Флаги рендера |
| 0x02 | 2 | ushort | Magic02 | Неизвестно |
| 0x04 | 2 | ushort | Magic04 | Неизвестно |
| 0x06 | 2 | ushort | Magic06 | Неизвестно |
| 0x08 | 2 | int16 | OffsetX | Нормализованный X (÷32767 для -1..1) |
| 0x0A | 2 | int16 | OffsetY | Нормализованный Y (÷32767 для -1..1) |
| 0x0C | 2 | int16 | OffsetZ | Нормализованный Z (÷32767 для -1..1) |
| 0x0E | 2 | ushort | Magic14 | Неизвестно |
---
### Компонент 0B - Material Data (4 байта) - Только Ландшафт
Информация о материале/текстуре на каждый треугольник. Параллельный массив к Компоненту 15.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | HighWord | Индекс материала/текстуры |
| 0x02 | 2 | ushort | LowWord | Индекс треугольника (последовательный) |
---
### Компонент 0D - Draw Batches (0x14 = 20 байт) - Только Модель
Определяет батчи вызовов отрисовки.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | Flags | Флаги батча |
| 0x02 | 2 | - | Padding | - |
| 0x04 | 1 | byte | Magic04 | Неизвестно |
| 0x05 | 1 | byte | Magic05 | Неизвестно |
| 0x06 | 2 | ushort | Magic06 | Неизвестно |
| 0x08 | 2 | ushort | CountOf06 | Количество индексов для отрисовки |
| 0x0A | 4 | int32 | IndexInto06 | Начальный индекс в Компоненте 06 |
| 0x0E | 2 | ushort | CountOf03 | Количество вершин |
| 0x10 | 4 | int32 | IndexInto03 | Базовое смещение вершины в Компоненте 03 |
---
### Компонент 15 - Triangles (0x1C = 28 байт)
Прямые определения треугольников. Используется и Моделью и Ландшафтом,
но только Ландшафт использует их напрямую для рендеринга.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 4 | uint32 | Flags | Флаги треугольника (0x20000 = коллизия) |
| 0x04 | 4 | uint32 | MaterialData | Данные материала (см. ниже) |
| 0x08 | 2 | ushort | Vertex1Index | Индекс первой вершины |
| 0x0A | 2 | ushort | Vertex2Index | Индекс второй вершины |
| 0x0C | 2 | ushort | Vertex3Index | Индекс третьей вершины |
| 0x0E | 4 | uint32 | Magic0E | Неизвестно |
| 0x12 | 4 | uint32 | Magic12 | Неизвестно |
| 0x16 | 4 | uint32 | Magic16 | Неизвестно |
| 0x1A | 2 | ushort | Magic1A | Неизвестно |
#### MaterialData (0x04) - Структура материала
```
MaterialData = 0xFFFF_SSPP
│ │└─ PP: Основной материал (byte 0)
│ └─── SS: Вторичный материал для блендинга (byte 1)
└────── Всегда 0xFFFF (байты 2-3)
```
| Значение SS | Описание |
|:-----------:|----------|
| 0xFF | Сплошной материал (без блендинга) |
| 0x01-0xFE | Индекс вторичного материала для блендинга |
Примеры:
- `0xFFFFFF01` = Сплошной материал 1
- `0xFFFF0203` = Материал 3 с блендингом в материал 2
---
### Компонент 0A - External References (переменный размер)
Таблица строк для внешних ссылок на части меша. Формат:
```
[4 байта: длина] [байты строки] [null-терминатор]
...повтор...
```
Длина 0 означает пустую запись. Строки типа `"central"` имеют особое значение (flag |= 1).
---
## Пример: Ландшафт SC_1
```
Land.msh (SC_1):
├── 01: 256 тайлов (сетка 16×16)
├── 02: 512 сабмешей (256 LOD0 + 256 LOD1)
├── 03: 10 530 вершин
├── 04: 10 530 данных на вершину
├── 05: 10 530 данных на вершину
├── 0B: 7 882 записи материалов
├── 0E: 10 530 данных на вершину
├── 12: 10 530 микротекстурный маппинг
└── 15: 7 882 треугольника
├── LOD 0: 4 993 треугольника (тайлы 0-255 → сабмеши 0-255)
└── LOD 1: 2 889 треугольников (тайлы 0-255 → сабмеши 256-511)
```
---
## Использование
```csharp
var converter = new MshConverter();
// Автоопределение типа и конвертация в OBJ
converter.Convert("Land.msh", "terrain.obj", lodLevel: 0);
converter.Convert("robot.msh", "robot.obj", lodLevel: 0);
// Ручное определение типа
var archive = NResParser.ReadFile("mesh.msh").Archive;
var type = MshConverter.DetectMeshType(archive);
// Возвращает: MshType.Model или MshType.Landscape
```
---
## Формат WEA - Файлы материалов ландшафта
Файлы `.wea` — текстовые файлы, определяющие таблицу материалов для ландшафта.
### Формат
```
{count}
{index} {material_name}
{index} {material_name}
...
```
### Связь с Land.msh
Каждая карта имеет два файла материалов:
| Файл | Используется для | Треугольники в Comp15 |
|------|------------------|----------------------|
| `Land1.wea` | LOD0 (высокая детализация) | Первые N (сумма CountIn07 для LOD0) |
| `Land2.wea` | LOD1 (низкая детализация) | Остальные |
### Пример (SC_1)
**Land1.wea:**
```
4
0 B_S0
1 L04
2 L02
3 L00
```
**Land2.wea:**
```
4
0 DEFAULT
1 L05
2 L03
3 L01
```
### Маппинг материалов
Индекс материала в `Comp15.MaterialData & 0xFF` → строка в `.wea` файле.
```
Треугольник с MaterialData = 0xFFFF0102
└─ Основной материал = 02 → Land1.wea[2] = "L02"
└─ Блендинг с материалом = 01 → Land1.wea[1] = "L04"
```
### Типичные имена материалов
| Префикс | Назначение |
|---------|------------|
| L00-L05 | Текстуры ландшафта (grass, dirt, etc.) |
| B_S0 | Базовая текстура |
| DEFAULT | Фолбэк для LOD1 |
| WATER | Вода (поверхность) |
| WATER_BOT | Вода (дно) |
| WATER_M | Вода LOD1 |
---
## Источники
- Реверс-инжиниринг `Terrain.dll` (класс CLandscape)
- Декомпиляция Ghidra: `CLandscape::ctor` и `IMesh2_of_CLandscape::Render`

View File

@@ -1,77 +0,0 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
public static class Msh01
{
public static Msh01Component ReadComponent(FileStream mshFs, NResArchive archive)
{
var headerFileEntry = archive.Files.FirstOrDefault(x => x.FileType == "01 00 00 00");
if (headerFileEntry is null)
{
throw new Exception("Archive doesn't contain header file (01)");
}
var data = new byte[headerFileEntry.ElementCount * headerFileEntry.ElementSize];
mshFs.Seek(headerFileEntry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var dataSpan = data.AsSpan();
var elements = new List<SubMesh>((int)headerFileEntry.ElementCount);
for (var i = 0; i < headerFileEntry.ElementCount; i++)
{
var element = new SubMesh()
{
Type1 = dataSpan[i * headerFileEntry.ElementSize + 0],
Type2 = dataSpan[i * headerFileEntry.ElementSize + 1],
ParentIndex =
BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 2)),
OffsetIntoFile13 =
BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 4)),
IndexInFile08 =
BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 6))
};
element.Lod[0] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 8));
element.Lod[1] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 10));
element.Lod[2] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 12));
element.Lod[3] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 14));
element.Lod[4] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 16));
element.Lod[5] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 18));
element.Lod[6] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 20));
element.Lod[7] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 22));
element.Lod[8] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 24));
element.Lod[9] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 26));
element.Lod[10] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 28));
element.Lod[11] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 30));
element.Lod[12] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 32));
element.Lod[13] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 34));
element.Lod[14] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 36));
elements.Add(element);
}
return new Msh01Component()
{
Elements = elements
};
}
public class Msh01Component
{
public List<SubMesh> Elements { get; set; }
}
public class SubMesh
{
public byte Type1 { get; set; }
public byte Type2 { get; set; }
public short ParentIndex { get; set; }
public short OffsetIntoFile13 { get; set; }
public short IndexInFile08 { get; set; }
public ushort[] Lod { get; set; } = new ushort[15];
}
}

View File

@@ -1,193 +0,0 @@
using System.Buffers.Binary;
using Common;
using NResLib;
namespace ParkanPlayground;
public static class Msh02
{
public static Msh02Component ReadComponent(FileStream mshFs, NResArchive archive)
{
var fileEntry = archive.Files.FirstOrDefault(x => x.FileType == "02 00 00 00");
if (fileEntry is null)
{
throw new Exception("Archive doesn't contain 02 component");
}
var data = new byte[fileEntry.FileLength];
mshFs.Seek(fileEntry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var header = data.AsSpan(0, 0x8c); // 140 bytes header
var center = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x60)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x64)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x68))
);
var centerW = BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x6c));
var bb = new BoundingBox();
bb.BottomFrontLeft = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(4)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(8))
);
bb.BottomFrontRight = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(12)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(16)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(20))
);
bb.BottomBackRight = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(24)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(28)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(32))
);
bb.BottomBackLeft = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(36)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(40)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(44))
);
bb.TopFrontLeft = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(48)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(52)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(56))
);
bb.TopFrontRight = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(60)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(64)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(68))
);
bb.TopBackRight = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(72)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(76)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(80))
);
bb.TopBackLeft = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(84)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(88)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(92))
);
var bottom = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(112)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(116)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(120))
);
var top = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(124)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(128)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(132))
);
var xyRadius = BinaryPrimitives.ReadSingleLittleEndian(header.Slice(136));
List<Msh02Element> elements = new List<Msh02Element>();
var skippedHeader = data.AsSpan(0x8c); // skip header
for (var i = 0; i < fileEntry.ElementCount; i++)
{
var element = new Msh02Element();
element.StartIndexIn07 =
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 0));
element.CountIn07 =
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 2));
element.StartOffsetIn0d =
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 4));
element.ByteLengthIn0D =
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 6));
element.LocalMinimum = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 8)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 12)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 16))
);
element.LocalMaximum = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 20)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 24)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 28))
);
element.Center = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 32)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 36)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 40))
);
element.Vector4 = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 44)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 48)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 52))
);
element.Vector5 = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 56)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 60)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 64))
);
elements.Add(element);
_ = 5;
}
return new Msh02Component()
{
Header = new Msh02Header()
{
BoundingBox = bb,
Center = center,
CenterW = centerW,
Bottom = bottom,
Top = top,
XYRadius = xyRadius
},
Elements = elements
};
}
public class Msh02Component
{
public Msh02Header Header { get; set; }
public List<Msh02Element> Elements { get; set; }
}
/// <summary>
/// 140 байт в начале файла
/// </summary>
public class Msh02Header
{
public BoundingBox BoundingBox { get; set; }
public Vector3 Center { get; set; }
public float CenterW { get; set; }
public Vector3 Bottom { get; set; }
public Vector3 Top { get; set; }
public float XYRadius { get; set; }
}
public class Msh02Element
{
public ushort StartIndexIn07 { get; set; }
public ushort CountIn07 { get; set; }
public ushort StartOffsetIn0d { get; set; }
public ushort ByteLengthIn0D { get; set; }
public Vector3 LocalMinimum { get; set; }
public Vector3 LocalMaximum { get; set; }
public Vector3 Center { get; set; }
public Vector3 Vector4 { get; set; }
public Vector3 Vector5 { get; set; }
}
/// <summary>
/// 96 bytes - bounding box (8 points each 3 float = 96 bytes)
/// 0x60 bytes or 0x18 by 4 bytes
/// </summary>
public class BoundingBox
{
public Vector3 BottomFrontLeft { get; set; }
public Vector3 BottomFrontRight { get; set; }
public Vector3 BottomBackRight { get; set; }
public Vector3 BottomBackLeft { get; set; }
public Vector3 TopBackRight { get; set; }
public Vector3 TopFrontRight { get; set; }
public Vector3 TopBackLeft { get; set; }
public Vector3 TopFrontLeft { get; set; }
}
}

View File

@@ -1,35 +0,0 @@
using System.Buffers.Binary;
using Common;
using NResLib;
namespace ParkanPlayground;
public class Msh03
{
public static List<Vector3> ReadComponent(FileStream mshFs, NResArchive mshNres)
{
var verticesFileEntry = mshNres.Files.FirstOrDefault(x => x.FileType == "03 00 00 00");
if (verticesFileEntry is null)
{
throw new Exception("Archive doesn't contain vertices file (03)");
}
if (verticesFileEntry.ElementSize != 12)
{
throw new Exception("Vertices file (03) element size is not 12");
}
var verticesFile = new byte[verticesFileEntry.ElementCount * verticesFileEntry.ElementSize];
mshFs.Seek(verticesFileEntry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(verticesFile, 0, verticesFile.Length);
var vertices = verticesFile.Chunk(12).Select(x => new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(x.AsSpan(0)),
BinaryPrimitives.ReadSingleLittleEndian(x.AsSpan(4)),
BinaryPrimitives.ReadSingleLittleEndian(x.AsSpan(8))
)
).ToList();
return vertices;
}
}

View File

@@ -1,32 +0,0 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
public static class Msh06
{
public static List<ushort> ReadComponent(
FileStream mshFs, NResArchive archive)
{
var entry = archive.Files.FirstOrDefault(x => x.FileType == "06 00 00 00");
if (entry is null)
{
throw new Exception("Archive doesn't contain file (06)");
}
var data = new byte[entry.ElementCount * entry.ElementSize];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elements = new List<ushort>((int)entry.ElementCount);
for (var i = 0; i < entry.ElementCount; i++)
{
elements.Add(
BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(i * 2))
);
}
return elements;
}
}

View File

@@ -1,53 +0,0 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
public static class Msh07
{
public static List<Msh07Element> ReadComponent(
FileStream mshFs, NResArchive archive)
{
var entry = archive.Files.FirstOrDefault(x => x.FileType == "07 00 00 00");
if (entry is null)
{
throw new Exception("Archive doesn't contain file (07)");
}
var data = new byte[entry.ElementCount * entry.ElementSize];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elementBytes = data.Chunk(16);
var elements = elementBytes.Select(x => new Msh07Element()
{
Flags = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)),
Magic02 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(2)),
Magic04 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)),
Magic06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
OffsetX = BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(8)),
OffsetY = BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(10)),
OffsetZ = BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(12)),
Magic14 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(14)),
}).ToList();
return elements;
}
public class Msh07Element
{
public ushort Flags { get; set; }
public ushort Magic02 { get; set; }
public ushort Magic04 { get; set; }
public ushort Magic06 { get; set; }
// normalized vector X, need to divide by 32767 to get float in range -1..1
public short OffsetX { get; set; }
// normalized vector Y, need to divide by 32767 to get float in range -1..1
public short OffsetY { get; set; }
// normalized vector Z, need to divide by 32767 to get float in range -1..1
public short OffsetZ { get; set; }
public ushort Magic14 { get; set; }
}
}

View File

@@ -1,49 +0,0 @@
using System.Buffers.Binary;
using System.Text;
using NResLib;
namespace ParkanPlayground;
public class Msh0A
{
public static List<string> ReadComponent(FileStream mshFs, NResArchive archive)
{
var aFileEntry = archive.Files.FirstOrDefault(x => x.FileType == "0A 00 00 00");
if (aFileEntry is null)
{
throw new Exception("Archive doesn't contain 0A component");
}
var data = new byte[aFileEntry.FileLength];
mshFs.Seek(aFileEntry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
int pos = 0;
var strings = new List<string>();
while (pos < data.Length)
{
var len = BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(pos));
if (len == 0)
{
pos += 4; // empty entry, no string attached
strings.Add(""); // add empty string
}
else
{
// len is not 0, we need to read it
var strBytes = data.AsSpan(pos + 4, len);
var str = Encoding.UTF8.GetString(strBytes);
strings.Add(str);
pos += len + 4 + 1; // skip length prefix and string itself, +1, because it's null-terminated
}
}
if (strings.Count != aFileEntry.ElementCount)
{
throw new Exception("String count mismatch in 0A component");
}
return strings;
}
}

View File

@@ -1,55 +0,0 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
public static class Msh0D
{
public const int ElementSize = 20;
public static List<Msh0DElement> ReadComponent(
FileStream mshFs, NResArchive archive)
{
var entry = archive.Files.FirstOrDefault(x => x.FileType == "0D 00 00 00");
if (entry is null)
{
throw new Exception("Archive doesn't contain file (0D)");
}
var data = new byte[entry.ElementCount * entry.ElementSize];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elementBytes = data.Chunk(ElementSize);
var elements = elementBytes.Select(x => new Msh0DElement()
{
Flags = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)),
Magic04 = x.AsSpan(4)[0],
Magic05 = x.AsSpan(5)[0],
Magic06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
CountOf06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
IndexInto06 = BinaryPrimitives.ReadInt32LittleEndian(x.AsSpan(0xA)),
CountOf03 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0xE)),
IndexInto03 = BinaryPrimitives.ReadInt32LittleEndian(x.AsSpan(0x10)),
}).ToList();
return elements;
}
public class Msh0DElement
{
public uint Flags { get; set; }
// Magic04 и Magic06 обрабатываются вместе
public byte Magic04 { get; set; }
public byte Magic05 { get; set; }
public ushort Magic06 { get; set; }
public ushort CountOf06 { get; set; }
public int IndexInto06 { get; set; }
public ushort CountOf03 { get; set; }
public int IndexInto03 { get; set; }
}
}

View File

@@ -1,55 +0,0 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
public static class Msh15
{
public static List<Msh15Element> ReadComponent(
FileStream mshFs, NResArchive archive)
{
var entry = archive.Files.FirstOrDefault(x => x.FileType == "15 00 00 00");
if (entry is null)
{
throw new Exception("Archive doesn't contain file (15)");
}
var data = new byte[entry.ElementCount * entry.ElementSize];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elementBytes = data.Chunk(28);
var elements = elementBytes.Select(x => new Msh15Element()
{
Flags = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0)),
Magic04 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(4)),
Vertex1Index = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
Vertex2Index = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(10)),
Vertex3Index = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(12)),
Magic0E = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(14)),
Magic12 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(18)),
Magic16 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(22)),
Magic1A = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(26)),
}).ToList();
return elements;
}
public class Msh15Element
{
public uint Flags { get; set; }
public uint Magic04 { get; set; }
public ushort Vertex1Index { get; set; }
public ushort Vertex2Index { get; set; }
public ushort Vertex3Index { get; set; }
public uint Magic0E { get; set; }
public uint Magic12 { get; set; }
public uint Magic16 { get; set; }
public ushort Magic1A { get; set; }
}
}

View File

@@ -1,314 +0,0 @@
using System.Text;
using Common;
using NResLib;
namespace ParkanPlayground;
public enum MshType
{
Unknown,
Model, // Has component 06 (indices), 0D (batches), 07
Landscape // Has component 0B (per-triangle material), uses 15 directly
}
public class MshConverter
{
/// <summary>
/// Detects mesh type based on which components are present in the archive.
/// </summary>
public static MshType DetectMeshType(NResArchive archive)
{
bool hasComponent06 = archive.Files.Any(f => f.FileType == "06 00 00 00");
bool hasComponent0B = archive.Files.Any(f => f.FileType == "0B 00 00 00");
bool hasComponent0D = archive.Files.Any(f => f.FileType == "0D 00 00 00");
// Model: Uses indexed triangles via component 06 and batches via 0D
if (hasComponent06 && hasComponent0D)
return MshType.Model;
// Landscape: Uses direct triangles in component 15, with material data in 0B
if (hasComponent0B && !hasComponent06)
return MshType.Landscape;
return MshType.Unknown;
}
/// <summary>
/// Converts a .msh file to OBJ format, auto-detecting mesh type.
/// </summary>
/// <param name="mshPath">Path to the .msh file</param>
/// <param name="outputPath">Output OBJ path (optional, defaults to input name + .obj)</param>
/// <param name="lodLevel">LOD level to export (0 = highest detail)</param>
public void Convert(string mshPath, string? outputPath = null, int lodLevel = 0)
{
var mshNresResult = NResParser.ReadFile(mshPath);
if (mshNresResult.Archive is null)
{
Console.WriteLine($"ERROR: Failed to read NRes archive: {mshNresResult.Error}");
return;
}
var archive = mshNresResult.Archive;
var meshType = DetectMeshType(archive);
outputPath ??= Path.ChangeExtension(mshPath, ".obj");
Console.WriteLine($"Converting: {Path.GetFileName(mshPath)}");
Console.WriteLine($"Detected type: {meshType}");
using var fs = new FileStream(mshPath, FileMode.Open, FileAccess.Read, FileShare.Read);
switch (meshType)
{
case MshType.Model:
ConvertModel(fs, archive, outputPath, lodLevel);
break;
case MshType.Landscape:
ConvertLandscape(fs, archive, outputPath, lodLevel);
break;
default:
Console.WriteLine("ERROR: Unknown mesh type, cannot convert.");
break;
}
}
/// <summary>
/// Converts a model mesh (robots, buildings, etc.) to OBJ.
/// Uses indexed triangles: 01 → 02 → 0D → 06 → 03
/// </summary>
private void ConvertModel(FileStream fs, NResArchive archive, string outputPath, int lodLevel)
{
var component01 = Msh01.ReadComponent(fs, archive);
var component02 = Msh02.ReadComponent(fs, archive);
var component03 = Msh03.ReadComponent(fs, archive);
var component06 = Msh06.ReadComponent(fs, archive);
var component07 = Msh07.ReadComponent(fs, archive);
var component0D = Msh0D.ReadComponent(fs, archive);
Console.WriteLine($"Vertices: {component03.Count}");
Console.WriteLine($"Pieces: {component01.Elements.Count}");
Console.WriteLine($"Submeshes: {component02.Elements.Count}");
using var sw = new StreamWriter(outputPath, false, new UTF8Encoding(false));
sw.WriteLine($"# Model mesh converted from {Path.GetFileName(outputPath)}");
sw.WriteLine($"# LOD level: {lodLevel}");
// Write all vertices
foreach (var v in component03)
sw.WriteLine($"v {v.X:F6} {v.Y:F6} {v.Z:F6}");
int exportedFaces = 0;
for (var pieceIndex = 0; pieceIndex < component01.Elements.Count; pieceIndex++)
{
var piece = component01.Elements[pieceIndex];
// Get submesh index for requested LOD
if (lodLevel >= piece.Lod.Length)
continue;
var submeshIdx = piece.Lod[lodLevel];
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
continue;
sw.WriteLine($"g piece_{pieceIndex}");
var submesh = component02.Elements[submeshIdx];
var batchStart = submesh.StartOffsetIn0d;
var batchCount = submesh.ByteLengthIn0D;
for (var batchIdx = 0; batchIdx < batchCount; batchIdx++)
{
var batch = component0D[batchStart + batchIdx];
var baseVertex = batch.IndexInto03;
var indexStart = batch.IndexInto06;
var indexCount = batch.CountOf06;
for (int i = 0; i < indexCount; i += 3)
{
var i1 = baseVertex + component06[indexStart + i];
var i2 = baseVertex + component06[indexStart + i + 1];
var i3 = baseVertex + component06[indexStart + i + 2];
sw.WriteLine($"f {i1 + 1} {i2 + 1} {i3 + 1}");
exportedFaces++;
}
}
}
Console.WriteLine($"Exported: {component03.Count} vertices, {exportedFaces} faces");
Console.WriteLine($"Output: {outputPath}");
}
/// <summary>
/// Converts a landscape mesh (terrain) to OBJ.
/// Uses direct triangles: 01 → 02 → 15 (via StartIndexIn07/CountIn07)
/// </summary>
private void ConvertLandscape(FileStream fs, NResArchive archive, string outputPath, int lodLevel)
{
var component01 = Msh01.ReadComponent(fs, archive);
var component02 = Msh02.ReadComponent(fs, archive);
var component03 = Msh03.ReadComponent(fs, archive);
var component15 = Msh15.ReadComponent(fs, archive);
Console.WriteLine($"Vertices: {component03.Count}");
Console.WriteLine($"Triangles: {component15.Count}");
Console.WriteLine($"Tiles: {component01.Elements.Count}");
Console.WriteLine($"Submeshes: {component02.Elements.Count}");
using var sw = new StreamWriter(outputPath, false, new UTF8Encoding(false));
sw.WriteLine($"# Landscape mesh converted from {Path.GetFileName(outputPath)}");
sw.WriteLine($"# LOD level: {lodLevel}");
sw.WriteLine($"# Tile grid: {(int)Math.Sqrt(component01.Elements.Count)}x{(int)Math.Sqrt(component01.Elements.Count)}");
// Write all vertices
foreach (var v in component03)
sw.WriteLine($"v {v.X:F6} {v.Y:F6} {v.Z:F6}");
int exportedFaces = 0;
for (var tileIdx = 0; tileIdx < component01.Elements.Count; tileIdx++)
{
var tile = component01.Elements[tileIdx];
// Get submesh index for requested LOD
if (lodLevel >= tile.Lod.Length)
continue;
var submeshIdx = tile.Lod[lodLevel];
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
continue;
sw.WriteLine($"g tile_{tileIdx}");
var submesh = component02.Elements[submeshIdx];
// For landscape, StartIndexIn07 = triangle start index, CountIn07 = triangle count
var triangleStart = submesh.StartIndexIn07;
var triangleCount = submesh.CountIn07;
for (var triOffset = 0; triOffset < triangleCount; triOffset++)
{
var triIdx = triangleStart + triOffset;
if (triIdx >= component15.Count)
{
Console.WriteLine($"WARNING: Triangle index {triIdx} out of range for tile {tileIdx}");
continue;
}
var tri = component15[triIdx];
sw.WriteLine($"f {tri.Vertex1Index + 1} {tri.Vertex2Index + 1} {tri.Vertex3Index + 1}");
exportedFaces++;
}
}
Console.WriteLine($"Exported: {component03.Count} vertices, {exportedFaces} faces");
Console.WriteLine($"Output: {outputPath}");
}
public record Face(Vector3 P1, Vector3 P2, Vector3 P3);
public static void ExportCube(string filePath, Vector3[] points)
{
if (points.Length != 8)
throw new ArgumentException("Cube must have exactly 8 points.");
using (StreamWriter writer = new StreamWriter(filePath))
{
// Write vertices
foreach (var p in points)
{
writer.WriteLine($"v {p.X} {p.Y} {p.Z}");
}
// Write faces (each face defined by 4 vertices, using 1-based indices)
int[][] faces = new int[][]
{
new int[] { 1, 2, 3, 4 }, // bottom
new int[] { 5, 6, 7, 8 }, // top
new int[] { 1, 2, 6, 5 }, // front
new int[] { 2, 3, 7, 6 }, // right
new int[] { 3, 4, 8, 7 }, // back
new int[] { 4, 1, 5, 8 } // left
};
foreach (var f in faces)
{
writer.WriteLine($"f {f[0]} {f[1]} {f[2]} {f[3]}");
}
}
}
public static void ExportCubesAtPositions(string filePath, List<Vector3> centers, float size = 2f)
{
float half = size / 2f;
using (StreamWriter writer = new StreamWriter(filePath))
{
int vertexOffset = 0;
foreach (var c in centers)
{
// Generate 8 vertices for this cube
Vector3[] vertices = new Vector3[]
{
new Vector3(c.X - half, c.Y - half, c.Z - half),
new Vector3(c.X + half, c.Y - half, c.Z - half),
new Vector3(c.X + half, c.Y - half, c.Z + half),
new Vector3(c.X - half, c.Y - half, c.Z + half),
new Vector3(c.X - half, c.Y + half, c.Z - half),
new Vector3(c.X + half, c.Y + half, c.Z - half),
new Vector3(c.X + half, c.Y + half, c.Z + half),
new Vector3(c.X - half, c.Y + half, c.Z + half)
};
// Write vertices
foreach (var v in vertices)
{
writer.WriteLine($"v {v.X} {v.Y} {v.Z}");
}
// Define faces (1-based indices, counter-clockwise)
int[][] faces = new int[][]
{
new int[] { 1, 2, 3, 4 }, // bottom
new int[] { 5, 6, 7, 8 }, // top
new int[] { 1, 2, 6, 5 }, // front
new int[] { 2, 3, 7, 6 }, // right
new int[] { 3, 4, 8, 7 }, // back
new int[] { 4, 1, 5, 8 } // left
};
// Write faces with offset
foreach (var f in faces)
{
writer.WriteLine(
$"f {f[0] + vertexOffset} {f[1] + vertexOffset} {f[2] + vertexOffset} {f[3] + vertexOffset}");
}
vertexOffset += 8;
}
}
}
void Export(string filePath, IEnumerable<Vector3> vertices, List<IndexedEdge> edges)
{
using (var writer = new StreamWriter(filePath))
{
writer.WriteLine("# Exported OBJ file");
// Write vertices
foreach (var v in vertices)
{
writer.WriteLine($"v {v.X:F2} {v.Y:F2} {v.Z:F2}");
}
// Write edges as lines ("l" elements in .obj format)
foreach (var e in edges)
{
// OBJ uses 1-based indexing
writer.WriteLine($"l {e.Index1 + 1} {e.Index2 + 1}");
}
}
}
}

View File

@@ -2,16 +2,19 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" />
<ProjectReference Include="..\NResLib\NResLib.csproj" /> <ProjectReference Include="..\NResLib\NResLib.csproj" />
<ProjectReference Include="..\PalLib\PalLib.csproj" />
<ProjectReference Include="..\ScrLib\ScrLib.csproj" /> <ProjectReference Include="..\ScrLib\ScrLib.csproj" />
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" /> <ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
<ProjectReference Include="..\MaterialLib\MaterialLib.csproj" /> </ItemGroup>
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
<ItemGroup>
<PackageReference Include="SharpDisasm" Version="1.1.11" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,332 +1,116 @@
using System; using System.Buffers.Binary;
using System.Collections.Generic; using System.Numerics;
using System.IO; using System.Text.Json;
using System.Text; using ScrLib;
using ParkanPlayground.Effects; using SharpDisasm;
using static ParkanPlayground.Effects.FxidReader; using VarsetLib;
Console.OutputEncoding = Encoding.UTF8;
if (args.Length == 0) // var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\default.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scr_pl_1.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scream.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scream1.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\varset.var";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\preload.lda";
//
// var fs = new FileStream(path, FileMode.Open);
//
// var count = fs.ReadInt32LittleEndian();
//
// Span<byte> data = stackalloc byte[0x124];
//
// for (var i = 0; i < count; i++)
// {
// fs.ReadExactly(data);
// }
//
// Console.WriteLine(
// fs.Position == fs.Length
// );
// var items = VarsetParser.Parse(path);
// Console.WriteLine(items.Count);
// Span<byte> flt = stackalloc byte[4];
// flt[0] = 0x7f;
// flt[1] = 0x7f;
// flt[2] = 0xff;
// flt[3] = 0xff;
// var f = BinaryPrimitives.ReadSingleBigEndian(flt);
//
// Console.WriteLine(f);
// return;
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MisLoad.dll";
var path = "C:\\ParkanUnpacked\\Land.msh\\2_03 00 00 00_Land.bin";
var fs = new FileStream(path, FileMode.Open);
var outputFs = new FileStream("Land.obj", FileMode.Create);
var sw = new StreamWriter(outputFs);
List<Vector3D> points = [];
var count = 0;
while (fs.Position < fs.Length)
{ {
Console.WriteLine("Usage: ParkanPlayground <effects-directory-or-fxid-file>"); var x = fs.ReadFloatLittleEndian();
return; var y = fs.ReadFloatLittleEndian();
var z = fs.ReadFloatLittleEndian();
var vertex = new Vector3D(x, y, z);
sw.WriteLine($"v {x} {y} {z}");
var seenIndex = points.FindIndex(vec => vec == vertex);
if (seenIndex != -1)
{
vertex.Duplicates = seenIndex;
}
points.Add(vertex);
count++;
} }
var path = args[0]; File.WriteAllText("human-readable.json", JsonSerializer.Serialize(points, new JsonSerializerOptions()
bool anyError = false;
var sizeByType = new Dictionary<byte, int>
{ {
[1] = 0xE0, // 1: Billboard WriteIndented = true
[2] = 0x94, // 2: Sound }));
[3] = 0xC8, // 3: AnimParticle
[4] = 0xCC, // 4: AnimBillboard
[5] = 0x70, // 5: Trail
[6] = 0x04, // 6: Point
[7] = 0xD0, // 7: Plane
[8] = 0xF8, // 8: Model
[9] = 0xD0, // 9: AnimModel
[10] = 0xD0, // 10: Cube
};
// Check for --dump-headers flag Console.WriteLine($"Total vertices: {count}");
bool dumpHeaders = args.Length > 1 && args[1] == "--dump-headers";
if (Directory.Exists(path))
// for (int i = 0; i < count / 4; i++)
public record Vector3D(float X, float Y, float Z)
{ {
var files = Directory.EnumerateFiles(path, "*.bin").ToList(); public int Duplicates { get; set; }
if (dumpHeaders)
{
// Collect all headers for analysis
var headers = new List<(string name, EffectHeader h)>();
foreach (var file in files)
{
try
{
using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
if (fs.Length >= 60)
{
headers.Add((Path.GetFileName(file), ReadEffectHeader(br)));
}
}
catch { }
}
// Analyze unique values
Console.WriteLine("=== UNIQUE VALUES ANALYSIS ===\n");
var uniqueUnk1 = headers.Select(x => x.h.Unknown1).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Unknown1 unique values ({uniqueUnk1.Count}): {string.Join(", ", uniqueUnk1)}");
var uniqueUnk2 = headers.Select(x => x.h.Unknown2).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Unknown2 unique values ({uniqueUnk2.Count}): {string.Join(", ", uniqueUnk2.Select(x => x.ToString("F2")))}");
var uniqueFlags = headers.Select(x => x.h.Flags).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Flags unique values ({uniqueFlags.Count}): {string.Join(", ", uniqueFlags.Select(x => $"0x{x:X4}"))}");
var uniqueUnk3 = headers.Select(x => x.h.Unknown3).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Unknown3 unique values ({uniqueUnk3.Count}): {string.Join(", ", uniqueUnk3)}");
Console.WriteLine($"Unknown3 as hex: {string.Join(", ", uniqueUnk3.Select(x => $"0x{x:X3}"))}");
Console.WriteLine($"Unknown3 decoded (hi.lo): {string.Join(", ", uniqueUnk3.Select(x => $"{x >> 8}.{x & 0xFF}"))}");
// Check reserved bytes
var nonZeroReserved = headers.Where(x => x.h.Reserved.Any(b => b != 0)).ToList();
Console.WriteLine($"\nFiles with non-zero Reserved bytes: {nonZeroReserved.Count} / {headers.Count}");
// Check scales
var uniqueScales = headers.Select(x => (x.h.ScaleX, x.h.ScaleY, x.h.ScaleZ)).Distinct().ToList();
Console.WriteLine($"Unique scale combinations: {string.Join(", ", uniqueScales.Select(s => $"({s.ScaleX:F2},{s.ScaleY:F2},{s.ScaleZ:F2})"))}");
Console.WriteLine("\n=== SAMPLE HEADERS (first 30) ===");
Console.WriteLine($"{"File",-40} | {"Cnt",3} | {"U1",2} | {"Duration",8} | {"U2",6} | {"Flags",6} | {"U3",4} | Scale");
Console.WriteLine(new string('-', 100));
foreach (var (name, h) in headers.Take(30))
{
Console.WriteLine($"{name,-40} | {h.ComponentCount,3} | {h.Unknown1,2} | {h.Duration,8:F2} | {h.Unknown2,6:F2} | 0x{h.Flags:X4} | {h.Unknown3,4} | ({h.ScaleX:F1},{h.ScaleY:F1},{h.ScaleZ:F1})");
}
}
else
{
foreach (var file in files)
{
if (!ValidateFxidFile(file))
{
anyError = true;
}
}
Console.WriteLine(anyError
? "Validation finished with errors."
: "All FXID files parsed successfully.");
}
} }
else if (File.Exists(path)) // var indices = string.Join(" ", Enumerable.Range(1, count));
//
// sw.WriteLine($"l {indices}");
//
// fs.Seek(0x1000, SeekOrigin.Begin);
//
// byte[] buf = new byte[34];
// fs.ReadExactly(buf);
//
// var disassembler = new SharpDisasm.Disassembler(buf, ArchitectureMode.x86_32);
// foreach (var instruction in disassembler.Disassemble())
// {
// Console.WriteLine($"{instruction.PC - instruction.Offset}: {instruction}");
//
// new Instruction()
// {
// Action = instruction.Mnemonic.ToString(),
// Arguments = {instruction.Operands[0].ToString()}
// };
// }
public class Instruction
{ {
anyError = !ValidateFxidFile(path); public string Action { get; set; } = "";
Console.WriteLine(anyError ? "Validation failed." : "Validation OK.");
} public List<string> Arguments { get; set; } = [];
else
{
Console.WriteLine($"Path not found: {path}");
}
void DumpEffectHeader(string path)
{
try
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
if (fs.Length < 60)
{
Console.WriteLine($"{Path.GetFileName(path)}: file too small");
return;
}
var h = ReadEffectHeader(br);
// Format reserved bytes as hex (show first 8 bytes for brevity)
var reservedHex = BitConverter.ToString(h.Reserved, 0, Math.Min(8, h.Reserved.Length)).Replace("-", " ");
if (h.Reserved.Length > 8) reservedHex += "...";
// Check if reserved has any non-zero bytes
bool reservedAllZero = h.Reserved.All(b => b == 0);
Console.WriteLine($"{Path.GetFileName(path),-40} | {h.ComponentCount,7} | {h.Unknown1,4} | {h.Duration,8:F2} | {h.Unknown2,8:F2} | 0x{h.Flags:X4} | {h.Unknown3,4} | {(reservedAllZero ? "(all zero)" : reservedHex),-20} | ({h.ScaleX:F2}, {h.ScaleY:F2}, {h.ScaleZ:F2})");
}
catch (Exception ex)
{
Console.WriteLine($"{Path.GetFileName(path)}: {ex.Message}");
}
}
bool ValidateFxidFile(string path)
{
try
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
const int headerSize = 60; // sizeof(EffectHeader) on disk
if (fs.Length < headerSize)
{
Console.WriteLine($"{path}: file too small ({fs.Length} bytes).");
return false;
}
var header = ReadEffectHeader(br);
var typeCounts = new Dictionary<byte, int>();
for (int i = 0; i < header.ComponentCount; i++)
{
long blockStart = fs.Position;
if (fs.Position + 4 > fs.Length)
{
Console.WriteLine($"{path}: component {i}: unexpected EOF before type (offset 0x{fs.Position:X}, size 0x{fs.Length:X}).");
return false;
}
uint typeAndFlags = br.ReadUInt32();
byte type = (byte)(typeAndFlags & 0xFF);
if (!typeCounts.TryGetValue(type, out var count))
{
count = 0;
}
typeCounts[type] = count + 1;
if (!sizeByType.TryGetValue(type, out int blockSize))
{
Console.WriteLine($"{path}: component {i}: unknown type {type} (typeAndFlags=0x{typeAndFlags:X8}).");
return false;
}
int remaining = blockSize - 4;
if (fs.Position + remaining > fs.Length)
{
Console.WriteLine($"{path}: component {i}: block size 0x{blockSize:X} runs past EOF (blockStart=0x{blockStart:X}, fileSize=0x{fs.Length:X}).");
return false;
}
if (type == 1)
{
var def = ReadBillboardComponent(br, typeAndFlags);
if (def.Reserved.Length != 0x50)
{
Console.WriteLine($"{path}: component {i}: type 1 reserved length {def.Reserved.Length}, expected 0x50.");
return false;
}
}
else if (type == 2)
{
var def = ReadSoundComponent(br, typeAndFlags);
if (def.SoundNameAndReserved.Length != 0x40)
{
Console.WriteLine($"{path}: component {i}: type 2 reserved length {def.SoundNameAndReserved.Length}, expected 0x40.");
return false;
}
}
else if (type == 3)
{
var def = ReadAnimParticleComponent(br, typeAndFlags);
if (def.Reserved.Length != 0x38)
{
Console.WriteLine($"{path}: component {i}: type 3 reserved length {def.Reserved.Length}, expected 0x38.");
return false;
}
}
else if (type == 4)
{
var def = ReadAnimBillboardComponent(br, typeAndFlags);
if (def.Reserved.Length != 0x3C)
{
Console.WriteLine($"{path}: component {i}: type 4 reserved length {def.Reserved.Length}, expected 0x3C.");
return false;
}
}
else if (type == 5)
{
var def = ReadTrailComponent(br, typeAndFlags);
if (def.Unknown04To10.Length != 0x10)
{
Console.WriteLine($"{path}: component {i}: type 5 prefix length {def.Unknown04To10.Length}, expected 0x10.");
return false;
}
if (def.TextureNameAndReserved.Length != 0x40)
{
Console.WriteLine($"{path}: component {i}: type 5 tail length {def.TextureNameAndReserved.Length}, expected 0x40.");
return false;
}
}
else if (type == 6)
{
// Point components have no extra bytes beyond the 4-byte typeAndFlags header.
var def = ReadPointComponent(typeAndFlags);
}
else if (type == 7)
{
var def = ReadPlaneComponent(br, typeAndFlags);
if (def.Base.Reserved.Length != 0x38)
{
Console.WriteLine($"{path}: component {i}: type 7 base reserved length {def.Base.Reserved.Length}, expected 0x38.");
return false;
}
}
else if (type == 8)
{
var def = ReadModelComponent(br, typeAndFlags);
if (def.TextureNameAndFlags.Length != 0x40)
{
Console.WriteLine($"{path}: component {i}: type 8 tail length {def.TextureNameAndFlags.Length}, expected 0x40.");
return false;
}
}
else if (type == 9)
{
var def = ReadAnimModelComponent(br, typeAndFlags);
if (def.TextureNameAndFlags.Length != 0x48)
{
Console.WriteLine($"{path}: component {i}: type 9 tail length {def.TextureNameAndFlags.Length}, expected 0x48.");
return false;
}
}
else if (type == 10)
{
var def = ReadCubeComponent(br, typeAndFlags);
if (def.Base.Reserved.Length != 0x3C)
{
Console.WriteLine($"{path}: component {i}: type 10 base reserved length {def.Base.Reserved.Length}, expected 0x3C.");
return false;
}
}
else
{
// Skip the remaining bytes for other component types.
fs.Position += remaining;
}
}
// Dump a compact per-file summary of component types and counts.
var sb = new StringBuilder();
bool first = true;
foreach (var kv in typeCounts)
{
if (!first)
{
sb.Append(", ");
}
sb.Append(kv.Key);
sb.Append('x');
sb.Append(kv.Value);
first = false;
}
Console.WriteLine($"{path}: components={header.ComponentCount}, types=[{sb}]");
if (fs.Position != fs.Length)
{
Console.WriteLine($"{path}: parsed to 0x{fs.Position:X}, but file size is 0x{fs.Length:X} (leftover {fs.Length - fs.Position} bytes).");
return false;
}
return true;
}
catch (Exception ex)
{
Console.WriteLine($"{path}: exception while parsing: {ex.Message}");
return false;
}
} }

View File

@@ -1,10 +0,0 @@
typedef struct BoundingBox {
Vector3 BottomFrontLeft;
Vector3 BottomFrontRight;
Vector3 BottomBackRight;
Vector3 BottomBackLeft;
Vector3 TopBackRight;
Vector3 TopFrontRight;
Vector3 TopBackLeft;
Vector3 TopFrontLeft;
} BoundingBox;

File diff suppressed because it is too large Load Diff

View File

@@ -1,60 +0,0 @@
typedef struct CLandscape {
IComponent * IComponent;
CGameObject game_object;
ITerrain * ITerrain;
IMesh2 * IMesh2;
I0x1 * I0x1;
IControl * IControl;
I0x22 * IUnknown_0x22;
undefined4 01_data_ptr;
MSH_02_file * msh_02_data_ptr;
undefined4 0b_data_ptr;
// collapse unknown fields
int[3][5] field_532;
// collapse unknown fields
undefined4 IComponent_owner;
KindaArray array_0x278;
int _count_unk_obj_array_elemsize_212;
undefined4 vertices_count;
uint 15_element_count;
// collapse unknown fields
Vector3 * 03_vertices_data_ptr;
int 03_vertices_stride;
undefined4 04_data_ptr;
undefined4 04_stride;
// collapse unknown fields
undefined4 05_data_ptr;
undefined4 05_stride;
undefined4 12_microtexture_mapping_data_ptr;
undefined4 12_microtexture_mapping_stride;
// collapse unknown fields
MshMetaForLandscape * mshmeta_ptr;
// collapse unknown fields
KindaArray array_0x7a40;
BoundingBox bounding_box_from_msh_02;
IMatManager * * IMatManager;
undefined4 wear_ptr;
IEffectManager * * ResolvedIEffectManager;
ISystemArealMap * * ResolvedISystemArealMap;
ICollManager * * ResolvedICollManager;
ISoundPool * * ISoundPool;
I3DSound * * I3DSound;
ILightManager * * ILightManager_owned;
KindaArray array_0x7c00;
KindaArray array_0x7c0c;
CShade * Shade;
undefined4 I3DRender??;
undefined4 field_31776;
undefined4 flags_of_mesh_part;
INResFile * * INResFile;
KindaArray array_0x7c2c;
undefined4 current_visible_primitives_count;
IMesh2 * * meshes;
IGameObject * * game_objects;
// collapse unknown fields
KindaArray array_0x7c84;
undefined4 * field_31888;
undefined4 _CheckMaxBasementAngleStep;
CollisionContext m_CollisionContext;
BoundingBox bounding_box;
} CLandscape;

File diff suppressed because it is too large Load Diff

510
README.md
View File

@@ -2,13 +2,11 @@
<div align="center"> <div align="center">
<img width="300" height="300" src="https://github.com/user-attachments/assets/dcd9ac8f-7d30-491c-ae6c-537267beb7dc" alt="x86 Registers" /> <img width="300" height="300" src="https://github.com/user-attachments/assets/dcd9ac8f-7d30-491c-ae6c-537267beb7dc" alt="x86 Registers" />
<img width="817" height="376" alt="Image" src="https://github.com/user-attachments/assets/c4959106-9da4-4c78-a2b7-6c94e360a89e" />
</div> </div>
## Сборка проекта ## Сборка проекта
Проект написан на C# под `.NET 9` Проект написан на C# под `.NET 8`
Вам должно хватить `dotnet build` для сборки всех проектов отдельно. Вам должно хватить `dotnet build` для сборки всех проектов отдельно.
@@ -16,13 +14,13 @@
### Состояние проекта ### Состояние проекта
- Поддержка всех `NRes` файлов - звуки, музыка, текстуры, карты и другие файлы. Есть документация. - Распаковка всех `NRes` файлов
- Поддержка всех `TEXM` текстур. Есть документация. - Распаковка всех `TEXM` текстур
- Поддержка файлов миссий `.tma`. + формат 565 работает некорректно
- Поддержка шрифтов TFNT. + не понятно назначение двух магических чисел в заголовке
- Поддержка файлов скриптов `.scr`. - Распаковка данных миссии `.tma`. Пока работает чтение ареалов и кланов.
- Поддержка файлов параметров `.var`. - Распаковка файла NL. Есть только декодирование заголовка. Формат кажется не используется игрой, а реверс бинарника игры то ещё занятие.
- Поддержка файлов схем объектов `.dat`. - Распаковка текстуры шрифта формата TFNT. Встроен прямо в UI. По сути шрифт это 4116 байт заголовка и текстура TEXM сразу после.
### Структура проекта ### Структура проекта
@@ -36,6 +34,47 @@
Я конечно стараюсь, но ничего не обещаю. Я конечно стараюсь, но ничего не обещаю.
#### NResUI
UI приложение на OpenGL + ImGui.
Туда постепенно добавляю логику.
#### NResLib
Библиотека распаковки формата NRes и всех файлов, которые им запакованы.
Есть логика импорта и экспорта. Работа не завершена, но уже сейчас можно читать любые архивы такого формата.
#### TexmLib
Библиотека распаковки текстур TEXM.
Есть логика импорта и экспорта, хотя к UI последняя не подключена.
#### NLUnpacker
Приложение распаковки NL.
Работа приостановлена, т.к. кажется игра не использует эти файлы.
#### MissionDataUnpacker
Приложение распаковки миссий `.tma`.
Готово чтение ареалов и кланов. Пока в процессе.
#### ParkanPlayground
Пустой проект, использую для локальных тестов.
#### TextureDecoder
Приложение для экспорта текстур TEXM.
Изначально тут игрался с текстурами.
## Для Reverse Engineering-а использую Ghidra ## Для Reverse Engineering-а использую Ghidra
### Наблюдения ### Наблюдения
@@ -47,457 +86,6 @@
- Игра активно и обильно течёт по памяти, оставляя после чтения файлов их `MapViewOfFile` и подобные штуки. - Игра активно и обильно течёт по памяти, оставляя после чтения файлов их `MapViewOfFile` и подобные штуки.
- Игра нормально не работает на Win10. Мне помог dgVoodoo. Хотя с ним не работает `MisEditor`. - Игра нормально не работает на Win10. Мне помог dgVoodoo. Хотя с ним не работает `MisEditor`.
## Как быстро найти текст среди всех файлов игры
```shell
grep -rl --include="*" "s_tree_05" .
```
## Как быстро найти байты среди всех файлов игры
```shell
grep -rlU $'\x73\x5f\x74\x72\x65\x65\x5f\x30\x35' .
```
## Как работает игра
Главное меню:
Игра сканирует хардкод папку `missions` на наличие файлов миссий. (буквально 01, 02, 03 и т.д.)
Сначала игра читает название миссии из файла `descr` - тут название для меню.
- Одиночные игры - `missions/single.{index}/descr`
- Тренировочные миссии - `missions/tutorial.{index}/descr`
- Кампания - `missions/campaign/campaign.{index1}/descr`
* Далее используются подпапки - `missions/campaign/campaign.{index1}/mission.{index2}/descr`
Как только игра не находит файл `descr`, заканчивается итерация по папкам (понял, т.к. пробуется файл 05 - он не существует).
Загрузка миссии:
Читается файл `ui/game_resources.cfg`
Из этого файла загружаются ресурсы
- `library = "ui\\ui.lib"` - загружается файл `ui.lib`
- `library = "ui\\font.lib"` - загружается файл `font.lib`
- `library = "sounds.lib"` - загружается файл `sounds.lib`
- `library = "voices.lib"` - загружается файл `voices.lib`
Затем игра читает `save/saveslots.cfg` - тут слоты сохранения
Затем `Comp.ini` - тут системные функции, которые используются для загрузки объектов.
```
IComponent ** LoadSomething(undefined4, undefined4, undefined4, undefined4)
```
- `Host.url` - этого файла нет
- `palettes.lib` - тут палитры, но этот NRes пустой
- `system.rlb` - не понятно что
- `Textures.lib` - тут текстуры
- `Material.lib` - тут какие-то материалы - не понятно
- `LightMap.lib` - видимо это карты освещения - не понятно
- `sys.lib` - не понятно
- `ScanCode.dsc` - текстовый файл с мапом клавиш
- `command.dsc` - текстовый файл с мапом клавиш
Тут видимо идёт конфигурация ввода
- `table_1.man` - текстовый файл
- `table_2.man` - текстовый файл
- `hero.man` - текстовый файл
- `addition.man` - текстовый файл
- Снова `table_1.man`
- Снова `table_1.man`
- `M1.tbl` - текстовый файл
- Снова `table_2.man`
- Снова `table_2.man`
- `M2.tbl` - текстовый файл
- Снова `hero.man`
- Снова `hero.man`
- `HERO.TBL`
- Снова `addition.man`
- `ui/hq.cfg`
- Снова `ui/hq.cfg`
Дальше непосредственно читается миссия
- `mission.cfg` - метадата миссии
- `units\\units\\prebld\\scr_pre1.dat` из метаданных `object prebuild` - `cp` файл (грузятся подряд все)
- Опять `ui/hq.cfg`
- `mistips.mis` - описание для игрока (экран F1)
- `scancode.dsc` - хз
- `command.dsc` - хз
- `ui_hero.man` - хз
- `ui_bots.man` - хз
- `ui_hq.man` - хз
- `ui_other.man` - хз
- Цикл чтения курсоров
* `ui/cursor.cfg` - тут настройки курсора.
* `ui/{name}` - курсор
- Снова `mission.cfg` - метадата миссии
- `descr` - название
- `data/textres.cfg` - конфиг текстов
- Снова `mission.cfg` - метадата миссии
- Ещё раз `mission.cfg` - метадата миссии
- `ui/minimap.lib` - NRes с текстурами миникарты.
- `messages.cfg` - Tutorial messages
УРА НАКОНЕЦ-ТО `data.tma`
- Из `.tma` берётся LAND строка (я её так назвал)
- `DATA\\MAPS\\SC_3\\land1.wea`
- `DATA\\MAPS\\SC_3\\land2.wea`
- `BuildDat.lst` - Behaviour will use these schemes to Build Fortification
- `DATA\\MAPS\\SC_3\\land.map`
- `DATA\\MAPS\\SC_3\\land.msh`
- `effects.rlb`
Цикл по кланам из `.tma`
- `MISSIONS\\SCRIPTS\\screampl.scr`
- `varset.var`
- `MISSIONS\\SCRIPTS\\varset.var`
- `MISSIONS\\SCRIPTS\\screampl.fml`
- `missions/single.01/sky.ske`
- `missions/single.01/sky.wea`
Дальше начинаются объекты игры
- `"UNITS\\BUILDS\\BUNKER\\mbunk01.dat"` - cp файл
## Загрузка `cp` файлов
`cp` файл - схема. Он содержит дерево частей объекта.
`cp` файл читается в `ArealMap.dll/CreateObjectFromScheme`
В зависимости от типа объекта внутри схемы (байты 4..8) выбирается функция, с помощью которой загружается схема.
Функция выбирается на основе файла `Comp.ini`.
- Для ClassBuilding (0x80000000) - вызывается функция c классом 3 (по таблице ниже Building).
- Для всех остальных - функция с классом 4 (по таблице ниже Agent).
На основе файла `Comp.ini` и первом вызове внутри функции `World3D.dll/CreateObject` ремаппинг id:
| Logic ID | ClassName | Function |
|:--------:|:------------:|--------------------------------|
| 1 | Landscape | `terrain.dll LoadLandscape` |
| 2 | Agent | `animesh.dll LoadAgent` |
| 3 | Building | `terrain.dll LoadBuilding` |
| 4 | Agent | `animesh.dll LoadAgent` |
| 5 | Camera | `terrain.dll LoadCamera` |
| 7 | Atmosphere | `terrain.dll CreateAtmosphere` |
| 9 | Agent | `animesh.dll LoadAgent` |
| 10 | Agent | `animesh.dll LoadAgent` |
| 11 | Research | `misload.dll LoadResearch` |
| 12 | Agent | `animesh.dll LoadAgent` |
Будет дополняться по мере реверса.
Всем этим функциям передаётся `nres_file_name, nres_entry_name, 0, player_id`
## `fr FORT` файл
Всегда 0x80 байт
Содержит 2 ссылки на файлы:
- `.bas`
- `.ctl` - вызывается `LoadAgent`
## `.msh`
### Описание ниже валидно только для моделей роботов и зданий.
##### Land.msh использует другой формат, хотя 03 файл это всё ещё точки.
Загружается в `AniMesh.dll/LoadAniMesh`
- Тип 01 - заголовок. Он хранит список деталей (submesh) в разных LOD
```
нулевому элементу добавляется флаг 0x1000000
Содержит 2 ссылки на файлы анимаций (короткие - файл 13, длинные - файл 08)
Если интерполируется анимация -0.5s короче чем magic1 у файла 13
И у файла есть OffsetIntoFile13
И ushort значение в файле 13 по этому оффсету > IndexInFile08 (это по-моему выполняется всегда)
Тогда вместо IndexInFile08 используется значение из файла 13 по этому оффсету (второй байт)
```
- Тип 02 - описание одного LOD Submesh
```
Вначале идёт заголовок 0x8C (140) байт
В заголовке:
8 Vector3 (x,y,z) - bounding box
1 Vector4 - center
1 Vector3 - bottom
1 Vector3 - top
1 float - xy_radius
Далее инфа про куски меша
```
- Тип 03 - это вершины (vertex)
- Тип 06 - индексы треугольников в файле 03
- Тип 04 - скорее всего какие-то цвета RGBA или типа того
- Тип 08 - меш-анимации (см файл 01)
```
Индексируется по IndexInFile08 из файла 01 либо по файлу 13 через OffsetIntoFile13
Структура:
Vector3 position;
float time; // содержит только целые секунды
short rotation_x; // делится на 32767
short rotation_y; // делится на 32767
short rotation_z; // делится на 32767
short rotation_w; // делится на 32767
---
Игра интерполирует анимацию между текущим стейтом и следующим по time.
Если время интерполяции совпадает с исходным time, жёстко берётся первый стейт из 0x13.
Если время интерполяции совпадает с конечным time, жёстко берётся второй стейт из 0x13.
Если ни то и ни другое, тогда t = (time - souce.time) / (dest.time - source.time)
```
- Тип 12 - microtexture mapping
- Тип 13 - короткие меш-анимации (почему я это не дописал?)
```
Буквально (hex)
00 01 01 02 ...
```
- Тип 0A - ссылка на части меша, не упакованные в текущий меш (например у бункера 4 и 5 части хранятся в parts.rlb)
```
Не имеет фиксированной длины. Хранит строки в следующем формате.
Игра обращается по индексу, пропуская суммарную длину и пропуская 4 байта на каждую строку (длина).
т.е. буквально файл выглядит так
00 00 00 00 - пустая строка
03 00 00 00 - длина строки 1
73 74 72 00 - строка "str" + null terminator
.. и повторяется до конца файла
Кол-во элементов из файла 01 должно быть равно кол-ву строк в этом файле, хотя игра это не проверяет.
Если у элемента эта строка равна "central", ему выставляется флаг (flag |= 1)
```
## `.wea`
Загружается в `World3D.dll/LoadMatManager`
По сути это текстовый файл состоящий из 2 частей:
- Материалы
```
{count}
{id} {name}
```
- Карты освещения
```
LIGHTMAPS
{count}
{id} {name}
```
Может как-то анимироваться. Как - пока не понятно.
## Lightmap (TEXM)
У текстур использующихся как карты освещения специальные имена.
`some_name.00` или `some_name.0`
Первая цифра после `.` это группа палет (всего 0x11), вторая цифра индекс в этой группе.
```
All lightmaps named whatever.0 → they all end up with no DirectDraw palette attached.
```
## Люблю разработчиков
У них получилось сделать LightSource и MaterialDescriptor чётко одинаковыми по размеру (0x5c) из-за чего я потерял 3 дня прыгая по указателям
## `FXID` - файл эффектов
По сути представляет собой последовательный список саб-эффектов идущих друг за другом.
Всего существует 9 (1..9) видов эффектов: !описать по мере реверса
Выглядит так, словно весь файл это тоже эффект сам по себе.
```
0x00-0x04 Type (игра обрезает 1 байт, и поддерживает только значения 1-9)
0x04-0x08 unknown
0x08-0x0C unknown
0x0C-0x10 unknown
0x10-0x14 EffectTemplateFlags
...
0x30-0x34 ScaleX
0x34-0x38 ScaleY
0x38-0x3C ScaleZ
enum EffectTemplateFlags : uint32_t
{
EffectTemplateFlag_RandomizeStrength = 0x0001, // used in UpdateDust
EffectTemplateFlag_RandomOffset = 0x0008, // random worldTransform offset
EffectTemplateFlag_TriangularShape = 0x0020, // post-process strength as 0→1→0
EffectTemplateFlag_OnlyWhenEnvBit0Off = 0x0080, // gating in ComputeEffectStrength
EffectTemplateFlag_OnlyWhenEnvBit0On = 0x0100, // gating in ComputeEffectStrength
EffectTemplateFlag_MultiplyByLife = 0x0200, // multiply strength by life progress
EffectTemplateFlag_EnvFlag2_IfNotSet = 0x0800, // if NOT set → envFlags |= 0x02
EffectTemplateFlag_EnvFlag10_IfSet = 0x1000, // if set → envFlags |= 0x10
EffectTemplateFlag_AlwaysEmitDust = 0x0010, // ignore piece state / flags
EffectTemplateFlag_IgnorePieceState = 0x8000, // treat piece default state as OK
EffectTemplateFlag_AutoDeleteAtFull = 0x0002, // delete when strength >= 1
EffectTemplateFlag_DetachAfterEmit = 0x0004, // detach from attachment after update
};
```
# ЕСЛИ ГДЕ-ТО ВИДИШЬ PTR_func_ret1_no_args, ТО ЭТО ShaderConfig
В рендеринге порядок дата-стримов такой
position
normal
color
# Внутренняя система ID
- `1` - unknown (implemented by CLandscape) видимо ILandscape
- `3` - unknown (implemented by CAtmosphere) видимо IAtmosphere
- `4` - IShader
- `5` - ITerrain
- `6` - IGameObject
- `7` - ISettings
- `8` - ICamera
- `9` - IQueue
- `10` - IControl
- `0xb` - IAnimation
- `0xc` - IShadeStatsBuilder (придумал сам implemented by CShade)
- `0xd` - IMatManager
- `0xe` - ILightManager
- `0xf` - IShade
- `0x10` - IBehaviour
- `0x11` - IBasement
- `0x12` - ICamera2 или IBufferingCamera
- `0x13` - IEffectManager
- `0x14` - IPosition
- `0x15` - IAgent
- `0x16` - ILifeSystem
- `0x17` - IBuilding - точно он, т.к. ArealMap.CreateObject на него проверяет
- `0x18` - IMesh2
- `0x19` - IManManager
- `0x20` - IJointMesh
- `0x21` - IShadowProcessor (придумал сам implemented by CShade)
- `0x22` - unknown (implement by CLandscape)
- `0x23` - IGameSettingsRoot
- `0x24` - IGameObject2
- `0x25` - unknown (implemented by CAniMesh)
- `0x26` - unknown (implemented by CAniMesh and CControl and CWizard)
- `0x28` - ICollObject
- `0x29` - IPhysicalModel
- `0x101` - I3DRender
- `0x102` - ITexture (writable)
- `0x103` - IColorLookup
- `0x104` - IBitmapFont
- `0x105` - INResFile
- `0x106` - NResFileMetadata
- `0x107` - I3DSound
- `0x108` - IListenerTransform
- `0x109` - ISoundPool
- `0x10a` - ISoundBuffer
- `0x10c` - ICDPlayer
- `0x10d` - IVertexBuffer
- `0x201` - IWizard
- `0x202` - IItemManager
- `0x203` - ICollManager
- `0x301` - IArealMap
- `0x302` - ISystemArealMap
- `0x303` - IHallway
- `0x304` - IDistributor
- `0x401` - ISuperAI
- `0x501` - MissionData
- `0x502` - ResTree
- `0x700` - INetWatcher
- `0x701` - INetworkInterface
## SuperAI = Clan с точки зрения индексации
Т.е. у каждого клана свой SuperAI
## Опции
World3D.dll содержит функцию CreateGameSettings.
Она создаёт объект настроек и далее вызывает методы в соседних библиотеках.
- Terrain.dll - InitializeSettings
- Effect.dll - InitializeSettings
- Control.dll - InitializeSettings
Остальные наверное не трогают настройки.
| Resource ID | wOptionID | Name | Default | Description |
|:-----------:|:---------------:|:--------------------------:|:-------:|--------------------|
| 1 | 100 (0x64) | "Texture detail" | | |
| 2 | 101 (0x65) | "3D Sound" | | |
| 3 | 102 (0x66) | "Mouse sensitivity" | | |
| 4 | 103 (0x67) | "Joystick sensitivity" | | |
| 5 | !not a setting! | "Illegal wOptionID" | | |
| 6 | 104 (0x68) | "Wait for retrace" | | |
| 7 | 105 (0x69) | "Inverse mouse X" | | |
| 8 | 106 (0x6a) | "Inverse mouse Y" | | |
| 9 | 107 (0x6b) | "Inverse joystick X" | | |
| 10 | 108 (0x6c) | "Inverse joystick Y" | | |
| 11 | 109 (0x6d) | "Use BumpMapping" | | |
| 12 | 110 (0x6e) | "3D Sound quality" | | |
| 13 | 90 (0x5a) | "Reverse sound" | | |
| 14 | 91 (0x5b) | "Sound buffer frequency" | | |
| 15 | 92 (0x5c) | "Play sound buffer always" | | |
| 16 | 93 (0x5d) | "Select best sound device" | | |
| ---- | 30 (0x1e) | ShadeConfig | | из файла shade.cfg |
| ---- | (0x8001e) | | | добавляет AniMesh |
## Goodies
```c++
// Тип положения объекта в пространстве. Используется при расчёте эффектов,
// расстановке объектов и для получения/выдачи их матриц трансформации.
enum EPlacementType
{
// 0: Полная мировая матрица, ориентированная "смотрю на цель".
// Входная matrix — это мировая матрица (look-at к какой-то цели).
// Движок при необходимости переводит её в локальное/костное пространство
// (через inverse(parent/joint) * world и т.п.).
// Смысл: "Построй мне мировую матрицу так, чтобы я смотрел на цель;
// если я привязан к кости — учти это."
Placement_WorldLookAtTarget = 0,
// 1: Мировая матрица, где основная ось объекта (this->direction)
// выровнена по направлению к цели.
// Входная matrix — мировая; логика выравнивания использует внутренний
// вектор direction и целевую точку/направление.
// Смысл: "Используй мой внутренний direction как основную ось
// и как можно лучше направь её в сторону цели."
Placement_WorldAlignDirectionToTarget = 1,
// 2: Мировая матрица, полностью задаётся внутренним состоянием объекта.
// Входная matrix трактуется как "standalone world" — готовая мировая
// матрица объекта. Движок лишь переводит её во внутреннее локальное/
// костное пространство (inverse(parent/joint) * world), без look-at
// и без выравнивания по цели.
// Смысл: "Вот моя конечная мировая матрица. Просто встрои её в иерархию,
// не трогая ориентацию дополнительной логикой."
Placement_WorldFromStoredTransform = 2,
// 3: Мировая матрица, ориентированная вдоль вектора движения, но с учётом цели.
// Используется предыдущая мировая позиция + текущая + target, чтобы
// построить базис: основная ось — направление движения, вспомогательная —
// направление к цели.
// Смысл: "Ориентируй меня по вектору моего движения, но также учитывай цель."
Placement_WorldAlignMotionToTarget = 3
};
```
## Контакты ## Контакты
Вы можете связаться со мной в [Telegram](https://t.me/bird_egop). Вы можете связаться со мной в [Telegram](https://t.me/bird_egop).

39
ScrLib/Extensions.cs Normal file
View File

@@ -0,0 +1,39 @@
using System.Buffers.Binary;
using System.Text;
namespace ScrLib;
public static class Extensions
{
public static int ReadInt32LittleEndian(this FileStream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadInt32LittleEndian(buf);
}
public static float ReadFloatLittleEndian(this FileStream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadSingleLittleEndian(buf);
}
public static string ReadLengthPrefixedString(this FileStream fs)
{
var len = fs.ReadInt32LittleEndian();
if (len == 0)
{
return "";
}
var buffer = new byte[len];
fs.ReadExactly(buffer, 0, len);
return Encoding.ASCII.GetString(buffer, 0, len);
}
}

View File

@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" /> <PropertyGroup>
</ItemGroup> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project> </Project>

View File

@@ -1,18 +1,11 @@
using Common; namespace ScrLib;
namespace ScrLib;
public class ScrParser public class ScrParser
{ {
public static ScrFile ReadFile(string filePath) public static ScrFile ReadFile(string filePath)
{ {
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); var fs = new FileStream(filePath, FileMode.Open);
return ReadFile(fs);
}
public static ScrFile ReadFile(Stream fs)
{
var scrFile = new ScrFile(); var scrFile = new ScrFile();
scrFile.Magic = fs.ReadInt32LittleEndian(); scrFile.Magic = fs.ReadInt32LittleEndian();

44
TestDisassembler.cs Normal file
View File

@@ -0,0 +1,44 @@
using X86Disassembler.X86;
namespace TestDisassembler;
public class Program
{
public static void Main(string[] args)
{
// Test the specific byte sequence that's causing issues
byte[] codeBytes = HexStringToByteArray("816B1078563412");
// Create a disassembler with the code
Disassembler disassembler = new Disassembler(codeBytes, 0x1000);
// Disassemble the code
var instructions = disassembler.Disassemble();
// Print the number of instructions
Console.WriteLine($"Number of instructions: {instructions.Count}");
// Print each instruction
for (int i = 0; i < instructions.Count; i++)
{
Console.WriteLine($"Instruction {i+1}: {instructions[i].Mnemonic} {instructions[i].Operands}");
}
}
private static byte[] HexStringToByteArray(string hex)
{
// Remove any non-hex characters
hex = hex.Replace(" ", "").Replace("-", "");
// Create a byte array
byte[] bytes = new byte[hex.Length / 2];
// Convert each pair of hex characters to a byte
for (int i = 0; i < hex.Length; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
}

View File

@@ -0,0 +1,44 @@
using X86Disassembler.X86;
namespace TestDisassembler;
public class Program
{
public static void Main(string[] args)
{
// Test the specific byte sequence with segment override prefix that's causing issues
byte[] codeBytes = HexStringToByteArray("26FF7510");
// Create a disassembler with the code
Disassembler disassembler = new Disassembler(codeBytes, 0x1000);
// Disassemble the code
var instructions = disassembler.Disassemble();
// Print the number of instructions
Console.WriteLine($"Number of instructions: {instructions.Count}");
// Print each instruction
for (int i = 0; i < instructions.Count; i++)
{
Console.WriteLine($"Instruction {i+1}: {instructions[i].Mnemonic} {instructions[i].Operands}");
}
}
private static byte[] HexStringToByteArray(string hex)
{
// Remove any non-hex characters
hex = hex.Replace(" ", "").Replace("-", "");
// Create a byte array
byte[] bytes = new byte[hex.Length / 2];
// Convert each pair of hex characters to a byte
for (int i = 0; i < hex.Length; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\X86Disassembler\X86Disassembler.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -6,10 +6,10 @@ public static class Extensions
{ {
return format switch return format switch
{ {
0x22B8 => 32, 8888 => 32,
0x115C => 16, 4444 => 16,
0x235 => 16, 565 => 16,
0x378 => 32, 888 => 32,
0 => 32 0 => 32
}; };
} }

View File

@@ -14,7 +14,7 @@ namespace TexmLib;
/// <param name="MipmapCount">Кол-во мипмапов (уменьшенные копии текстуры)</param> /// <param name="MipmapCount">Кол-во мипмапов (уменьшенные копии текстуры)</param>
/// <param name="Stride">Сколько БИТ занимает 1 пиксель</param> /// <param name="Stride">Сколько БИТ занимает 1 пиксель</param>
/// <param name="Magic1">Неизвестно</param> /// <param name="Magic1">Неизвестно</param>
/// <param name="FormatOptionFlags">Дополнительные флаги для текстуры</param> /// <param name="Magic2">Неизвестно</param>
/// <param name="Format">Формат пикселя(4444, 8888, 888)</param> /// <param name="Format">Формат пикселя(4444, 8888, 888)</param>
public record TexmHeader( public record TexmHeader(
string TexmAscii, string TexmAscii,
@@ -23,7 +23,7 @@ public record TexmHeader(
int MipmapCount, int MipmapCount,
int Stride, int Stride,
int Magic1, int Magic1,
int FormatOptionFlags, int Magic2,
int Format int Format
); );

View File

@@ -1,7 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -22,7 +22,7 @@ public class TexmParser
var mipmapCountBytes = headerBytes[12..16]; var mipmapCountBytes = headerBytes[12..16];
var strideBytes = headerBytes[16..20]; var strideBytes = headerBytes[16..20];
var magic1Bytes = headerBytes[20..24]; var magic1Bytes = headerBytes[20..24];
var formatOptionFlagsBytes = headerBytes[24..28]; var magic2Bytes = headerBytes[24..28];
var formatBytes = headerBytes[28..32]; var formatBytes = headerBytes[28..32];
var texmAscii = Encoding.ASCII.GetString(texmHeader).Trim('\0'); var texmAscii = Encoding.ASCII.GetString(texmHeader).Trim('\0');
@@ -31,7 +31,7 @@ public class TexmParser
var mipmapCount = BinaryPrimitives.ReadInt32LittleEndian(mipmapCountBytes); var mipmapCount = BinaryPrimitives.ReadInt32LittleEndian(mipmapCountBytes);
var stride = BinaryPrimitives.ReadInt32LittleEndian(strideBytes); var stride = BinaryPrimitives.ReadInt32LittleEndian(strideBytes);
var magic1 = BinaryPrimitives.ReadInt32LittleEndian(magic1Bytes); var magic1 = BinaryPrimitives.ReadInt32LittleEndian(magic1Bytes);
var formatOptionFlags = BinaryPrimitives.ReadInt32LittleEndian(formatOptionFlagsBytes); var magic2 = BinaryPrimitives.ReadInt32LittleEndian(magic2Bytes);
var format = BinaryPrimitives.ReadInt32LittleEndian(formatBytes); var format = BinaryPrimitives.ReadInt32LittleEndian(formatBytes);
if (texmAscii != "Texm") if (texmAscii != "Texm")
@@ -51,7 +51,7 @@ public class TexmParser
mipmapCount, mipmapCount,
stride, stride,
magic1, magic1,
formatOptionFlags, magic2,
format format
); );

34
TextureDecoder/Program.cs Normal file
View File

@@ -0,0 +1,34 @@
using System.Buffers.Binary;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using TexmLib;
var folder = "C:\\Projects\\CSharp\\ParkanPlayground\\ParkanPlayground\\bin\\Debug\\net8.0\\ui.lib";
var files = Directory.EnumerateFiles(folder);
List<TexmFile> textureFiles = [];
foreach (var file in files)
{
try
{
var fs = new FileStream(file, FileMode.Open);
var parseResult = TexmParser.ReadFromStream(fs, file);
textureFiles.Add(parseResult.TexmFile);
Console.WriteLine($"Successfully read: {file}");
}
catch
{
Console.WriteLine($"Failed read: {file}");
}
}
foreach (var textureFile in textureFiles)
{
await textureFile.WriteToFolder("unpacked");
Console.WriteLine($"Unpacked {Path.GetFileName(textureFile.FileName)} into folder");
}

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,6 +0,0 @@
namespace VarsetLib;
/// <summary>
/// Бинарный файл, который можно найти в behpsp.res
/// </summary>
public record BinaryVarsetFile(int Count, List<BinaryVarsetItem> Items);

View File

@@ -1,93 +0,0 @@
using Common;
namespace VarsetLib;
public class BinaryVarsetFileParser
{
public static BinaryVarsetFile Parse(string path)
{
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
return Parse(fs);
}
public static BinaryVarsetFile Parse(Stream fs)
{
var count = fs.ReadInt32LittleEndian();
var items = new List<BinaryVarsetItem>(count);
Span<byte> buf4 = stackalloc byte[4];
for (int i = 0; i < count; i++)
{
var valueLength = fs.ReadInt32LittleEndian();
var valueType = (BinaryVarsetValueType)fs.ReadInt32LittleEndian();
fs.ReadExactly(buf4);
var magic3 = new IntFloatValue(buf4);
var name = fs.ReadLengthPrefixedString();
var string2 = fs.ReadLengthPrefixedString();
fs.ReadExactly(buf4);
var magic4 = new IntFloatValue(buf4);
fs.ReadExactly(buf4);
var magic5 = new IntFloatValue(buf4);
fs.ReadExactly(buf4);
var magic6 = new IntFloatValue(buf4);
fs.ReadExactly(buf4);
var magic7 = new IntFloatValue(buf4);
if(string2.Length != 0)
{
_ = 5;
}
if (valueType is BinaryVarsetValueType.Bool)
{
items.Add(new BinaryVarsetItem<bool>(
valueLength,
valueType,
magic3.AsInt != 0,
name,
string2,
magic4.AsInt != 0,
magic5.AsInt != 0,
magic6.AsInt != 0,
magic7.AsInt != 0));
}
else if (valueType is BinaryVarsetValueType.Dword)
{
items.Add(new BinaryVarsetItem<uint>(
valueLength,
valueType,
(uint)magic3.AsInt,
name,
string2,
(uint)magic4.AsInt,
(uint)magic5.AsInt,
(uint)magic6.AsInt,
(uint)magic7.AsInt));
}
else if (valueType is BinaryVarsetValueType.Float)
{
items.Add(new BinaryVarsetItem<float>(
valueLength,
valueType,
magic3.AsFloat,
name,
string2,
magic4.AsFloat,
magic5.AsFloat,
magic6.AsFloat,
magic7.AsFloat));
}
else
{
throw new InvalidOperationException($"Unknown value type {valueType}");
}
}
return new BinaryVarsetFile(count, items);
}
}

View File

@@ -1,35 +0,0 @@
using System.Diagnostics;
using Common;
namespace VarsetLib;
[DebuggerDisplay("{DebugDisplay}")]
public abstract record BinaryVarsetItem()
{
protected abstract string DebugDisplay { get; }
}
public record BinaryVarsetItem<TValue>(
int ValueLength, // длина значения
BinaryVarsetValueType ValueType, // тип значения
TValue Magic3, // кажется 0 всегда
string Name, // имя переменной
string String2, // кажется всегда пусто
TValue Magic4,
TValue Magic5,
TValue Magic6, // минимум
TValue Magic7 // максимум
) : BinaryVarsetItem
{
protected override string DebugDisplay => $"{typeof(TValue).Name}, {ValueLength} bytes. magic3: {Magic3}. {Name,-30} {String2} - {Magic4} {Magic5} {Magic6} {Magic7}";
};
public enum BinaryVarsetValueType
{
Float0 = 0,
Int = 1,
Bool = 2,
Float = 3,
CharPtr = 4,
Dword = 5
}

View File

@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" /> <PropertyGroup>
</ItemGroup> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project> </Project>

View File

@@ -4,15 +4,8 @@ public class VarsetParser
{ {
public static List<VarsetItem> Parse(string path) public static List<VarsetItem> Parse(string path)
{ {
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); FileStream fs = new FileStream(path, FileMode.Open);
return Parse(fs);
}
public static List<VarsetItem> Parse(Stream fs)
{
try
{
var reader = new StreamReader(fs); var reader = new StreamReader(fs);
List<VarsetItem> varsetItems = []; List<VarsetItem> varsetItems = [];
@@ -43,16 +36,14 @@ public class VarsetParser
var openParenthesisIndex = line.IndexOf("("); var openParenthesisIndex = line.IndexOf("(");
var closeParenthesisIndex = line.IndexOf(")"); var closeParenthesisIndex = line.IndexOf(")");
if (openParenthesisIndex == -1 || closeParenthesisIndex == -1 || if (openParenthesisIndex == -1 || closeParenthesisIndex == -1 || closeParenthesisIndex <= openParenthesisIndex)
closeParenthesisIndex <= openParenthesisIndex)
{ {
Console.WriteLine($"Error on line: {lineIndex}! VAR() format invalid"); Console.WriteLine($"Error on line: {lineIndex}! VAR() format invalid");
lineIndex++; lineIndex++;
continue; continue;
} }
var arguments = line.Substring(openParenthesisIndex + 1, var arguments = line.Substring(openParenthesisIndex + 1, closeParenthesisIndex - openParenthesisIndex - 1);
closeParenthesisIndex - openParenthesisIndex - 1);
var parts = arguments.Trim() var parts = arguments.Trim()
.Split(','); .Split(',');
@@ -74,9 +65,4 @@ public class VarsetParser
return varsetItems; return varsetItems;
} }
catch
{
return [];
}
}
} }

180
Visualisator/Program.cs Normal file
View File

@@ -0,0 +1,180 @@
// Configure window options
using System.Buffers.Binary;
using System.Numerics;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
public static class Program
{
private static string vertexShaderSource = @"
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 uMVP;
void main()
{
gl_Position = uMVP * vec4(aPos, 1.0);
gl_PointSize = 8.0;
}
";
private static string fragmentShaderSource = @"
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0, 1.0, 1.0, 1.0); // White points
}
";
private static IWindow? window;
private static GL? gl = null;
private static uint shaderProgram = uint.MaxValue;
private static uint vao = uint.MaxValue;
private static uint vbo = uint.MaxValue;
private static Matrix4x4 mvp = new Matrix4x4();
private static float[] points = [];
public static void Main(string[] args)
{
var path = "C:\\ParkanUnpacked\\Land.msh\\2_03 00 00 00_Land.bin";
var bytes = File.ReadAllBytes(path);
points = new float[bytes.Length / 4];
for (int i = 0; i < bytes.Length / 4; i++)
{
points[i] = BinaryPrimitives.ReadSingleBigEndian(bytes.AsSpan()[(i * 4)..]);
}
var options = WindowOptions.Default;
options.API = new GraphicsAPI(ContextAPI.OpenGL, new APIVersion(3, 3));
options.Title = "3D Points with Silk.NET";
window = Window.Create(options);
window.Load += OnLoad;
window.Render += OnRender;
window.Run();
}
unsafe static void OnLoad()
{
gl = window.CreateOpenGL();
// Compile shaders
uint vertexShader = gl.CreateShader(ShaderType.VertexShader);
gl.ShaderSource(vertexShader, vertexShaderSource);
gl.CompileShader(vertexShader);
CheckShaderCompile(vertexShader);
uint fragmentShader = gl.CreateShader(ShaderType.FragmentShader);
gl.ShaderSource(fragmentShader, fragmentShaderSource);
gl.CompileShader(fragmentShader);
CheckShaderCompile(fragmentShader);
// Create shader program
shaderProgram = gl.CreateProgram();
gl.AttachShader(shaderProgram, vertexShader);
gl.AttachShader(shaderProgram, fragmentShader);
gl.LinkProgram(shaderProgram);
CheckProgramLink(shaderProgram);
gl.DeleteShader(vertexShader);
gl.DeleteShader(fragmentShader);
// Create VAO and VBO
vao = gl.GenVertexArray();
gl.BindVertexArray(vao);
vbo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
unsafe
{
fixed (float* ptr = points)
{
gl.BufferData(
BufferTargetARB.ArrayBuffer,
(nuint) (points.Length * sizeof(float)),
ptr,
BufferUsageARB.StaticDraw
);
}
}
gl.VertexAttribPointer(
0,
3,
VertexAttribPointerType.Float,
false,
3 * sizeof(float),
(void*) 0
);
gl.EnableVertexAttribArray(0);
gl.BindVertexArray(0); // Unbind VAO
gl.Enable(EnableCap.DepthTest);
}
unsafe static void OnRender(double dt)
{
gl.ClearColor(
0.1f,
0.1f,
0.1f,
1.0f
);
gl.Clear((uint) (ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit));
// Set up MVP matrix
Matrix4x4 view = Matrix4x4.CreateLookAt(
new Vector3(100, 100, 40), // Camera position
Vector3.Zero, // Look at origin
Vector3.UnitY
); // Up direction
Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(
(float) Math.PI / 4f, // 45 degrees
(float) window.Size.X / window.Size.Y,
0.1f,
100f
);
mvp = view * projection;
gl.UseProgram(shaderProgram);
// Set MVP matrix (transpose=true for column-major format)
int mvpLocation = gl.GetUniformLocation(shaderProgram, "uMVP");
fixed (Matrix4x4* ptr = &mvp)
{
gl.UniformMatrix4(
mvpLocation,
1,
true,
(float*) ptr
);
}
gl.BindVertexArray(vao);
gl.DrawArrays(PrimitiveType.Points, 0, (uint) (points.Length / 3));
}
// Error checking methods
static void CheckShaderCompile(uint shader)
{
gl.GetShader(shader, ShaderParameterName.CompileStatus, out int success);
if (success == 0)
Console.WriteLine(gl.GetShaderInfoLog(shader));
}
static void CheckProgramLink(uint program)
{
gl.GetProgram(program, ProgramPropertyARB.LinkStatus, out int success);
if (success == 0)
Console.WriteLine(gl.GetProgramInfoLog(program));
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="NativeFileDialogSharp" Version="0.5.0" />
<PackageReference Include="Silk.NET" Version="2.22.0" />
<PackageReference Include="Silk.NET.OpenGL.Extensions.ImGui" Version="2.22.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,22 @@
namespace X86Disassembler.Analysers;
/// <summary>
/// Represents a disassembled function with its control flow graph
/// </summary>
public class AsmFunction
{
/// <summary>
/// The starting address of the function
/// </summary>
public ulong Address { get; set; }
/// <summary>
/// The list of basic blocks that make up the function
/// </summary>
public List<InstructionBlock> Blocks { get; set; } = [];
public override string ToString()
{
return $"{Address:X8}\n{string.Join("\n", Blocks)}";
}
}

View File

@@ -0,0 +1,554 @@
using X86Disassembler.X86;
using X86Disassembler.X86.Operands;
namespace X86Disassembler.Analysers;
/// <summary>
/// Disassembles code into basic blocks by following control flow instructions.
/// A basic block is a sequence of instructions with a single entry point (the first instruction)
/// and a single exit point (the last instruction, typically a jump or return).
/// </summary>
public class BlockDisassembler
{
// The buffer containing the code to disassemble
private readonly byte[] _codeBuffer;
// The length of the buffer
private readonly int _length;
// The base address of the code
private readonly ulong _baseAddress;
/// <summary>
/// Initializes a new instance of the BlockDisassembler class
/// </summary>
/// <param name="codeBuffer">The raw code bytes to be disassembled</param>
/// <param name="baseAddress">The base RVA (Relative Virtual Address) of the code section</param>
public BlockDisassembler(byte[] codeBuffer, ulong baseAddress)
{
_codeBuffer = codeBuffer;
_length = codeBuffer.Length;
_baseAddress = baseAddress;
}
/// <summary>
/// Disassembles code starting from the specified RVA address by following control flow.
/// Creates blocks of instructions separated by jumps, branches, and returns.
/// </summary>
/// <param name="rvaAddress">The RVA (Relative Virtual Address) to start disassembly from</param>
/// <returns>A list of instruction blocks representing the control flow of the code</returns>
public AsmFunction DisassembleFromAddress(uint rvaAddress)
{
// Create instruction decoder for parsing the code buffer
InstructionDecoder decoder = new InstructionDecoder(_codeBuffer, _length);
// Track visited addresses to prevent infinite loops
HashSet<ulong> visitedAddresses = [];
// Queue of addresses to process (breadth-first approach)
Queue<ulong> addressQueue = [];
// Calculate the file offset from the RVA by subtracting the base address
// Store the file offset for processing, but we'll convert back to RVA when creating blocks
ulong fileOffset = rvaAddress - _baseAddress;
addressQueue.Enqueue(fileOffset);
// Keep track of the original entry point RVA for the function
ulong entryPointRVA = rvaAddress;
// List to store discovered basic blocks
List<InstructionBlock> blocks = [];
// Dictionary to track blocks by address for quick lookup
Dictionary<ulong, InstructionBlock> blocksByAddress = new Dictionary<ulong, InstructionBlock>();
while (addressQueue.Count > 0)
{
// Get the next address to process
var address = addressQueue.Dequeue();
// Skip if we've already visited this address
if (!visitedAddresses.Add(address))
{
// Skip addresses we've already processed
continue;
}
// Position the decoder at the current address
decoder.SetPosition((int) address);
// Collect instructions for this block
List<Instruction> instructions = [];
// Get the current block if it exists (for tracking predecessors)
InstructionBlock? currentBlock = null;
if (blocksByAddress.TryGetValue(address, out var existingBlock))
{
currentBlock = existingBlock;
}
// Process instructions until we hit a control flow change
while (true)
{
// Get the current position
ulong currentPosition = (ulong)decoder.GetPosition();
// If we've stepped onto an existing block, create a new block up to this point
// and stop processing this path (to avoid duplicating instructions)
if (blocksByAddress.TryGetValue(currentPosition, out var targetBlock) && currentPosition != address)
{
// We've stepped onto an existing block, create a new one up to this point
// Register this block and establish the relationship with the target block
var newBlock = RegisterBlock(blocks, address, instructions, null, false, false);
blocksByAddress[address] = newBlock;
// Add the target block as a successor to the new block
newBlock.Successors.Add(targetBlock);
// Add the new block as a predecessor to the target block
targetBlock.Predecessors.Add(newBlock);
break;
}
// Decode the next instruction
var instruction = decoder.DecodeInstruction();
// Handle decoding failures
if (instruction is null)
{
throw new InvalidOperationException($"Unexpectedly failed to decode instruction at {address}");
}
// Add the instruction to the current block
instructions.Add(instruction);
// Check for conditional jump (e.g., JZ, JNZ, JLE)
// For conditional jumps, we need to follow both the jump target and the fall-through path
if (instruction.Type.IsConditionalJump())
{
// Get the jump target address
uint jumpTargetAddress = instruction.StructuredOperands[0].GetValue();
// Get the fall-through address (next instruction after this jump)
uint fallThroughAddress = (uint)decoder.GetPosition();
// Register this block (it ends with a conditional jump)
var newBlock = RegisterBlock(blocks, address, instructions, currentBlock, false, false);
blocksByAddress[address] = newBlock;
// Register the target block if it doesn't exist yet
InstructionBlock? jumpTargetBlock = null;
if (blocksByAddress.TryGetValue(jumpTargetAddress, out var existingTargetBlock))
{
jumpTargetBlock = existingTargetBlock;
}
else
{
// We'll create this block later when we process the queue
// For now, just queue it for processing
addressQueue.Enqueue(jumpTargetAddress);
}
// Register the fall-through block if it doesn't exist yet
InstructionBlock? fallThroughBlock = null;
if (blocksByAddress.TryGetValue(fallThroughAddress, out var existingFallThroughBlock))
{
fallThroughBlock = existingFallThroughBlock;
}
else
{
// We'll create this block later when we process the queue
// For now, just queue it for processing
addressQueue.Enqueue(fallThroughAddress);
}
// If the jump target block exists, add it as a successor to the current block
if (jumpTargetBlock != null)
{
newBlock.Successors.Add(jumpTargetBlock);
jumpTargetBlock.Predecessors.Add(newBlock);
}
// If the fall-through block exists, add it as a successor to the current block
if (fallThroughBlock != null)
{
newBlock.Successors.Add(fallThroughBlock);
fallThroughBlock.Predecessors.Add(newBlock);
}
break;
}
// Check for unconditional jump (e.g., JMP)
// For unconditional jumps, we only follow the jump target
if (instruction.Type.IsRegularJump())
{
// Get the jump target address
uint jumpTargetAddress = instruction.StructuredOperands[0].GetValue();
// Register this block (it ends with an unconditional jump)
var newBlock = RegisterBlock(blocks, address, instructions, currentBlock, false, false);
blocksByAddress[address] = newBlock;
// Register the target block if it doesn't exist yet
InstructionBlock? jumpTargetBlock = null;
if (blocksByAddress.TryGetValue(jumpTargetAddress, out var existingTargetBlock))
{
jumpTargetBlock = existingTargetBlock;
}
else
{
// We'll create this block later when we process the queue
// For now, just queue it for processing
addressQueue.Enqueue(jumpTargetAddress);
}
// If the jump target block exists, add it as a successor to the current block
if (jumpTargetBlock != null)
{
newBlock.Successors.Add(jumpTargetBlock);
jumpTargetBlock.Predecessors.Add(newBlock);
}
break;
}
// Check for return instruction (e.g., RET, RETF)
// Returns end a block without any successors
if (instruction.Type.IsRet())
{
// Register this block (it ends with a return)
var newBlock = RegisterBlock(blocks, address, instructions, currentBlock, false, false);
blocksByAddress[address] = newBlock;
break;
}
}
}
// Since blocks aren't necessarily ordered (ASM can jump anywhere it likes)
// we need to sort the blocks ourselves
blocks.Sort((b1, b2) => b1.Address.CompareTo(b2.Address));
// First, establish the successor and predecessor relationships based on file offsets
// This is done by analyzing the last instruction of each block
foreach (var block in blocks)
{
if (block.Instructions.Count == 0) continue;
var lastInstruction = block.Instructions[^1];
// Check if the last instruction is a conditional jump
if (lastInstruction.Type.IsConditionalJump())
{
// Get the jump target address (file offset)
ulong targetAddress = 0;
if (lastInstruction.StructuredOperands.Count > 0 && lastInstruction.StructuredOperands[0] is RelativeOffsetOperand relOp)
{
targetAddress = relOp.TargetAddress;
}
// Find the target block
var targetBlock = blocks.FirstOrDefault(b => b.Address == targetAddress);
if (targetBlock != null)
{
// Add the target block as a successor to this block
if (!block.Successors.Contains(targetBlock))
{
block.Successors.Add(targetBlock);
}
// Add this block as a predecessor to the target block
if (!targetBlock.Predecessors.Contains(block))
{
targetBlock.Predecessors.Add(block);
}
// For conditional jumps, also add the fall-through block as a successor
// The fall-through block is the one that immediately follows this block in memory
// Find the next block in address order
var nextBlock = blocks.OrderBy(b => b.Address).FirstOrDefault(b => b.Address > block.Address);
if (nextBlock != null)
{
// The fall-through block is the one that immediately follows this block in memory
var fallThroughBlock = nextBlock;
// Add the fall-through block as a successor to this block
if (!block.Successors.Contains(fallThroughBlock))
{
block.Successors.Add(fallThroughBlock);
}
// Add this block as a predecessor to the fall-through block
if (!fallThroughBlock.Predecessors.Contains(block))
{
fallThroughBlock.Predecessors.Add(block);
}
}
}
}
// Check if the last instruction is an unconditional jump
else if (lastInstruction.Type == InstructionType.Jmp)
{
// Get the jump target address (file offset)
ulong targetAddress = 0;
if (lastInstruction.StructuredOperands.Count > 0 && lastInstruction.StructuredOperands[0] is RelativeOffsetOperand relOp)
{
targetAddress = relOp.TargetAddress;
}
// Find the target block
var targetBlock = blocks.FirstOrDefault(b => b.Address == targetAddress);
if (targetBlock != null)
{
// Add the target block as a successor to this block
if (!block.Successors.Contains(targetBlock))
{
block.Successors.Add(targetBlock);
}
// Add this block as a predecessor to the target block
if (!targetBlock.Predecessors.Contains(block))
{
targetBlock.Predecessors.Add(block);
}
}
}
// For non-jump instructions that don't end the function (like Ret), add the fall-through block
else if (!lastInstruction.Type.IsRet())
{
// The fall-through block is the one that immediately follows this block in memory
// Find the next block in address order
var nextBlock = blocks.OrderBy(b => b.Address).FirstOrDefault(b => b.Address > block.Address);
if (nextBlock != null)
{
// The fall-through block is the one that immediately follows this block in memory
var fallThroughBlock = nextBlock;
// Add the fall-through block as a successor to this block
if (!block.Successors.Contains(fallThroughBlock))
{
block.Successors.Add(fallThroughBlock);
}
// Add this block as a predecessor to the fall-through block
if (!fallThroughBlock.Predecessors.Contains(block))
{
fallThroughBlock.Predecessors.Add(block);
}
}
}
}
// Store the original file offset for each block in a dictionary
Dictionary<InstructionBlock, ulong> blockToFileOffset = new Dictionary<InstructionBlock, ulong>();
foreach (var block in blocks)
{
blockToFileOffset[block] = block.Address;
}
// Convert all block addresses from file offsets to RVA
// and update the block dictionary for quick lookup
Dictionary<ulong, InstructionBlock> rvaBlocksByAddress = new Dictionary<ulong, InstructionBlock>();
Dictionary<ulong, ulong> fileOffsetToRvaMap = new Dictionary<ulong, ulong>();
// First pass: create a mapping from file offset to RVA for each block
foreach (var block in blocks)
{
// Get the original file offset address
ulong blockFileOffset = block.Address;
// Calculate the RVA address
ulong blockRvaAddress = blockFileOffset + _baseAddress;
// Store the mapping
fileOffsetToRvaMap[blockFileOffset] = blockRvaAddress;
}
// Second pass: update all blocks to use RVA addresses
foreach (var block in blocks)
{
// Get the original file offset address
ulong blockFileOffset = block.Address;
// Update the block's address to RVA
ulong blockRvaAddress = fileOffsetToRvaMap[blockFileOffset];
block.Address = blockRvaAddress;
// Add to the dictionary for quick lookup
rvaBlocksByAddress[blockRvaAddress] = block;
}
// Now update all successors and predecessors to use the correct RVA addresses
foreach (var block in blocks)
{
// Create new lists for successors and predecessors with the correct RVA addresses
List<InstructionBlock> updatedSuccessors = new List<InstructionBlock>();
List<InstructionBlock> updatedPredecessors = new List<InstructionBlock>();
// Update successors
foreach (var successor in block.Successors)
{
// Get the original file offset of the successor
if (blockToFileOffset.TryGetValue(successor, out ulong successorFileOffset))
{
// Look up the RVA address in our mapping
if (fileOffsetToRvaMap.TryGetValue(successorFileOffset, out ulong successorRvaAddress))
{
// Find the block with this RVA address
if (rvaBlocksByAddress.TryGetValue(successorRvaAddress, out var rvaSuccessor))
{
updatedSuccessors.Add(rvaSuccessor);
}
}
}
}
// Update predecessors
foreach (var predecessor in block.Predecessors)
{
// Get the original file offset of the predecessor
if (blockToFileOffset.TryGetValue(predecessor, out ulong predecessorFileOffset))
{
// Look up the RVA address in our mapping
if (fileOffsetToRvaMap.TryGetValue(predecessorFileOffset, out ulong predecessorRvaAddress))
{
// Find the block with this RVA address
if (rvaBlocksByAddress.TryGetValue(predecessorRvaAddress, out var rvaPredecessor))
{
updatedPredecessors.Add(rvaPredecessor);
}
}
}
}
// Replace the old lists with the updated ones
block.Successors = updatedSuccessors;
block.Predecessors = updatedPredecessors;
}
// Create a new AsmFunction with the RVA address
var asmFunction = new AsmFunction()
{
Address = entryPointRVA,
Blocks = blocks,
};
// Verify that the entry block exists (no need to log this information)
return asmFunction;
}
/// <summary>
/// Creates and registers a new instruction block in the blocks collection
/// </summary>
/// <param name="blocks">The list of blocks to add to</param>
/// <param name="address">The starting address of the block</param>
/// <param name="instructions">The instructions contained in the block</param>
/// <param name="currentBlock">The current block being processed (null if this is the first block)</param>
/// <param name="isJumpTarget">Whether this block is a jump target</param>
/// <param name="isFallThrough">Whether this block is a fall-through from another block</param>
/// <returns>The newly created block</returns>
public InstructionBlock RegisterBlock(
List<InstructionBlock> blocks,
ulong address,
List<Instruction> instructions,
InstructionBlock? currentBlock = null,
bool isJumpTarget = false,
bool isFallThrough = false)
{
// Check if a block already exists at this address
var existingBlock = blocks.FirstOrDefault(b => b.Address == address);
if (existingBlock != null)
{
// If the current block is not null, update the relationships
if (currentBlock != null)
{
// Add the existing block as a successor to the current block if not already present
if (!currentBlock.Successors.Contains(existingBlock))
{
currentBlock.Successors.Add(existingBlock);
}
// Add the current block as a predecessor to the existing block if not already present
if (!existingBlock.Predecessors.Contains(currentBlock))
{
existingBlock.Predecessors.Add(currentBlock);
}
}
return existingBlock;
}
// Create a new block with the provided address and instructions
var block = new InstructionBlock()
{
Address = address,
Instructions = new List<Instruction>(instructions) // Create a copy of the instructions list
};
// Add the block to the collection
blocks.Add(block);
// If the current block is not null, update the relationships
if (currentBlock != null)
{
// Add the new block as a successor to the current block
currentBlock.Successors.Add(block);
// Add the current block as a predecessor to the new block
block.Predecessors.Add(currentBlock);
}
return block;
}
}
/// <summary>
/// Represents a basic block of instructions with a single entry and exit point
/// </summary>
public class InstructionBlock
{
/// <summary>
/// The starting address of the block
/// </summary>
public ulong Address { get; set; }
/// <summary>
/// The list of instructions contained in this block
/// </summary>
public List<Instruction> Instructions { get; set; } = [];
/// <summary>
/// The blocks that can transfer control to this block
/// </summary>
public List<InstructionBlock> Predecessors { get; set; } = [];
/// <summary>
/// The blocks that this block can transfer control to
/// </summary>
public List<InstructionBlock> Successors { get; set; } = [];
/// <summary>
/// Returns a string representation of the block, including its address, instructions, and control flow information
/// </summary>
public override string ToString()
{
// Create a string for predecessors
string predecessorsStr = Predecessors.Count > 0
? $"Predecessors: {string.Join(", ", Predecessors.Select(p => $"0x{p.Address:X8}"))}"
: "No predecessors";
// Create a string for successors
string successorsStr = Successors.Count > 0
? $"Successors: {string.Join(", ", Successors.Select(s => $"0x{s.Address:X8}"))}"
: "No successors";
// Return the complete string representation
return $"Address: 0x{Address:X8}\n{predecessorsStr}\n{successorsStr}\n{string.Join("\n", Instructions)}";
}
}

View File

@@ -0,0 +1,56 @@
namespace X86Disassembler.Analysers;
public abstract class Address(ulong value, ulong imageBase)
{
/// <summary>
/// The actual value of the address, not specifically typed.
/// </summary>
protected readonly ulong Value = value;
/// <summary>
/// PE.ImageBase from which this address is constructed
/// </summary>
protected readonly ulong ImageBase = imageBase;
}
/// <summary>
/// Absolute address in the PE file
/// </summary>
public class FileAbsoluteAddress(ulong value, ulong imageBase) : Address(value, imageBase)
{
public ulong GetValue()
{
return Value;
}
public virtual VirtualAddress AsImageBaseAddress()
{
return new VirtualAddress(Value + ImageBase, ImageBase);
}
public virtual FileAbsoluteAddress AsFileAbsolute()
{
return this;
}
}
/// <summary>
/// Address from PE.ImageBase
/// </summary>
public class VirtualAddress : FileAbsoluteAddress
{
public VirtualAddress(ulong value, ulong imageBase) : base(value, imageBase)
{
}
public override VirtualAddress AsImageBaseAddress()
{
return this;
}
public override FileAbsoluteAddress AsFileAbsolute()
{
return new FileAbsoluteAddress(Value - ImageBase, ImageBase);
}
}

Some files were not shown because too many files have changed in this diff Show More