diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 629c826..3b7f6ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,12 +40,13 @@ jobs: id: conan uses: turtlebrowser/get-conan@main with: - version: 1.54.0 + version: 2.0.9 - name: Make build folder and install Conan dependencies run: | mkdir build - conan install . -s build_type=Release --install-folder=build + conan profile detect --force + conan install . --output-folder=build --build=missing -s build_type=Release - name: Generate projects run: | @@ -61,39 +62,21 @@ jobs: - name: Move assets to distribution folder run: | mkdir dist - mv build/bin/BMEdit.exe dist - mv build/bin/d3dcompiler_47.dll dist - mv build/bin/opengl32sw.dll dist - mv build/bin/Qt6Core.dll dist - mv build/bin/Qt6Gui.dll dist - mv build/bin/Qt6OpenGL.dll dist - mv build/bin/Qt6OpenGLWidgets.dll dist - mv build/bin/Qt6Svg.dll dist - mv build/bin/Qt6Widgets.dll dist - mv build/bin/Qt63DCore.dll dist - mv build/bin/Qt63DRender.dll dist - mv build/bin/Qt63DInput.dll dist - mv build/bin/Qt63DAnimation.dll dist - mv build/bin/Qt63DExtras.dll dist - mv build/bin/Qt6Network.dll dist - mv build/bin/Qt6Concurrent.dll dist - mv build/bin/zip.dll dist - mv build/bin/translations dist/translations - mv build/bin/styles dist/styles - mv build/bin/platforms dist/platforms - mv build/bin/renderers dist/renderers - mv build/bin/sceneparsers dist/sceneparsers - mv build/bin/networkinformation dist/networkinformation - mv build/bin/tls dist/tls - mv build/bin/geometryloaders dist/geometryloaders - mv build/bin/imageformats dist/imageformats - mv build/bin/iconengines dist/iconengines + mv build/Release/BMEdit.exe dist + mv build/Release/*.dll dist + mv build/Release/styles dist/styles + mv build/Release/platforms dist/platforms + mv build/Release/networkinformation dist/networkinformation + mv build/Release/tls dist/tls + mv build/Release/imageformats dist/imageformats + mv build/Release/iconengines dist/iconengines mv Assets/g1 dist/g1 + mv Assets/scripts dist/scripts mv Assets/TypesRegistry.json dist mv README.md dist - name: Upload build artifacts - BMEdit - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: "BMEdit" path: dist diff --git a/.gitignore b/.gitignore index 622cc72..a8a3d88 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ cmake-build-* BMEdit/Editor/UI/UI/*.autosave ThirdParty/zlib/zconf.h.included Test -venv \ No newline at end of file +venv +Notes.txt \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 63856cc..764928f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "ThirdParty/fmt"] path = ThirdParty/fmt url = https://github.com/fmtlib/fmt +[submodule "ThirdParty/glm"] + path = ThirdParty/glm + url = https://github.com/g-truc/glm diff --git a/Assets/TypesRegistry.json b/Assets/TypesRegistry.json index 12f22ab..a2b5340 100644 --- a/Assets/TypesRegistry.json +++ b/Assets/TypesRegistry.json @@ -115,5 +115,6 @@ "0x200458": "ZHFlowLnkObj", "0x20045C": "ZMardiGrassHFlow", "0x20045F": "ZHFlowM11" - } + }, + "script_incs": "scripts" } \ No newline at end of file diff --git a/Assets/g1/CCheckInside.json b/Assets/g1/CCheckInside.json index 207692a..7206569 100644 --- a/Assets/g1/CCheckInside.json +++ b/Assets/g1/CCheckInside.json @@ -3,10 +3,10 @@ "parent": "ZEventBase", "kind": "TypeKind.COMPLEX", "properties": [ - { "name": "property_2C", "typename": "ZGEOMREF", "offset": 44 }, - { "name": "property_30", "typename": "ZGEOMREF", ",offset": 48 }, - { "name": "property_34", "typename": "ZMsg", "offset": 52 }, - { "name": "property_36", "typename": "ZMsg", "offset": 54 }, + { "name": "rTarget", "typename": "ZGEOMREF", "offset": 44 }, + { "name": "rObserverActor", "typename": "ZGEOMREF", ",offset": 48 }, + { "name": "OnEnterMSG", "typename": "ZMsg", "offset": 52 }, + { "name": "OnLeaveMSG", "typename": "ZMsg", "offset": 54 }, { "name": "property_38", "typename": "PRPOpCode.Bool", "offset": 56 }, { "name": "property_39", "typename": "PRPOpCode.Bool", "offset": 57 }, { "name": "property_3A", "typename": "PRPOpCode.Bool", "offset": 58 }, diff --git a/Assets/g1/ECollisionMask.json b/Assets/g1/ECollisionMask.json new file mode 100644 index 0000000..89bb00c --- /dev/null +++ b/Assets/g1/ECollisionMask.json @@ -0,0 +1,15 @@ +{ + "typename": "ECollisionMask", + "kind": "TypeKind.ENUM", + "enum": { + "COLIMASK_All" : 1, + "COLIMASK_Background": 2, + "COLIMASK_Shot": 4, + "COLIMASK_WaterGlass": 8, + "COLIMASK_NoWalk": 16, + "COLIMASK_Sight": 32, + "COLIMASK_Hero": 64, + "COLIMASK_Camera": 128, + "COLIMASK_NPC": 256 + } +} \ No newline at end of file diff --git a/Assets/g1/PostMissionPF.json b/Assets/g1/PostMissionPF.json new file mode 100644 index 0000000..b020594 --- /dev/null +++ b/Assets/g1/PostMissionPF.json @@ -0,0 +1,7 @@ +{ + "typename": "PostMissionPF", + "parent": "ZEventBase", + "kind": "TypeKind.COMPLEX", + "properties": [ + ] +} \ No newline at end of file diff --git a/Assets/g1/ZAction.json b/Assets/g1/ZAction.json index b44b194..02787db 100644 --- a/Assets/g1/ZAction.json +++ b/Assets/g1/ZAction.json @@ -3,7 +3,7 @@ "parent": "ZEventBase", "kind": "TypeKind.COMPLEX", "properties": [ - { "name": "ActionName", "typename": "PRPOpCode.String" }, + { "name": "ActionName", "typename": "ZLocalizedString" }, { "name": "IsVisible", "typename": "PRPOpCode.Bool" }, { "name": "ActionType", "typename": "ZAction.EActionType", "offset": 44 }, { "name": "MessageID", "typename": "ZMsg", "offset": 48 }, diff --git a/Assets/g1/ZGEOMREF.json b/Assets/g1/ZGEOMREF.json index c40d57e..12ef845 100644 --- a/Assets/g1/ZGEOMREF.json +++ b/Assets/g1/ZGEOMREF.json @@ -1,5 +1,6 @@ { "typename": "ZGEOMREF", "kind": "TypeKind.ALIAS", - "alias": "PRPOpCode.String" + "alias": "PRPOpCode.String", + "editor": "SelectGeomTool" } \ No newline at end of file diff --git a/Assets/g1/ZGuardQuarterController.json b/Assets/g1/ZGuardQuarterController.json index dfbe780..df987c3 100644 --- a/Assets/g1/ZGuardQuarterController.json +++ b/Assets/g1/ZGuardQuarterController.json @@ -3,6 +3,6 @@ "parent": "ZEventBase", "kind": "TypeKind.COMPLEX", "properties": [ - { "name": "property_DC", "typename": "ZGEOMREF", "offset": 220 } + { "name": "rWeaponStorage", "typename": "ZGEOMREF", "offset": 220 } ] } \ No newline at end of file diff --git a/Assets/g1/ZHM3Actor.json b/Assets/g1/ZHM3Actor.json index 6aa42f8..a3a72d4 100644 --- a/Assets/g1/ZHM3Actor.json +++ b/Assets/g1/ZHM3Actor.json @@ -8,7 +8,7 @@ "parent": "ZActor", "properties": [ { - "name": "property_900", + "name": "rDisguise", "offset": 2304, "typename": "ZGEOMREF" }, diff --git a/Assets/g1/ZHM3ItemTemplateWeapon.json b/Assets/g1/ZHM3ItemTemplateWeapon.json index 437e1e1..3b7c4c0 100644 --- a/Assets/g1/ZHM3ItemTemplateWeapon.json +++ b/Assets/g1/ZHM3ItemTemplateWeapon.json @@ -28,7 +28,7 @@ "typename": "ZHM3ItemTemplateWeapon.EHM3WeaponScope" }, { - "name": "property_178", + "name": "rReloadAnim", "offset": 372, "typename": "PRPOpCode.String" }, @@ -38,12 +38,12 @@ "typename": "PRPOpCode.String" }, { - "name": "property_180", + "name": "rHoldAnim", "offset": 380, "typename": "PRPOpCode.String" }, { - "name": "property_184", + "name": "rRecoilAnim", "offset": 384, "typename": "PRPOpCode.String" }, diff --git a/Assets/g1/ZHM3LevelControlM13.json b/Assets/g1/ZHM3LevelControlM13.json index f63c671..9764928 100644 --- a/Assets/g1/ZHM3LevelControlM13.json +++ b/Assets/g1/ZHM3LevelControlM13.json @@ -16,7 +16,7 @@ { "name": "rJournalistActor", "typename": "ZGEOMREF", "offset": 1540 }, { "name": "property_608", "typename": "ZGEOMREF", "offset": 1544 }, { "name": "rPriestActor", "typename": "ZGEOMREF", "offset": 1548 }, - { "name": "property_610", "typename": "ZGEOMREF", "offset": 1552 }, - { "name": "property_614", "typename": "ZGEOMREF", "offset": 1556 } + { "name": "rHitmanDieCutSequence", "typename": "ZGEOMREF", "offset": 1552 }, + { "name": "rMoviePlayer", "typename": "ZGEOMREF", "offset": 1556 } ] } \ No newline at end of file diff --git a/Assets/g1/ZItem.json b/Assets/g1/ZItem.json index cc0398a..313963d 100644 --- a/Assets/g1/ZItem.json +++ b/Assets/g1/ZItem.json @@ -8,7 +8,7 @@ "parent": "ZGROUP", "properties": [ { - "name": "property_4C", + "name": "rItemTemplate", "offset": 76, "typename": "ZGEOMREF" }, diff --git a/Assets/g1/ZItemTemplateWeapon.json b/Assets/g1/ZItemTemplateWeapon.json index 1003159..aab91db 100644 --- a/Assets/g1/ZItemTemplateWeapon.json +++ b/Assets/g1/ZItemTemplateWeapon.json @@ -23,7 +23,7 @@ "typename": "ZItemTemplateWeapon.WEAPONTYPE" }, { - "name": "property_A4", + "name": "rAmmoTemplate", "offset": 164, "typename": "ZREFTAB32" }, @@ -78,12 +78,12 @@ "typename": "PRPOpCode.Bool" }, { - "name": "property_E0", + "name": "rModelStates", "offset": 224, "typename": "ZREFTAB32" }, { - "name": "property_FC", + "name": "rFlash", "offset": 252, "typename": "ZGEOMREF" }, diff --git a/Assets/g1/ZLocalizedString.json b/Assets/g1/ZLocalizedString.json new file mode 100644 index 0000000..affc011 --- /dev/null +++ b/Assets/g1/ZLocalizedString.json @@ -0,0 +1,6 @@ +{ + "typename": "ZLocalizedString", + "kind": "TypeKind.ALIAS", + "alias": "PRPOpCode.String", + "editor": "SelectLocalizationKey" +} \ No newline at end of file diff --git a/Assets/g1/ZROOM.json b/Assets/g1/ZROOM.json index cb20387..faf0ea4 100644 --- a/Assets/g1/ZROOM.json +++ b/Assets/g1/ZROOM.json @@ -8,12 +8,14 @@ "parent": "ZTreeGroup", "properties": [ { - "name": "property_6C", + "name": "iNeighboursCount", + "__comment__": "I'm not sure about that. 6C and 84 sometimes same", "offset": 108, "typename": "PRPOpCode.Int32" }, { - "name": "property_84", + "name": "iExitsCount", + "__comment__": "I'm not sure about that. 6C and 84 sometimes same", "offset": 132, "typename": "PRPOpCode.Int8" }, diff --git a/Assets/g1/ZScriptC.ScriptID.json b/Assets/g1/ZScriptC.ScriptID.json new file mode 100644 index 0000000..9cf244a --- /dev/null +++ b/Assets/g1/ZScriptC.ScriptID.json @@ -0,0 +1,6 @@ +{ + "typename": "ZScriptC.ScriptID", + "kind": "TypeKind.ALIAS", + "alias": "PRPOpCode.String", + "editor": "SelectGameScript" +} \ No newline at end of file diff --git a/Assets/g1/ZScriptC.json b/Assets/g1/ZScriptC.json index 6b83440..4df43da 100644 --- a/Assets/g1/ZScriptC.json +++ b/Assets/g1/ZScriptC.json @@ -4,6 +4,6 @@ "kind": "TypeKind.COMPLEX", "skip_unexposed_properties": true, "properties": [ - { "name": "ScriptName", "typename": "PRPOpCode.String" } + { "name": "ScriptName", "typename": "ZScriptC.ScriptID", "__note": "DO NOT MODIFY THIS NAME!!!" } ] } \ No newline at end of file diff --git a/Assets/scripts/AllLevels.json b/Assets/scripts/AllLevels.json new file mode 100644 index 0000000..7f542e2 --- /dev/null +++ b/Assets/scripts/AllLevels.json @@ -0,0 +1,624 @@ +{ + "alllevels\\civilian": { + "parameters": [ + { + "kind": "UNKNOWN", + "opcodes": [ + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray", + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray", + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray" + ] + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rHelpPointList" + }, + { + "kind": "UNKNOWN", + "opcodes": [ + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray", + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray", + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray" + ] + } + ] + }, + "alllevels\\guard": { + "parameters": [ + { + "kind": "UNKNOWN", + "opcodes": [ + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray", + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray", + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray" + ] + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rHelpPointList" + }, + { + "kind": "UNKNOWN", + "opcodes": [ + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray", + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray", + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray" + ] + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rPrimaryWeapon" + }, + { + "kind": "UNKNOWN", + "opcodes": [ + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray", + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray" + ] + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rBodyStorage" + }, + { + "kind": "UNKNOWN", + "opcodes": [ + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray", + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray" + ] + } + ] + }, + "alllevels\\radiocontroller": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownMask" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue3" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue6" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue7" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue8" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue9" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "fUnknownValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "fUnknownValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\rat": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rUnkRef" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rUnkRef1" } + ] + }, + "alllevels\\useable\\useable_wait": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "fUnkValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "fUnkValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue3" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "fUnkValue4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "fUnkValue6" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\useable\\useable_sitdown": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnkValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue3" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue6" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\useable\\useable_playanim": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sAnimationName" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnkValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue3" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnkValue4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnkValue6" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue7" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue8" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "fUnkValue9" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue10" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\useable\\useable_playubanim": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sAnimationName" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnknownValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue3" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnknownValue4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "rUnknownValue6" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue7" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\useable\\useable_piss": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\useable\\useable_smoking": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rCigarettes" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue3" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\useable\\useable_consumeobject": { + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rItems" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnkValue3" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rDummyObject" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnkValue5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\useable\\useable_lookaround": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnkValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnkValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\useable\\useable_playoneliner": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sAnim0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sAnim1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sAnim2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sAnim3" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue6" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue7" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue8" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\useable\\useable_waypointnotify": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\vehicles\\car": { + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rCheckInside" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\kissingcouple": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "rCheckInside" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue6" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rUnknownGeomRef" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnkValue8" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rMale" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue11" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue12" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\armedbartender": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue3" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnkValue4" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue6" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue7" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnkValue8" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue9" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue10" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rBartendingPos" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rBarguestUsepoint" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rGlass" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rGlassPos" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rBottle" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rBottlePos" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rSponge" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rGlassToClean" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnkValue19" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\bird": { + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rFlyBox" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue1" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue2" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue3" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "iUnknownValue4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "iUnknownValue5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "iUnknownValue6" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue7" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue8" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue9" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue10" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue11" } + ] + }, + "alllevels\\alligator": { + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rBox" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rUnknown" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rHidePos" } + ] + }, + "alllevels\\armed": { + "parent": "alllevels\\civilian", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPrimaryWeapon" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue8" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue9" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "alllevels\\poodle": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnknownValue3" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rOwnerHuman" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnknownValue6" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnknownValue7" } + ] + }, + "alllevels\\useable\\useable_playanim_pickup": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "rANIM_0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iPlayCount_0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "rANIM_1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iPlayCount_1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "rANIM_2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iPlayCount_2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnknownValue5" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnknownValue6" } + ] + } +} \ No newline at end of file diff --git a/Assets/scripts/Hideout.json b/Assets/scripts/Hideout.json new file mode 100644 index 0000000..810f2e5 --- /dev/null +++ b/Assets/scripts/Hideout.json @@ -0,0 +1,77 @@ +{ + "hideout\\hideout_levelcontrol": { + "parameters": [ + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rListAllWeapons" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rListAllAmmo" + } + ] + }, + "hideout\\hideout_canary": { + "parameters": [ + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnknownValue0" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnknownValue1" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnknownValue2" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue3" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "iUnknownValue4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "iUnknownValue5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "iUnknownValue6" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue7" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue8" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue9" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue10" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnknownValue11" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rBirdPos" } + ] + }, + "hideout\\hideout_happyrat": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "iUnknownValue3" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rRatRace" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rArrow" } + ] + } +} \ No newline at end of file diff --git a/Assets/scripts/M00.json b/Assets/scripts/M00.json new file mode 100644 index 0000000..20e86b3 --- /dev/null +++ b/Assets/scripts/M00.json @@ -0,0 +1,424 @@ +{ + "m00\\m00_levelcontrol": { + "parameters": [ + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rMrSwingKing" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rScoop" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rFrontGateGuard" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rLightRack0" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rSpotsList" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rWinchA" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rWinchA_Broken" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rDruglabActorsList" + } + ] + }, + "m00\\m00_sniper": { "parent": "alllevels\\guard" }, + "m00\\m00_scoopsguards": { + "parent": "alllevels\\guard", + "parameters": [ + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rIdlePos" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rShootPos" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rScoop" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rScoopsVictim" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rGuardSearchZone_0" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rGuardSearchZone_1" + } + ] + }, + "m00\\m00_scoopsbiatch": { + "parent": "alllevels\\civilian", + "parameters": [ + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rBlowPos" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rScoop" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rVictim" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rStandIdlePos" + } + ] + }, + "m00\\scoop": { + "parent": "alllevels\\guard", + "parameters": [ + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rBed" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rScoopsVictim" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rDesertEagleItem" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rGunPos" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rSnoopsTroops" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rFluffer" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rAlertPos" + } + ] + }, + "m00\\m00_scoopsvictim": { + "parent": "alllevels\\guard", + "parameters": [ + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rVictimStand" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rScoop" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rGuard0" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rGuard1" + } + ] + }, + "m00\\entrancebirdsgroundbox": { + "parameters": [{ "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rBirdList" }] + }, + "m00\\swingking": { + "parent": "alllevels\\civilian", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rSecretary" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rUnknown" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rCameraTarget" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rChair" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosBegging" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rItem_Photo" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rItem_Phone" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPhoneLoc" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosTalkPhone" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosBuzzerSound" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosBegDesk" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosHitmanDialog" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rSwingKingPosForDialog" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "rUnknownFlags_0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "rUnknownFlags_1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "m00\\flirtingguard": { + "parent": "alllevels\\guard", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rSecretary" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rMrSwingKing" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosGuardFlirt" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosGuardWindow" } + ] + }, + "m00\\secretary": { + "parent": "alllevels\\civilian", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rFlirtingGuard" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rMrSwingKing" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosSecretarySitDown" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosSecretaryTalkToSwingKing" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosSecretaryFlirt" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rSodaItem" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rDrinkPlacePos" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosDrink" } + ] + }, + "m00\\m00_playinggangster": { + "parent": "alllevels\\guard", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "__m00_playinggangster__Unk0" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rCardsItem" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPlayingGuard" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "__m00_playinggangster__Unk1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPissingGuard" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rChair" } + ] + }, + "m00\\m00_tensiongangster": { + "parent": "alllevels\\guard", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rTensionGuardPos" } + ] + }, + "m00\\m00_friskguard": { + "parent": "alllevels\\guard", + "parameters": [] + }, + "m00\\m00_druglabgangster": { + "parent": "alllevels\\guard", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rUtilitiesBox" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rRepairUtilBox_Guy" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rStandGuard" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rStandGuardInDarkness" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "__m00_druglabgangster__Unk0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rIgnoreSoundsBox" } + ] + }, + "m00\\guardinelevator": { + "parent": "alllevels\\guard", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosInElevator" } + ] + }, + "m00\\druggirl": { + "parent": "alllevels\\civilian", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rMakeDrugs_UsePoint0" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rMakeDrugs_UsePoint1" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rMakeDrugs_UsePoint2" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "__druggirl__Unk0" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "__druggirl__Unk1" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "__druggirl__Unk2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "m00\\lawyer": { + "parent": "alllevels\\civilian", + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "__lawyer__Unk0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "__lawyer__Unk1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "__lawyer__Unk2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "__druggirl__Unk3" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "__lawyer__Unk4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rSitPosition" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPsychoBob" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "__lawyer__Unk5" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "__lawyer__UnkMask0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + }, + "m00\\chemist": { + "parent": "alllevels\\civilian", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rDoor" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rStandPos" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rTakeMoneyPos" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rKnockSound" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rKeycard_Item" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rLevelControl" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWaitTillDoor" } + ] + }, + "m00\\m00_testergangster_01": { + "parent": "alllevels\\guard", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rSecondGuy" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rChair" } + ] + }, + "m00\\m00_gateguard": { + "parent": "alllevels\\civilian", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rHitmanPos" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rUnk" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosAtGate" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rCutSequencePlayer" } + ] + }, + "m00\\m00_talkinggangstab": { + "parent": "alllevels\\guard", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rTalkingGuardC" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rFaceThisWayAfterDistraction" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPos" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rIgnoreDistractionsThatAreInsideThisBox" } + ] + }, + "m00\\m00_talkinggangstac": { + "parent": "alllevels\\guard", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rTalkingGuardC" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rFaceThisWayAfterDistraction" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPos" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rIgnoreDistractionsThatAreInsideThisBox" } + ] + }, + "m00\\torturegangster": { + "parent": "alllevels\\guard", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rLawyer" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rInterrogatePoint" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rSmokePoint" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rGasolineItem" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rGasolineEmitter" } + ] + }, + "m00\\m00_basebird": { + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rFlyBox" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rUnk0" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rGroundPoints" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnk1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "fUnk2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "fUnk3" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "fUnk4" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnk5" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnk6" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnk7" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnk8" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rBirdEffectList" } + ] + }, + "m00\\peckingbird": { + "parent": "m00\\m00_basebird", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rArrowDir" } + ] + }, + "m00\\entrancebird": { + "parent": "m00\\m00_basebird", + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "peckingbird_Unk0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] } + ] + } +} \ No newline at end of file diff --git a/Assets/scripts/M12.json b/Assets/scripts/M12.json new file mode 100644 index 0000000..97fa788 --- /dev/null +++ b/Assets/scripts/M12.json @@ -0,0 +1,324 @@ +{ + "m12\\m12_touristwithsuitcase": { + "parent": "alllevels\\civilian", + "parameters": [ + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rGuidePerson" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rTargetPlace" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rSuitcase" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rPutDownSuitcaseHere" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rSmokeHere" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rXrayPutDown" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rXrayPickup" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rPutDownSuitcaseXRAYHere" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "cSecurityXray" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rInterrogationPlace" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rFollowToGuard" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "cEntranceController" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "BoxPosEntranceArea" + } + ] + }, + "m12\\m12_touristpushable": { + "parent": "alllevels\\civilian", + "parameters": [ + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rPushPoint0" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "pathThrownOut" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rAlarmController" + } + ] + }, + "m12\\m12_tourist": { + "parent": "alllevels\\civilian", + "parameters": [ + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rGuidePerson" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rMuseumFoyer" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rPosEntranceCheckDone" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "cEntranceController" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "BoxPosFrontArea" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "BoxPosEntranceArea" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rCameraItem" + }, + { + "kind": "UNKNOWN", + "opcodes": [ + "PRPOpCode.Array" + ] + }, + { + "kind": "VARIABLE", + "typename": "PRPOpCode.Int32", + "name": "iPersonInPhotoIdx" + }, + { + "kind": "UNKNOWN", + "opcodes": [ + "PRPOpCode.EndArray" + ] + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rPhotoPos" + } + ] + }, + "m12\\m12_tourguide": { + "parent": "alllevels\\civilian", + "parameters": [ + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rTourList" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rTouristTourPositionsList" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rTouristList" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rlTouristList" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "cEntranceController" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rPosWaitForTourists" + } + ] + }, + "m12\\m12_museumstaff": { "parent": "alllevels\\civilian" }, + "m12\\m12_museumstaffinshower": { "parent": "alllevels\\civilian" }, + "m12\\m12_guardbase": { "parent": "alllevels\\guard" }, + "m12\\m12_marinebase": { + "parent": "m12\\m12_guardbase", + "parameters": [ + { + "kind": "UNKNOWN", + "opcodes": [ + "PRPOpCode.Array", "PRPOpCode.Int32", "PRPOpCode.EndArray" + ] + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rPosMontrePosition" + } + ] + }, + "m12\\m12_marinecameraguard": { "parent": "m12\\m12_marinebase" }, + "m12\\m12_marinexray": { + "parent": "m12\\m12_marinebase", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rSecurityCheckpoint" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rGuardPoint" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rList_IgnoreHitmanAs" } + ] + }, + "m12\\m12_marineentrance": { "parent": "m12\\m12_marinexray" }, + "m12\\m12_suitcasebasementcontroller": { + "parameters": [ + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rFrom" + }, + { + "kind": "VARIABLE", + "typename": "ZGEOMREF", + "name": "rIDK" + } + ] + }, + "m12\\m12_albinoassassin": { + "parent": "alllevels\\armed", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPathOvalOffice" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosMrXIntermezzo" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rMrX" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rMrX_CheckBox" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosDialogue_Hitman" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosDialogue_Albino" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rTrigger_HitmanNearOvalOffice_inside" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rTrigger_HitmanNearOvalOffice_outside" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPoshitmanNearingOutsided" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rHitmanEnterOvalOffice" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rBombActivatorItem" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rVirtualAimPoint" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rCoverBoxesList" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPathToTheRoof" }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Float32", "name": "fSeeHitmanDistance" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rBombsList" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rListVisibleBombs" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rHMHighNoonScaffold" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosHighNoonShootScaffold" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWW_HMHighNoonClimbWall" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosHighNoonShootClimbWall" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rBombController" } + ] + }, + "m12\\m12_mrx": { + "parent": "alllevels\\armed", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rDog" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rDogPath_PissOut" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rDogPath_PathBack" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rFirstLady" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPickUpDog" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rMyOffice" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPath_PatrolOffice" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rTrigger_MrXOfficeHitman" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rTrigger_OfficeMrX" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rHMAS_Carpenter" } + ] + }, + "m12\\m12_officebase": { "parent": "alllevels\\civilian" }, + "m12\\m12_carpenteralt": { + "parent": "alllevels\\civilian", + "parameters": [ + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownParameter_M12_0" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownParameter_M12_1" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPosDialog" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rCarpenter_01" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rItem" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rToolbox" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPathHammerMrX" } + ] + }, + "m12\\m12_justice": { "parent": "alllevels\\poodle" }, + "m12\\m12_firstlady": { + "parent": "alllevels\\civilian", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rIntercom" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rIntercomMrX" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rDog" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rInterludePos" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rMrX" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rNSA_Guy" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rNSA_Office" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWalk_Chamber" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rMrX_Office" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWalk_NSA" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWalkInterlude" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPathBackToChamber" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rTrigger_DogExchangeBox" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rTrigger_StartMrX_FirstLady" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPathChamberWalk" } + ] + }, + "m12\\m12_securitycheckpointguard": { + "parent": "m12\\m12_marinebase", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rTargetCivilian" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue_M12_SecurityCheckpointGuard_1" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rSuitcaseStorage" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rPickUpSuitcase" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rGuardPoint21" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue_M12_SecurityCheckpointGuard_5" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue_M12_SecurityCheckpointGuard_6" } + ] + } +} \ No newline at end of file diff --git a/Assets/scripts/M13.json b/Assets/scripts/M13.json new file mode 100644 index 0000000..8cc3943 --- /dev/null +++ b/Assets/scripts/M13.json @@ -0,0 +1,67 @@ +{ + "m13\\m13_levelcontrol": {}, + "m13\\journalist": { + "parent": "alllevels\\civilian", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rVoiceRecorder" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWheelchairGuy" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWheelchairGuy_ForGame" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWheelchairGuy_MOVETOCREATION" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rLookout" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWaypointsList" } + ] + }, + "m13\\wheelchair": { + "parameters": [ + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue0" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue1" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.Array"] }, + { "kind": "VARIABLE", "typename": "PRPOpCode.Int32", "name": "iUnknownValue2" }, + { "kind": "UNKNOWN", "opcodes": ["PRPOpCode.EndArray"] }, + + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue3" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWheelchairGuy" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWheelChair_for_game" } + ] + }, + "m13\\wheelchairguy": { + "parent": "alllevels\\armed", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWheelChair_for_game_0" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWheelChair_for_game_1" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rJournalist_01" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rScar" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWaypoint_Mausoleum" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWaypoint_lookout" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWaypoint_Escape" } + ] + }, + "m13\\priest": { + "parent": "alllevels\\civilian", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rBible" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWaypoint_Podium" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWaypoint_preach" } + ] + }, + "m13\\secretservice": { + "parent": "alllevels\\guard", + "parameters": [ + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rCinematicTargets" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rInsideChurchRoom" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue15" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue16" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWheelchairGuy" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rWheelchairGuy_ForGame" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rNearVehiclesWaypoint" }, + { "kind": "VARIABLE", "typename": "PRPOpCode.String", "name": "sUnknownValue20" }, + { "kind": "VARIABLE", "typename": "ZGEOMREF", "name": "rGuardList" } + ] + } +} \ No newline at end of file diff --git a/BMEdit/Editor/CMakeLists.txt b/BMEdit/Editor/CMakeLists.txt index f66525f..d0d1b3c 100644 --- a/BMEdit/Editor/CMakeLists.txt +++ b/BMEdit/Editor/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.17) project(Editor VERSION 1.0.0 LANGUAGES CXX) +# --- Deps +add_subdirectory(ThirdParty/RenderDoc) + # --- Language standard set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -10,7 +13,7 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC OFF) -find_package(Qt6 6.4.1 REQUIRED COMPONENTS Widgets OpenGL OpenGLWidgets 3DCore 3DRender 3DInput 3DLogic) +find_package(Qt6 REQUIRED COMPONENTS Widgets OpenGL OpenGLWidgets) find_package(OpenGL REQUIRED) message(STATUS "Found Qt component versions: ") @@ -62,6 +65,8 @@ target_compile_definitions(Editor ) # --- Required GameLib & Qt6 -target_link_libraries(Editor PUBLIC Qt6::Widgets OpenGL::GL Qt6::OpenGL Qt6::OpenGLWidgets Qt6::3DCore Qt6::3DRender Qt6::3DLogic) -target_link_libraries(Editor PRIVATE GameLib) -target_link_libraries(Editor PRIVATE zip zlib bz2 lzma zstd_static) \ No newline at end of file +target_link_libraries(Editor PUBLIC Qt6::Widgets OpenGL::GL Qt6::OpenGL Qt6::OpenGLWidgets) +target_link_libraries(Editor PRIVATE GameLib RenderDoc::AppInterface) +target_link_libraries(Editor PRIVATE ${libzip_LIBRARIES} ${ZLIB_LIBRARIES} ${libsquish_LIBRARIES}) +target_include_directories(Editor PRIVATE ${ZLIB_INCLUDE_DIRS} ${libzip_INCLUDE_DIRS} ${libsquish_INCLUDE_DIRS}) +target_compile_definitions(Editor PRIVATE ${libzip_DEFINITIONS} ${libsquish_DEFINITIONS}) \ No newline at end of file diff --git a/BMEdit/Editor/Include/Delegates/TypePropertyItemDelegate.h b/BMEdit/Editor/Include/Delegates/TypePropertyItemDelegate.h index dabc368..2b61107 100644 --- a/BMEdit/Editor/Include/Delegates/TypePropertyItemDelegate.h +++ b/BMEdit/Editor/Include/Delegates/TypePropertyItemDelegate.h @@ -25,6 +25,9 @@ namespace delegates const QStyleOptionViewItem &option, const QModelIndex &index) const override; + protected: + bool eventFilter(QObject* editor, QEvent* event) override; + private slots: void commitDataChunk(); void commitDataChunkAndCloseEditor(); diff --git a/BMEdit/Editor/Include/Editor/EditorInstance.h b/BMEdit/Editor/Include/Editor/EditorInstance.h index 4223a03..e3e77ac 100644 --- a/BMEdit/Editor/Include/Editor/EditorInstance.h +++ b/BMEdit/Editor/Include/Editor/EditorInstance.h @@ -31,6 +31,7 @@ namespace editor { void exportAsset(gamelib::io::AssetKind assetKind); bool exportPRP(const QString &filePath); + bool exportLOC(const QString &filePath); signals: void levelLoadSuccess(); diff --git a/BMEdit/Editor/Include/Editor/TextureProcessor.h b/BMEdit/Editor/Include/Editor/TextureProcessor.h new file mode 100644 index 0000000..9612fe7 --- /dev/null +++ b/BMEdit/Editor/Include/Editor/TextureProcessor.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include + + +namespace editor +{ + class TextureProcessor + { + public: + // Compress/decompress + + /** + * @fn decompressRGBA + * @brief decompress TEXEntry into RGBA buffer from various formats (RGBA,U8V8,I8,DXT1,DXT3) + * @param texEntry - TEX file entry to decompress + * @param realWidth - [out] calculated width of texture (by mip level) + * @param realHeight - [out] calculated height of texture (by mip level) + * @param mipLevel - mip level, pass 0 when no mip levels presented + * @return valid pointer to memory of size W*H*4 with pixels, or nullptr on error occurred + */ + static std::unique_ptr decompressRGBA(const gamelib::tex::TEXEntry& texEntry, uint16_t& realWidth, uint16_t& realHeight, std::size_t mipLevel = 0); + + // Import/Export + static bool exportTEXEntryAsPNG(const gamelib::tex::TEXEntry& texEntry, const std::filesystem::path& filePath, std::size_t mipLevel = 0); + static bool importTextureToEntry(gamelib::tex::TEXEntry& texEntry, const QString& texturePath, const QString& textureName, gamelib::tex::TEXEntryType targetFormat, uint8_t mipLevelsCount); + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Models/GameScriptsTreeModel.h b/BMEdit/Editor/Include/Models/GameScriptsTreeModel.h new file mode 100644 index 0000000..695112c --- /dev/null +++ b/BMEdit/Editor/Include/Models/GameScriptsTreeModel.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include + + +namespace models +{ + class GameScriptsTreeModel : public QAbstractItemModel + { + Q_OBJECT + + public: + struct ScripTreeNode + { + enum class NodeType { + STN_ROOT, /// < Just a ROOT subject. No name, no selector + STN_BONE, /// < Bone (temp node). Unable to use as real path + STN_SCRIPT /// < Script itself. Could be used as final value + }; + + NodeType type { NodeType::STN_ROOT }; + QString name {}; + QString fullPath {}; + QWeakPointer parent {}; + QList> children {}; + + ScripTreeNode(); + ScripTreeNode(NodeType nt, QString&& sn ,QString&& sfn); + }; + + public: + explicit GameScriptsTreeModel(QObject *parent = nullptr); + + QVariant data(const QModelIndex &index, int role) const override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + private: + void buildTree(); + + private: + QSharedPointer m_root { nullptr }; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Models/GeomControllerListModel.h b/BMEdit/Editor/Include/Models/GeomControllerListModel.h index 6fb7ee1..1092e17 100644 --- a/BMEdit/Editor/Include/Models/GeomControllerListModel.h +++ b/BMEdit/Editor/Include/Models/GeomControllerListModel.h @@ -27,6 +27,8 @@ namespace models void setGeom(gamelib::scene::SceneObject *sceneObject); void resetGeom(); + void updateControllersList(); + private: [[nodiscard]] bool isReady() const; diff --git a/BMEdit/Editor/Include/Models/LocalizationTreeModel.h b/BMEdit/Editor/Include/Models/LocalizationTreeModel.h new file mode 100644 index 0000000..4dd84ca --- /dev/null +++ b/BMEdit/Editor/Include/Models/LocalizationTreeModel.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include + + +namespace models +{ + class LocalizationTreeModel : public QAbstractItemModel + { + Q_OBJECT + public: + explicit LocalizationTreeModel(QObject *parent = nullptr); + explicit LocalizationTreeModel(const gamelib::Level *level, QObject *parent = nullptr); + + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + void setLevel(const gamelib::Level *level); + void resetLevel(); + QModelIndex getRootIndex() const; + + private: + [[nodiscard]] bool isValidLevel() const; + + private: + const gamelib::Level *m_level { nullptr }; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Models/ModelsLocator.h b/BMEdit/Editor/Include/Models/ModelsLocator.h new file mode 100644 index 0000000..ad788a0 --- /dev/null +++ b/BMEdit/Editor/Include/Models/ModelsLocator.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include +#include +#include + + +namespace models +{ + /** + * @brief This class holds all sharable models to share them between dialogs/widgets/etc. + */ + struct ModelsLocator final + { + ModelsLocator() = default; + ModelsLocator(const ModelsLocator&) = delete; + ModelsLocator(ModelsLocator&&) = delete; + + + // Instances + static std::unique_ptr s_SceneTreeModel; + static std::unique_ptr s_GameScriptsTreeModel; + static std::unique_ptr s_LocalizationTreeModel; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Models/SceneObjectControllerModel.h b/BMEdit/Editor/Include/Models/SceneObjectControllerModel.h index b29ed82..627bb0e 100644 --- a/BMEdit/Editor/Include/Models/SceneObjectControllerModel.h +++ b/BMEdit/Editor/Include/Models/SceneObjectControllerModel.h @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include +#include namespace models @@ -10,6 +12,7 @@ namespace models class SceneObjectControllerModel : public ValueModelBase { Q_OBJECT + public: SceneObjectControllerModel(QObject *parent = nullptr); @@ -18,11 +21,17 @@ namespace models void setControllerIndex(int controllerIndex); void resetController(); + private: + void addSugarViews(const gamelib::Type* pControllerType, gamelib::Value& v, const std::string& scriptName); + void removeSugarViews(const gamelib::Type* pControllerType, gamelib::Value& v); + private slots: void onValueChanged(); private: + static constexpr int kUnset = -1; + gamelib::scene::SceneObject *m_geom { nullptr }; - int m_currentControllerIndex = -1; + int m_currentControllerIndex = kUnset; }; } \ No newline at end of file diff --git a/BMEdit/Editor/Include/Models/SceneObjectPropertiesModel.h b/BMEdit/Editor/Include/Models/SceneObjectPropertiesModel.h index 7b9745f..8464273 100644 --- a/BMEdit/Editor/Include/Models/SceneObjectPropertiesModel.h +++ b/BMEdit/Editor/Include/Models/SceneObjectPropertiesModel.h @@ -23,6 +23,9 @@ namespace models void resetLevel(); void resetGeom(); + signals: + void objectPropertiesChanged(const gamelib::scene::SceneObject*); + private slots: void onValueChanged(); diff --git a/BMEdit/Editor/Include/Models/ScenePrimitivesFilterModel.h b/BMEdit/Editor/Include/Models/ScenePrimitivesFilterModel.h deleted file mode 100644 index 058dfee..0000000 --- a/BMEdit/Editor/Include/Models/ScenePrimitivesFilterModel.h +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -#include -#include -#include - - -namespace models -{ - constexpr std::array g_DefaultVertexFormats = { - gamelib::prm::PRMVertexBufferFormat::VBF_VERTEX_10, - gamelib::prm::PRMVertexBufferFormat::VBF_VERTEX_24, - gamelib::prm::PRMVertexBufferFormat::VBF_VERTEX_28, - gamelib::prm::PRMVertexBufferFormat::VBF_VERTEX_34 - }; - - enum ScenePrimitivesFilterEntry : int - { - FilterAllow_Zero = 1 << 0, - FilterAllow_Unknown = 1 << 1, - FilterAllow_Description = 1 << 2, - FilterAllow_Index = 1 << 3, - FilterAllow_Vertex = 1 << 4, - - // Common values - Allow_None = 0, - Allow_All = FilterAllow_Zero | FilterAllow_Unknown | FilterAllow_Description | FilterAllow_Index | FilterAllow_Vertex - }; - - class ScenePrimitivesFilterModel : public QSortFilterProxyModel - { - Q_OBJECT - public: - using QSortFilterProxyModel::QSortFilterProxyModel; - - [[nodiscard]] int getFilterMask() const; - void setAllowAll(); - void setAllowNone(); - void addFilterEntry(ScenePrimitivesFilterEntry entry); - void removeFilterEntry(ScenePrimitivesFilterEntry entry); - bool isVertexFormatAllowed(gamelib::prm::PRMVertexBufferFormat vertexBufferFormat); - void setVertexFormatAllowed(gamelib::prm::PRMVertexBufferFormat vertexBufferFormat, bool isAllowed); - void resetToDefaults(); - - protected: - [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; - - private: - int m_filterMask { 0 }; - std::set m_allowedVertexFormats { g_DefaultVertexFormats.begin(), g_DefaultVertexFormats.end() }; - }; -} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Models/ScenePrimitivesModel.h b/BMEdit/Editor/Include/Models/ScenePrimitivesModel.h deleted file mode 100644 index a8332ed..0000000 --- a/BMEdit/Editor/Include/Models/ScenePrimitivesModel.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include -#include - - -namespace models -{ - class ScenePrimitivesModel : public QAbstractTableModel - { - Q_OBJECT - - private: - enum ColumnID : int { - CID_INDEX = 0, // Index of chunk - CID_KIND, // Kind of chunk - CID_SIZE, // Size of chunk - CID_INDICES, // Only for index buffer - CID_VERTICES, // Only for vertex buffer - CID_PTR_OBJECTS, // Only for description - CID_PTR_PARTS, // Only for description - - //NOTE: Don't forgot to add string view at g_ColNames array (see .cpp for details) - - // --- END --- - CID_MAX_COLUMNS - }; - - public: - ScenePrimitivesModel(QObject *parent = nullptr); - - int rowCount(const QModelIndex &parent) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; - bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - QVariant headerData(int section, Qt::Orientation orientation, - int role = Qt::DisplayRole) const override; - Qt::ItemFlags flags(const QModelIndex &index) const override; - - void setLevel(gamelib::Level* level); - void resetLevel(); - - private: - gamelib::Level* m_level { nullptr }; - }; -} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Models/SceneTextureFilterModel.h b/BMEdit/Editor/Include/Models/SceneTextureFilterModel.h new file mode 100644 index 0000000..214a785 --- /dev/null +++ b/BMEdit/Editor/Include/Models/SceneTextureFilterModel.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + + +namespace models +{ + class SceneTextureFilterModel : public QSortFilterProxyModel + { + Q_OBJECT + public: + SceneTextureFilterModel(QObject* parent = nullptr); + + void setTextureNameFilter(const QString& query); + [[nodiscard]] const QString& getTextureNameFilter() const; + + protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; + + private: + QString m_textureNameFilter {}; + mutable QMap m_filterResultsCache; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Models/SceneTexturesModel.h b/BMEdit/Editor/Include/Models/SceneTexturesModel.h new file mode 100644 index 0000000..17d3ac5 --- /dev/null +++ b/BMEdit/Editor/Include/Models/SceneTexturesModel.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + + +namespace gamelib +{ + class Level; +} + +namespace models +{ + class SceneTexturesModel : public QAbstractTableModel + { + Q_OBJECT + public: + enum ColumnID : int { + INDEX = 0, + NAME, + RESOLUTION, + FORMAT, + OFFSET, +#ifndef NDEBUG + CUBEMAP, +#endif + + MAX_COLUMNS, + }; + + /** + * @enum Roles + * @brief Contains all custom roles + */ + enum Roles : int { + R_TEXTURE_REF = Qt::UserRole + 1 ///< Get texture ref by QModelIndex + }; + + public: + SceneTexturesModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex& parent) const override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + + void setLevel(const gamelib::Level *level); + void resetLevel(); + const gamelib::Level *getLevel() const; + gamelib::Level *getLevel(); + + private: + bool isReady() const; + + private: + const gamelib::Level *m_level { nullptr }; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/Camera.h b/BMEdit/Editor/Include/Render/Camera.h new file mode 100644 index 0000000..6ee322d --- /dev/null +++ b/BMEdit/Editor/Include/Render/Camera.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace render +{ + using CameraMovementMask = uint8_t; + + enum CameraMovementMaskValues : CameraMovementMask + { + CM_FORWARD = 1 << 0, + CM_BACKWARD = 1 << 1, + CM_LEFT = 1 << 2, + CM_RIGHT = 1 << 3, + + CM_SPEEDUP_MOD = 1 << 7 + }; + + class Camera + { + public: + static constexpr float kDefaultDt = 1.f / 60.f; + + Camera() = default; + Camera(float fov, const glm::vec3& vPosition, const glm::ivec2& vScreenSize); + + // Getters + [[nodiscard]] float getFOV() const { return m_fFov; } + [[nodiscard]] float getYaw() const { return m_fYaw; } + [[nodiscard]] float getPitch() const { return m_fPitch; } + [[nodiscard]] float getSpeed() const { return m_fSpeed; } + [[nodiscard]] float getSensitivity() const { return m_fSensitivity; } + [[nodiscard]] const glm::vec3& getPosition() const { return m_vPosition; } + [[nodiscard]] const glm::vec3& getDirection() const { return m_vLookDirection; } + [[nodiscard]] const glm::vec3& getUp() const { return m_vUp; } + [[nodiscard]] const glm::vec3& getWorldUp() const { return m_vWorldUp; } + [[nodiscard]] const glm::vec3& getRight() const { return m_vRight; } + [[nodiscard]] const glm::mat4& getView() const { return m_mView; } + [[nodiscard]] const glm::mat4& getProjection() const { return m_mProj; } + [[nodiscard]] const glm::mat4& getProjView() const { return m_mProjView; } + + [[nodiscard]] Ray getRayFromScreen(const glm::ivec2& vScreenPos) const; + [[nodiscard]] Ray getRayFromScreen(float x, float y) const; + + // Setters + void setFOV(float fov); + void setSpeed(float speed); + void setSensitivity(float sens); + void setViewport(int width, int height); + void setPosition(const glm::vec3& vPosition); + + // Movement + void handleKeyboardMovement(CameraMovementMask movementMask = CameraMovementMaskValues::CM_FORWARD, float dt = kDefaultDt); + void processMouseMovement(float xoffset, float yoffset, float dt = kDefaultDt); + [[nodiscard]] bool canSeeObject(const glm::vec3& vMin, const glm::vec3& vMax) const; + [[nodiscard]] bool canSeeObject(const gamelib::BoundingBox& bbox) const; + [[nodiscard]] bool canSeeObject(const gamelib::Plane& plane) const; + + private: + void update(); + + private: + // Camera parameters + glm::ivec2 m_vScreenSize { 0 }; + float m_fFov { 80.f }; + float m_fYaw { -90.f }; + float m_fPitch { .0f }; + float m_fSpeed { 2.5f }; + float m_fSensitivity { 0.1f }; + float m_fNearPlane { .01f }; + float m_fFarPlane { 10'000.f }; + + // Calculated things + glm::vec3 m_vPosition { .0f }; + glm::vec3 m_vLookDirection { .0f, .0f, -1.f }; + glm::vec3 m_vUp { .0f, 1.f, .0f }; + glm::vec3 m_vRight { .0f }; + glm::vec3 m_vWorldUp { .0f, 1.0f, .0f }; // On init same to m_vUp + + glm::mat4 m_mView { 1.f }; + glm::mat4 m_mProj { 1.f }; + glm::mat4 m_mProjView { 1.f }; + + // Frustum + Frustum m_sFrustum {}; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/Frustum.h b/BMEdit/Editor/Include/Render/Frustum.h new file mode 100644 index 0000000..8bae8ab --- /dev/null +++ b/BMEdit/Editor/Include/Render/Frustum.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + + +namespace render +{ + class Frustum + { + public: + Frustum() = default; + + void setup(const glm::mat4& mProjView) + { + m_vPlanes[0] = glm::vec4(mProjView[0][3] + mProjView[0][0], mProjView[1][3] + mProjView[1][0], mProjView[2][3] + mProjView[2][0], mProjView[3][3] + mProjView[3][0]); + m_vPlanes[1] = glm::vec4(mProjView[0][3] - mProjView[0][0], mProjView[1][3] - mProjView[1][0], mProjView[2][3] - mProjView[2][0], mProjView[3][3] - mProjView[3][0]); + m_vPlanes[2] = glm::vec4(mProjView[0][3] + mProjView[0][1], mProjView[1][3] + mProjView[1][1], mProjView[2][3] + mProjView[2][1], mProjView[3][3] + mProjView[3][1]); + m_vPlanes[3] = glm::vec4(mProjView[0][3] - mProjView[0][1], mProjView[1][3] - mProjView[1][1], mProjView[2][3] - mProjView[2][1], mProjView[3][3] - mProjView[3][1]); + m_vPlanes[4] = glm::vec4(mProjView[0][3] + mProjView[0][2], mProjView[1][3] + mProjView[1][2], mProjView[2][3] + mProjView[2][2], mProjView[3][3] + mProjView[3][2]); + m_vPlanes[5] = glm::vec4(mProjView[0][3] - mProjView[0][2], mProjView[1][3] - mProjView[1][2], mProjView[2][3] - mProjView[2][2], mProjView[3][3] - mProjView[3][2]); + + for (auto& vPlane : m_vPlanes) + { + float fLen = glm::length(glm::vec3(vPlane)); + vPlane /= fLen; + } + } + + [[nodiscard]] bool isBoxVisible(const glm::vec3& vMin, const glm::vec3& vMax) const + { + for (const auto& vPlane : m_vPlanes) + { + if (vPlane.x * (vPlane.x > 0 ? vMax.x : vMin.x) + vPlane.y * (vPlane.y > 0 ? vMax.y : vMin.y) + vPlane.z * (vPlane.z > 0 ? vMax.z : vMin.z) + vPlane.w <= 0) + { + return false; + } + } + return true; + } + + private: + std::array m_vPlanes; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/GLResource.h b/BMEdit/Editor/Include/Render/GLResource.h new file mode 100644 index 0000000..1b75b26 --- /dev/null +++ b/BMEdit/Editor/Include/Render/GLResource.h @@ -0,0 +1,9 @@ +#pragma once + +#include + + +namespace render +{ + static constexpr GLuint kInvalidResource = 0xFDEADC0D; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/GlacierVertex.h b/BMEdit/Editor/Include/Render/GlacierVertex.h new file mode 100644 index 0000000..38cbc90 --- /dev/null +++ b/BMEdit/Editor/Include/Render/GlacierVertex.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + + +namespace render +{ + struct GlacierVertex + { + glm::vec3 vPos {}; + glm::vec2 vUV {}; + + static const VertexFormatDescription g_FormatDescription; + }; + + struct SimpleVertex + { + glm::vec3 vPos {}; + + SimpleVertex() = default; + SimpleVertex(const glm::vec3& v1) : vPos(v1) {} + + static const VertexFormatDescription g_FormatDescription; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/Model.h b/BMEdit/Editor/Include/Render/Model.h new file mode 100644 index 0000000..9adbe54 --- /dev/null +++ b/BMEdit/Editor/Include/Render/Model.h @@ -0,0 +1,148 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace render +{ + class Mesh + { + private: + VertexFormatDescription m_vertexFormat {}; + uint32_t m_maxVerticesNr { 0u }; + uint32_t m_maxIndicesNr { 0u }; + bool m_bIsDynamic { false }; + + public: + GLuint vao { kInvalidResource }; + GLuint vbo { kInvalidResource }; + GLuint ibo { kInvalidResource }; + GLuint glTextureId { kInvalidResource }; /// Render OpenGL texture resource handle + uint16_t materialId { 0 }; /// Id of material from Glacier mesh (just copy) + uint8_t variationId { 0 }; // Id of variation (some meshes could be attached to abstract 'variation' so each variation could be interpreted as 'group of meshes') + std::optional renderTopology; // Override topology (for debug only) + std::optional defaultColor; // Override default material color (for debug only) + + int trianglesCount { 0 }; + + [[nodiscard]] bool isDynamic() const { return m_bIsDynamic; } + + /** + * @brief Construct and upload vertex data into VAO + VBO + IBO + * @note For dynamic buffers allowed to use upload(...) method. Otherwise it will do nothing + * @tparam TVertex + * @param gapi + * @param vertexFormat + * @param vertices + * @param indices + * @param bIsDynamic + * @return + */ + template + bool setup(QOpenGLFunctions_3_3_Core* gapi, const VertexFormatDescription& vertexFormat, const std::vector& vertices, const std::vector& indices, bool bIsDynamic) + { + if (vao != kInvalidResource || vbo != kInvalidResource || ibo != kInvalidResource) + { + assert(false && "Need discard resource before create a new one!"); + return false; + } + + if (!gapi || vertexFormat.getEntries().empty() || vertices.empty()) + return false; + + return setup(gapi, + vertexFormat, + reinterpret_cast(vertices.data()), static_cast(vertices.size()), + indices.empty() ? nullptr : reinterpret_cast(indices.data()), indices.empty() ? 0 : static_cast(indices.size()), + bIsDynamic); + } + + /** + * @brief Update vertex and index buffer (able to update only vertex buffer, but unable to update only index buffer because it may be a root of inconsistent) + * @note This method will return false when not enough space in buffer (initialized at setup)! + * @note This method will return false when user trying to upload index buffer without initialise index buffer in setup! + * @tparam TVertex + * @param gapi + * @param vertices + * @param indices + * @return + */ + template + bool update(QOpenGLFunctions_3_3_Core* gapi, uint32_t verticesOffset, const std::vector& vertices, uint32_t indicesOffset = 0, const std::vector& indices = {}) + { + if (!gapi || vertices.empty()) + return false; + + if (!m_bIsDynamic) + return false; + + if (vao == kInvalidResource || vbo == kInvalidResource) + { + assert(false && "Call setup() before update!"); + return false; + } + + return update(gapi, + verticesOffset, reinterpret_cast(vertices.data()), static_cast(vertices.size()), + indicesOffset, indices.empty() ? nullptr : reinterpret_cast(indices.data()), indices.empty() ? 0 : static_cast(indices.size())); + } + + /** + * @brief Discard all resources and makes object invalid + * @param gapi + */ + void discard(QOpenGLFunctions_3_3_Core* gapi); + + /** + * @brief Do render of mesh + * @param gapi + * @param topology - which element topology stored inside buffer + */ + void render(QOpenGLFunctions_3_3_Core* gapi, RenderTopology topology = RenderTopology::RT_TRIANGLES) const; + + private: + /** + * @brief Create & upload vertices & indices into a single mesh + * @param gapi + * @param vertexFormat + * @param vertices + * @param verticesCount + * @param indices + * @param indicesCount + * @return true if everything is ok + */ + bool setup(QOpenGLFunctions_3_3_Core* gapi, const VertexFormatDescription& vertexFormat, const uint8_t* vertices, uint32_t verticesCount, const uint8_t* indices, uint32_t indicesCount, bool bDynamic); + + /** + * @brief Update vertex & index buffer (or only vertex buffer) + * @param gapi + * @param verticesOffset + * @param vertices + * @param verticesCount + * @param indices + * @param indicesCount + * @param indicesOffset + * @return + */ + bool update(QOpenGLFunctions_3_3_Core* gapi, uint32_t verticesOffset, const uint8_t* vertices, uint32_t verticesCount, uint32_t indicesOffset, const uint8_t* indices, uint32_t indicesCount); + }; + + struct Model + { + std::vector meshes {}; + std::optional boundingBoxMesh {}; // mesh with bounding box + gamelib::BoundingBox boundingBox {}; + [[maybe_unused]] uint32_t chunkId {0u}; + + void discard(QOpenGLFunctions_3_3_Core* gapi); + + bool setupBoundingBox(QOpenGLFunctions_3_3_Core* gapi); + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/Ray.h b/BMEdit/Editor/Include/Render/Ray.h new file mode 100644 index 0000000..085e246 --- /dev/null +++ b/BMEdit/Editor/Include/Render/Ray.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + + +namespace render +{ + struct Ray + { + glm::vec3 vOrigin { .0f }; + glm::vec3 vDirection { .0f }; + + [[nodiscard]] bool intersect(const gamelib::BoundingBox& boundingBox, bool bAllowRaysStartingInsideTargetBbox) const; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/RenderEntry.h b/BMEdit/Editor/Include/Render/RenderEntry.h new file mode 100644 index 0000000..fc47d6b --- /dev/null +++ b/BMEdit/Editor/Include/Render/RenderEntry.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + + +namespace render +{ + enum TextureSlotId : uint32_t + { + kMapDiffuse = 0, + kMapSpecularMask, + kMapEnvironment, + kMapReflectionMask, + kMapReflectionFallOff, + kMapIllumination, + kMapTranslucency, + + // should be last + kMaxTextureSlot + }; + + struct RenderEntry + { + // Render params + uint32_t iPrimitiveId { 0 }; // ID of primitive from PRM + uint32_t iMeshIndex { 0 }; // Index of mesh from PRM primitive + uint32_t iTrianglesNr { 0 }; // Count of triangles or elements + RenderTopology renderTopology { RenderTopology::RT_TRIANGLES }; // Topology of geometry buffer + +#ifdef QT_DEBUG + std::string debugGroupId {}; +#endif + + // World params + glm::vec3 vPosition { .0f }; // World position + glm::mat4 mWorldTransform { 1.f }; // Converted and translated matrix (ready to use in OpenGL) + glm::mat3 mLocalOriginalTransform { 1.f }; // Original local matrix + + // Mesh instance + render::Mesh* pMesh { nullptr }; + + struct Material + { + uint16_t id { 0 }; // Id of material from MAT file + + std::string sBaseMatClass {}; // Name of base material class + std::string sInstanceMatName {}; // Name of material instance + + // Parameters + glm::vec4 vDiffuseColor { 1.f }; + glm::vec4 gm_vZBiasOffset { .0f }; + glm::vec4 v4Opacity { 1.f }; + glm::vec4 v4Bias { .0f }; + int32_t iAlphaREF { 255 }; + + // Render State + gamelib::mat::MATRenderState renderState {}; + + // Textures + std::array textures { 0 }; + + // Shader program + render::Shader* pShader { nullptr }; + } material; + }; + + using RenderEntriesList = std::list; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/RenderTopology.h b/BMEdit/Editor/Include/Render/RenderTopology.h new file mode 100644 index 0000000..4e35df8 --- /dev/null +++ b/BMEdit/Editor/Include/Render/RenderTopology.h @@ -0,0 +1,19 @@ +#pragma once + +#include + + +namespace render +{ + enum class RenderTopology : uint8_t + { + RT_NONE = 0, + RT_POINTS, + RT_LINES, + RT_LINE_STRIP, + RT_LINE_LOOP, + RT_TRIANGLES, + RT_TRIANGLE_STRIP, + RT_TRIANGLE_FAN + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/Shader.h b/BMEdit/Editor/Include/Render/Shader.h new file mode 100644 index 0000000..e90bb4d --- /dev/null +++ b/BMEdit/Editor/Include/Render/Shader.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + + +namespace render +{ + struct Shader + { + GLuint vertexProgramId { kInvalidResource }; + GLuint fragmentProgramId { kInvalidResource }; + GLuint programId { kInvalidResource }; + + Shader() = default; + + void discard(QOpenGLFunctions_3_3_Core* gapi); + + void bind(QOpenGLFunctions_3_3_Core* gapi); + + void unbind(QOpenGLFunctions_3_3_Core* gapi); + + bool compile(QOpenGLFunctions_3_3_Core* gapi, const std::string& vertexProgram, const std::string& fragmentProgram, std::string& error); + + void setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, float s); + void setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, std::int32_t s); + void setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, const glm::vec2& v); + void setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, const glm::ivec2& v); + void setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, const glm::vec3& v); + void setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, const glm::vec4& v); + void setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, const glm::mat3& v); + void setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, const glm::mat4& v); + GLint resolveLocation(QOpenGLFunctions_3_3_Core* gapi, const std::string& id); + + private: + bool compileUnit(QOpenGLFunctions_3_3_Core* gapi, GLuint unitId, GLenum unitType, const std::string& unitSource, std::string& error); + + private: + std::map m_locationsCache; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/ShaderConstants.h b/BMEdit/Editor/Include/Render/ShaderConstants.h new file mode 100644 index 0000000..a744e79 --- /dev/null +++ b/BMEdit/Editor/Include/Render/ShaderConstants.h @@ -0,0 +1,15 @@ +#pragma once + + +namespace render +{ + struct ShaderConstants + { + static constexpr const char* kModelTransform = "i_uTransform.model"; + static constexpr const char* kCameraProjection = "i_uCamera.proj"; + static constexpr const char* kCameraView = "i_uCamera.view"; + static constexpr const char* kCameraResolution = "i_uCamera.resolution"; + static constexpr const char* kColor = "i_Color"; + static constexpr const char* kMaterial = "i_uMaterial"; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/Texture.h b/BMEdit/Editor/Include/Render/Texture.h new file mode 100644 index 0000000..5cfd8aa --- /dev/null +++ b/BMEdit/Editor/Include/Render/Texture.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + + +namespace render +{ + struct Texture + { + uint16_t width { 0 }; + uint16_t height { 0 }; + GLuint texture { kInvalidResource }; + std::optional index {}; /// Index of texture from TEX container + std::optional texPath {}; /// [Optional] Path to texture in TEX container (path may not be defined in TEX!) + + Texture(); + + bool setup(QOpenGLFunctions_3_3_Core* gapi, const gamelib::tex::TEXEntry& gTex); + + void discard(QOpenGLFunctions_3_3_Core* gapi); + + void bind(QOpenGLFunctions_3_3_Core* gapi); + + void unbind(QOpenGLFunctions_3_3_Core* gapi); + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Render/VertexFormatDescription.h b/BMEdit/Editor/Include/Render/VertexFormatDescription.h new file mode 100644 index 0000000..f2350d7 --- /dev/null +++ b/BMEdit/Editor/Include/Render/VertexFormatDescription.h @@ -0,0 +1,72 @@ +// +// Code from this file is based on BestByte Framework created by DronCode +// +#pragma once + +#include +#include +#include + + +namespace render +{ + /** + * @note If you would like to change this you need to add changes in g_typeSizeMap and g_entryTypeToGLType arrays. + */ + enum class VertexDescriptionEntryType + { + VDE_None = 0, + VDE_Bool, + VDE_Int32, + VDE_UInt32, + VDE_Float32, + VDE_Vec2, + VDE_Vec3, + VDE_Vec4, + VDE_IVec2, + VDE_IVec3, + VDE_IVec4, + VDE_Mat3x3, + VDE_Mat4x4 + }; + + struct VertexDescriptionEntry + { + uint32_t index { 0 }; + uint32_t offset { 0 }; + uint32_t size { 0 }; + VertexDescriptionEntryType type { VertexDescriptionEntryType::VDE_None }; + bool normalized { false }; + }; + + class VertexFormatDescription + { + public: + using Entries = std::vector; + using Visitor = std::function; + + VertexFormatDescription(); + + VertexFormatDescription& addField(uint32_t index, VertexDescriptionEntryType type, bool normalized = false); + + [[nodiscard]] const Entries& getEntries() const; + void visit(const Visitor& visitor) const; + + [[nodiscard]] uint32_t getStride() const; + + private: + void updateOrder() const; + + private: + mutable Entries m_entries {}; + uint32_t m_stride { 0 }; + mutable bool m_isOrdered { false }; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Types/QGlacierValue.h b/BMEdit/Editor/Include/Types/QGlacierValue.h index c110a88..d1b509c 100644 --- a/BMEdit/Editor/Include/Types/QGlacierValue.h +++ b/BMEdit/Editor/Include/Types/QGlacierValue.h @@ -1,7 +1,7 @@ #pragma once -#include #include +#include namespace types diff --git a/BMEdit/Editor/Include/Types/QTextureREF.h b/BMEdit/Editor/Include/Types/QTextureREF.h new file mode 100644 index 0000000..0c95920 --- /dev/null +++ b/BMEdit/Editor/Include/Types/QTextureREF.h @@ -0,0 +1,20 @@ +#pragma once + +#include + + +namespace models +{ + class SceneTexturesModel; +} + +namespace types +{ + struct QTextureREF + { + uint32_t textureIndex; + const models::SceneTexturesModel* ownerModel { nullptr }; + }; +} + +Q_DECLARE_METATYPE(types::QTextureREF) \ No newline at end of file diff --git a/BMEdit/Editor/Include/Widgets/BitSetViewWidget.h b/BMEdit/Editor/Include/Widgets/BitSetViewWidget.h new file mode 100644 index 0000000..5456af1 --- /dev/null +++ b/BMEdit/Editor/Include/Widgets/BitSetViewWidget.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include +#include + + +namespace widgets +{ + class BitSetViewWidget : public QWidget + { + Q_OBJECT + public: + using Bits = std::vector>; // Bit name to bit index + + BitSetViewWidget(QWidget* parent = nullptr); + + void setPossibleValues(const Bits& values); + + [[nodiscard]] const Bits& getPossibleValues() const; + [[nodiscard]] Bits getChecked() const; + [[nodiscard]] Bits getUnchecked() const; + + void setValue(uint32_t value); + void resetValue(); + [[nodiscard]] uint32_t getValue() const; + + void reset(); + + signals: + void valueChanged(uint32_t value); + + private: + void clearView(); + void buildView(); + void updateView(); + + private: + uint32_t m_intRepr { 0u }; // it's 32, for 64 write new widget please + Bits m_allowedValues {}; + QScopedPointer m_pLayout { nullptr }; + std::vector> m_bitNrToCheckBox {}; + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Widgets/EditorToolFactory.h b/BMEdit/Editor/Include/Widgets/EditorToolFactory.h new file mode 100644 index 0000000..1068030 --- /dev/null +++ b/BMEdit/Editor/Include/Widgets/EditorToolFactory.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + + +namespace widgets +{ + struct EditorToolFactory + { + static TypePropertyWidget* createToolById(QWidget* parent, const std::string &hintId); + }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Include/Widgets/GeomControllersWidget.h b/BMEdit/Editor/Include/Widgets/GeomControllersWidget.h index 75f5531..2e88a5a 100644 --- a/BMEdit/Editor/Include/Widgets/GeomControllersWidget.h +++ b/BMEdit/Editor/Include/Widgets/GeomControllersWidget.h @@ -1,6 +1,9 @@ #pragma once #include +#include +#include +#include #include @@ -44,8 +47,14 @@ namespace widgets void geomReset(); void editControllers(); + private: + static QStringList getAllPossibleControllerNamesFromTypesDb(); + private: void setup(); + void updateAvailableControllersList(); + void addControllerToGeom(const QString& controllerName); + void removeCurrentController(); private: Ui::GeomControllersWidget *m_ui { nullptr }; @@ -54,5 +63,8 @@ namespace widgets models::GeomControllerListModel *m_geomControllersListModel { nullptr }; models::SceneObjectControllerModel *m_controllerPropertiesModel{ nullptr }; delegates::TypePropertyItemDelegate *m_controllerEditorDelegate{ nullptr }; + + QScopedPointer m_availableToAddControllersModel { nullptr }; // All possible controller classes + QScopedPointer m_availableToAddControllersProxyModel { nullptr }; // Filtered by user input controller classes }; } \ No newline at end of file diff --git a/BMEdit/Editor/Include/Widgets/SceneRenderWidget.h b/BMEdit/Editor/Include/Widgets/SceneRenderWidget.h new file mode 100644 index 0000000..e97d504 --- /dev/null +++ b/BMEdit/Editor/Include/Widgets/SceneRenderWidget.h @@ -0,0 +1,287 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + + +class QOpenGLFunctions_3_3_Core; + +namespace widgets +{ + using RenderModeFlags = uint8_t; + + enum RenderMode : RenderModeFlags + { + RM_TEXTURE = 1 << 0, ///< Render objects with textures + RM_WIREFRAME = 1 << 1, ///< Render object in wireframe mode + + RM_NON_ALPHA_OBJECTS = 1 << 3, ///< Render only non transparent objects + RM_ALPHA_OBJECTS = 1 << 4, ///< Render only transparent objects + + // Common + RM_ALL = RM_TEXTURE | RM_WIREFRAME | RM_NON_ALPHA_OBJECTS | RM_ALPHA_OBJECTS, ///< Render anything + RM_DEFAULT = RM_TEXTURE | RM_NON_ALPHA_OBJECTS | RM_ALPHA_OBJECTS, ///< Render in texture mode with alpha/non-alpha objects + }; + + struct RenderStats + { + QString currentRoom {}; + int allowedObjects { 0 }; + int rejectedObjects { 0 }; + float fFrameTime { .0f }; // how much time used for render this frame + }; + + enum class EObjectPriority + { + EP_STATIC_OBJECT = 0, + EP_DYNAMIC_OBJECT = 1 + }; + + struct RayCastObjectDescription + { + EObjectPriority ePrio { EObjectPriority::EP_STATIC_OBJECT }; + float fRayOriginDistance { .0f }; + gamelib::scene::SceneObject::Ptr pObject { nullptr }; + + // Operators + bool operator<(const RayCastObjectDescription& another) const; + }; + + struct SeebleObject + { + EObjectPriority ePrio { EObjectPriority::EP_STATIC_OBJECT }; + gamelib::scene::SceneObject::Ptr pObject {}; + }; + + class SceneRenderWidget : public QOpenGLWidget + { + Q_OBJECT + public: + SceneRenderWidget(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); + ~SceneRenderWidget() noexcept override; + + void setLevel(gamelib::Level* pLevel); + void resetLevel(); + + [[nodiscard]] render::Camera& getCamera() { return m_camera; } + [[nodiscard]] const render::Camera& getCamera() const { return m_camera; } + + [[nodiscard]] float getFOV() const { return m_camera.getFOV(); } + void setFOV(float fov) { m_camera.setFOV(fov); } + + void setGeomViewMode(gamelib::scene::SceneObject* sceneObject); + void setWorldViewMode(); + void resetViewMode(); + + void setSelectedObject(gamelib::scene::SceneObject* sceneObject); + void resetSelectedObject(); + + [[nodiscard]] RenderModeFlags getRenderMode() const; + void setRenderMode(RenderModeFlags renderMode); + void resetRenderMode(); + + void moveCameraTo(const glm::vec3& position); + + void reloadTexture(uint32_t textureIndex); + + bool shouldRenderPortals() const; + void setShouldRenderPortals(bool bVal); + + bool shouldRenderRoomBoundingBox() const; + void setShouldRenderRoomBoundingBox(bool bVal); + + int32_t getGameObjectPrimitiveId(const gamelib::scene::SceneObject::Ptr& pObject) const; + int32_t getGameObjectPrimitiveId(const gamelib::scene::SceneObject* pObject) const; + + glm::mat4 getGameObjectTransform(const gamelib::scene::SceneObject::Ptr& pObject) const; + glm::mat4 getGameObjectTransform(const gamelib::scene::SceneObject* pObject) const; + + std::optional getGameObjectBoundingBox(const gamelib::scene::SceneObject::Ptr& pObject, bool bWorldTransform = true) const; + std::optional getGameObjectBoundingBox(const gamelib::scene::SceneObject* pObject, bool bWorldTransform = true) const; + + std::vector performRayCastToScene(const QPointF& screenSpace, const gamelib::scene::SceneObject::Ptr& pStartObject = nullptr) const; + + signals: + void resourcesReady(); + void resourceLoadFailed(const QString& reason); + void frameReady(const RenderStats& stats); + + void worldSelectionChanged(const std::vector& selectedObjects); + + public slots: + void onRedrawRequested(); + + // Use when object properties changed and his 'world transform' could be changed. + void onObjectMoved(gamelib::scene::SceneObject* sceneObject); + + protected: + void initializeGL() override; + void paintGL() override; + void resizeGL(int w, int h) override; + + void keyPressEvent(QKeyEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + + private: + void doLoadTextures(QOpenGLFunctions_3_3_Core* glFunctions); + void doLoadGeometry(QOpenGLFunctions_3_3_Core* glFunctions); + void doCompileShaders(QOpenGLFunctions_3_3_Core* glFunctions); + void doResetCameraState(QOpenGLFunctions_3_3_Core* glFunctions); + void doPrepareInvalidatedResources(QOpenGLFunctions_3_3_Core* glFunctions); + [[nodiscard]] glm::ivec2 getViewportSize() const; + + void collectRenderList(const render::Camera& camera, const gamelib::scene::SceneObject* pRootGeom, render::RenderEntriesList& entries, RenderStats& stats, bool bIgnoreVisibility); + void collectRenderEntriesIntoRenderList(const gamelib::scene::SceneObject* pRootGeom, render::RenderEntriesList& entries, RenderStats& stats, bool bIgnoreVisibility, bool bBreakOnChild = false); + void performRender(QOpenGLFunctions_3_3_Core* glFunctions, const render::RenderEntriesList& entries, const render::Camera& camera, const std::function& filter); + + void invalidateRenderList(); + + void buildRoomCache(QOpenGLFunctions_3_3_Core* glFunctions); + + /** + * @brief Method trying to find a new room for current camera (if camera not in that room of bRejectLastResult is true) + * @param stats - reference to render stats object (method updates room name if new room presented) + * @param bRejectLastResult - pass true to reject current room and try to find a new one + */ + void updateCameraRoomAttachment(RenderStats& stats, bool bRejectLastResult = true); + + private: + void beginDebugGroup(std::string_view groupName); + void endDebugGroup(); + + private: + // Data + gamelib::Level* m_pLevel { nullptr }; + + // Camera & world view + render::Camera m_camera {}; + uint8_t m_renderMode = RenderMode::RM_DEFAULT; + + // State + enum class ELevelState : uint8_t + { + LS_NONE = 0, + LS_LOAD_TEXTURES = 1, + LS_LOAD_GEOMETRY = 2, + LS_COMPILE_SHADERS = 3, + LS_RESET_CAMERA_STATE = 4, + LS_READY + }; + + ELevelState m_eState { ELevelState::LS_NONE }; + QPoint m_mouseLastPosition {}; + bool m_bFirstMouseQuery { true }; + bool m_bRenderPortals { false }; // Should we render portals between rooms (debug view) + bool m_bRenderRoomBoundingBox { false }; // Should we render room bounding box (of all rooms) + + // View mode + enum class EViewMode : uint8_t + { + VM_WORLD_VIEW, + VM_GEOM_PREVIEW + }; + + EViewMode m_eViewMode { EViewMode::VM_WORLD_VIEW }; + gamelib::scene::SceneObject* m_pSceneObjectToView {}; + gamelib::scene::SceneObject* m_pSelectedSceneObject { nullptr }; + + render::RenderEntriesList m_renderList {}; + + struct GLResources; + std::unique_ptr m_resources; + + struct RoomDef + { + enum class ELocation : int { + eUNDEFINED = 0, + eOUTSIDE = 1, + eINSIDE = 2, + eBOTH = 3, + }; + + enum class EBoundingBoxSource : int { + BBS_ROOM_COLLISION_MESH, ///< Calculated via collision mesh + BBS_ZBOUNDS_AUTO_EXPAND, ///< Calculated by ZBOUND object points + BBS_AUTO_ROOM_EXPAND, ///< Calculated as expand of all objects in room + BBS_NONE ///< Not calculated or other BoundingBox source (auto-gen as example) + }; + + /** + * @brief Weak pointer to entity which represent room + */ + gamelib::scene::SceneObject::Ref rRoom {}; + + /** + * @brief World space bounding box which cover whole room. Typically it's been built from collision box, but sometimes it could be a expanded bbox (expanded by children objects) + */ + gamelib::BoundingBox vBoundingBox {}; + + /** + * @brief Type of room location. Seee ELocation.json for details + */ + ELocation eLocation { ELocation::eUNDEFINED }; + + /** + * @brief How bounding box calculated + */ + EBoundingBoxSource eBoundingBoxSource { EBoundingBoxSource::BBS_NONE }; + + /** + * @brief Information about room exits + */ + std::vector aExists {}; + + /** + * @brief Information about neighbour rooms + */ + std::vector aNeighbours {}; + + /** + * @brief Room eXit geom boxes + */ + std::unique_ptr mExitsDebugModel { nullptr }; + + /** + * @brief This list contains objects which could be visible in this specific room + */ + std::vector vObjects {}; + + /** + * @brief Room bounding box debug model + */ + std::unique_ptr mBBoxModel { nullptr }; + + /** + * @brief Means "is this room created because no other rooms exists" + */ + bool bIsVirtualBigRoom { false }; + }; + + std::list m_rooms {}; + std::list m_cameraInRooms {}; + + private: + void computeRoomBoundingBox(RoomDef& d); + }; +} diff --git a/BMEdit/Editor/Include/Widgets/TypePropertyWidget.h b/BMEdit/Editor/Include/Widgets/TypePropertyWidget.h index 53d7f30..112dc4b 100644 --- a/BMEdit/Editor/Include/Widgets/TypePropertyWidget.h +++ b/BMEdit/Editor/Include/Widgets/TypePropertyWidget.h @@ -17,8 +17,10 @@ namespace widgets public: explicit TypePropertyWidget(QWidget* parent = nullptr); - void setValue(const types::QGlacierValue &value); - [[nodiscard]] const types::QGlacierValue &getValue() const; + virtual void setValue(const types::QGlacierValue &value); + [[nodiscard]] virtual const types::QGlacierValue &getValue() const; + + virtual bool canHookFocus() const; signals: void valueChanged(); diff --git a/BMEdit/Editor/Resources/BMEdit.qrc b/BMEdit/Editor/Resources/BMEdit.qrc index d77923a..34f2845 100644 --- a/BMEdit/Editor/Resources/BMEdit.qrc +++ b/BMEdit/Editor/Resources/BMEdit.qrc @@ -2,6 +2,7 @@ Icons/Remove.png Icons/Add.png + Icons/Folder.png Icons/Glacier/Geom.png Icons/Glacier/Group.png Icons/Glacier/Camera.png @@ -11,5 +12,14 @@ Icons/Glacier/Weapon.png Icons/Glacier/Cloth.png Icons/Glacier/Unknown.png + Icons/Glacier/Script.png + + Textures/unsupported_material.png + Textures/missing_texture.png + + Shaders/ColoredEntity_GL33.vsh + Shaders/ColoredEntity_GL33.fsh + Shaders/TexturedEntity_GL33.vsh + Shaders/TexturedEntity_GL33.fsh \ No newline at end of file diff --git a/BMEdit/Editor/Resources/Icons/Folder.png b/BMEdit/Editor/Resources/Icons/Folder.png new file mode 100644 index 0000000..ddef4b5 Binary files /dev/null and b/BMEdit/Editor/Resources/Icons/Folder.png differ diff --git a/BMEdit/Editor/Resources/Icons/Glacier/Script.png b/BMEdit/Editor/Resources/Icons/Glacier/Script.png new file mode 100644 index 0000000..61188b6 Binary files /dev/null and b/BMEdit/Editor/Resources/Icons/Glacier/Script.png differ diff --git a/BMEdit/Editor/Resources/Shaders/ColoredEntity_GL33.fsh b/BMEdit/Editor/Resources/Shaders/ColoredEntity_GL33.fsh new file mode 100644 index 0000000..4c01a9f --- /dev/null +++ b/BMEdit/Editor/Resources/Shaders/ColoredEntity_GL33.fsh @@ -0,0 +1,35 @@ +#version 330 core +// +// This file is a part of BMEdit project +// Description: Basic shader to render color filled entity & gizmo +// + +struct Material +{ + // See Common.fx for details + // Common uniforms + vec4 v4DiffuseColor; + vec4 gm_vZBiasOffset; + vec4 v4Opacity; + vec4 v4Bias; + int alphaREF; + + // Textures + sampler2D mapDiffuse; + sampler2D mapSpecularMask; + sampler2D mapEnvironment; + sampler2D mapReflectionMask; + sampler2D mapReflectionFallOff; + sampler2D mapIllumination; + sampler2D mapTranslucency; +}; + +uniform Material i_uMaterial; + +// Out +out vec4 o_FragColor; + +void main() +{ + o_FragColor = i_uMaterial.v4DiffuseColor; +} \ No newline at end of file diff --git a/BMEdit/Editor/Resources/Shaders/ColoredEntity_GL33.vsh b/BMEdit/Editor/Resources/Shaders/ColoredEntity_GL33.vsh new file mode 100644 index 0000000..cd0c076 --- /dev/null +++ b/BMEdit/Editor/Resources/Shaders/ColoredEntity_GL33.vsh @@ -0,0 +1,30 @@ +#version 330 core +// +// This file is a part of BMEdit project +// Description: Basic shader to render color filled entity & gizmo +// + +// Layout +layout (location = 0) in vec3 aPos; + +// Common +struct Camera +{ + mat4 proj; + mat4 view; + ivec2 resolution; +}; + +struct Transform +{ + mat4 model; +}; + +// Uniforms +uniform Camera i_uCamera; +uniform Transform i_uTransform; + +void main() +{ + gl_Position = i_uCamera.proj * i_uCamera.view * i_uTransform.model * vec4(aPos.x, aPos.y, aPos.z, 1.0); +} \ No newline at end of file diff --git a/BMEdit/Editor/Resources/Shaders/TexturedEntity_GL33.fsh b/BMEdit/Editor/Resources/Shaders/TexturedEntity_GL33.fsh new file mode 100644 index 0000000..23f0163 --- /dev/null +++ b/BMEdit/Editor/Resources/Shaders/TexturedEntity_GL33.fsh @@ -0,0 +1,38 @@ +#version 330 core +// +// This file is a part of BMEdit project +// Description: Basic shader to render textured entity +// +in vec2 g_TexCoord; + +struct Material +{ + // See Common.fx for details + // Common uniforms + vec4 v4DiffuseColor; + vec4 gm_vZBiasOffset; + vec4 v4Opacity; + vec4 v4Bias; + float fZOffset; + int alphaREF; + + // Textures + sampler2D mapDiffuse; + sampler2D mapSpecularMask; + sampler2D mapEnvironment; + sampler2D mapReflectionMask; + sampler2D mapReflectionFallOff; + sampler2D mapIllumination; + sampler2D mapTranslucency; +}; + +uniform Material i_uMaterial; + + +// Out +out vec4 o_FragColor; + +void main() +{ + o_FragColor = texture(i_uMaterial.mapDiffuse, g_TexCoord); +} \ No newline at end of file diff --git a/BMEdit/Editor/Resources/Shaders/TexturedEntity_GL33.vsh b/BMEdit/Editor/Resources/Shaders/TexturedEntity_GL33.vsh new file mode 100644 index 0000000..8bb0bf1 --- /dev/null +++ b/BMEdit/Editor/Resources/Shaders/TexturedEntity_GL33.vsh @@ -0,0 +1,62 @@ +#version 330 core +// +// This file is a part of BMEdit project +// Description: Basic shader to render textured entity +// + +// Layout +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec2 aUV; + +// Common +struct Camera +{ + mat4 proj; + mat4 view; + ivec2 resolution; +}; + +struct Transform +{ + mat4 model; +}; + +struct Material +{ + // See Common.fx for details + // Common uniforms + vec4 v4DiffuseColor; + vec4 gm_vZBiasOffset; + vec4 v4Opacity; + vec4 v4Bias; + float fZOffset; + int alphaREF; + + // Textures + sampler2D mapDiffuse; + sampler2D mapSpecularMask; + sampler2D mapEnvironment; + sampler2D mapReflectionMask; + sampler2D mapReflectionFallOff; + sampler2D mapIllumination; + sampler2D mapTranslucency; +}; + +uniform Material i_uMaterial; + +// Uniforms +uniform Camera i_uCamera; +uniform Transform i_uTransform; + +// Out +out vec2 g_TexCoord; + +void main() +{ + vec4 vOut = i_uCamera.proj * i_uCamera.view * i_uTransform.model * vec4(aPos.x, aPos.y, aPos.z, 1.0); + vOut -= i_uMaterial.gm_vZBiasOffset; + vOut.z -= i_uMaterial.fZOffset; + + gl_Position = vOut; + g_TexCoord = aUV; +} \ No newline at end of file diff --git a/BMEdit/Editor/Resources/Textures/missing_texture.png b/BMEdit/Editor/Resources/Textures/missing_texture.png new file mode 100644 index 0000000..07cfc41 Binary files /dev/null and b/BMEdit/Editor/Resources/Textures/missing_texture.png differ diff --git a/BMEdit/Editor/Resources/Textures/unsupported_material.png b/BMEdit/Editor/Resources/Textures/unsupported_material.png new file mode 100644 index 0000000..470cc5c Binary files /dev/null and b/BMEdit/Editor/Resources/Textures/unsupported_material.png differ diff --git a/BMEdit/Editor/Source/Delegates/TypePropertyItemDelegate.cpp b/BMEdit/Editor/Source/Delegates/TypePropertyItemDelegate.cpp index 84998d4..f7f44d8 100644 --- a/BMEdit/Editor/Source/Delegates/TypePropertyItemDelegate.cpp +++ b/BMEdit/Editor/Source/Delegates/TypePropertyItemDelegate.cpp @@ -4,14 +4,17 @@ #include #include #include +#include #include #include +#include #include #include #include #include #include #include +#include #include @@ -53,7 +56,9 @@ namespace delegates static bool isRefTab(const types::QGlacierValue &data) { if (data.views.empty()) + { return false; + } const gamelib::ValueView& view = data.views.at(0); if (auto type = view.getType()) @@ -75,29 +80,46 @@ namespace delegates widgets::TypePropertyWidget *editor = nullptr; auto data = index.data(Qt::EditRole).value(); - if (data.instructions.empty()) - { - // Empty widget - editor = new widgets::TypePropertyWidget(parent); - } - else if (isValueCouldBePresentedBySimpleView(data)) - { - // If trivial thing (int, string, enum (?), bool) we may show simple widgets::TypeSimplePropertyWidget (inherited of widgets::TypePropertyWidget) - editor = new widgets::TypeSimplePropertyWidget(parent); - } - else if (isValueCouldBePresentedAsSimpleVectorWidget(data)) - { - // It's vector (3 elements) - editor = new widgets::TypeVector3PropertyWidget(parent); - } - else if (isValueCouldBePresentedAsSimpleMatrixWidget(data, Matrix3x3::Rows, Matrix3x3::Columns)) + if (data.views.size() == 1) { - // It's matrix 3x3 - editor = new widgets::TypeMatrixPropertyWidget(Matrix3x3::Rows, Matrix3x3::Columns, parent); + if (auto* pType = data.views[0].getType(); pType && pType->getKind() == gamelib::TypeKind::ALIAS) + { + const auto* pAsAlias = reinterpret_cast(pType); + if (pAsAlias->hasToolHint()) + { + // Nice! Use this hint! + const auto& toolHintId = pAsAlias->getToolHint(); + editor = widgets::EditorToolFactory::createToolById(parent, toolHintId); + } + } } - else if (isRefTab(data)) + + if (!editor) { - editor = new widgets::TypeRefTabPropertyWidget(parent); + if (data.instructions.empty()) + { + // Empty widget + editor = new widgets::TypePropertyWidget(parent); + } + else if (isValueCouldBePresentedBySimpleView(data)) + { + // If trivial thing (int, string, enum (?), bool) we may show simple widgets::TypeSimplePropertyWidget (inherited of widgets::TypePropertyWidget) + editor = new widgets::TypeSimplePropertyWidget(parent); + } + else if (isValueCouldBePresentedAsSimpleVectorWidget(data)) + { + // It's vector (3 elements) + editor = new widgets::TypeVector3PropertyWidget(parent); + } + else if (isValueCouldBePresentedAsSimpleMatrixWidget(data, Matrix3x3::Rows, Matrix3x3::Columns)) + { + // It's matrix 3x3 + editor = new widgets::TypeMatrixPropertyWidget(Matrix3x3::Rows, Matrix3x3::Columns, parent); + } + else if (isRefTab(data)) + { + editor = new widgets::TypeRefTabPropertyWidget(parent); + } } if (editor) @@ -193,6 +215,20 @@ namespace delegates } } + bool TypePropertyItemDelegate::eventFilter(QObject* editor, QEvent* event) + { + if (event->type() == QEvent::FocusOut) + { + if (auto* pEditor = qobject_cast(editor); pEditor && pEditor->canHookFocus()) + { + // Do not send this event to delegate. We can handle focus and it's ok + return true; + } + } + + return QStyledItemDelegate::eventFilter(editor, event); + } + void TypePropertyItemDelegate::commitDataChunk() { auto editor = qobject_cast(sender()); diff --git a/BMEdit/Editor/Source/Edtor/EditorInstance.cpp b/BMEdit/Editor/Source/Edtor/EditorInstance.cpp index 543eee5..632060c 100644 --- a/BMEdit/Editor/Source/Edtor/EditorInstance.cpp +++ b/BMEdit/Editor/Source/Edtor/EditorInstance.cpp @@ -215,4 +215,32 @@ namespace editor { return true; } + + bool EditorInstance::exportLOC(const QString &filePath) + { + std::vector locFileBuffer; + + if (!m_currentLevel) + { + return false; + } + + QFile locFile(filePath); + if (!locFile.open(QIODeviceBase::OpenModeFlag::WriteOnly | QIODeviceBase::OpenModeFlag::Truncate | QIODeviceBase::OpenModeFlag::Unbuffered)) + { + return false; + } + + m_currentLevel->dumpAsset(gamelib::io::AssetKind::LOCALIZATION, locFileBuffer); + + if (locFileBuffer.empty()) + { + return false; + } + + QByteArray raw(reinterpret_cast(locFileBuffer.data()), static_cast(locFileBuffer.size())); + locFile.write(raw); + + return true; + } } \ No newline at end of file diff --git a/BMEdit/Editor/Source/Edtor/TextureProcessor.cpp b/BMEdit/Editor/Source/Edtor/TextureProcessor.cpp new file mode 100644 index 0000000..2bed69f --- /dev/null +++ b/BMEdit/Editor/Source/Edtor/TextureProcessor.cpp @@ -0,0 +1,344 @@ +#include +#include // for PNG/JPG exporters +#include // for BMP exporter +#include // for in-memory exporters +#include // for DXT1/DXT3 compression/decompression/import/export +#include // std::pow +#include + + +namespace editor +{ + std::unique_ptr TextureProcessor::decompressRGBA(const gamelib::tex::TEXEntry& textureEntry, uint16_t& realWidth, uint16_t& realHeight, std::size_t mipLevel) + { + using EntryType = gamelib::tex::TEXEntryType; + + realWidth = static_cast(textureEntry.m_width / std::pow(2, mipLevel)); + realHeight = static_cast(textureEntry.m_height / std::pow(2, mipLevel)); + + if (mipLevel > textureEntry.m_numOfMipMaps) + { + assert(false && "Bad mip"); + return nullptr; + } + + if (auto format = textureEntry.m_type1; format == EntryType::ET_BITMAP_32) + { + // RGBA: just copy into dest memory and return new buffer + auto result = std::make_unique(realWidth * realHeight * 4); + std::memcpy(result.get(), textureEntry.m_mipLevels.at(mipLevel).m_buffer.get(), realWidth * realHeight * 4); + return result; + } + else if (format == EntryType::ET_BITMAP_DXT1 || format == EntryType::ET_BITMAP_DXT3) + { + // DXT1, DXT3: Process via squish + int flags = 0; + + flags |= (format == EntryType::ET_BITMAP_DXT1 ? squish::kDxt1 : 0); + flags |= (format == EntryType::ET_BITMAP_DXT3 ? squish::kDxt3 : 0); + auto result = std::make_unique(static_cast(realWidth) * static_cast(realHeight) * 4); + squish::DecompressImage(result.get(), realWidth, realHeight, textureEntry.m_mipLevels.at(mipLevel).m_buffer.get(), flags); + + return result; + } + else if (format == EntryType::ET_BITMAP_PAL) + { + // PAL: This format based on palette. Generally we need to iterate over each "src pixel" and find color in palette (just jmp) + if (!textureEntry.m_palPalette.has_value()) + { + assert(false && "Bad PAL palette!"); + return nullptr; + } + + const auto& palette = textureEntry.m_palPalette.value(); + const int totalSrcPixels = static_cast(realWidth) * static_cast(realHeight); + const int totalDstPixels = totalSrcPixels * 4; + auto result = std::make_unique(totalDstPixels); + + for (int i = 0; i < totalSrcPixels; ++i) + { + *reinterpret_cast(&result.get()[i * 4]) = *reinterpret_cast(&palette.m_data.get()[4 * textureEntry.m_mipLevels.at(mipLevel).m_buffer.get()[i]]); + } + + return result; + } + else if (format == EntryType::ET_BITMAP_U8V8) + { + // Reversed by DronCode (ZBitmapU8V8::sub_43E8E0) + const int totalSrcPixels = static_cast(realWidth) * static_cast(realHeight); + const int totalPixels = totalSrcPixels * 4; + auto result = std::make_unique(totalPixels); + + for (int i = 0; i < totalSrcPixels; i++) + { + const uint16_t RG = *reinterpret_cast(&textureEntry.m_mipLevels.at(mipLevel).m_buffer.get()[i * 2 + 0]); + const uint32_t color = (RG << 8) | 0xFF0000FF; + *reinterpret_cast(&result.get()[i * 4 + 0]) = color; + } + + return result; + } + else if (format == EntryType::ET_BITMAP_I8) + { + // Reversed by DronCode (ZBitmapI8::sub_43EA90) + const int totalSrcPixels = static_cast(realWidth) * static_cast(realHeight); + const int totalPixels = totalSrcPixels * 4; + auto result = std::make_unique(totalPixels); + + for (int i = 0; i < totalSrcPixels; i++) + { + *reinterpret_cast(&result.get()[i * 4]) = 0x1010101u * static_cast(textureEntry.m_mipLevels.at(mipLevel).m_buffer.get()[i]); + } + + return result; + } + + //PALO: Obsolete thing. See ZBitmapPalOpac::sub_43E720 for details. To reverse when we will find at least 1 usage. + + assert(false && "Unsupported routine!"); + return nullptr; + } + + bool TextureProcessor::exportTEXEntryAsPNG(const gamelib::tex::TEXEntry& texEntry, const std::filesystem::path& filePath, std::size_t mipLevel) + { + uint16_t w, h; + auto buffer = TextureProcessor::decompressRGBA(texEntry, w, h, mipLevel); + + if (!buffer) + { + return false; + } + + QImage image(buffer.get(), w, h, QImage::Format::Format_RGBA8888); + image.save(QString::fromStdString(filePath.string()), "PNG", 100); + + return true; + } + + bool TextureProcessor::importTextureToEntry(gamelib::tex::TEXEntry& targetTexture, const QString& texturePath, const QString& textureName, gamelib::tex::TEXEntryType targetFormat, uint8_t mipLevelsNr) + { + // Read texture as RGBA, convert to final format, generate MIP levels and override TEXEntry, PALPalette and other stuff + QImage sourceImage { texturePath }; + if (sourceImage.isNull()) + return false; + + // Convert to RGBA8888 + sourceImage.convertTo(QImage::Format::Format_RGBA8888); + + auto isPow2 = [](uint32_t v) -> bool + { + return (v & (v - 1)) == 0; + }; + + // Check that source image is a pow of 2 + if (!isPow2(sourceImage.width()) || !isPow2(sourceImage.height())) + { + auto getPreviousPowOf2 = [](uint32_t current) -> uint32_t + { +#ifdef Q_CC_MSVC + return 1u << ((sizeof(current) * 8 - 1) - _lzcnt_u32(current)); +#else + return 1u << ((sizeof(current) * 8 - 1) - __builtin_clz(current)); +#endif + }; + + uint32_t w = sourceImage.width(); + uint32_t h = sourceImage.height(); + + if (!isPow2(sourceImage.width())) + { + w = getPreviousPowOf2(w); + } + + if (!isPow2(sourceImage.height())) + { + h = getPreviousPowOf2(h); + } + + auto newImage = sourceImage.scaled(static_cast(w), static_cast(h), Qt::AspectRatioMode::IgnoreAspectRatio, Qt::TransformationMode::FastTransformation); + sourceImage = std::move(newImage); + } + + if (targetFormat == gamelib::tex::TEXEntryType::ET_BITMAP_PAL || targetFormat == gamelib::tex::TEXEntryType::ET_BITMAP_PAL_OPAC) + { + // Because in PAL and PAL_OPAC we can't have more than 1 palette + mipLevelsNr = 1; + } + + std::vector mipLevels; + mipLevels.resize(mipLevelsNr); + + mipLevels[0] = std::move(sourceImage); + for (int mipLevel = 1; mipLevel < mipLevelsNr; mipLevel++) + { + auto currentW = static_cast(mipLevels[0].width() / std::pow(2, mipLevel)); + auto currentH = static_cast(mipLevels[0].height() / std::pow(2, mipLevel)); + + mipLevels[mipLevel] = mipLevels[0].scaled(currentW, currentH, Qt::AspectRatioMode::IgnoreAspectRatio, Qt::TransformationMode::FastTransformation); + } + + targetTexture.m_mipLevels.resize(mipLevelsNr); + + for (int i = 0; i < mipLevelsNr; i++) + { + const auto& sourceMIP = mipLevels[i]; + auto& textureMIP = targetTexture.m_mipLevels[i]; + + switch (targetFormat) + { + case gamelib::tex::TEXEntryType::ET_BITMAP_I8: + // Use simple grayscale and divide result by 0x10 + { + std::unique_ptr pixelBuffer = std::make_unique(sourceMIP.width() * sourceMIP.height()); + + for (int pixelIndex = 0; pixelIndex < sourceMIP.width() * sourceMIP.height(); pixelIndex++) + { + const uint8_t r = *reinterpret_cast(&sourceMIP.bits()[pixelIndex * 4 + 0]); + const uint8_t g = *reinterpret_cast(&sourceMIP.bits()[pixelIndex * 4 + 1]); + const uint8_t b = *reinterpret_cast(&sourceMIP.bits()[pixelIndex * 4 + 2]); + // Do we need to use alpha too? Idk + const uint32_t mid = ((r + g + b) / 3) / 0x10; + *reinterpret_cast(&pixelBuffer[pixelIndex]) = static_cast(mid); + } + + textureMIP.m_mipLevelSize = sourceMIP.width() * sourceMIP.height(); + textureMIP.m_buffer = std::move(pixelBuffer); + } + break; + case gamelib::tex::TEXEntryType::ET_BITMAP_U8V8: + // Just use only red and green pixels + { + std::unique_ptr pixelBuffer = std::make_unique(sourceMIP.width() * sourceMIP.height() * 2); + + for (int pixelIndex = 0; pixelIndex < sourceMIP.width() * sourceMIP.height(); pixelIndex++) + { + *reinterpret_cast(&pixelBuffer[pixelIndex * 2]) = + (*reinterpret_cast(&sourceMIP.bits()[pixelIndex * 4 + 0])) << 8 | + *reinterpret_cast(&sourceMIP.bits()[pixelIndex * 4 + 1]); + } + + textureMIP.m_mipLevelSize = sourceMIP.width() * sourceMIP.height(); + textureMIP.m_buffer = std::move(pixelBuffer); + } + break; + case gamelib::tex::TEXEntryType::ET_BITMAP_PAL: + // "EZ" + { + /** + * @note: In this format we using dithering and this need to have only 1 MIP level. + * We can't have more than 1 MIP level because we can't guarantee that for other MIP levels our palette will be OK. + * So, we will break loop after first entry done + * + * @note: Ok, it really stupid solution, but in Format_Indexed8 format itself has max 256 palette so we can convert image to this pixel format, + * write into memory and collect palette itself. + * + * @note: It's really weird results, quality bad, need to investigate it later. Workaround: use RGBA! + */ + QImage bitmap = sourceMIP.convertToFormat( + QImage::Format::Format_Indexed8, + Qt::ImageConversionFlag::ThresholdDither | Qt::ImageConversionFlag::ThresholdAlphaDither | Qt::ImageConversionFlag::AvoidDither + ); + std::unique_ptr pixelBuffer = std::make_unique(bitmap.width() * bitmap.height()); + + // Ok, now it loaded, process pixels + std::uint8_t currentIndex = 0; + std::map colorToIndex; + + std::uint32_t pixelIndex = 0; + for (int y = 0; y < bitmap.height(); y++) + { + for (int x = 0; x < bitmap.width(); x++) + { + QRgb pixel = bitmap.pixel(x, y); + + if (colorToIndex.contains(pixel)) + { + pixelBuffer[pixelIndex] = colorToIndex[pixel]; + } + else + { + currentIndex++; + colorToIndex[pixel] = currentIndex; + pixelBuffer[pixelIndex] = currentIndex; + } + + pixelIndex++; + } + } + + if (colorToIndex.size() > 256) + { + // Too big palette! + return false; + } + + // Ok, we have a palette in colorToIndex and now we need to save only values into our buffer + gamelib::tex::PALPalette palette; + palette.m_size = colorToIndex.size(); + palette.m_data = std::make_unique(palette.m_size * 4); + + std::uint32_t colorIndex = 0; + for (const auto& [color, _gIndex] : colorToIndex) + { + *reinterpret_cast(&palette.m_data.get()[colorIndex * 4]) = color; + ++colorIndex; + } + + // Save palette + targetTexture.m_palPalette.emplace(std::move(palette)); + + // Now save pixel buffer + textureMIP.m_buffer = std::move(pixelBuffer); + textureMIP.m_mipLevelSize = bitmap.width() * bitmap.height(); + } + break; + case gamelib::tex::TEXEntryType::ET_BITMAP_32: + // Copy data + { + uint32_t w = sourceMIP.width(); + uint32_t h = sourceMIP.height(); + textureMIP.m_mipLevelSize = w * h * 4; + textureMIP.m_buffer = std::make_unique(textureMIP.m_mipLevelSize); + std::memcpy(textureMIP.m_buffer.get(), sourceMIP.bits(), textureMIP.m_mipLevelSize); + } + break; + case gamelib::tex::TEXEntryType::ET_BITMAP_DXT1: + case gamelib::tex::TEXEntryType::ET_BITMAP_DXT3: + // Use libsquish to create DXT1/DXT3 buffers + { + uint32_t w = sourceMIP.width(); + uint32_t h = sourceMIP.height(); + int flags = 0; + + flags |= (targetFormat == gamelib::tex::TEXEntryType::ET_BITMAP_DXT1 ? squish::kDxt1 : 0); + flags |= (targetFormat == gamelib::tex::TEXEntryType::ET_BITMAP_DXT3 ? squish::kDxt3 : 0); + + // Calculate & allocate space + textureMIP.m_mipLevelSize = squish::GetStorageRequirements(static_cast(w), static_cast(h), flags); + textureMIP.m_buffer = std::make_unique(textureMIP.m_mipLevelSize); + + // Compress image directly into pre-allocated space + squish::CompressImage( + reinterpret_cast(sourceMIP.bits()), + static_cast(w), static_cast(h), + (void*)textureMIP.m_buffer.get(), + flags, + nullptr); + } + break; + default: + return false; + } + } + + // Update internals + targetTexture.m_width = static_cast(mipLevels[0].width()); + targetTexture.m_height = static_cast(mipLevels[0].height()); + targetTexture.m_fileName = textureName.isEmpty() ? std::nullopt : std::make_optional(textureName.toStdString()); + targetTexture.m_type1 = targetTexture.m_type2 = targetFormat; + targetTexture.m_numOfMipMaps = static_cast(targetTexture.m_mipLevels.size()); + targetTexture.m_fileSize = targetTexture.calculateSize(); // Update file size after all manipulations + + return true; + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Models/GameScriptsTreeModel.cpp b/BMEdit/Editor/Source/Models/GameScriptsTreeModel.cpp new file mode 100644 index 0000000..7575159 --- /dev/null +++ b/BMEdit/Editor/Source/Models/GameScriptsTreeModel.cpp @@ -0,0 +1,182 @@ +#include +#include +#include + + +namespace models +{ + GameScriptsTreeModel::ScripTreeNode::ScripTreeNode() = default; + + GameScriptsTreeModel::ScripTreeNode::ScripTreeNode(models::GameScriptsTreeModel::ScripTreeNode::NodeType nt, QString &&sn, QString &&sfn) + : type(nt), name(std::move(sn)), fullPath(std::move(sfn)) + { + } + + GameScriptsTreeModel::GameScriptsTreeModel(QObject *parent) : QAbstractItemModel(parent) + { + buildTree(); + } + + QVariant GameScriptsTreeModel::data(const QModelIndex &index, int role) const + { + if (!m_root) return {}; + + const auto* pScript = reinterpret_cast(index.constInternalPointer()); + if (!pScript) return {}; + + if (role == Qt::DisplayRole) + { + return pScript->name; + } + + if (role == Qt::DecorationRole) + { + static QIcon kFolderIcon(":/bmedit/folder_icon.png"); + static QIcon kScriptIcon(":/bmedit/script_icon.png"); + static QIcon kUnknownIcon(":/bmedit/unknown_icon.png"); + + switch (pScript->type) + { + case ScripTreeNode::NodeType::STN_ROOT: + case ScripTreeNode::NodeType::STN_BONE: + return kFolderIcon; + break; + case ScripTreeNode::NodeType::STN_SCRIPT: + return kScriptIcon; + } + + return kUnknownIcon; + } + + if (role == Qt::ItemDataRole::ToolTipRole) + { + return pScript->fullPath; + } + + return {}; + } + + QModelIndex GameScriptsTreeModel::index(int row, int column, const QModelIndex &parent) const + { + if (!hasIndex(row, column, parent) || !m_root) + { + return QModelIndex {}; + } + + ScripTreeNode* pScript = nullptr; + if (!parent.isValid()) + { + pScript = m_root.get(); + } + else + { + pScript = static_cast(parent.internalPointer()); + } + + if (row >= 0 && row < pScript->children.size()) + { + return createIndex(row, column, (const void*)pScript->children[row].get()); + } + + return {}; + } + + QModelIndex GameScriptsTreeModel::parent(const QModelIndex &index) const + { + if (!index.isValid() || !m_root) + { + return {}; + } + + auto* child = static_cast(index.internalPointer()); + if (auto parent = child->parent.lock()) + { + if (parent == m_root) + { + return {}; + } + else + { + int row = 0; + + for (int i = 0; i < parent->children.size(); ++i) + { + if (parent->children[i].get() == child) + { + row = i; + break; + } + } + + return createIndex(row, 0, (const void*)parent.get()); + } + } + + return {}; + } + + int GameScriptsTreeModel::rowCount(const QModelIndex &parent) const + { + if (!m_root) + { + return 0; + } + + if (!parent.isValid()) + { + return m_root && !m_root->children.empty() ? static_cast(m_root->children.size()) : 0; + } + + return static_cast(static_cast(parent.internalPointer())->children.size()); + } + + int GameScriptsTreeModel::columnCount(const QModelIndex &parent) const + { + return m_root ? 1 : 0; + } + + QVariant GameScriptsTreeModel::headerData(int section, Qt::Orientation orientation, int role) const + { + return m_root ? QVariant::fromValue(QString("Game Scripts")) : QVariant {}; + } + + void insertIntoTree(const QString& str, const QSharedPointer& root) + { + // Split the string into parts based on the separator + QStringList parts = str.split('\\'); + + // Start from the root + QSharedPointer current = root; + + for (int i = 0; i < parts.size(); ++i) { + bool found = false; + // Check if the current part already exists as a child + foreach (const QSharedPointer& child, current->children) { + if (child->name == parts[i]) { + current = child; + found = true; + break; + } + } + + // If the part is not found, create a new node + if (!found) { + GameScriptsTreeModel::ScripTreeNode::NodeType type = (i < parts.size() - 1) ? GameScriptsTreeModel::ScripTreeNode::NodeType::STN_BONE : GameScriptsTreeModel::ScripTreeNode::NodeType::STN_SCRIPT; + QString fullPath = (current != root) ? current->fullPath + '\\' + parts[i] : parts[i]; + QSharedPointer newNode(new GameScriptsTreeModel::ScripTreeNode(type, std::move(parts[i]), std::move(fullPath))); + newNode->parent = current; + current->children.append(newNode); + current = newNode; + } + } + } + + void GameScriptsTreeModel::buildTree() + { + m_root = QSharedPointer::create(ScripTreeNode::NodeType::STN_ROOT, "ROOT", "ROOT"); + + gamelib::TypeRegistry::getInstance().forEachScript([this](const std::string& name, const gamelib::ScriptInfo& /*info*/) { + insertIntoTree(QString::fromStdString(name), m_root); + }); + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Models/GeomControllerListModel.cpp b/BMEdit/Editor/Source/Models/GeomControllerListModel.cpp index c9f673d..55b69a1 100644 --- a/BMEdit/Editor/Source/Models/GeomControllerListModel.cpp +++ b/BMEdit/Editor/Source/Models/GeomControllerListModel.cpp @@ -34,7 +34,10 @@ namespace models } if (role == Qt::DisplayRole) { - return QString::fromStdString(m_sceneObject->getControllers().at(index.row()).name); + if (const auto& controllers = m_sceneObject->getControllers(); controllers.size() > index.row()) + { + return QString::fromStdString(controllers.at(index.row()).name); + } } return {}; @@ -98,6 +101,17 @@ namespace models endResetModel(); } + void GeomControllerListModel::updateControllersList() + { + if (!m_sceneObject) { + return; + } + + beginResetModel(); + // Geom unchanged) + endResetModel(); + } + bool GeomControllerListModel::isReady() const { return m_sceneObject != nullptr; diff --git a/BMEdit/Editor/Source/Models/LocalizationTreeModel.cpp b/BMEdit/Editor/Source/Models/LocalizationTreeModel.cpp new file mode 100644 index 0000000..0c3e4b8 --- /dev/null +++ b/BMEdit/Editor/Source/Models/LocalizationTreeModel.cpp @@ -0,0 +1,251 @@ +#include + + +namespace models +{ + LocalizationTreeModel::LocalizationTreeModel(QObject *parent) : QAbstractItemModel(parent) + { + } + + LocalizationTreeModel::LocalizationTreeModel(const gamelib::Level *level, QObject *parent) : QAbstractItemModel(parent) + { + setLevel(level); + } + + QVariant LocalizationTreeModel::data(const QModelIndex &index, int role) const + { + if (!isValidLevel()) + { + return QVariant {}; + } + + const auto* node = reinterpret_cast(index.constInternalPointer()); + if (!node) return {}; + + if (role == Qt::EditRole) + { + if (index.column() == 0) + { + return QString::fromStdString(node->name); + } + + if (index.column() == 1 && node->canHaveValue()) + { + return QString::fromStdString(node->value); + } + } + + if (role == Qt::DisplayRole) + { + if (index.column() == 0) return QString::fromStdString(node->name); + if (index.column() == 1 && node->canHaveValue()) + { + return QString::fromStdString(node->value); + } + + if (index.column() == 1 && node->type == gamelib::loc::LOCTreeNodeType::EMPTY_BLOCK) + { + return QString("(EMPTY)"); + } + + return {}; + } + + if (role == Qt::ItemDataRole::ToolTipRole) + { + if (index.column() == 0) + { + QStringList locationPath {}; + + auto currentNode = node; + while (currentNode) + { + locationPath.push_front(QString::fromStdString(currentNode->name)); + const auto& parent = currentNode->parent.lock(); + currentNode = parent ? parent.get() : nullptr; + } + + return locationPath.join('/'); + } + + if (index.column() == 1 && node->canHaveValue()) + { + return QString::fromStdString(node->value); + } + } + + return {}; + } + + bool LocalizationTreeModel::setData(const QModelIndex &index, const QVariant &value, int role) + { + if (!isValidLevel() || role != Qt::EditRole || !value.canConvert()) return false; + + auto* node = reinterpret_cast(index.internalPointer()); + if (!node) return false; + + if (index.column() == 0) + { + node->name = value.value().toStdString(); + return true; + } + + if (index.column() == 1 && node->canHaveValue()) + { + node->value = value.value().toStdString(); + return true; + } + + return false; + } + + QModelIndex LocalizationTreeModel::index(int row, int column, const QModelIndex &parent) const + { + if (!hasIndex(row, column, parent) || !isValidLevel()) + { + return QModelIndex {}; + } + + gamelib::loc::LOCTreeNode* node = nullptr; + if (!parent.isValid()) + { + node = m_level->getLevelLocalization()->localizationRoot.get(); + } + else + { + node = static_cast(parent.internalPointer()); + } + + if (row >= 0 && row < node->children.size()) + { + return createIndex(row, column, (const void*)node->children[row].get()); + } + + return {}; + } + + QModelIndex LocalizationTreeModel::parent(const QModelIndex &index) const + { + if (!index.isValid() || !isValidLevel()) + { + return {}; + } + + auto* child = static_cast(index.internalPointer()); + if (auto parent = child->parent.lock()) + { + if (parent == m_level->getLevelLocalization()->localizationRoot) + { + return {}; + } + else + { + int row = 0; + + for (int i = 0; i < parent->children.size(); ++i) + { + if (parent->children[i].get() == child) + { + row = i; + break; + } + } + + return createIndex(row, 0, (const void*)parent.get()); + } + } + + return {}; + } + + int LocalizationTreeModel::rowCount(const QModelIndex &parent) const + { + if (!isValidLevel()) return 0; + + if (!parent.isValid()) + { + const auto& root = m_level->getLevelLocalization()->localizationRoot; + + return root->children.empty() ? 0 : static_cast(root->children.size()); + } + + if (auto node = static_cast(parent.internalPointer())) + { + if (node->type == gamelib::loc::CHILDREN) + return static_cast(node->children.size()); + + return 0; // only value + } + + return 0; + } + + int LocalizationTreeModel::columnCount(const QModelIndex &parent) const + { + if (!isValidLevel()) return 0; + return 2; // 1 for tree, 1 for value + } + + QVariant LocalizationTreeModel::headerData(int section, Qt::Orientation orientation, int role) const + { + if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) + { + if (section == 0) return "Tree"; + if (section == 1) return "Value"; + } + + return QAbstractItemModel::headerData(section, orientation, role); + } + + Qt::ItemFlags LocalizationTreeModel::flags(const QModelIndex &index) const + { + Qt::ItemFlags flags = QAbstractItemModel::flags(index); + const auto* node = reinterpret_cast(index.constInternalPointer()); + + if (!node) return flags; + + if (index.column() == 0) + { + flags |= Qt::ItemIsEditable; + } + + if (index.column() == 1) + { + if (node->canHaveValue()) + { + flags |= Qt::ItemIsEditable; + } + } + + return flags; + } + + void LocalizationTreeModel::setLevel(const gamelib::Level *level) + { + beginResetModel(); + m_level = level; + endResetModel(); + } + + void LocalizationTreeModel::resetLevel() + { + beginResetModel(); + m_level = nullptr; + endResetModel(); + } + + QModelIndex LocalizationTreeModel::getRootIndex() const + { + if (!m_level || !m_level->getLevelLocalization()) + { + return {}; + } + + return createIndex(0, 0, (const void*)m_level->getLevelLocalization()->localizationRoot.get()); + } + + bool LocalizationTreeModel::isValidLevel() const + { + return m_level && m_level->getLevelLocalization(); + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Models/ModelsLocator.cpp b/BMEdit/Editor/Source/Models/ModelsLocator.cpp new file mode 100644 index 0000000..eb2a7b6 --- /dev/null +++ b/BMEdit/Editor/Source/Models/ModelsLocator.cpp @@ -0,0 +1,9 @@ +#include + + +namespace models +{ + std::unique_ptr ModelsLocator::s_SceneTreeModel { nullptr }; + std::unique_ptr ModelsLocator::s_GameScriptsTreeModel { nullptr }; + std::unique_ptr ModelsLocator::s_LocalizationTreeModel { nullptr }; +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Models/SceneObjectControllerModel.cpp b/BMEdit/Editor/Source/Models/SceneObjectControllerModel.cpp index f8fa53f..a996282 100644 --- a/BMEdit/Editor/Source/Models/SceneObjectControllerModel.cpp +++ b/BMEdit/Editor/Source/Models/SceneObjectControllerModel.cpp @@ -1,20 +1,28 @@ #include +#include +#include +#include +#include using namespace models; +static constexpr std::string_view kScriptC = "ZScriptC"; +static const std::string kScriptNamePN = "ScriptName"; + SceneObjectControllerModel::SceneObjectControllerModel(QObject *parent) : ValueModelBase(parent) { - connect(this, &ValueModelBase::valueChanged, [=]() { onValueChanged(); }); + connect(this, &ValueModelBase::valueChanged, [this]() { onValueChanged(); }); } void SceneObjectControllerModel::setGeom(gamelib::scene::SceneObject *geom) { const bool isNewGeom = m_geom != geom; beginResetModel(); + m_geom = geom; - m_currentControllerIndex = -1; + m_currentControllerIndex = kUnset; endResetModel(); if (isNewGeom) @@ -26,8 +34,10 @@ void SceneObjectControllerModel::setGeom(gamelib::scene::SceneObject *geom) void SceneObjectControllerModel::resetGeom() { beginResetModel(); + + // and after that we've ready to do smth else m_geom = nullptr; - m_currentControllerIndex = -1; + m_currentControllerIndex = kUnset; endResetModel(); resetValue(); @@ -40,26 +50,138 @@ void SceneObjectControllerModel::setControllerIndex(int controllerIndex) return; } + auto& controller = m_geom->getControllers().at(controllerIndex); + beginResetModel(); + + // reset controller index m_currentControllerIndex = controllerIndex; endResetModel(); - setValue(m_geom->getControllers().at(controllerIndex).properties); + if (controller.type->getName() == kScriptC) + { + // Ok, need handle this correctly + gamelib::Value proxy { controller.properties }; + + // And here we need to update proxy views (just add mappings) + addSugarViews(controller.type, proxy, proxy.getObject(kScriptNamePN)); + + // And store it here + setValue(proxy); + } + else + { + // Default way + setValue(controller.properties); + } } void SceneObjectControllerModel::resetController() { beginResetModel(); - m_currentControllerIndex = -1; + m_currentControllerIndex = kUnset; endResetModel(); resetValue(); } +void SceneObjectControllerModel::addSugarViews(const gamelib::Type* pControllerType, gamelib::Value &v, const std::string &scriptName) +{ + // Ok, it must be pretty easy. First of all we need to find a script description in TypeRegistry + // Then we need to copy all unexposed instructions to 'v' bucket and add views for VARIABLE entries + const auto asDefault = pControllerType->makeDefaultPropertiesPack(); //TODO: Less hacks, please + const int baseViewsNr = asDefault.getEntries().size(); + const int baseSize = asDefault.getInstructions().size(); + + if (auto scriptInfo = gamelib::TypeRegistry::getInstance().getScriptInfo(scriptName); scriptInfo.has_value()) + { + // Ok, let's insert that data + for (const auto& ent : scriptInfo.value().entries) + { + // need to rebase this thing + gamelib::ValueEntry temp = ent; + temp.instructions.iOffset += baseSize; + v += temp; + } + } +} + +void SceneObjectControllerModel::removeSugarViews(const gamelib::Type* pControllerType, gamelib::Value &v) +{ + // Just reset views & entries from owner type + if (pControllerType->getKind() == gamelib::TypeKind::COMPLEX) + { + v.removeEntriesAndViewsSince( + pControllerType->makeDefaultPropertiesPack().getEntries().size() + ); + } +} + void SceneObjectControllerModel::onValueChanged() { - if (m_geom && m_currentControllerIndex != -1 && m_currentControllerIndex >= 0 && m_currentControllerIndex < m_geom->getControllers().size() && getValue().has_value()) + if (m_geom && m_currentControllerIndex != kUnset && m_currentControllerIndex >= 0 && m_currentControllerIndex < m_geom->getControllers().size() && getValue().has_value()) { - m_geom->getControllers().at(m_currentControllerIndex).properties = getValue().value(); + auto& controller = m_geom->getControllers().at(m_currentControllerIndex); + + if (controller.type->getName() == kScriptC) + { + // Here we need to drop our modified views and store 'typed' original views + gamelib::Value proxy = getValue().value(); + + const bool bScriptChanged = controller.properties.getObject(kScriptNamePN) != proxy.getObject(kScriptNamePN); + const std::string kNewScriptName = proxy.getObject(kScriptNamePN); + + if (bScriptChanged && gamelib::TypeRegistry::getInstance().hasScriptInfo(kNewScriptName)) + { + // Take original first instruction + std::vector instructions = { + getValue().value().getInstructions()[0] + }; + + // Nice! Now we've ready to append a new bunch of properties + const auto scriptInfo = gamelib::TypeRegistry::getInstance().getScriptInfo(kNewScriptName); + + for (const auto& instruction : scriptInfo->initialInstructions) + { + instructions.push_back(instruction); + } + + // And remember: don't forget about SkipMark opcode. Game iterating until not see this opcode and will crash + instructions.emplace_back(gamelib::prp::PRPOpCode::SkipMark); + + // Create new properties bucket + gamelib::Value newProperties { controller.type, instructions }; + + // Add 1 entry (original property markup) + newProperties += getValue().value().getEntries()[0]; + + // Save + controller.properties = newProperties; + + // And add extra stubs + for (const auto& ent : scriptInfo->entries) + { + auto newEnt = ent; + newEnt.instructions.iOffset += 1; // Add +1 because instruction #0 - our root instruction + newProperties += newEnt; + } + + // Done + setValue(newProperties); + } + else + { + // Desugar this stub + removeSugarViews(controller.type, proxy); + + // Save + controller.properties = proxy; + } + } + else + { + // Default way + controller.properties = getValue().value(); + } } } \ No newline at end of file diff --git a/BMEdit/Editor/Source/Models/SceneObjectPropertiesModel.cpp b/BMEdit/Editor/Source/Models/SceneObjectPropertiesModel.cpp index 02f1a16..a9b0ef2 100644 --- a/BMEdit/Editor/Source/Models/SceneObjectPropertiesModel.cpp +++ b/BMEdit/Editor/Source/Models/SceneObjectPropertiesModel.cpp @@ -76,7 +76,9 @@ namespace models if (value.value() != m_geom->getProperties()) { - m_geom->getProperties() = value.value(); + m_geom->setProperties(value.value()); + + emit objectPropertiesChanged(m_geom); } } } \ No newline at end of file diff --git a/BMEdit/Editor/Source/Models/ScenePrimitivesFilterModel.cpp b/BMEdit/Editor/Source/Models/ScenePrimitivesFilterModel.cpp deleted file mode 100644 index fccbaef..0000000 --- a/BMEdit/Editor/Source/Models/ScenePrimitivesFilterModel.cpp +++ /dev/null @@ -1,128 +0,0 @@ -#include -#include -#include - - -using namespace models; - - - -int ScenePrimitivesFilterModel::getFilterMask() const -{ - return m_filterMask; -} - -void ScenePrimitivesFilterModel::setAllowAll() -{ - if (m_filterMask != ScenePrimitivesFilterEntry::Allow_All) - { - m_filterMask = ScenePrimitivesFilterEntry::Allow_All; - invalidateRowsFilter(); - } -} - -void ScenePrimitivesFilterModel::setAllowNone() -{ - if (m_filterMask != ScenePrimitivesFilterEntry::Allow_None) - { - m_filterMask = ScenePrimitivesFilterEntry::Allow_None; - invalidateRowsFilter(); - } -} - -void ScenePrimitivesFilterModel::addFilterEntry(ScenePrimitivesFilterEntry entry) -{ - if (!(m_filterMask & entry)) - { - m_filterMask |= entry; - invalidateRowsFilter(); - } -} - -void ScenePrimitivesFilterModel::removeFilterEntry(ScenePrimitivesFilterEntry entry) -{ - if (m_filterMask & entry) - { - m_filterMask &= ~entry; - invalidateRowsFilter(); - } -} - -bool ScenePrimitivesFilterModel::isVertexFormatAllowed(gamelib::prm::PRMVertexBufferFormat vertexBufferFormat) -{ - return m_allowedVertexFormats.contains(vertexBufferFormat); -} - -void ScenePrimitivesFilterModel::setVertexFormatAllowed(gamelib::prm::PRMVertexBufferFormat vertexBufferFormat, bool isAllowed) -{ - if (isAllowed) - { - auto [iter, isInserted] = m_allowedVertexFormats.emplace(vertexBufferFormat); - if (isInserted) - { - invalidateRowsFilter(); - } - } - else - { - if (auto it = m_allowedVertexFormats.find(vertexBufferFormat); it != m_allowedVertexFormats.end()) - { - m_allowedVertexFormats.erase(vertexBufferFormat); - invalidateRowsFilter(); - } - } -} - -void ScenePrimitivesFilterModel::resetToDefaults() -{ - setAllowAll(); - m_allowedVertexFormats = { g_DefaultVertexFormats.begin(), g_DefaultVertexFormats.end() }; -} - -bool ScenePrimitivesFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const -{ - // Pre-opt - if (m_filterMask == ScenePrimitivesFilterEntry::Allow_None) - { - return false; - } - - if (m_filterMask == ScenePrimitivesFilterEntry::Allow_All) - { - return true; - } - - QModelIndex entryIndex = sourceModel()->index(sourceRow, 0, sourceParent); - - const auto value = sourceModel()->data(entryIndex, types::kChunkKindRole).value(); - - switch (value) - { - case gamelib::prm::PRMChunkRecognizedKind::CRK_ZERO_CHUNK: return m_filterMask & ScenePrimitivesFilterEntry::FilterAllow_Zero; - case gamelib::prm::PRMChunkRecognizedKind::CRK_INDEX_BUFFER: return m_filterMask & ScenePrimitivesFilterEntry::FilterAllow_Index; - case gamelib::prm::PRMChunkRecognizedKind::CRK_VERTEX_BUFFER: - { - if (m_filterMask & ScenePrimitivesFilterEntry::FilterAllow_Vertex) - { - if (g_DefaultVertexFormats.size() != m_allowedVertexFormats.size()) - { - auto format = sourceModel()->data(entryIndex, types::kChunkVertexFormatRole).value(); - if (m_allowedVertexFormats.contains(format)) - { - return true; - } - } - else - { - return true; - } - } - - return false; - } - case gamelib::prm::PRMChunkRecognizedKind::CRK_DESCRIPTION_BUFFER: return m_filterMask & ScenePrimitivesFilterEntry::FilterAllow_Description; - case gamelib::prm::PRMChunkRecognizedKind::CRK_UNKNOWN_BUFFER: return m_filterMask & ScenePrimitivesFilterEntry::FilterAllow_Unknown; - } - - return false; -} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Models/ScenePrimitivesModel.cpp b/BMEdit/Editor/Source/Models/ScenePrimitivesModel.cpp deleted file mode 100644 index 8550bb0..0000000 --- a/BMEdit/Editor/Source/Models/ScenePrimitivesModel.cpp +++ /dev/null @@ -1,168 +0,0 @@ -#include -#include -#include - - -using namespace models; - -ScenePrimitivesModel::ScenePrimitivesModel(QObject *parent) : QAbstractTableModel(parent) -{ -} - -int ScenePrimitivesModel::rowCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent); - - if (!m_level) - return 0; - - return static_cast(m_level->getLevelGeometry()->chunks.size()); -} - -int ScenePrimitivesModel::columnCount(const QModelIndex &parent) const -{ - return ColumnID::CID_MAX_COLUMNS; -} - -QVariant ScenePrimitivesModel::data(const QModelIndex &index, int role) const -{ - auto getKindAsQString = [](gamelib::prm::PRMChunkRecognizedKind kind) -> QString - { - switch (kind) - { - case gamelib::prm::PRMChunkRecognizedKind::CRK_UNKNOWN_BUFFER: return "UNKNOWN BUFFER"; - case gamelib::prm::PRMChunkRecognizedKind::CRK_VERTEX_BUFFER: return "VERTEX BUFFER"; - case gamelib::prm::PRMChunkRecognizedKind::CRK_INDEX_BUFFER: return "INDEX BUFFER"; - case gamelib::prm::PRMChunkRecognizedKind::CRK_DESCRIPTION_BUFFER: return "DESCRIPTION BUFFER"; - case gamelib::prm::PRMChunkRecognizedKind::CRK_ZERO_CHUNK: return ""; - } - - return "???"; - }; - - auto getVertexFormatAsQString = [](gamelib::prm::PRMVertexBufferFormat bufferFormat) -> QString - { - switch (bufferFormat) - { - case gamelib::prm::PRMVertexBufferFormat::VBF_VERTEX_10: return "Vertex Format 10"; - case gamelib::prm::PRMVertexBufferFormat::VBF_VERTEX_24: return "Vertex Format 24"; - case gamelib::prm::PRMVertexBufferFormat::VBF_VERTEX_28: return "Vertex Format 28"; - case gamelib::prm::PRMVertexBufferFormat::VBF_VERTEX_34: return "Vertex Format 34"; - case gamelib::prm::PRMVertexBufferFormat::VBF_UNKNOWN_VERTEX: return "UNKNOWN FORMAT"; - } - - return "???"; - }; - - if (!m_level) - { - return {}; - } - - auto& chk = m_level->getLevelGeometry()->chunks.at(index.row()); - - if (role == Qt::DisplayRole) - { - switch (static_cast(index.column())) - { - case ColumnID::CID_INDEX: return chk.getIndex(); - case ColumnID::CID_KIND: return getKindAsQString(chk.getKind()); - case ColumnID::CID_SIZE: return chk.getBuffer().size(); - case ColumnID::CID_VERTICES: - { - if (chk.getKind() == gamelib::prm::PRMChunkRecognizedKind::CRK_VERTEX_BUFFER) - { - return getVertexFormatAsQString(chk.getVertexBufferHeader()->vertexFormat); - } - break; - } - case ColumnID::CID_INDICES: - { - if (chk.getKind() == gamelib::prm::PRMChunkRecognizedKind::CRK_INDEX_BUFFER) - { - return chk.getIndexBufferHeader()->indicesCount; - } - break; - } - case ColumnID::CID_PTR_OBJECTS: - { - if (chk.getKind() == gamelib::prm::PRMChunkRecognizedKind::CRK_DESCRIPTION_BUFFER) - { - return QString("%1 (0x%2)").arg(chk.getDescriptionBufferHeader()->ptrObjects).arg(chk.getDescriptionBufferHeader()->ptrObjects, 8, 16, QChar('0')); - } - break; - } - case ColumnID::CID_PTR_PARTS: - { - if (chk.getKind() == gamelib::prm::PRMChunkRecognizedKind::CRK_DESCRIPTION_BUFFER) - { - return QString("%1 (0x%2)").arg(chk.getDescriptionBufferHeader()->ptrParts).arg(chk.getDescriptionBufferHeader()->ptrParts, 8, 16, QChar('0')); - } - break; - } - default: return {}; - } - } - else if (role == types::kChunkKindRole) - { - return QVariant::fromValue(chk.getKind()); - } - else if (role == types::kChunkIndexRole) - { - return chk.getIndex(); - } - else if (role == types::kChunkVertexFormatRole) - { - if (auto chkVertexBufferHeader = chk.getVertexBufferHeader(); chkVertexBufferHeader) - { - return QVariant::fromValue(chkVertexBufferHeader->vertexFormat); - } - } - - return {}; -} - -bool ScenePrimitivesModel::setData(const QModelIndex &index, const QVariant &value, int role) -{ - Q_UNUSED(index); - Q_UNUSED(value); - Q_UNUSED(role); - // This model is read only - return false; -} - -QVariant ScenePrimitivesModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - if (role == Qt::DisplayRole && orientation == Qt::Orientation::Horizontal) - { - constexpr std::array g_ColNames = { - "Index", "Kind (chunk)", "Size (chunk)", "Indices", "Vertices", "Objects REF", "Parts REF" - }; - - return QString::fromStdString(g_ColNames.at(section).data()); - } - - return {}; -} - -Qt::ItemFlags ScenePrimitivesModel::flags(const QModelIndex &index) const -{ - return Qt::ItemFlag::ItemIsSelectable | Qt::ItemFlag::ItemIsEnabled; -} - -void ScenePrimitivesModel::setLevel(gamelib::Level* level) -{ - if (level) - { - beginResetModel(); - m_level = level; - endResetModel(); - } -} - -void ScenePrimitivesModel::resetLevel() -{ - beginResetModel(); - m_level = nullptr; - endResetModel(); -} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Models/SceneTextureFilterModel.cpp b/BMEdit/Editor/Source/Models/SceneTextureFilterModel.cpp new file mode 100644 index 0000000..470f255 --- /dev/null +++ b/BMEdit/Editor/Source/Models/SceneTextureFilterModel.cpp @@ -0,0 +1,57 @@ +#include +#include + + +namespace models +{ + SceneTextureFilterModel::SceneTextureFilterModel(QObject* parent) : QSortFilterProxyModel(parent) + { + } + + void SceneTextureFilterModel::setTextureNameFilter(const QString& query) + { + beginResetModel(); + { + m_textureNameFilter = query; + m_filterResultsCache.clear(); + } + endResetModel(); + } + + const QString& SceneTextureFilterModel::getTextureNameFilter() const + { + return m_textureNameFilter; + } + + bool SceneTextureFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const // NOLINT(misc-no-recursion) + { + if (m_textureNameFilter.isEmpty()) + return true; // Allow all + + QAbstractItemModel *srcModel = sourceModel(); + + if (sourceParent.isValid() && sourceRow >= srcModel->rowCount(sourceParent)) + { + return false; + } + + QModelIndex index = srcModel->index(sourceRow, SceneTexturesModel::ColumnID::NAME, sourceParent); + if (m_filterResultsCache.contains(index)) + { + return m_filterResultsCache[index]; + } + + // Try build cache + QRegularExpression regex(m_textureNameFilter, QRegularExpression::CaseInsensitiveOption); + QString text = srcModel->data(index, Qt::DisplayRole).toString(); + if (regex.match(text).hasMatch()) + { + m_filterResultsCache[index] = true; + return true; + } + + // Or fill false + m_filterResultsCache[index] = false; + return false; + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Models/SceneTexturesModel.cpp b/BMEdit/Editor/Source/Models/SceneTexturesModel.cpp new file mode 100644 index 0000000..0886b61 --- /dev/null +++ b/BMEdit/Editor/Source/Models/SceneTexturesModel.cpp @@ -0,0 +1,168 @@ +#include +#include +#include + +using namespace models; + + +SceneTexturesModel::SceneTexturesModel(QObject *parent) : QAbstractTableModel(parent) +{ +} + +int SceneTexturesModel::rowCount(const QModelIndex &parent) const +{ + if (!isReady()) return 0; + return static_cast(m_level->getSceneTextures()->entries.size()); +} + +int SceneTexturesModel::columnCount(const QModelIndex &parent) const +{ + if (!isReady()) return 0; + return ColumnID::MAX_COLUMNS; +} + +QVariant SceneTexturesModel::data(const QModelIndex &index, int role) const +{ + if (!isReady() || index.row() >= m_level->getSceneTextures()->entries.size()) + { + return {}; + } + + if (index.column() == ColumnID::INDEX) + { + if (role == Qt::DisplayRole) + { + // May be customized + return m_level->getSceneTextures()->entries.at(index.row()).m_index; + } + + if (role == Qt::UserRole) + { + // DO NOT CUSTOMIZE THIS + return m_level->getSceneTextures()->entries.at(index.row()).m_index; + } + } + + if (index.column() == ColumnID::OFFSET && role == Qt::DisplayRole) + { + return m_level->getSceneTextures()->entries.at(index.row()).m_offset; + } + + if (index.column() == ColumnID::NAME && role == Qt::DisplayRole) + { + if (auto& name = m_level->getSceneTextures()->entries.at(index.row()).m_fileName; name.has_value()) + { + return QString::fromStdString(name.value()); + } + + return QString(); + } +#ifndef NDEBUG + if (index.column() == ColumnID::CUBEMAP && role == Qt::DisplayRole) + { + return QString("%1").arg(m_level->getSceneTextures()->entries.at(index.row()).m_flags, 8, 16); + } +#endif + + if (index.column() == ColumnID::RESOLUTION && role == Qt::DisplayRole) + { + const auto& entry = m_level->getSceneTextures()->entries.at(index.row()); + return QString("%1x%2").arg(entry.m_width).arg(entry.m_height); + } + + if (index.column() == ColumnID::FORMAT && role == Qt::DisplayRole) + { + auto formatToQString = [](gamelib::tex::TEXEntryType type) -> QString { + switch (type) + { + case gamelib::tex::TEXEntryType::ET_BITMAP_I8: return "I8"; + case gamelib::tex::TEXEntryType::ET_BITMAP_EMBM: return "EMBM"; + case gamelib::tex::TEXEntryType::ET_BITMAP_DOT3: return "DOT3"; + case gamelib::tex::TEXEntryType::ET_BITMAP_CUBE: return "CUBE"; + case gamelib::tex::TEXEntryType::ET_BITMAP_DMAP: return "DMAP"; + case gamelib::tex::TEXEntryType::ET_BITMAP_PAL: return "PAL (Neg)"; + case gamelib::tex::TEXEntryType::ET_BITMAP_PAL_OPAC: return "PAL (Opac)"; + case gamelib::tex::TEXEntryType::ET_BITMAP_32: return "RGBA"; + case gamelib::tex::TEXEntryType::ET_BITMAP_U8V8: return "U8V8"; + case gamelib::tex::TEXEntryType::ET_BITMAP_DXT1: return "DXT1"; + case gamelib::tex::TEXEntryType::ET_BITMAP_DXT3: return "DXT3"; + default: + assert(false && "Unknown entry"); + return "Unknown"; + } + }; + + return formatToQString(m_level->getSceneTextures()->entries.at(index.row()).m_type1); + } + + if (role == SceneTexturesModel::Roles::R_TEXTURE_REF) + { + types::QTextureREF ref; + ref.ownerModel = this; + ref.textureIndex = m_level->getSceneTextures()->entries.at(index.row()).m_index; + return QVariant::fromValue(ref); + } + + return {}; +} + +bool SceneTexturesModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + //TODO: Fixme later + return false; +} + +QVariant SceneTexturesModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) + { + if (section == ColumnID::NAME) return QString("Name"); + if (section == ColumnID::OFFSET) return QString("Offset"); + if (section == ColumnID::INDEX) return QString("Index"); + if (section == ColumnID::FORMAT) return QString("Format"); + if (section == ColumnID::RESOLUTION) return QString("Resolution"); +#ifndef NDEBUG + if (section == ColumnID::CUBEMAP) return QString("CubeMap"); +#endif + } + + return QAbstractTableModel::headerData(section, orientation, role); +} + +Qt::ItemFlags SceneTexturesModel::flags(const QModelIndex &index) const +{ + return Qt::ItemFlag::ItemIsSelectable | Qt::ItemFlag::ItemIsEnabled; //TODO: Fixme later +} + +void SceneTexturesModel::setLevel(const gamelib::Level *level) +{ + if (m_level == level) return; + + beginResetModel(); + m_level = level; + endResetModel(); +} + +void SceneTexturesModel::resetLevel() +{ + if (!isReady()) return; + + beginResetModel(); + m_level = nullptr; + endResetModel(); +} + +const gamelib::Level *SceneTexturesModel::getLevel() const +{ + return m_level; +} + +gamelib::Level *SceneTexturesModel::getLevel() +{ + return const_cast(m_level); // Bruh. TODO: Refactor later +} + +bool SceneTexturesModel::isReady() const +{ + return m_level != nullptr; +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Render/Camera.cpp b/BMEdit/Editor/Source/Render/Camera.cpp new file mode 100644 index 0000000..3e9b724 --- /dev/null +++ b/BMEdit/Editor/Source/Render/Camera.cpp @@ -0,0 +1,149 @@ +#include + + +namespace render +{ + Camera::Camera(float fov, const glm::vec3 &vPosition, const glm::ivec2 &vScreenSize) + : m_fFov(fov), m_vPosition(vPosition), m_vScreenSize(vScreenSize) + { + update(); + } + + Ray Camera::getRayFromScreen(const glm::ivec2& vScreenPos) const + { + return getRayFromScreen(static_cast(vScreenPos.x), static_cast(vScreenPos.y)); + } + + Ray Camera::getRayFromScreen(float x, float y) const + { + constexpr float kSign = 1.f; + glm::vec4 vRayClip = glm::vec4((2.f * x) / static_cast(m_vScreenSize.x) - 1.f, 1.f - (2.f * y) / static_cast(m_vScreenSize.y), kSign, 1.f); + glm::vec4 vRayEye = glm::inverse(m_mProj) * vRayClip; + vRayEye = glm::vec4 { vRayEye.x, vRayEye.y, kSign, .0f }; + glm::vec3 vRayWorld = glm::normalize(glm::vec3(glm::inverse(m_mView) * vRayEye)); + return { getPosition(), vRayWorld }; + } + + void Camera::setFOV(float fov) + { + if (m_fFov != fov) + { + m_fFov = fov; + update(); + } + } + + void Camera::setSpeed(float speed) + { + if (speed > 0.f) + { + m_fSpeed = speed; + } + } + + void Camera::setSensitivity(float sens) + { + if (sens > .0f) + { + m_fSensitivity = sens; + } + } + + void Camera::setViewport(int width, int height) + { + if (m_vScreenSize.x != width || m_vScreenSize.y != height) + { + m_vScreenSize.x = width; + m_vScreenSize.y = height; + + update(); + } + } + + void Camera::setPosition(const glm::vec3& vPosition) + { + m_vPosition = vPosition; + update(); + } + + // Movement + void Camera::handleKeyboardMovement(CameraMovementMask movementMask, float dt) + { + // Handle keyboard movement + const float fSpeedUp = (movementMask & CM_SPEEDUP_MOD) ? 4.0f : 1.0f; + const float fVelocity = m_fSpeed * fSpeedUp; + + if ((movementMask & CM_FORWARD) && (movementMask & CM_BACKWARD)) movementMask &= ~(CM_FORWARD | CM_BACKWARD); + if ((movementMask & CM_LEFT) && (movementMask & CM_RIGHT)) movementMask &= ~(CM_LEFT | CM_RIGHT); + if (movementMask == CM_SPEEDUP_MOD) movementMask = 0; + + if (movementMask > 0) { + if (movementMask & CM_FORWARD) m_vPosition += m_vLookDirection * fVelocity; + if (movementMask & CM_BACKWARD) m_vPosition -= m_vLookDirection * fVelocity; + if (movementMask & CM_LEFT) m_vPosition += m_vRight * fVelocity; + if (movementMask & CM_RIGHT) m_vPosition -= m_vRight * fVelocity; + + update(); + } + } + + void Camera::processMouseMovement(float xoffset, float yoffset, float dt) + { + // Handle mouse movement + xoffset *= m_fSensitivity; + yoffset *= m_fSensitivity; + + m_fYaw += xoffset; + m_fPitch += yoffset; + + if (m_fPitch > 89.0f) + m_fPitch = 89.0f; + + if (m_fPitch < -89.0f) + m_fPitch = -89.0f; + + update(); + } + + bool Camera::canSeeObject(const glm::vec3& vMin, const glm::vec3& vMax) const + { + return m_sFrustum.isBoxVisible(vMin, vMax); + } + + bool Camera::canSeeObject(const gamelib::BoundingBox& bbox) const + { + return m_sFrustum.isBoxVisible(bbox.min, bbox.max); + } + + bool Camera::canSeeObject(const gamelib::Plane& plane) const + { + const glm::vec3 vU = plane.getPoint(1) - plane.getPoint(0); + const glm::vec3 vV = plane.getPoint(2) - plane.getPoint(0); + const glm::vec3 vNormal = glm::cross(vU, vV); + + if (glm::dot(vNormal, m_vLookDirection) >= 0) { + return false; + } + + return true; // NOTE: Maybe we really need to check this, but it works well for now + //return m_sFrustum.isPlaneVisible(plane); + } + + void Camera::update() + { + glm::vec3 vFront { .0f }; + vFront.x = cos(glm::radians(m_fYaw)) * cos(glm::radians(m_fPitch)); + vFront.y = sin(glm::radians(m_fPitch)); + vFront.z = sin(glm::radians(m_fYaw)) * cos(glm::radians(m_fPitch)); + m_vLookDirection = glm::normalize(vFront); + + m_vRight = glm::normalize(glm::cross(m_vLookDirection, m_vWorldUp)); + m_vUp = glm::normalize(glm::cross(m_vRight, m_vLookDirection)); + + m_mView = glm::lookAtLH(m_vPosition, m_vPosition + m_vLookDirection, m_vUp); + m_mProj = glm::perspectiveFovLH(glm::radians(m_fFov), static_cast(m_vScreenSize.x), static_cast(m_vScreenSize.y), m_fNearPlane, m_fFarPlane); + m_mProjView = m_mProj * m_mView; + + m_sFrustum.setup(m_mProjView); + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Render/GlacierVertex.cpp b/BMEdit/Editor/Source/Render/GlacierVertex.cpp new file mode 100644 index 0000000..dc0fc9c --- /dev/null +++ b/BMEdit/Editor/Source/Render/GlacierVertex.cpp @@ -0,0 +1,14 @@ +#include + + +namespace render +{ + const VertexFormatDescription GlacierVertex::g_FormatDescription = + VertexFormatDescription() + .addField(0, VertexDescriptionEntryType::VDE_Vec3, false) + .addField(1, VertexDescriptionEntryType::VDE_Vec2, false); + + const VertexFormatDescription SimpleVertex::g_FormatDescription = + VertexFormatDescription() + .addField(0, VertexDescriptionEntryType::VDE_Vec3, false); +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Render/Model.cpp b/BMEdit/Editor/Source/Render/Model.cpp new file mode 100644 index 0000000..de366cf --- /dev/null +++ b/BMEdit/Editor/Source/Render/Model.cpp @@ -0,0 +1,207 @@ +#include +#include + + +namespace render +{ + static constexpr GLenum g_entryTypeToGLType[] = { + GL_NONE, + GL_BOOL, + GL_INT, + GL_UNSIGNED_INT, + GL_FLOAT, + GL_FLOAT, + GL_FLOAT, + GL_FLOAT, + GL_INT, + GL_INT, + GL_INT, + GL_FLOAT, + GL_FLOAT, + }; + + bool Mesh::setup(QOpenGLFunctions_3_3_Core* gapi, const VertexFormatDescription& vertexFormat, const uint8_t* vertices, uint32_t verticesCount, const uint8_t* indices, uint32_t indicesCount, bool bDynamic) + { + // Allocate resources + gapi->glGenVertexArrays(1, &vao); + gapi->glGenBuffers(1, &vbo); + if (indices != nullptr) + gapi->glGenBuffers(1, &ibo); + + // Attach VAO + gapi->glBindVertexArray(vao); + gapi->glBindBuffer(GL_ARRAY_BUFFER, vbo); + + // Upload vertices + gapi->glBufferData(GL_ARRAY_BUFFER, vertexFormat.getStride() * verticesCount, vertices, bDynamic ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW); + + // Upload indices (if required) + if (indices != nullptr) + { + gapi->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); + gapi->glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast(sizeof(uint16_t) * indicesCount), indices, bDynamic ? GL_DYNAMIC_DRAW : GL_STATIC_DRAW); + } + + // Initialize vertex format + vertexFormat.visit([gapi](uint32_t index, uint32_t offset, uint32_t size, uint32_t stride, VertexDescriptionEntryType t, bool isNormalized) { + const int componentsNr = static_cast(size) / 4; + const GLboolean normalized = isNormalized ? GL_TRUE : GL_FALSE; + const GLenum glType = g_entryTypeToGLType[static_cast(t)]; + + gapi->glEnableVertexAttribArray(index); + gapi->glVertexAttribPointer(index, componentsNr, glType, normalized, static_cast(stride), reinterpret_cast(static_cast(offset))); + }); + + // Save data + m_vertexFormat = vertexFormat; + m_bIsDynamic = bDynamic; + + if (indices != nullptr) + { + trianglesCount = static_cast(indicesCount / 3); + } + else + { + trianglesCount = static_cast(verticesCount / 3); + } + + m_maxVerticesNr = verticesCount; + m_maxIndicesNr = indices != nullptr ? indicesCount : 0u; + + return true; + } + + bool Mesh::update(QOpenGLFunctions_3_3_Core* gapi, uint32_t verticesOffset, const uint8_t* vertices, uint32_t verticesCount, uint32_t indicesOffset, const uint8_t* indices, uint32_t indicesCount) // NOLINT(*-make-member-function-const) + { + gapi->glBindBuffer(GL_ARRAY_BUFFER, vbo); + gapi->glBufferSubData(GL_ARRAY_BUFFER, static_cast(verticesOffset), static_cast(verticesCount * m_vertexFormat.getStride()), vertices); + gapi->glBindBuffer(GL_ARRAY_BUFFER, 0); + + if (indices) + { + gapi->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo); + gapi->glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, static_cast(indicesOffset), static_cast(indicesCount * sizeof(uint16_t)), indices); + gapi->glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + } + + return true; + } + + void Mesh::discard(QOpenGLFunctions_3_3_Core *gapi) + { + if (vao != kInvalidResource) + { + gapi->glDeleteVertexArrays(1, &vao); + vao = kInvalidResource; + } + + if (vbo != kInvalidResource) + { + gapi->glDeleteBuffers(1, &vbo); + vbo = kInvalidResource; + } + + if (ibo != kInvalidResource) + { + gapi->glDeleteBuffers(1, &ibo); + ibo = kInvalidResource; + } + + m_vertexFormat = {}; + m_maxVerticesNr = 0u; + m_maxIndicesNr = 0u; + m_bIsDynamic = false; + trianglesCount = 0; + } + + void Model::discard(QOpenGLFunctions_3_3_Core *gapi) + { + if (boundingBoxMesh.has_value()) + { + boundingBoxMesh.value().discard(gapi); + boundingBoxMesh = std::nullopt; + } + + for (auto& mesh : meshes) + { + mesh.discard(gapi); + } + + meshes.clear(); + } + + bool Model::setupBoundingBox(QOpenGLFunctions_3_3_Core *gapi) + { + VertexFormatDescription vertexFormat{}; + vertexFormat.addField(0, VertexDescriptionEntryType::VDE_Vec3, false); + + const glm::vec3& vMin = boundingBox.min; + const glm::vec3& vMax = boundingBox.max; + + std::vector vertices { + glm::vec3{vMin.x, vMin.y, vMin.z}, glm::vec3{vMin.x, vMin.y, vMax.z}, + glm::vec3{vMin.x, vMax.y, vMin.z}, glm::vec3{vMin.x, vMax.y, vMax.z}, + glm::vec3{vMax.x, vMin.y, vMin.z}, glm::vec3{vMax.x, vMin.y, vMax.z}, + glm::vec3{vMax.x, vMax.y, vMin.z}, glm::vec3{vMax.x, vMax.y, vMax.z} + }; + + std::vector indices { + 0, 1, 1, 3, 3, 2, 2, 0, + 4, 5, 5, 7, 7, 6, 6, 4, + 0, 4, 1, 5, 2, 6, 3, 7 + }; + + Mesh& mesh = boundingBoxMesh.emplace(); + mesh.glTextureId = 0u; + mesh.materialId = 0u; + return mesh.setup(gapi, vertexFormat, vertices, indices, false); + } + + void Mesh::render(QOpenGLFunctions_3_3_Core* gapi, render::RenderTopology topology) const + { + if (vao == kInvalidResource) + { + assert(false && "Not initialised!"); + return; + } + + assert(trianglesCount > 0); + + // Select topology + GLenum glTopology = GL_NONE; + switch (topology) + { + case RenderTopology::RT_NONE: + { + assert(false && "Invalid topology"); + return; + } + case RenderTopology::RT_POINTS: glTopology = GL_POINTS; break; + case RenderTopology::RT_LINES: glTopology = GL_LINES; break; + case RenderTopology::RT_LINE_STRIP: glTopology = GL_LINE_STRIP; break; + case RenderTopology::RT_LINE_LOOP: glTopology = GL_LINE_LOOP; break; + case RenderTopology::RT_TRIANGLES: glTopology = GL_TRIANGLES; break; + case RenderTopology::RT_TRIANGLE_STRIP: glTopology = GL_TRIANGLE_STRIP; break; + case RenderTopology::RT_TRIANGLE_FAN: glTopology = GL_TRIANGLE_FAN; break; + } + + + // Activate us + gapi->glBindVertexArray(vao); + + // Perform draw + if (ibo != kInvalidResource) + { + // Indexed + gapi->glDrawElements(glTopology, (trianglesCount * 3), GL_UNSIGNED_SHORT, nullptr); + } + else + { + // Arrays + gapi->glDrawArrays(GL_TRIANGLES, 0, trianglesCount); + } + + // Deactivate us + gapi->glBindVertexArray(0); + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Render/Ray.cpp b/BMEdit/Editor/Source/Render/Ray.cpp new file mode 100644 index 0000000..a9682c8 --- /dev/null +++ b/BMEdit/Editor/Source/Render/Ray.cpp @@ -0,0 +1,25 @@ +#include + + +namespace render +{ + bool Ray::intersect(const gamelib::BoundingBox& boundingBox, bool bAllowRaysStartingInsideTargetBbox) const + { + const float t1 = (boundingBox.min.x - vOrigin.x) / vDirection.x; + const float t2 = (boundingBox.max.x - vOrigin.x) / vDirection.x; + const float t3 = (boundingBox.min.y - vOrigin.y) / vDirection.y; + const float t4 = (boundingBox.max.y - vOrigin.y) / vDirection.y; + const float t5 = (boundingBox.min.z - vOrigin.z) / vDirection.z; + const float t6 = (boundingBox.max.z - vOrigin.z) / vDirection.z; + + const float tMin = glm::max(glm::max(glm::min(t1, t2), glm::min(t3, t4)), glm::min(t5, t6)); + const float tMax = glm::min(glm::min(glm::max(t1, t2), glm::max(t3, t4)), glm::max(t5, t6)); + + if (tMax < .0f || tMin > tMax || (!bAllowRaysStartingInsideTargetBbox && tMin < 0.f)) + { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Render/Shader.cpp b/BMEdit/Editor/Source/Render/Shader.cpp new file mode 100644 index 0000000..bf1db71 --- /dev/null +++ b/BMEdit/Editor/Source/Render/Shader.cpp @@ -0,0 +1,211 @@ +#include +#include + + +namespace render +{ + + void Shader::discard(QOpenGLFunctions_3_3_Core *gapi) + { + if (vertexProgramId != kInvalidResource) + { + gapi->glDeleteShader(vertexProgramId); + vertexProgramId = kInvalidResource; + } + + if (fragmentProgramId != kInvalidResource) + { + gapi->glDeleteShader(fragmentProgramId); + fragmentProgramId = kInvalidResource; + } + + if (programId != kInvalidResource) + { + gapi->glDeleteShader(programId); + programId = kInvalidResource; + } + } + + void Shader::bind(QOpenGLFunctions_3_3_Core *gapi) + { + if (programId != kInvalidResource) + { + gapi->glUseProgram(programId); + } + } + + void Shader::unbind(QOpenGLFunctions_3_3_Core *gapi) + { + gapi->glUseProgram(0); + } + + bool Shader::compile(QOpenGLFunctions_3_3_Core *gapi, const std::string &vertexProgram, const std::string &fragmentProgram, std::string &error) + { + // Allocate root program + programId = gapi->glCreateProgram(); + vertexProgramId = gapi->glCreateShader(GL_VERTEX_SHADER); + fragmentProgramId = gapi->glCreateShader(GL_FRAGMENT_SHADER); + + // Compile vertex program + if (!compileUnit(gapi, vertexProgramId, GL_VERTEX_SHADER, vertexProgram, error)) + { + qDebug() << "Failed to compile vertex shader program"; + assert(false && "Failed to compile vertex shader program"); + return false; + } + + gapi->glAttachShader(programId, vertexProgramId); + + // Compile fragment program + if (!compileUnit(gapi, fragmentProgramId, GL_FRAGMENT_SHADER, fragmentProgram, error)) + { + qDebug() << "Failed to compile vertex shader program"; + assert(false && "Failed to compile fragment shader program"); + return false; + } + + gapi->glAttachShader(programId, fragmentProgramId); + + // Linking + gapi->glLinkProgram(programId); + + // Check linking status + GLint linkingIsOK = GL_FALSE; + + gapi->glGetProgramiv(programId, GL_LINK_STATUS, &linkingIsOK); + if (linkingIsOK == GL_FALSE) + { + constexpr int kCompileLogSize = 512; + + char linkLog[kCompileLogSize] = { 0 }; + GLint length { 0 }; + + gapi->glGetProgramInfoLog(programId, kCompileLogSize, &length, &linkLog[0]); + + error = std::string(&linkLog[0], length); + qDebug() << "Failed to link shader program: " << QString::fromStdString(error); + + assert(false); + return false; + } + + // Done + return true; + } + + bool Shader::compileUnit(QOpenGLFunctions_3_3_Core *gapi, GLuint unitId, GLenum unitType, const std::string &unitSource, std::string &error) + { + const GLchar* glSrc = reinterpret_cast(unitSource.c_str()); + const GLint glLen = static_cast(unitSource.length()); + + gapi->glShaderSource(unitId, 1, &glSrc, &glLen); + gapi->glCompileShader(unitId); + + GLint isCompiled = 0; + gapi->glGetShaderiv(unitId, GL_COMPILE_STATUS, &isCompiled); + + if (isCompiled == GL_FALSE) + { + constexpr int kCompileLogSize = 512; + + char compileLog[kCompileLogSize] = { 0 }; + GLint length { 0 }; + + gapi->glGetShaderInfoLog(unitId, kCompileLogSize, &length, &compileLog[0]); + + error = std::string(&compileLog[0], length); + qDebug() << "Failed to compile shader program: " << QString::fromStdString(error); + + assert(false && "Failed to compile unit!"); + return false; + } + + return true; + } + + void Shader::setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, float s) + { + GLint location = resolveLocation(gapi, id); + if (location == -1) + return; + + gapi->glUniform1f(location, s); + } + + void Shader::setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, std::int32_t s) + { + GLint location = resolveLocation(gapi, id); + if (location == -1) + return; + + gapi->glUniform1i(location, s); + } + + void Shader::setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, const glm::vec2& v) + { + GLint location = resolveLocation(gapi, id); + if (location == -1) + return; + + gapi->glUniform2fv(location, 1, glm::value_ptr(v)); + } + + void Shader::setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, const glm::ivec2& v) + { + GLint location = resolveLocation(gapi, id); + if (location == -1) + return; + + gapi->glUniform2iv(location, 1, glm::value_ptr(v)); + } + + void Shader::setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, const glm::vec3& v) + { + GLint location = resolveLocation(gapi, id); + if (location == -1) + return; + + gapi->glUniform3fv(location, 1, glm::value_ptr(v)); + } + + void Shader::setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, const glm::vec4& v) + { + GLint location = resolveLocation(gapi, id); + if (location == -1) + return; + + gapi->glUniform4fv(location, 1, glm::value_ptr(v)); + } + + void Shader::setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, const glm::mat3& v) + { + GLint location = resolveLocation(gapi, id); + if (location == -1) + return; + + gapi->glUniformMatrix3fv(location, 1, GL_FALSE, glm::value_ptr(v)); + } + + void Shader::setUniform(QOpenGLFunctions_3_3_Core* gapi, const std::string& id, const glm::mat4& v) + { + GLint location = resolveLocation(gapi, id); + if (location == -1) + return; + + gapi->glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(v)); + } + + GLint Shader::resolveLocation(QOpenGLFunctions_3_3_Core* gapi, const std::string& id) + { + if (auto it = m_locationsCache.find(id); it == m_locationsCache.end()) + { + GLint result = gapi->glGetUniformLocation(programId, id.c_str()); + m_locationsCache[id] = result; + return result; + } + else + { + return it->second; + } + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Render/Texture.cpp b/BMEdit/Editor/Source/Render/Texture.cpp new file mode 100644 index 0000000..5e3f943 --- /dev/null +++ b/BMEdit/Editor/Source/Render/Texture.cpp @@ -0,0 +1,69 @@ +#include +#include + + +namespace render +{ + Texture::Texture() = default; + + bool Texture::setup(QOpenGLFunctions_3_3_Core* gapi, const gamelib::tex::TEXEntry& gTex) + { + if (texture != kInvalidResource) + { + assert(false && "Resource must be not initialised here!"); + return false; + } + + uint16_t w { 0 }, h { 0 }; + std::unique_ptr decompressedMemBlk = editor::TextureProcessor::decompressRGBA(gTex, w, h, 0); // + if (!decompressedMemBlk) + { + return false; + } + + // Store texture index from TEX container + index = std::make_optional(gTex.m_index); + texPath = gTex.m_fileName; // just copy file name from tex (if it defined!) + width = w; + height = h; + + // Create GL resource + gapi->glGenTextures(1, &texture); + gapi->glBindTexture(GL_TEXTURE_2D, texture); + gapi->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, decompressedMemBlk.get()); + + gapi->glGenerateMipmap(GL_TEXTURE_2D); + gapi->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + gapi->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + gapi->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + gapi->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + gapi->glBindTexture(GL_TEXTURE_2D, 0); + + return true; + } + + void Texture::discard(QOpenGLFunctions_3_3_Core *gapi) + { + width = height = 0; + + if (texture != kInvalidResource) + { + gapi->glDeleteTextures(1, &texture); + texture = kInvalidResource; + } + } + + void Texture::bind(QOpenGLFunctions_3_3_Core* gapi) + { + if (texture != kInvalidResource) + { + gapi->glBindTexture(GL_TEXTURE_2D, texture); + } + } + + void Texture::unbind(QOpenGLFunctions_3_3_Core* gapi) + { + gapi->glBindTexture(GL_TEXTURE_2D, 0); + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Render/VertexFormatDescription.cpp b/BMEdit/Editor/Source/Render/VertexFormatDescription.cpp new file mode 100644 index 0000000..a25227c --- /dev/null +++ b/BMEdit/Editor/Source/Render/VertexFormatDescription.cpp @@ -0,0 +1,94 @@ +// +// Code from this file is based on BestByte Framework created by DronCode +// +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace render +{ + static constexpr uint32_t g_typeSizeMap[] = { + 0, // None + sizeof(bool), + sizeof(int32_t), + sizeof(uint32_t), + sizeof(float), + sizeof(glm::vec2), + sizeof(glm::vec3), + sizeof(glm::vec4), + sizeof(glm::ivec2), + sizeof(glm::ivec3), + sizeof(glm::ivec4), + sizeof(glm::mat3x3), + sizeof(glm::mat4x4) + }; + + VertexFormatDescription::VertexFormatDescription() = default; + + VertexFormatDescription& VertexFormatDescription::addField(uint32_t index, VertexDescriptionEntryType type, bool normalized) + { + if (type == VertexDescriptionEntryType::VDE_None) + { + assert(false); + return *this; + } + + auto& ent = m_entries.emplace_back(); + ent.type = type; + ent.size = g_typeSizeMap[static_cast(type)]; + ent.normalized = normalized; + ent.index = index; + + m_isOrdered = false; + m_stride += ent.size; + + return *this; + } + + const VertexFormatDescription::Entries& VertexFormatDescription::getEntries() const + { + updateOrder(); + + return m_entries; + } + + void VertexFormatDescription::visit(const render::VertexFormatDescription::Visitor &visitor) const + { + updateOrder(); + + for (const auto& [index, offset, size, type, normalized] : m_entries) + { + visitor(index, offset, size, m_stride, type, normalized); + } + } + + uint32_t VertexFormatDescription::getStride() const + { + return m_stride; + } + + void VertexFormatDescription::updateOrder() const + { + if (!m_isOrdered) + { + m_isOrdered = true; + + std::sort(m_entries.begin(), m_entries.end(), [](const VertexDescriptionEntry& a, const VertexDescriptionEntry& b) { + return a.index < b.index; + }); + + size_t offset = 0; + for (auto& entry : m_entries) + { + entry.offset = offset; + offset += entry.size; + } + } + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Widgets/BitSetViewWidget.cpp b/BMEdit/Editor/Source/Widgets/BitSetViewWidget.cpp new file mode 100644 index 0000000..0969cc3 --- /dev/null +++ b/BMEdit/Editor/Source/Widgets/BitSetViewWidget.cpp @@ -0,0 +1,190 @@ +#include +#include + + +namespace widgets +{ + void clearLayout(QLayout* pLayout) // NOLINT(*-no-recursion) + { + if (!pLayout) + return; + + QLayoutItem* pItem = nullptr; + + while ((pItem = pLayout->takeAt(0))) + { + if (pItem->layout()) + { + clearLayout(pItem->layout()); + delete pItem->layout(); + } + + if (pItem->widget()) + { + delete pItem->widget(); + } + + delete pItem; + } + } + + BitSetViewWidget::BitSetViewWidget(QWidget *parent) : QWidget(parent) + { + m_pLayout.reset(new QVBoxLayout(this)); + setLayout(m_pLayout.get()); + } + + void BitSetViewWidget::setPossibleValues(const BitSetViewWidget::Bits &values) + { + m_allowedValues = values; + m_intRepr = 0; + m_bitNrToCheckBox.clear(); + +#ifdef QT_DEBUG + // static check + { + QSet knowUrName; + + for (const auto& [name, _] : values) + { + if (knowUrName.contains(name)) + { + Q_ASSERT_X(false, __FILE__, "KEY DUPLICATE!!!"); + return; + } + + // store + knowUrName.insert(name); + } + } +#endif + + // rebuild view + if (!m_allowedValues.empty()) + { + clearView(); + } + + buildView(); + // no need to call updateView here because no value - no view + } + + const BitSetViewWidget::Bits& BitSetViewWidget::getPossibleValues() const + { + return m_allowedValues; + } + + BitSetViewWidget::Bits BitSetViewWidget::getChecked() const + { + Bits r {}; + + for (const auto& [name, idx] : m_allowedValues) + { + if ((m_intRepr & (1 << idx)) != 0) + { + r.emplace_back(name, idx); + } + } + + return r; + } + + BitSetViewWidget::Bits BitSetViewWidget::getUnchecked() const + { + Bits r {}; + + for (const auto& [name, idx] : m_allowedValues) + { + if ((m_intRepr & (1 << idx)) == 0) + { + r.emplace_back(name, idx); + } + } + + return r; + } + + void BitSetViewWidget::setValue(uint32_t value) + { + if (value != m_intRepr) + { + m_intRepr = value; + updateView(); + + emit valueChanged(m_intRepr); + } + } + + void BitSetViewWidget::resetValue() + { + setValue(0u); + } + + uint32_t BitSetViewWidget::getValue() const + { + return m_intRepr; + } + + void BitSetViewWidget::reset() + { + m_allowedValues.clear(); + m_bitNrToCheckBox.clear(); + clearView(); + } + + void BitSetViewWidget::clearView() + { + clearLayout(m_pLayout.get()); + } + + void BitSetViewWidget::buildView() + { + for (const auto& [name, bitIdx] : m_allowedValues) + { + auto* pCheckBox = new QCheckBox(name, this); + m_bitNrToCheckBox.emplace_back(name, pCheckBox); + m_pLayout->addWidget(pCheckBox); + + connect(pCheckBox, &QCheckBox::toggled, [this, bitIdx](bool checked) { + const auto oldValue = m_intRepr; + if (checked) + { + m_intRepr |= (1 << bitIdx); + } + else + { + m_intRepr &= ~(1 << bitIdx); + } + + if (oldValue != m_intRepr) + { + emit valueChanged(m_intRepr); + } + }); + } + } + + void BitSetViewWidget::updateView() + { + int index = 0; + for (const auto& [name, pCurrentCheckBox] : m_bitNrToCheckBox) + { + if (!pCurrentCheckBox) + { + Q_ASSERT(pCurrentCheckBox != nullptr); + ++index; + continue; + } + + { + QSignalBlocker blocker { pCurrentCheckBox }; + const auto bitIdx = m_allowedValues[index].second; + + const bool bExpectedValue = static_cast((m_intRepr & (1 << bitIdx))); + pCurrentCheckBox->setChecked(bExpectedValue); + } + + ++index; + } + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Widgets/EditorToolFactory.cpp b/BMEdit/Editor/Source/Widgets/EditorToolFactory.cpp new file mode 100644 index 0000000..11afb0e --- /dev/null +++ b/BMEdit/Editor/Source/Widgets/EditorToolFactory.cpp @@ -0,0 +1,38 @@ +#include + +// Widgets +#include +#include +#include +#include + + +namespace widgets +{ + // Base factory + TypePropertyWidget *EditorToolFactory::createToolById(QWidget* parent, const std::string &hintId) + { + TypePropertyWidget* pResult = nullptr; + if (hintId == "SelectGeomTool") + { + pResult = SelectSceneObjectTool::Create(parent); + } + + if (hintId == "SelectGameScript") + { + pResult = SelectScriptTool::Create(parent); + } + + if (hintId == "SelectLocalizationKey") + { + pResult = SelectLocalizationTool::Create(parent); + } + + if (!pResult) + { + qWarning() << "For hint '" << QString::fromStdString(hintId) << "' no editor created. Check name please"; + } + + return pResult; + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Widgets/GeomControllersWidget.cpp b/BMEdit/Editor/Source/Widgets/GeomControllersWidget.cpp index 608329c..1ddef98 100644 --- a/BMEdit/Editor/Source/Widgets/GeomControllersWidget.cpp +++ b/BMEdit/Editor/Source/Widgets/GeomControllersWidget.cpp @@ -3,6 +3,11 @@ #include #include #include +#include +#include +#include +#include + using namespace widgets; @@ -35,25 +40,32 @@ void GeomControllersWidget::setGeom(gamelib::scene::SceneObject *sceneObject) if (sceneObject && sceneObject != m_sceneObject) { - if (!sceneObject->getControllers().empty()) { + // Filter allTypes QSignalBlocker blocker(m_ui->controllerSelector); // Load possible controllers m_geomControllersListModel->setGeom(sceneObject); - // Select controller - m_ui->controllerSelector->setEnabled(true); - } - else - { - m_geomControllersListModel->resetGeom(); - m_ui->controllerSelector->setEnabled(false); + // + if (!sceneObject->getControllers().empty()) + { + // Select controller + m_ui->controllerSelector->setEnabled(true); + m_ui->removeControllerButton->setEnabled(true); + } + else + { + m_ui->controllerSelector->setEnabled(false); + m_ui->removeControllerButton->setEnabled(false); + } } m_controllerPropertiesModel->setGeom(sceneObject); m_sceneObject = sceneObject; + updateAvailableControllersList(); + emit geomChanged(); } } @@ -62,6 +74,16 @@ void GeomControllersWidget::resetGeom() { switchToDefaults(); + // Discard geom + m_geomControllersListModel->resetGeom(); + m_controllerPropertiesModel->resetGeom(); + + // Discard current geom + m_sceneObject = nullptr; + + // Clear available controller type names + m_availableToAddControllersModel->setStringList({}); + emit geomReset(); } @@ -88,10 +110,7 @@ void GeomControllersWidget::setController(int controllerIndex) void GeomControllersWidget::switchToDefaults() { m_controllerPropertiesModel->resetValue(); - m_geomControllersListModel->resetGeom(); - m_sceneObject = nullptr; - m_ui->editControllerListButton->setEnabled(false); m_ui->controllerSelector->setEnabled(false); m_ui->controllerSelector->clear(); } @@ -115,7 +134,28 @@ void GeomControllersWidget::setup() m_ui->controllerProperties->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); m_ui->controllerProperties->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Interactive); - connect(m_ui->controllerSelector, &QComboBox::currentIndexChanged, [=](int newIndex) { + // new controllers menu + m_availableToAddControllersModel.reset(new QStringListModel(this)); + + m_availableToAddControllersProxyModel.reset(new QSortFilterProxyModel(this)); + m_availableToAddControllersProxyModel->setSourceModel(m_availableToAddControllersModel.get()); + + m_ui->allPossibleControllersList->setModel(m_availableToAddControllersProxyModel.get()); + + connect(m_ui->addSelectedControllerButton, &QPushButton::clicked, [=]() { + const auto selectedRows = m_ui->allPossibleControllersList->selectionModel()->selectedRows(); + if (selectedRows.isEmpty()) + return; + + addControllerToGeom(m_ui->allPossibleControllersList->model()->data(selectedRows[0], Qt::DisplayRole).value()); + }); + + connect(m_ui->searchEdit, &QLineEdit::textChanged, [this](const QString& newQuery) { + m_availableToAddControllersProxyModel->setFilterCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); + m_availableToAddControllersProxyModel->setFilterFixedString(newQuery); + }); + + connect(m_ui->controllerSelector, &QComboBox::currentIndexChanged, [this](int newIndex) { if (newIndex < 0) { switchToDefaults(); @@ -125,4 +165,104 @@ void GeomControllersWidget::setup() setController(newIndex); } }); + + connect(m_ui->removeControllerButton, &QPushButton::clicked, [this]() { + removeCurrentController(); + }); +} + +void GeomControllersWidget::updateAvailableControllersList() +{ + auto allTypes = getAllPossibleControllerNamesFromTypesDb(); + m_availableToAddControllersModel->setStringList(allTypes); +} + +void GeomControllersWidget::addControllerToGeom(const QString& controllerName) +{ + if (auto pType = gamelib::TypeRegistry::getInstance().findTypeByShortName(controllerName.toStdString())) + { + auto serializeControllerName = [](const std::string& controllerName) -> std::string + { + if (controllerName.empty()) return controllerName; + + std::string newName { controllerName }; + // See TypeRegistry::findTypeByShortName for details + if (newName[0] == 'Z' || newName[0] == 'C') + { + newName.erase(newName.begin(), newName.begin() + 1); + } + + if (newName.starts_with("HM3")) + { + newName.erase(newName.begin(), newName.begin() + 3); + } + + if (auto it = newName.find("Event"); it != std::string::npos) + { + newName.erase(it, 5); + } + + return newName; + }; + + const bool bAddedFirstController = m_sceneObject->getControllers().empty(); + + // Register a new controller + gamelib::scene::SceneObject::Controller& newController = m_sceneObject->getControllers().emplace_back(); + newController.name = serializeControllerName(pType->getName()); + newController.properties = pType->makeDefaultPropertiesPack(); + newController.type = pType; + + // Update UI + updateAvailableControllersList(); + + // Make it selectable + m_ui->controllerSelector->setEnabled(true); + m_ui->removeControllerButton->setEnabled(true); + + // Load possible controllers + m_geomControllersListModel->updateControllersList(); + + if (bAddedFirstController) + { + switchToFirstController(); + } + } +} + +void GeomControllersWidget::removeCurrentController() +{ + const int indexToRemove = m_ui->controllerSelector->currentIndex(); + if (indexToRemove < 0) + return; + + // Remove current controller + m_sceneObject->getControllers().erase(m_sceneObject->getControllers().begin() + indexToRemove); + + // update UI + m_ui->controllerSelector->setEnabled(!m_sceneObject->getControllers().empty()); + m_ui->removeControllerButton->setEnabled(!m_sceneObject->getControllers().empty()); + + // Discard current controller + m_controllerPropertiesModel->resetController(); + + // Remove controller from controllers list of current geom + m_geomControllersListModel->updateControllersList(); +} + +QStringList GeomControllersWidget::getAllPossibleControllerNamesFromTypesDb() +{ + QStringList result; + gamelib::TypeRegistry::getInstance().forEachType([&result](const gamelib::Type* pType) { + if (pType->getKind() == gamelib::TypeKind::COMPLEX) + { + const auto pAsComplex = static_cast(pType); + if (pAsComplex->isInheritedOf("ZEventBase")) + { + result.emplace_back(QString::fromStdString(pAsComplex->getName())); + } + } + }); + + return result; } \ No newline at end of file diff --git a/BMEdit/Editor/Source/Widgets/PrimitivePreviewWidget.cpp b/BMEdit/Editor/Source/Widgets/PrimitivePreviewWidget.cpp index 2964f11..6a13db4 100644 --- a/BMEdit/Editor/Source/Widgets/PrimitivePreviewWidget.cpp +++ b/BMEdit/Editor/Source/Widgets/PrimitivePreviewWidget.cpp @@ -75,15 +75,17 @@ void PrimitivePreviewWidget::resizeGL(int w, int h) void PrimitivePreviewWidget::doPreloadNewPrimitive() { - auto& chk = m_level->getLevelGeometry()->chunks.at(m_primitiveIndex); - if (chk.getKind() != gamelib::prm::PRMChunkRecognizedKind::CRK_DESCRIPTION_BUFFER) - { - // Invalid case: we've unable to draw model by non-descriptor index - m_primitiveIndex = 0u; - return; - } - - const auto descriptionHeader = chk.getDescriptionBufferHeader(); + m_primitiveIndex = 0u; // invalid case, do nothing + +// auto& chk = m_level->getLevelGeometry()->chunks.at(m_primitiveIndex); +// if (chk.getKind() != gamelib::prm::PRMChunkRecognizedKind::CRK_DESCRIPTION_BUFFER) +// { +// // Invalid case: we've unable to draw model by non-descriptor index +// m_primitiveIndex = 0u; +// return; +// } +// +// const auto descriptionHeader = chk.getDescriptionBufferHeader(); //TODO: Extract index buffer, extract vertex buffer, validate index buffer, validate vertex buffer. } diff --git a/BMEdit/Editor/Source/Widgets/SceneRenderWidget.cpp b/BMEdit/Editor/Source/Widgets/SceneRenderWidget.cpp new file mode 100644 index 0000000..dadb7a2 --- /dev/null +++ b/BMEdit/Editor/Source/Widgets/SceneRenderWidget.cpp @@ -0,0 +1,2311 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_WINDOWS +# define WIN32_LEAN_AND_MEAN +# define NOMINMAX +# include +# include +# include + +bool g_bShouldCaptureFrame = false; +RENDERDOC_API_1_1_2* g_pRenderDoc = nullptr; +#endif + +#ifdef QT_DEBUG +# include +#endif + + +namespace widgets +{ + static render::Shader* g_pLastKnownShader = nullptr; + + static bool canDrawGeom(const gamelib::scene::SceneObject* pObject) + { + using CM = gamelib::gms::ECollisionMask; + constexpr uint32_t kExpectedToSeeMask = CM::COLIMASK_Sight | CM::COLIMASK_Hero | CM::COLIMASK_NPC | CM::COLIMASK_Background; + + return pObject && (pObject->getGeomInfo().getColiBits() & kExpectedToSeeMask); + } + + struct RenderState + { + bool bHasBlend = false; + bool bHasAlphaTest = false; + bool bHasFog = false; + + gamelib::mat::MATBlendMode eBlendMode { gamelib::mat::MATBlendMode::BM_ADD }; + gamelib::mat::MATCullMode eCullMode { gamelib::mat::MATCullMode::CM_DontCare }; + + std::array aTextures { 0 }; + + void reset() + { + bHasFog = false; + bHasBlend = false; + bHasAlphaTest = false; + eBlendMode = gamelib::mat::MATBlendMode::BM_ADD; + eCullMode = gamelib::mat::MATCullMode::CM_DontCare; + + for (auto& tex : aTextures) + { + tex = std::numeric_limits::max(); + } + } + + void set(QOpenGLFunctions_3_3_Core* glFuncs, const gamelib::mat::MATRenderState& state); + void apply(QOpenGLFunctions_3_3_Core* glFuncs) const; + }; + + static RenderState g_RenderState{}; + + // Here stored geom names (common) where editor should avoid any rendering (it's too expensive and unnecessary for us) + static const std::set g_bannedObjectIds { + "AdditionalResources", "AllLevels/mainsceneincludes.zip", "AllLevels/equipment.zip" + }; + + bool RayCastObjectDescription::operator<(const widgets::RayCastObjectDescription& another) const + { + if (another.ePrio == ePrio) + { + if (std::fabsf(another.fRayOriginDistance - fRayOriginDistance) <= std::numeric_limits::epsilon()) + { + return false; // They are completely same (except object itself, but who cares?) + } + + return fRayOriginDistance < another.fRayOriginDistance; + } + + return static_cast(ePrio) < static_cast(another.ePrio); + } + + using namespace render; + + struct SceneRenderWidget::GLResources + { + std::vector m_textures {}; + std::vector m_shaders {}; + std::vector m_models {}; + std::unordered_map m_modelsCache {}; /// primitive index to model index in m_models + std::unordered_map m_modelTransformCache {}; /// transformations cache + std::unordered_map m_textureNameToGL {}; /// name of texture to it's OpenGL resource id + std::unordered_map m_textureIndexToGL {}; /// index of texture to it's OpenGL resource id + std::unordered_set m_invalidatedTextures; /// Set of textures who need to be reloaded on next frame + GLuint m_iGLDebugTexture { 0 }; + GLuint m_iGLMissingTexture { 0 }; + GLuint m_iGLUnsupportedMaterialTexture { 0 }; + size_t m_iTexturedShaderIdx = 0; + size_t m_iGizmoShaderIdx = 0; + + GLResources() {} + ~GLResources() {} + + void discard(QOpenGLFunctions_3_3_Core* gapi) + { + // Destroy textures + { + for (auto& texture : m_textures) + { + texture.discard(gapi); + } + + m_textures.clear(); + } + + // Destroy shaders + { + for (auto& shader : m_shaders) + { + shader.discard(gapi); + } + + m_shaders.clear(); + } + + // Destroy models + { + for (auto& model : m_models) + { + model.discard(gapi); + } + + m_models.clear(); + } + + // Empty cache + m_modelsCache.clear(); + m_modelTransformCache.clear(); + m_textureNameToGL.clear(); + m_textureIndexToGL.clear(); + m_invalidatedTextures.clear(); + + // Release refs + m_iGLDebugTexture = 0u; + m_iGLMissingTexture = 0u; + m_iGLUnsupportedMaterialTexture = 0u; + m_iTexturedShaderIdx = 0u; + m_iGizmoShaderIdx = 0u; + } + + [[nodiscard]] bool hasResources() const + { + return !m_textures.empty() || !m_shaders.empty() || !m_models.empty(); + } + }; + + SceneRenderWidget::SceneRenderWidget(QWidget *parent, Qt::WindowFlags f) : QOpenGLWidget(parent, f) + { + QSurfaceFormat format; + format.setDepthBufferSize(24); + format.setStencilBufferSize(8); + format.setVersion(3, 3); + format.setProfile(QSurfaceFormat::CoreProfile); + setFormat(format); + +#ifdef Q_OS_WINDOWS + if (HMODULE pRenderDocMod = GetModuleHandleA("renderdoc.dll")) + { + auto RENDERDOC_GetAPI = (pRENDERDOC_GetAPI)GetProcAddress(pRenderDocMod, "RENDERDOC_GetAPI"); + int ret = RENDERDOC_GetAPI(eRENDERDOC_API_Version_1_1_2, (void **)&g_pRenderDoc); + if (ret != 1) + { + g_pRenderDoc = nullptr; + } + } +#endif + } + + SceneRenderWidget::~SceneRenderWidget() noexcept = default; + + void SceneRenderWidget::initializeGL() + { +#ifdef Q_OS_WINDOWS + if (g_pRenderDoc) + { + g_pRenderDoc->SetActiveWindow( + context()->nativeInterface()->nativeContext(), + (void*)winId() + ); + } +#endif + } + + void SceneRenderWidget::paintGL() + { + RenderStats renderStats {}; + + auto renderStartTime = std::chrono::high_resolution_clock::now(); + + auto funcs = QOpenGLVersionFunctionsFactory::get(QOpenGLContext::currentContext()); + if (!funcs) { + qFatal("Could not obtain required OpenGL context version"); + return; + } + +#ifdef Q_OS_WINDOWS + const bool bCaptureStarted = g_bShouldCaptureFrame; + g_bShouldCaptureFrame = false; + if (g_pRenderDoc && bCaptureStarted) g_pRenderDoc->StartFrameCapture(nullptr, nullptr); +#endif + + // Begin frame + const auto vp = getViewportSize(); + funcs->glViewport(0, 0, vp.x, vp.y); + funcs->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + funcs->glClearColor(0.15f, 0.2f, 0.45f, 1.0f); + + // Z-Buffer testing + funcs->glEnable(GL_DEPTH_TEST); + + // Blending + funcs->glEnable(GL_BLEND); + funcs->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // NOTE: Before render anything we need to look at material and check MATRenderState. + // If it's applied we need to setup OpenGL into correct state to make perfect rendering + switch (m_eState) + { + case ELevelState::LS_NONE: + { + if (m_pLevel) { + // Create base for resources + assert(m_resources == nullptr && "Leaked resources"); + m_resources = std::make_unique(); + + // Run process + m_eState = ELevelState::LS_LOAD_TEXTURES; + } else if (m_resources && m_resources->hasResources()) { + m_resources->discard(funcs); + } + } + break; + case ELevelState::LS_LOAD_TEXTURES: + { + doLoadTextures(funcs); + } + break; + case ELevelState::LS_LOAD_GEOMETRY: + { + doLoadGeometry(funcs); + } + break; + case ELevelState::LS_COMPILE_SHADERS: + { + doCompileShaders(funcs); + } + break; + case ELevelState::LS_RESET_CAMERA_STATE: + { + doResetCameraState(funcs); + } + break; + case ELevelState::LS_READY: + { + // Prepare invalidated stuff + doPrepareInvalidatedResources(funcs); + + // Reset render state + g_RenderState.reset(); + g_RenderState.apply(funcs); + + // Render scene + gamelib::scene::SceneObject* pRoot = nullptr; + bool bIgnoreVisibility = false; + + if (m_eViewMode == EViewMode::VM_WORLD_VIEW) + { + pRoot = m_pLevel->getSceneObjects()[0].get(); + } + else if (m_eViewMode == EViewMode::VM_GEOM_PREVIEW) + { + if (m_pSceneObjectToView) + { + bIgnoreVisibility = true; + pRoot = m_pSceneObjectToView; + } + } + + if (!pRoot) break; + + if (m_renderList.empty()) + { + collectRenderList(m_camera, pRoot, m_renderList, renderStats, bIgnoreVisibility); + } + + if (!m_renderList.empty()) + { + auto onlyNonAlpha = [](const render::RenderEntry& entry) -> bool { return !entry.material.renderState.isAlphaTestEnabled() && !entry.material.renderState.isBlendEnabled(); }; + auto onlyAlpha = [](const render::RenderEntry& entry) -> bool { return entry.material.renderState.isAlphaTestEnabled() || entry.material.renderState.isBlendEnabled(); }; + + // 2 pass rendering: first render only non-alpha objects + if (m_renderMode & RenderMode::RM_NON_ALPHA_OBJECTS) + { + beginDebugGroup("NON_ALPHA_OBJECTS"); + performRender(funcs, m_renderList, m_camera, onlyNonAlpha); + endDebugGroup(); + } + + // then render only alpha objects + if (m_renderMode & RenderMode::RM_ALPHA_OBJECTS) + { + beginDebugGroup("ALPHA_OBJECTS"); + performRender(funcs, m_renderList, m_camera, onlyAlpha); + endDebugGroup(); + } + + // Submit stats + if (!m_renderList.empty()) + { + auto renderEndTime = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed = renderEndTime - renderStartTime; + renderStats.fFrameTime = elapsed.count(); + emit frameReady(renderStats); + } + } + } + break; + } + +#ifdef Q_OS_WINDOWS + if (g_pRenderDoc && bCaptureStarted) g_pRenderDoc->EndFrameCapture(nullptr, nullptr); +#endif + + if (g_pLastKnownShader) g_pLastKnownShader->unbind(funcs); + g_pLastKnownShader = nullptr; + } + + void SceneRenderWidget::resizeGL(int w, int h) + { + Q_UNUSED(w) + Q_UNUSED(h) + + // Update projection + m_camera.setViewport(w, h); + + // Because our list of visible objects could be changed here (???) + invalidateRenderList(); + } + + void SceneRenderWidget::keyPressEvent(QKeyEvent* event) + { +#ifdef Q_OS_WINDOWS + if (event->key() == Qt::Key_F8) + { + g_bShouldCaptureFrame = true; + } +#endif + + if (m_pLevel) + { + render::CameraMovementMask movementMask {}; + + if (event->key() == Qt::Key_W) + { + movementMask |= render::CameraMovementMaskValues::CM_FORWARD; + } + + if (event->key() == Qt::Key_S) + { + movementMask |= render::CameraMovementMaskValues::CM_BACKWARD; + } + + if (event->key() == Qt::Key_A) + { + movementMask |= render::CameraMovementMaskValues::CM_LEFT; + } + + if (event->key() == Qt::Key_D) + { + movementMask |= render::CameraMovementMaskValues::CM_RIGHT; + } + + if (event->modifiers().testFlag(Qt::KeyboardModifier::ShiftModifier)) + { + movementMask |= render::CameraMovementMaskValues::CM_SPEEDUP_MOD; + } + + if ((movementMask & CM_FORWARD) && (movementMask & CM_BACKWARD)) movementMask &= ~(CM_FORWARD | CM_BACKWARD); + if ((movementMask & CM_LEFT) && (movementMask & CM_RIGHT)) movementMask &= ~(CM_LEFT | CM_RIGHT); + + if (movementMask > 0 && movementMask != (CM_SPEEDUP_MOD)) + { + m_camera.handleKeyboardMovement(movementMask /* dt */); + + invalidateRenderList(); + repaint(); + } + } + + QOpenGLWidget::keyPressEvent(event); + } + + void SceneRenderWidget::mouseDoubleClickEvent(QMouseEvent* event) + { + QOpenGLWidget::mouseDoubleClickEvent(event); + } + + void SceneRenderWidget::mouseMoveEvent(QMouseEvent* event) + { + if (m_pLevel) + { + float xpos = static_cast(event->pos().x()); + float ypos = static_cast(event->pos().y()); + + if (m_bFirstMouseQuery) + { + m_mouseLastPosition = event->pos(); + m_bFirstMouseQuery = false; + return; + } + + float xOffset = static_cast(xpos - m_mouseLastPosition.x()); + float yOffset = static_cast(m_mouseLastPosition.y() - ypos); + + m_mouseLastPosition = event->pos(); + + // Update camera + const float kMinMovement = 0.001f; + if (std::fabsf(xOffset - kMinMovement) > std::numeric_limits::epsilon() || std::fabsf(yOffset - kMinMovement) > std::numeric_limits::epsilon()) + { + invalidateRenderList(); + m_camera.processMouseMovement(xOffset, yOffset /* dt */); + } + } + + repaint(); + QOpenGLWidget::mouseMoveEvent(event); + } + + void SceneRenderWidget::mousePressEvent(QMouseEvent* event) + { + QOpenGLWidget::mousePressEvent(event); + + if (event->button() == Qt::MouseButton::RightButton && !m_pLevel->getSceneObjects().empty()) + { + // Begin ray cast + const auto vMouseClickPos = event->position(); + + // If no rooms on level we should use ROOT as initial point (not recommended in MOST cases) + // Two step raycast: 1 - to room bbox (allow to run ray from room) + // 2 - to in-room objects + +// auto result = performRayCastToScene(vMouseClickPos, (!m_pLastRoom && m_rooms.empty()) ? m_pLevel->getSceneObjects()[0] : nullptr); +// if (!result.empty()) +// { +// emit worldSelectionChanged(result); +// } + } + } + + void SceneRenderWidget::mouseReleaseEvent(QMouseEvent* event) + { + QOpenGLWidget::mouseReleaseEvent(event); + + m_bFirstMouseQuery = true; + m_mouseLastPosition = QPoint(0, 0); + } + + void SceneRenderWidget::setLevel(gamelib::Level *pLevel) + { + if (m_pLevel != pLevel) + { + m_resources = nullptr; + m_eState = ELevelState::LS_NONE; + m_pLevel = pLevel; + m_bFirstMouseQuery = true; + invalidateRenderList(); + resetViewMode(); + resetRenderMode(); + } + } + + void SceneRenderWidget::resetLevel() + { + // reset GPU caches & other optimisations + g_pLastKnownShader = nullptr; + g_RenderState.reset(); + + // drop resources + if (m_pLevel != nullptr) + { + m_resources = nullptr; + m_eState = ELevelState::LS_NONE; + m_pLevel = nullptr; + m_bFirstMouseQuery = true; + invalidateRenderList(); + resetViewMode(); + resetRenderMode(); + repaint(); + } + } + + void SceneRenderWidget::setGeomViewMode(gamelib::scene::SceneObject* sceneObject) + { + assert(sceneObject != nullptr); + + if (sceneObject != m_pSceneObjectToView) + { + m_eViewMode = EViewMode::VM_GEOM_PREVIEW; + m_pSceneObjectToView = sceneObject; + invalidateRenderList(); + repaint(); + } + } + + void SceneRenderWidget::setWorldViewMode() + { + m_eViewMode = EViewMode::VM_WORLD_VIEW; + m_pSceneObjectToView = nullptr; + invalidateRenderList(); + repaint(); + } + + void SceneRenderWidget::resetViewMode() + { + setWorldViewMode(); + } + + void SceneRenderWidget::setSelectedObject(gamelib::scene::SceneObject* sceneObject) + { + if (!m_pLevel) + return; + + auto flags = sceneObject->getGeomInfo().getGeomFlags(); + bool isBit4Set = flags & (1 << 4); + + if (m_pSelectedSceneObject != sceneObject && sceneObject != nullptr) + { + m_pSelectedSceneObject = sceneObject; + invalidateRenderList(); + repaint(); + } + } + + void SceneRenderWidget::resetSelectedObject() + { + if (m_pSelectedSceneObject != nullptr) + { + m_pSelectedSceneObject = nullptr; + + if (m_pLevel) + { + invalidateRenderList(); + repaint(); + } + } + } + + RenderModeFlags SceneRenderWidget::getRenderMode() const + { + return m_renderMode; + } + + void SceneRenderWidget::setRenderMode(RenderModeFlags renderMode) + { + m_renderMode = renderMode; + repaint(); + } + + void SceneRenderWidget::resetRenderMode() + { + m_renderMode = RenderMode::RM_DEFAULT; + repaint(); + } + + void SceneRenderWidget::moveCameraTo(const glm::vec3& position) + { + if (!m_pLevel) + return; + + m_camera.setPosition(position); + repaint(); + } + + void SceneRenderWidget::reloadTexture(uint32_t textureIndex) + { + if (!m_pLevel) + return; + + m_resources->m_invalidatedTextures.insert(textureIndex); + } + + bool SceneRenderWidget::shouldRenderPortals() const + { + return m_bRenderPortals; + } + + void SceneRenderWidget::setShouldRenderPortals(bool bVal) + { + if (m_bRenderPortals != bVal) + { + m_bRenderPortals = bVal; + repaint(); + } + } + + bool SceneRenderWidget::shouldRenderRoomBoundingBox() const + { + return m_bRenderRoomBoundingBox; + } + + void SceneRenderWidget::setShouldRenderRoomBoundingBox(bool bVal) + { + if (m_bRenderRoomBoundingBox != bVal) + { + m_bRenderRoomBoundingBox = bVal; + repaint(); + } + } + + int32_t SceneRenderWidget::getGameObjectPrimitiveId(const gamelib::scene::SceneObject::Ptr& pObject) const + { + if (!pObject) return 0; + + return getGameObjectPrimitiveId(pObject.get()); + } + + int32_t SceneRenderWidget::getGameObjectPrimitiveId(const gamelib::scene::SceneObject* pObject) const + { + if (!pObject) return 0; + + if (pObject->isInheritedOf("ZItem")) // Need support of item ammo & item container here + { + //ZItems has no PrimId. Instead of this they are refs to another geom by path + auto rItemTemplatePath = pObject->getProperties().getObject("rItemTemplate"); + const auto pItemTemplate = m_pLevel->getSceneObjectByGEOMREF(rItemTemplatePath); + + if (pItemTemplate) + { + gamelib::scene::SceneObject::Ptr pItem = nullptr; + + // Item found by path. That's cool! But this is not an item, for item need to ask Ground... object inside + for (const auto& childRef : pItemTemplate->getChildren()) + { + if (auto child = childRef.lock(); child && child->getName().starts_with("Ground")) + { + pItem = child; + break; + } + } + + if (pItem) + { + // Nice! Now we ready to replace primId + return pItem->getProperties().getObject("PrimId"); + } + } + } + + return pObject->getProperties().getObject("PrimId", 0); + } + + glm::mat4 SceneRenderWidget::getGameObjectTransform(const gamelib::scene::SceneObject::Ptr& pObject) const + { + if (!pObject) return glm::mat4(1.f); + return getGameObjectTransform(pObject.get()); + } + + glm::mat4 SceneRenderWidget::getGameObjectTransform(const gamelib::scene::SceneObject* pObject) const + { + if (auto it = m_resources->m_modelTransformCache.find(const_cast(pObject)); it != m_resources->m_modelTransformCache.end()) + { + return it->second; + } + else + { + glm::mat4 mWorldTransform = pObject->getWorldTransform(); + m_resources->m_modelTransformCache[const_cast(pObject)] = mWorldTransform; + return mWorldTransform; + } + + // idk) + return glm::mat4(1.f); + } + + std::optional SceneRenderWidget::getGameObjectBoundingBox(const gamelib::scene::SceneObject::Ptr& pObject, bool bWorldTransform) const + { + if (!pObject) return std::nullopt; + return getGameObjectBoundingBox(pObject.get(), bWorldTransform); + } + + std::optional SceneRenderWidget::getGameObjectBoundingBox(const gamelib::scene::SceneObject* pObject, bool bWorldTransform) const + { + if (!pObject) return std::nullopt; + + auto primId = getGameObjectPrimitiveId(pObject); + if (primId == 0) + { + return std::nullopt; + } + + const Model& model = m_resources->m_models[m_resources->m_modelsCache[primId]]; + if (bWorldTransform) + { + glm::mat4 mWorldTransform = getGameObjectTransform(pObject); + return gamelib::BoundingBox::toWorld(model.boundingBox, mWorldTransform); + } + + return model.boundingBox; + } + + std::vector SceneRenderWidget::performRayCastToScene(const QPointF& screenSpace, const gamelib::scene::SceneObject::Ptr& pStartObject) const + { + render::Ray sRay = m_camera.getRayFromScreen(static_cast(screenSpace.x()), + static_cast(screenSpace.y())); + std::vector collectedObjects {}; + + gamelib::scene::SceneObject* pRoot = pStartObject.get(); + + // Need to find intersects with this thing. Need to visit only current room + if (pRoot) + { + using R = gamelib::scene::SceneObject::EVisitResult; + + static EObjectPriority s_CurrentPrio = EObjectPriority::EP_STATIC_OBJECT; + + auto hitObjVisitor = [&sRay, &collectedObjects, this](const gamelib::scene::SceneObject::Ptr& pObject) -> R { + if (auto bbox = getGameObjectBoundingBox(pObject); bbox.has_value()) + { + // Need to exclude objects where bbox origin is inside + if (sRay.intersect(bbox.value(), false)) + { + auto& obj = collectedObjects.emplace_back(); + obj.ePrio = s_CurrentPrio; + obj.pObject = pObject; + obj.fRayOriginDistance = glm::distance(sRay.vOrigin, pObject->getPosition()); + return R::VR_NEXT; + } + } + + return R::VR_CONTINUE; + }; + + // Hit dynamic (not implemented yet) + s_CurrentPrio = EObjectPriority::EP_DYNAMIC_OBJECT; // Now dynamic objects + // TODO: Iterate over dynamic objects and check collision with them + + // Hit static + s_CurrentPrio = EObjectPriority::EP_STATIC_OBJECT; // Now static objects + pRoot->visitChildren(hitObjVisitor); + + // Sort hit list by distance to camera + std::sort(collectedObjects.begin(), collectedObjects.end()); + + return collectedObjects; + } + + return collectedObjects; + } + + void SceneRenderWidget::onRedrawRequested() + { + if (m_pLevel) + repaint(); + } + + void SceneRenderWidget::onObjectMoved(gamelib::scene::SceneObject* sceneObject) + { + if (!sceneObject || !m_pLevel || !m_resources) + return; + + + // Invalidate self + m_resources->m_modelTransformCache[sceneObject] = sceneObject->getWorldTransform(); + + // Visit limited subtree + int iDepth = 2; // max 2 objects, otherwise it's better to make full invalidation (in case when user wants to move some huge object) + sceneObject->visitChildren([this, &iDepth](const gamelib::scene::SceneObject::Ptr& pObject) -> gamelib::scene::SceneObject::EVisitResult { + // Update transform + m_resources->m_modelTransformCache[pObject.get()] = pObject->getWorldTransform(); + + --iDepth; + return iDepth > 0 ? gamelib::scene::SceneObject::EVisitResult::VR_CONTINUE // Go deeper + : gamelib::scene::SceneObject::EVisitResult::VR_STOP_ALL; // Out of limit + }); + + invalidateRenderList(); //TODO: Need invalidate only object, not whole list! + repaint(); + } + +#define LEVEL_SAFE_CHECK() \ + if (!m_pLevel) \ + { \ + m_eState = ELevelState::LS_NONE; \ + if (m_resources) \ + { \ + m_resources->discard(glFunctions); \ + } \ + return; \ + } + + void SceneRenderWidget::doLoadTextures(QOpenGLFunctions_3_3_Core* glFunctions) + { + LEVEL_SAFE_CHECK() + + // Do it at once + // TODO: Optimize and load "chunk by chunk" + for (const auto& texture : m_pLevel->getSceneTextures()->entries) + { + // TODO: Support mip levels here? + if (texture.m_mipLevels.empty()) + { + // create null texture + m_resources->m_textures.emplace_back(); + qWarning() << "Failed to load texture #" << texture.m_index << ". Reason: no mip levels (empty texture)"; + continue; + } + + // Ok, texture is ok - load it + Texture newTexture {}; + + if (!newTexture.setup(glFunctions, texture)) + { + m_resources->m_textures.emplace_back(); + qWarning() << "Failed to load texture #" << texture.m_index << ". Reason: setup failed"; + continue; + } + + // Precache debug texture if it's not precached yet + static constexpr const char* kGlacierMissingTex = "_Glacier/Missing_01"; + static constexpr const char* kWorldColiTex = "_TEST/Worldcoli"; + + if (m_resources->m_iGLDebugTexture == 0 && texture.m_fileName.has_value() && (texture.m_fileName.value() == kGlacierMissingTex || texture.m_fileName.value() == kWorldColiTex)) + { + m_resources->m_iGLDebugTexture = newTexture.texture; + } + + // Update cache + if (newTexture.texPath.has_value()) + { + m_resources->m_textureNameToGL[newTexture.texPath.value()] = newTexture.texture; + } + + if (newTexture.index.has_value()) + { + m_resources->m_textureIndexToGL[newTexture.index.value()] = newTexture.texture; + } + + // Save texture + m_resources->m_textures.emplace_back(newTexture); + } + + // And load extra textures (render specific) + auto uploadQImageToGPU = [](QOpenGLFunctions_3_3_Core* gapi, const QImage& image) -> GLuint + { + GLuint textureId; + gapi->glGenTextures(1, &textureId); + gapi->glBindTexture(GL_TEXTURE_2D, textureId); + gapi->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image.constBits()); + + gapi->glGenerateMipmap(GL_TEXTURE_2D); + gapi->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + gapi->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + gapi->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + gapi->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + gapi->glBindTexture(GL_TEXTURE_2D, 0); + + return textureId; + }; + + { + QImage missingTextureImage = QImage(":/bmedit/mtl_missing_texture.png").convertToFormat(QImage::Format_RGBA8888, Qt::AutoColor); + + auto& missingTexture = m_resources->m_textures.emplace_back(); + missingTexture.texture = uploadQImageToGPU(glFunctions, missingTextureImage); + missingTexture.width = missingTextureImage.width(); + missingTexture.height = missingTextureImage.height(); + + m_resources->m_iGLMissingTexture = missingTexture.texture; + } + + { + QImage unsupportedMaterialTextureImage = QImage(":/bmedit/mtl_unsupported.png").convertToFormat(QImage::Format_RGBA8888, Qt::AutoColor); + + auto& unsupportedMaterial = m_resources->m_textures.emplace_back(); + unsupportedMaterial.texture = uploadQImageToGPU(glFunctions, unsupportedMaterialTextureImage); + unsupportedMaterial.width = unsupportedMaterialTextureImage.width(); + unsupportedMaterial.height = unsupportedMaterialTextureImage.height(); + + m_resources->m_iGLUnsupportedMaterialTexture = unsupportedMaterial.texture; + } + + // It's done + qDebug() << "All textures (" << m_pLevel->getSceneTextures()->entries.size() << ") are loaded and ready to be used"; + m_eState = ELevelState::LS_LOAD_GEOMETRY; + repaint(); // call to force jump into next state + } + + void SceneRenderWidget::doLoadGeometry(QOpenGLFunctions_3_3_Core* glFunctions) + { + LEVEL_SAFE_CHECK() + + // TODO: Optimize and load "chunk by chunk" + for (const auto& model : m_pLevel->getLevelGeometry()->primitives.models) + { + if (model.meshes.empty()) + { + // create null model + m_resources->m_models.emplace_back(); + qWarning() << "Failed to load model of chunk " << model.chunk << ". Reason: no meshes (empty model)"; + continue; + } + + Model& glModel = m_resources->m_models.emplace_back(); + glModel.chunkId = model.chunk; + glModel.boundingBox = gamelib::BoundingBox(model.boundingBox.vMin, model.boundingBox.vMax); + + // And create mesh for bounding box + glModel.setupBoundingBox(glFunctions); + + // Store cache + m_resources->m_modelsCache[model.chunk] = m_resources->m_models.size() - 1; + + // Lookup mesh + int meshIdx = 0; + for (const auto& mesh : model.meshes) + { + if (mesh.vertices.empty()) + { + // create empty mesh + glModel.meshes.emplace_back(); + qWarning() << "Failed to load mesh #" << meshIdx << " of model at chunk " << model.chunk << ". Reason: no meshes (empty model)"; + ++meshIdx; + continue; + } + + // Convert vertices & indices to single memory chunk + std::vector vertices; + std::vector indices; + + vertices.resize(mesh.vertices.size()); + indices.reserve(mesh.indices.size() * 3); // each 'index' subject contains three values + + for (int i = 0; i < mesh.vertices.size(); i++) + { + vertices[i].vPos = mesh.vertices[i]; + + if (mesh.uvs.empty()) + { + vertices[i].vUV = glm::vec2(.0f); // TODO: Idk what I should do here... + } + else + { + vertices[i].vUV = mesh.uvs[i]; + } + } + + for (const auto& [a,b,c] : mesh.indices) + { + indices.emplace_back(a); + indices.emplace_back(b); + indices.emplace_back(c); + } + + // And upload it to GPU + Mesh& glMesh = glModel.meshes.emplace_back(); + glMesh.trianglesCount = mesh.trianglesCount; + glMesh.variationId = mesh.variationId; + + if (!glMesh.setup(glFunctions, GlacierVertex::g_FormatDescription, vertices, indices, false)) + { + qWarning() << "Failed to upload mesh #" << meshIdx << " of model at chunk " << model.chunk << ". Reason: failed to upload resource to GPU!"; + ++meshIdx; + continue; + } + + // Precache color texture + glMesh.materialId = mesh.material_id; + + if (glMesh.materialId > 0) + { + // Use material (for meshes) + // First of all we need to know that 'shadows' and other things must be filtered here + const auto& instances = m_pLevel->getLevelMaterials()->materialInstances; + const auto& classes = m_pLevel->getLevelMaterials()->materialClasses; + const auto& matInstance = instances[mesh.material_id - 1]; + + if (const auto& parentName = matInstance.getParentName(); parentName == "StaticShadow" || parentName == "StaticShadowTextureShadow" || matInstance.getName().find("AlwaysInShadow") != std::string::npos) + { + // Shadows - do not use texturing (and don't show for now) + glMesh.glTextureId = kInvalidResource; + } + else if (parentName == "Bad") + { + // Use 'bad' debug texture + glMesh.glTextureId = m_resources->m_iGLUnsupportedMaterialTexture; + } + else + { + bool bTextureFound = false; + + // Here we need to find 'color' texture. In most cases we able to use matDiffuse as color texture + for (const auto& binder : matInstance.getBinders()) + { + if (bTextureFound) + break; + + for (const auto& texture : binder.textures) + { + if (texture.getName() == "mapDiffuse" && (texture.getTextureId() != 0 || !texture.getTexturePath().empty())) + { + // And find texture in textures pool + for (const auto& textureResource : m_resources->m_textures) + { + switch (texture.getPresentedTextureSources()) + { + case gamelib::mat::PresentedTextureSource::PTS_NOTHING: + continue; // Nothing + + case gamelib::mat::PresentedTextureSource::PTS_TEXTURE_ID: + { + // Only texture id + if (textureResource.index.has_value() && textureResource.index.value() == texture.getTextureId()) + { + // Good + glMesh.glTextureId = textureResource.texture; + bTextureFound = true; + break; + } + } + break; + case gamelib::mat::PresentedTextureSource::PTS_TEXTURE_PATH: + { + // Only path + if (textureResource.texPath.has_value() && textureResource.texPath.value() == texture.getTexturePath()) + { + // Good + glMesh.glTextureId = textureResource.texture; + bTextureFound = true; + break; + } + } + break; + default: + { + // Bad case! Undefined behaviour! + assert(false && "Impossible case!"); + continue; + } + } + } + + if (!bTextureFound) + { + // Use error texture + glMesh.glTextureId = m_resources->m_iGLMissingTexture; + } + + // But mark us as 'found' + bTextureFound = true; + + // Done + break; + } + } + } + + // For debug only +// if (glMesh.glTextureId == kInvalidResource) +// { +// glMesh.glTextureId = m_resources->m_iGLMissingTexture; +// } + } + } + else if (mesh.textureId > 0) + { + // Use texture here (for sprites). Need to find that texture in loaded textures list + for (const auto& texture : m_resources->m_textures) + { + if (texture.index.has_value() && texture.index.value() == mesh.textureId) + { + glMesh.glTextureId = texture.texture; + break; + } + } + } + // Otherwise no texture. So, we will render only bounding box (if it needed) + + // Next mesh + ++meshIdx; + } + } + + // Then load rooms cache + buildRoomCache(glFunctions); + + qDebug() << "All models (" << m_pLevel->getLevelGeometry()->primitives.models.size() << ") are loaded & ready to use!"; + m_eState = ELevelState::LS_COMPILE_SHADERS; + repaint(); // call to force jump into next state + } + + void SceneRenderWidget::doCompileShaders(QOpenGLFunctions_3_3_Core* glFunctions) + { + LEVEL_SAFE_CHECK() + + // Load shaders from resources + QFile coloredEntityVertexShader(":/bmedit/mtl_colored_gl33.vsh"); + QFile coloredEntityFragmentShader(":/bmedit/mtl_colored_gl33.fsh"); + QFile texturedEntityVertexShader(":/bmedit/mtl_textured_gl33.vsh"); + QFile texturedEntityFragmentShader(":/bmedit/mtl_textured_gl33.fsh"); + + coloredEntityVertexShader.open(QIODevice::ReadOnly); + coloredEntityFragmentShader.open(QIODevice::ReadOnly); + texturedEntityVertexShader.open(QIODevice::ReadOnly); + texturedEntityFragmentShader.open(QIODevice::ReadOnly); + + const std::string texturedEntityVertexShaderSource = texturedEntityVertexShader.readAll().toStdString(); + const std::string texturedEntityFragmentShaderSource = texturedEntityFragmentShader.readAll().toStdString(); + const std::string coloredEntityVertexShaderSource = coloredEntityVertexShader.readAll().toStdString(); + const std::string coloredEntityFragmentShaderSource = coloredEntityFragmentShader.readAll().toStdString(); + + if (texturedEntityVertexShaderSource.empty()) + { + emit resourceLoadFailed(QString("Failed to compile shaders (textured:vertex): no embedded asset found.")); + return; + } + + if (texturedEntityFragmentShaderSource.empty()) + { + emit resourceLoadFailed(QString("Failed to compile shaders (textured:fragment): no embedded asset found.")); + return; + } + + if (coloredEntityVertexShaderSource.empty()) + { + emit resourceLoadFailed(QString("Failed to compile shaders (colored:vertex): no embedded asset found.")); + return; + } + + if (coloredEntityFragmentShaderSource.empty()) + { + emit resourceLoadFailed(QString("Failed to compile shaders (colored:fragment): no embedded asset found.")); + return; + } + + // Compile shaders + std::string compileError; + { + Shader texturedShader; + + if (!texturedShader.compile(glFunctions, texturedEntityVertexShaderSource, texturedEntityFragmentShaderSource, compileError)) + { + m_pLevel = nullptr; + m_eState = ELevelState::LS_NONE; + + emit resourceLoadFailed(QString("Failed to compile shaders (textured): %1").arg(QString::fromStdString(compileError))); + return; + } + + m_resources->m_shaders.emplace_back(texturedShader); + m_resources->m_iTexturedShaderIdx = m_resources->m_shaders.size() - 1; + } + + { + Shader gizmoShader; + if (!gizmoShader.compile(glFunctions, coloredEntityVertexShaderSource, coloredEntityFragmentShaderSource, compileError)) + { + m_pLevel = nullptr; + m_eState = ELevelState::LS_NONE; + + emit resourceLoadFailed(QString("Failed to compile shaders (colored): %1").arg(QString::fromStdString(compileError))); + return; + } + + m_resources->m_shaders.emplace_back(gizmoShader); + m_resources->m_iGizmoShaderIdx = m_resources->m_shaders.size() - 1; + } + + qDebug() << "Shaders (" << m_resources->m_shaders.size() << ") compiled and ready to use!"; + m_eState = ELevelState::LS_RESET_CAMERA_STATE; + repaint(); // call to force jump into next state + } + + void SceneRenderWidget::doResetCameraState(QOpenGLFunctions_3_3_Core* glFunctions) + { + LEVEL_SAFE_CHECK() + + // ---------------------------------------------------------- + // Ok, first of all let's try to find where located ZPlayer of ZHitman3 object + gamelib::scene::SceneObject::Ptr player = nullptr; + + m_pLevel->forEachObjectOfType("ZHitman3", [&player](const gamelib::scene::SceneObject::Ptr& sceneObject) -> bool { + player = sceneObject; + return true; + }); + + if (player) + { + // Ok, level contains player. Let's take his room and move camera to player + const auto iPrimId = getGameObjectPrimitiveId(player); + const auto vPlayerPosition = player->getParent().lock()->getPosition(); + glm::vec3 vCameraPosition = vPlayerPosition; + + // In theory, we need to put camera around player, not in player. So we need to have bounding box of player to correct camera position + if (iPrimId != 0 && m_resources->m_modelsCache.contains(iPrimId)) + { + const auto& sBoundingBox = m_resources->m_models[m_resources->m_modelsCache[iPrimId]].boundingBox; + glm::vec3 vCenter = sBoundingBox.getCenter(); + vCenter.y += 1.5f * vCenter.y; + + vCameraPosition += vCenter; + } + + m_camera.setPosition(vCameraPosition); + qDebug() << "Move camera to object " << QString::fromStdString(player->getName()) << " at (" << vCameraPosition.x << ';' << vCameraPosition.y << ';' << vCameraPosition.z << ")"; + } + else + { + // Bad for us, player not found. Need to put camera somewhere else + qDebug() << "No player on scene. Camera moved to (0;0;0)"; + m_camera.setPosition(glm::vec3(0.f)); + } + + // ---------------------------------------------------------- + emit resourcesReady(); + + m_eState = ELevelState::LS_READY; // Done! + repaint(); // call to force jump into next state + } + + void SceneRenderWidget::doPrepareInvalidatedResources(QOpenGLFunctions_3_3_Core* glFunctions) + { + LEVEL_SAFE_CHECK() + + if (!m_resources->m_invalidatedTextures.empty()) + { + for (auto& texture : m_resources->m_textures) + { + if (texture.index.has_value() && m_resources->m_invalidatedTextures.contains(texture.index.value())) + { + const uint32_t textureIndex = texture.index.value(); + + // Unload texture + texture.discard(glFunctions); + + // Load texture (need to find actual entry in global textures pool... bruh) + const auto& allTextures = m_pLevel->getSceneTextures()->entries; + auto it = std::find_if(allTextures.begin(), allTextures.end(), [textureIndex](const gamelib::tex::TEXEntry& ent) -> bool { + return ent.m_index == textureIndex; + }); + + if (it != allTextures.end()) + { + // Erase cache + if (it->m_fileName.has_value()) + { + m_resources->m_textureNameToGL.erase(it->m_fileName.value()); + } + m_resources->m_textureIndexToGL.erase(it->m_index); + + // Reload + if (texture.setup(glFunctions, *it)) + { + // Update cache + if (texture.texPath.has_value()) + { + m_resources->m_textureNameToGL[texture.texPath.value()] = texture.texture; + } + + if (texture.index.has_value()) + { + m_resources->m_textureIndexToGL[texture.index.value()] = texture.texture; + } + + // Done + qDebug() << "Texture #" << textureIndex << " reloaded!"; + } + else + { + qWarning() << "Failed to update texture #" << textureIndex; + } + } + + // Validated + m_resources->m_invalidatedTextures.erase(textureIndex); + } + } + } + } + + glm::ivec2 SceneRenderWidget::getViewportSize() const + { + return { QWidget::width(), QWidget::height() }; + } + + void SceneRenderWidget::collectRenderList(const render::Camera& camera, const gamelib::scene::SceneObject* pRootGeom, render::RenderEntriesList& entries, RenderStats& stats, bool bIgnoreVisibility) + { + if (!m_pLevel || m_pLevel->getSceneObjects().empty()) + { + return; + } + + // Update room + updateCameraRoomAttachment(stats); + + if (pRootGeom != m_pLevel->getSceneObjects()[0].get()) + { + // Render from specific node (no performance optimisations here) + collectRenderEntriesIntoRenderList(pRootGeom, entries, stats, bIgnoreVisibility); + } + else + { + // Try to render + std::set visitedObjects {}; + + for (const auto& pRoom : m_cameraInRooms) + { + for (const SeebleObject& sObject : pRoom->vObjects) + { + if (visitedObjects.contains(sObject.pObject.get())) + continue; // Skip because it's in render list already + + if (!canDrawGeom(sObject.pObject.get())) + continue; + + if (auto bbox = getGameObjectBoundingBox(sObject.pObject, true); bbox.has_value() && m_camera.canSeeObject(bbox.value())) + { + // Need to render it + collectRenderEntriesIntoRenderList(sObject.pObject.get(), entries, stats, bIgnoreVisibility, true); + + visitedObjects.insert(sObject.pObject.get()); + } + } + } + } + + // Add debug stuff + if (m_bRenderPortals || m_bRenderRoomBoundingBox) + { + for (const auto &sRoomDef : m_rooms) + { + if (sRoomDef.mExitsDebugModel && m_bRenderPortals) + { + for (const auto &sMesh : sRoomDef.mExitsDebugModel->meshes) + { + render::RenderEntry &exitPlaneRenderEntry = entries.emplace_back(); + + // Render params + exitPlaneRenderEntry.iPrimitiveId = 0; + exitPlaneRenderEntry.iMeshIndex = 0; + exitPlaneRenderEntry.iTrianglesNr = 0; + exitPlaneRenderEntry.renderTopology = sMesh.renderTopology.value_or(render::RenderTopology::RT_TRIANGLES); + + // World params + exitPlaneRenderEntry.vPosition = glm::vec3(.0f); + exitPlaneRenderEntry.mWorldTransform = glm::mat4(1.f); + exitPlaneRenderEntry.mLocalOriginalTransform = glm::mat3(1.f); + exitPlaneRenderEntry.pMesh = const_cast(&sMesh); + + // Material + render::RenderEntry::Material &material = exitPlaneRenderEntry.material; + constexpr float kOpacity = 0.1f; + material.vDiffuseColor = sMesh.defaultColor.value_or(glm::vec4(1.f, 1.f, 0.f, kOpacity)); + material.renderState = gamelib::mat::MATRenderState("#BMEDIT/OPACITY_AREA", + true, true, true, false, false, + kOpacity, + 0.f, + 255, + gamelib::mat::MATCullMode::CM_DontCare, + gamelib::mat::MATBlendMode::BM_ADD, + gamelib::mat::MATValU()); + material.pShader = &m_resources->m_shaders[m_resources->m_iGizmoShaderIdx]; + } + } + + if (sRoomDef.mBBoxModel && m_bRenderRoomBoundingBox) + { + for (const auto &sMesh : sRoomDef.mBBoxModel->meshes) + { + render::RenderEntry &lineRenderEntry = entries.emplace_back(); + + // Render params + lineRenderEntry.iPrimitiveId = 0; + lineRenderEntry.iMeshIndex = 0; + lineRenderEntry.iTrianglesNr = 0; + lineRenderEntry.renderTopology = sMesh.renderTopology.value_or(render::RenderTopology::RT_LINES); + + // World params + lineRenderEntry.vPosition = glm::vec3(.0f); + lineRenderEntry.mWorldTransform = glm::mat4(1.f); + lineRenderEntry.mLocalOriginalTransform = glm::mat3(1.f); + lineRenderEntry.pMesh = const_cast(&sMesh); + + // Material + render::RenderEntry::Material &material = lineRenderEntry.material; + constexpr float kOpacity = 0.1f; + material.vDiffuseColor = sMesh.defaultColor.value_or(glm::vec4(1.f, 0.f, 0.f, kOpacity)); + material.renderState = gamelib::mat::MATRenderState("#BMEDIT/OPACITY_AREA", + true, true, true, false, false, + kOpacity, + 0.f, + 255, + gamelib::mat::MATCullMode::CM_DontCare, + gamelib::mat::MATBlendMode::BM_ADD, + gamelib::mat::MATValU()); + material.pShader = &m_resources->m_shaders[m_resources->m_iGizmoShaderIdx]; + } + } + } + } + + // Post sorting + entries.sort([&camera](const render::RenderEntry& a, const render::RenderEntry& b) -> bool { + // Check distance to camera + const float fADistanceToCamera = glm::length(camera.getPosition() - a.vPosition); + const float fBDistanceToCamera = glm::length(camera.getPosition() - b.vPosition); + + return fADistanceToCamera > fBDistanceToCamera; + }); + } + + void SceneRenderWidget::collectRenderEntriesIntoRenderList(const gamelib::scene::SceneObject* geom, render::RenderEntriesList& entries, RenderStats& stats, bool bIgnoreVisibility, bool bBreakOnChild) // NOLINT(*-no-recursion) + { + const bool bInvisible = geom->getProperties().getObject("Invisible", false); + const auto vPosition = geom->getPosition(); + auto primId = getGameObjectPrimitiveId(geom); + + // Calculate object world space bounding box and check that this bbox is visible by out camera + + if (const auto& n = geom->getType()->getName(); n == "ZSHADOWMESHOBJ" || n == "ZBOUND" || n == "ZLIGHT" || n == "ZENVIRONMENT" || n == "ZOMNILIGHT" || n == "ZSPOTLIGHT" || n == "ZSPOTLIGHTSQUARE") + { + // Do not draw us & our children + return; + } + + if (!bIgnoreVisibility) + if (bInvisible || !canDrawGeom(geom)) + return; + + if (g_bannedObjectIds.contains(std::string_view{geom->getName()}) || geom->getName().starts_with("CloneGroup_")) + return; + + // Check that our 'object' is not a collision box + if (auto parent = geom->getParent().lock(); parent && parent->getType()->getName() == "ZROOM" && parent->getName() == geom->getName()) + return; // Do not render collision meshes + + // Check that object could be rendered by any way + if (primId != 0 && m_resources->m_modelsCache.contains(primId)) + { + glm::mat4 mWorldTransform = getGameObjectTransform(geom); + + // Get model + const Model& model = m_resources->m_models[m_resources->m_modelsCache[primId]]; + gamelib::BoundingBox modelWorldBoundingBox = gamelib::BoundingBox::toWorld(model.boundingBox, mWorldTransform); + + if (m_camera.canSeeObject(glm::vec3(modelWorldBoundingBox.min), glm::vec3(modelWorldBoundingBox.max))) { + // Add bounding box to render list + { + if (geom == m_pSelectedSceneObject && model.boundingBoxMesh.has_value()) { + // Need to add mesh + render::RenderEntry &boundingBoxEntry = entries.emplace_back(); + + // Render params + boundingBoxEntry.iPrimitiveId = 0; + boundingBoxEntry.iMeshIndex = 0; + boundingBoxEntry.iTrianglesNr = 0; + boundingBoxEntry.renderTopology = render::RenderTopology::RT_LINES; +#ifdef QT_DEBUG + boundingBoxEntry.debugGroupId = "[BBOX] " + geom->getName(); +#endif + + // World params + boundingBoxEntry.vPosition = vPosition; + boundingBoxEntry.mWorldTransform = mWorldTransform; + boundingBoxEntry.mLocalOriginalTransform = geom->getOriginalTransform(); + boundingBoxEntry.pMesh = const_cast(&model.boundingBoxMesh.value()); + + // Material + render::RenderEntry::Material &material = boundingBoxEntry.material; + material.vDiffuseColor = glm::vec4(0.f, 0.f, 1.f, 1.f); + material.pShader = &m_resources->m_shaders[m_resources->m_iGizmoShaderIdx]; + } + } + + // increase allowed objects count + stats.allowedObjects++; + + // Add each 'mesh' into render list + for (int iMeshIdx = 0; iMeshIdx < model.meshes.size(); iMeshIdx++) { + const auto &mesh = model.meshes[iMeshIdx]; + + if (mesh.materialId == 0) + continue;// Unable to render (ZWINPIC!) + + // Filter by 'MeshVariantId' + const auto requiredVariationId = geom->getProperties().getObject("MeshVariantId", 0); + if (requiredVariationId != mesh.variationId) { + continue; + } + + // And store entry to renderer + render::RenderEntry renderEntry = {}; + + // Render params + renderEntry.iPrimitiveId = primId; + renderEntry.iMeshIndex = iMeshIdx; + renderEntry.iTrianglesNr = mesh.trianglesCount; + renderEntry.renderTopology = render::RenderTopology::RT_TRIANGLES; + + // World params + renderEntry.vPosition = vPosition; + renderEntry.mWorldTransform = mWorldTransform; + renderEntry.mLocalOriginalTransform = geom->getOriginalTransform(); + renderEntry.pMesh = const_cast(&mesh); + + // Material + render::RenderEntry::Material &material = renderEntry.material; + + const auto &instances = m_pLevel->getLevelMaterials()->materialInstances; + const auto &matInstance = instances[mesh.materialId - 1]; + + // Store parameters + material.id = mesh.materialId; + material.sInstanceMatName = matInstance.getName(); + material.sBaseMatClass = matInstance.getParentName(); + + if (!matInstance.getBinders().empty()) { + const auto &binder = matInstance.getBinders()[0];// NOTE: In future I'll rewrite this place, but for now it's enough + + // Store parameters + // TODO: Need collect all parameters here + + // Store render state + if (!binder.renderStates.empty()) { + // TODO: In future we need to learn how to use multiple render states (if there are able to be 'multiple') + material.renderState = binder.renderStates[0]; + } + + if (!material.renderState.isEnabled()) + { + // unable to see disabled material instance + continue; + } + + // Resolve & store textures + std::fill(material.textures.begin(), material.textures.end(), kInvalidResource); + + for (const auto &texture : binder.textures) { + if (texture.getPresentedTextureSources() == gamelib::mat::PresentedTextureSource::PTS_NOTHING) + continue;// No texture at all + + if (texture.getPresentedTextureSources() == gamelib::mat::PresentedTextureSource::PTS_TEXTURE_ID_AND_PATH) { + assert(false && "Idk how to handle this"); + continue; + } + + const auto &kind = texture.getName(); + + int textureSlotId = render::TextureSlotId::kMaxTextureSlot; + +#define MATCH_TEXTURE_KIND(mode, modeName) if (kind == modeName) { textureSlotId = mode; } + MATCH_TEXTURE_KIND(render::TextureSlotId::kMapDiffuse, "mapDiffuse") + MATCH_TEXTURE_KIND(render::TextureSlotId::kMapSpecularMask, "mapSpecularMask") + MATCH_TEXTURE_KIND(render::TextureSlotId::kMapEnvironment, "mapEnvironment") + MATCH_TEXTURE_KIND(render::TextureSlotId::kMapReflectionMask, "mapReflectionMask") + MATCH_TEXTURE_KIND(render::TextureSlotId::kMapReflectionFallOff, "mapReflectionFallOff") + MATCH_TEXTURE_KIND(render::TextureSlotId::kMapIllumination, "mapIllumination") + MATCH_TEXTURE_KIND(render::TextureSlotId::kMapTranslucency, "mapTranslucency") +#undef MATCH_TEXTURE_KIND + + if (textureSlotId == render::TextureSlotId::kMaxTextureSlot) + continue; + + // Now we need to find texture instance and associate it + if (texture.getPresentedTextureSources() == gamelib::mat::PresentedTextureSource::PTS_TEXTURE_ID) { + // Lookup in cache by texture id + if (auto it = m_resources->m_textureIndexToGL.find(texture.getTextureId()); it != m_resources->m_textureIndexToGL.end()) { + material.textures[textureSlotId] = it->second; + break; + } + } + + if (texture.getPresentedTextureSources() == gamelib::mat::PresentedTextureSource::PTS_TEXTURE_PATH) { + // Lookup in cache by texture path + if (auto it = m_resources->m_textureNameToGL.find(texture.getTexturePath()); it != m_resources->m_textureNameToGL.end()) { + material.textures[textureSlotId] = it->second; + break; + } + } + } + } + + // Store shader + material.pShader = &m_resources->m_shaders[m_resources->m_iTexturedShaderIdx]; + + // Push or not? + if (!std::all_of(material.textures.begin(), material.textures.end(), [](const auto &v) -> bool { return v == kInvalidResource; })) { +#ifdef QT_DEBUG + renderEntry.debugGroupId = "[Mesh " + std::to_string(iMeshIdx) + "/" + std::to_string(model.meshes.size()) + "] " + geom->getName(); +#endif + + entries.emplace_back(renderEntry); + } + } + } + else + { + // Increase rejected objects + stats.rejectedObjects++; + } + } + + if (bBreakOnChild) + return; + + // Visit others + for (const auto& child : geom->getChildren()) + { + if (auto g = child.lock()) + { + collectRenderEntriesIntoRenderList(g.get(), entries, stats, bIgnoreVisibility); + } + } + } + + void SceneRenderWidget::performRender(QOpenGLFunctions_3_3_Core* glFunctions, const render::RenderEntriesList& entries, const render::Camera& camera, const std::function& filter) + { + glm::ivec2 viewResolution = getViewportSize(); + + static constexpr std::array g_aTextureKindToLocation { + "i_uMaterial.mapDiffuse", + "i_uMaterial.mapSpecularMask", + "i_uMaterial.mapEnvironment", + "i_uMaterial.mapReflectionMask", + "i_uMaterial.mapReflectionFallOff", + "i_uMaterial.mapIllumination", + "i_uMaterial.mapTranslucency" + }; + + for (const auto& entry : entries) + { + if (!filter(entry)) + continue; // skipped by filter + +#ifdef QT_DEBUG + const bool bStartGroup = !entry.debugGroupId.empty(); + if (bStartGroup) beginDebugGroup(entry.debugGroupId); +#endif + + // Switch render state + g_RenderState.set(glFunctions, entry.material.renderState); + + // Activate material & setup parameters + render::Shader* shader = entry.material.pShader; + bool bShaderChanged = false; + + if (shader != g_pLastKnownShader) + { + if (g_pLastKnownShader) g_pLastKnownShader->unbind(glFunctions); + g_pLastKnownShader = shader; + + if (g_pLastKnownShader) g_pLastKnownShader->bind(glFunctions); + bShaderChanged = true; + } + + // Setup parameters (common) + g_pLastKnownShader->setUniform(glFunctions, ShaderConstants::kModelTransform, entry.mWorldTransform); + g_pLastKnownShader->setUniform(glFunctions, ShaderConstants::kCameraProjection, m_camera.getProjection()); + g_pLastKnownShader->setUniform(glFunctions, ShaderConstants::kCameraView, m_camera.getView()); + g_pLastKnownShader->setUniform(glFunctions, ShaderConstants::kCameraResolution, viewResolution); + + // TODO: Need to move into constants + g_pLastKnownShader->setUniform(glFunctions, "i_uMaterial.v4DiffuseColor", entry.material.vDiffuseColor); + g_pLastKnownShader->setUniform(glFunctions, "i_uMaterial.gm_vZBiasOffset", entry.material.renderState.hasZBias() ? entry.material.gm_vZBiasOffset : glm::vec4(0.f)); + g_pLastKnownShader->setUniform(glFunctions, "i_uMaterial.v4Opacity", entry.material.v4Opacity); + g_pLastKnownShader->setUniform(glFunctions, "i_uMaterial.v4Bias", entry.material.v4Bias); + g_pLastKnownShader->setUniform(glFunctions, "i_uMaterial.alphaREF", std::clamp(entry.material.iAlphaREF, 0, 255)); + g_pLastKnownShader->setUniform(glFunctions, "i_uMaterial.fZOffset", entry.material.renderState.getZOffset()); + + // Bind textures + for (int slotIdx = render::TextureSlotId::kMapDiffuse; slotIdx < render::TextureSlotId::kMaxTextureSlot; slotIdx++) + { + const auto& glTexture = entry.material.textures[slotIdx]; + + if (glTexture == kInvalidResource) + continue; + + if (g_RenderState.aTextures[slotIdx] != glTexture) + { + glFunctions->glActiveTexture(GL_TEXTURE0 + slotIdx); + glFunctions->glBindTexture(GL_TEXTURE_2D, glTexture); + g_RenderState.aTextures[slotIdx] = glTexture; + } + + if (bShaderChanged) + { + g_pLastKnownShader->setUniform(glFunctions, std::string(g_aTextureKindToLocation[slotIdx]), slotIdx); + } + } + + if (m_renderMode & RenderMode::RM_TEXTURE) + { + entry.pMesh->render(glFunctions, entry.renderTopology); + } + + if (m_renderMode & RenderMode::RM_WIREFRAME) + { + glFunctions->glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + entry.pMesh->render(glFunctions, entry.renderTopology); + glFunctions->glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + } + +#ifdef QT_DEBUG + if (bStartGroup) endDebugGroup(); +#endif + } + } + + void SceneRenderWidget::invalidateRenderList() + { + m_renderList.clear(); + } + + void SceneRenderWidget::computeRoomBoundingBox(RoomDef& d) + { + using R = gamelib::scene::SceneObject::EVisitResult; + + if (auto pRoom = d.rRoom.lock()) + { + // First of all we need try to lookup for 'CollisionMesh'. It has same name to room and be an STDOBJ + const auto& children = pRoom->getChildren(); + gamelib::scene::SceneObject* pCollisionMesh = nullptr; + pRoom->visitChildren([&pCollisionMesh, sTargetName = pRoom->getName()](const gamelib::scene::SceneObject::Ptr& pObject) -> R { + if (pObject->getName() == sTargetName/* && pObject->getType()->getName() == "ZSTDOBJ"*/) + { + pCollisionMesh = pObject.get(); + return R::VR_STOP_ALL; + } + + return R::VR_NEXT; // Do not go deeper + }); + + if (pCollisionMesh) + { + auto iPrimId = getGameObjectPrimitiveId(pCollisionMesh); + if (iPrimId != 0) + { + // Nice, collision mesh was found! Just use it as source for bbox of ZROOM + auto sBoundingBox = m_resources->m_models[m_resources->m_modelsCache[iPrimId]].boundingBox; + d.vBoundingBox = gamelib::BoundingBox::toWorld(sBoundingBox, pCollisionMesh->getWorldTransform()); + d.eBoundingBoxSource = RoomDef::EBoundingBoxSource::BBS_ROOM_COLLISION_MESH; + return; + } + } + + // Ok, let's try to find all ZBOUND objects and make BoundingBox + gamelib::BoundingBox sTempBbox {}; + int iZBoundObjsFound = 0; + pRoom->visitChildren([this, &sTempBbox, &iZBoundObjsFound](const gamelib::scene::SceneObject::Ptr& pObject) -> R { + if (pObject && pObject->getType()->getName() == "ZBOUND") + { + if (auto bbox = getGameObjectBoundingBox(pObject.get()); bbox.has_value()) + { + ++iZBoundObjsFound; + sTempBbox.expand(bbox.value()); + } + } + + return R::VR_NEXT; // Never go inside + }); + + if (iZBoundObjsFound > 0) + { + d.vBoundingBox = sTempBbox; + d.eBoundingBoxSource = RoomDef::EBoundingBoxSource::BBS_ZBOUNDS_AUTO_EXPAND; + return; + } + + // Old and hard way: just collect all visible objects with bboxes and combine them all into 1 single big bbox + bool bBboxInited = false; + + pRoom->visitChildren([&](const gamelib::scene::SceneObject::Ptr& pObject) -> R { + if (!pObject) + return R::VR_NEXT; + + if (auto bbox = getGameObjectBoundingBox(pObject.get()); bbox.has_value()) + { + if (!bBboxInited) + { + bBboxInited = true; + d.vBoundingBox = bbox.value(); + } + else + { + d.vBoundingBox.expand(bbox.value()); + } + + return R::VR_NEXT; + } + + return R::VR_CONTINUE; + }); + + d.eBoundingBoxSource = RoomDef::EBoundingBoxSource::BBS_AUTO_ROOM_EXPAND; + } + } + + void SceneRenderWidget::buildRoomCache(QOpenGLFunctions_3_3_Core* glFunctions) + { + using R = gamelib::scene::SceneObject::EVisitResult; + + // clear caches + m_rooms.clear(); + m_cameraInRooms.clear(); + + // Save pointer to BUF file + const auto bufFileView = m_pLevel->getStaticBuffer(); + + // Now we need to find ZGROUP who ends by _LOCATIONS and lookup from this ZGROUP inside + auto locationsIt = std::find_if( + m_pLevel->getSceneObjects().begin(), + m_pLevel->getSceneObjects().end(), + [](const gamelib::scene::SceneObject::Ptr& pObject) -> bool { + return pObject && pObject->getName().ends_with("_LOCATIONS.zip"); + }); + + if (locationsIt != m_pLevel->getSceneObjects().end()) + { + // we've able to use standard workflow + // Find ZROOMs + const gamelib::scene::SceneObject::Ptr& pNewRoot = *locationsIt; + + pNewRoot->visitChildren([this, bufFileView](const gamelib::scene::SceneObject::Ptr& pObject) -> R { + if (!pObject) + { + return R::VR_STOP_ALL; + } + + if (pObject->getType()->getName() == "ZROOM") + { + // Add and go next, do not go inside + auto& room = m_rooms.emplace_back(); + room.rRoom = pObject; + + //room.eLocation + static const std::map s_LocNameToKind { + { "eBOTH", RoomDef::ELocation::eBOTH }, + { "eINSIDE", RoomDef::ELocation::eINSIDE }, + { "eOUTSIDE", RoomDef::ELocation::eOUTSIDE }, + { "eUNDEFINED", RoomDef::ELocation::eUNDEFINED } + }; + const auto sLocation = pObject->getProperties().getObject("Location", ""); + + if (auto it = s_LocNameToKind.find(sLocation); it != s_LocNameToKind.end()) + { + room.eLocation = it->second; + } + else + { + room.eLocation = RoomDef::ELocation::eUNDEFINED; + assert(false && "Unknown room type, room will be ignored in optimisations loop"); + } + + //room.iExitsCount, room.ExitOffsets (Precache room exit boxes) + const auto iExistsCount = pObject->getProperties().getObject("iExitsCount", 0); + const auto iExistsOffset = pObject->getProperties().getObject("ExitOffsets", 0); + if (iExistsCount > 0 && iExistsOffset > 0) + { + room.aExists.reserve(iExistsCount); + + // Take a slice of data + constexpr auto kEntrySize = static_cast(sizeof(gamelib::gms::room::ZRoomExit)); + const auto roomExists = bufFileView.slice(iExistsOffset, kEntrySize * iExistsCount); + + for (int i = 0; i < iExistsCount; i++) + { + const auto exit = roomExists.slice((i * kEntrySize), kEntrySize); + auto& exitDef = room.aExists.emplace_back(); + gamelib::gms::room::ZRoomExit::deserialize(exitDef, exit); + } + } + + //room.iNeighboursCount, room.NeighborsOffset + const auto iNeighboursCount = pObject->getProperties().getObject("iNeighboursCount", 0); + const auto iNeighborsOffset = pObject->getProperties().getObject("NeighborsOffset", 0); + if (iNeighboursCount > 0 && iNeighborsOffset > 0) + { + room.aNeighbours.reserve(iNeighboursCount); + + constexpr auto kEntrySize = static_cast(sizeof(gamelib::gms::room::ZRoomNeighbor)); + const auto roomNeighbours = bufFileView.slice(iNeighborsOffset, kEntrySize * iNeighboursCount); + + for (int i = 0; i < iNeighboursCount; i++) + { + const auto neighbour = roomNeighbours.slice((i * kEntrySize), kEntrySize); + auto& neighbourDef = room.aNeighbours.emplace_back(); + gamelib::gms::room::ZRoomNeighbor::deserialize(neighbourDef, neighbour); + } + } + + // Compute room dimensions + computeRoomBoundingBox(room); + + // Collect objects list (all visible objects from room + dynamics from scene) + pObject->visitChildren([&room, this](const gamelib::scene::SceneObject::Ptr& pObj) -> R { + if (pObj->is("ZROOM")) return R::VR_CONTINUE; + + if (auto bbox = getGameObjectBoundingBox(pObj); bbox.has_value()) + { + SeebleObject& sObject = room.vObjects.emplace_back(); + sObject.pObject = pObj; + sObject.ePrio = EObjectPriority::EP_STATIC_OBJECT; + return R::VR_NEXT; // Go to next + } + return R::VR_CONTINUE; // go deeper + }); + + return R::VR_NEXT; + } + + // Go deep inside + return R::VR_CONTINUE; + }); + } + else + { + // No rooms found. Need to generate 1 big room + RoomDef& sVirtualRoom = m_rooms.emplace_back(); + + // Use really huge bbox (FLT32_MIN;FLT32_MIN;FLT32_MIN) (FLT32_MAX; FLT32_MAX; FLT32_MAX) + sVirtualRoom.vBoundingBox = gamelib::BoundingBox( + glm::vec3( + std::numeric_limits::min(), + std::numeric_limits::min(), + std::numeric_limits::min() + ), + glm::vec3( + std::numeric_limits::max(), + std::numeric_limits::max(), + std::numeric_limits::max() + ) + ); + + // Set location & flags + sVirtualRoom.eLocation = RoomDef::ELocation::eUNDEFINED; + sVirtualRoom.bIsVirtualBigRoom = true; + + // Collect objects + m_pLevel->getSceneObjects()[0]->visitChildren([&sVirtualRoom, this](const gamelib::scene::SceneObject::Ptr& pObj) -> R { + if (pObj->is("ZROOM")) return R::VR_CONTINUE; + + if (auto bbox = getGameObjectBoundingBox(pObj); bbox.has_value()) + { + SeebleObject& sObject = sVirtualRoom.vObjects.emplace_back(); + sObject.pObject = pObj; + sObject.ePrio = EObjectPriority::EP_STATIC_OBJECT; // Idk, but in this case all objects are STATIC + return R::VR_NEXT; // Go to next + } + + return R::VR_CONTINUE; // go deeper + }); + } + + if (m_rooms.size() > 1 && !m_rooms.begin()->bIsVirtualBigRoom) + { + // Visit all objects before any rooms + m_pLevel->getSceneObjects()[0]->visitChildren([this](const gamelib::scene::SceneObject::Ptr& pObject) -> R { + if (pObject->getName() == "Scar!scar") + { + printf("DEBUG\n"); + } + if (pObject->isInheritedOf("ZROOM")) return R::VR_NEXT; // Skip current branch + + if (auto rBBOX = getGameObjectBoundingBox(pObject, true); rBBOX.has_value()) + { + // Need to find in which room this subject should be + for (auto& sRoom : m_rooms) + { + if (sRoom.vBoundingBox.intersect(rBBOX.value())) + { + // Nice, save here + SeebleObject& sObject = sRoom.vObjects.emplace_back(); + sObject.pObject = pObject; + sObject.ePrio = EObjectPriority::EP_DYNAMIC_OBJECT; // mark as dynamic + + // break; // DronCode: Need to fix global bboxes before work with it. + } + } + + // Skip subtree (no visible inside) + return R::VR_NEXT; + } // skipped, no real reason to handle invisible object (but object could be visible after prop changed. be aware) + + return R::VR_CONTINUE; // go deeper + }); + } + + // Upload debug geom + for (auto& room : m_rooms) + { + if (!room.aExists.empty()) + { + room.mExitsDebugModel = std::make_unique(); + + for (const auto& sExit : room.aExists) + { + gamelib::Plane sPlane { sExit.v0, sExit.v1, sExit.v2, sExit.v3 }; + + // Plane + { + auto& exitMesh = room.mExitsDebugModel->meshes.emplace_back(); + exitMesh.glTextureId = render::kInvalidResource; + exitMesh.materialId = 0; + exitMesh.renderTopology = RenderTopology::RT_TRIANGLES; + + std::vector aVertices; + std::vector aIndices; + + sPlane.toTriangles(std::back_inserter(aVertices), std::back_inserter(aIndices)); + + exitMesh.setup(glFunctions, render::SimpleVertex::g_FormatDescription, aVertices, aIndices, false); + } + + // Normal vector direction + { + auto& exitMeshNormalView = room.mExitsDebugModel->meshes.emplace_back(); + exitMeshNormalView.glTextureId = render::kInvalidResource; + exitMeshNormalView.materialId = 0; + exitMeshNormalView.renderTopology = RenderTopology::RT_LINES; + exitMeshNormalView.defaultColor = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); + + const float kVecLength = sPlane.getSize() * 0.2f; //20% of size + + std::vector aVertices { + sPlane.getCenter(), + sPlane.getCenter() + (sPlane.getNormal() * kVecLength), + sPlane.getCenter() - (sPlane.getNormal() * kVecLength) + }; + + std::vector aIndices { 0, 1, 0, 2 }; + + exitMeshNormalView.setup(glFunctions, render::SimpleVertex::g_FormatDescription, aVertices, aIndices, false); + } + } + } + + // Add bounding box model + { + room.mBBoxModel = std::make_unique(); + + auto& bboxMesh = room.mBBoxModel->meshes.emplace_back(); + bboxMesh.glTextureId = render::kInvalidResource; + bboxMesh.materialId = 0; + bboxMesh.renderTopology = RenderTopology::RT_LINES; + + std::vector aVertices; + std::vector aIndices; + + room.vBoundingBox.toLines(std::back_inserter(aVertices), std::back_inserter(aIndices)); + bboxMesh.setup(glFunctions, render::SimpleVertex::g_FormatDescription, aVertices, aIndices, false); + } + } + } + + void SceneRenderWidget::updateCameraRoomAttachment(RenderStats& stats, bool bRejectLastResult) + { + m_cameraInRooms.clear(); + + for (const auto& sRoom : m_rooms) + { + if (sRoom.vBoundingBox.contains(m_camera.getPosition())) + { + m_cameraInRooms.emplace_back(&sRoom); + } + } + +#if 0 + /** + * Here is a place from hell. We need to know in which room camera and what rooms we can see from this place. + * + * First: + * IDK how to solve + * + * Second: + * Each room has "exits" and "neighbours". We just need to check what planes we can see from this room and this pos + dir (camera) + */ + std::list roomCandidates {}; + + for (const auto& sRoom : m_rooms) + { + if (sRoom.vBoundingBox.contains(m_camera.getPosition())) + { + roomCandidates.emplace_back(&sRoom); + } + } + + if (roomCandidates.empty()) + return; + + roomCandidates.sort([](const RoomDef* a, const RoomDef* b) { return a->eLocation < b->eLocation; }); + + RoomDef::ELocation currentLocation = (*roomCandidates.begin())->eLocation; + + for (const auto& sRoom : roomCandidates) + { + if (sRoom->eLocation != currentLocation) + continue; + + m_cameraInRooms.emplace_back(sRoom); + } +#endif + } + + void SceneRenderWidget::beginDebugGroup(std::string_view groupName) + { +#ifdef QT_DEBUG + QOpenGLContext::currentContext()->extraFunctions()->glPushDebugGroup(GL_DEBUG_SOURCE_APPLICATION, 1, -1, groupName.data()); +#endif + } + + void SceneRenderWidget::endDebugGroup() + { +#ifdef QT_DEBUG + QOpenGLContext::currentContext()->extraFunctions()->glPopDebugGroup(); +#endif + } + + void RenderState::set(QOpenGLFunctions_3_3_Core* gapi, const gamelib::mat::MATRenderState &state) + { + if (state.isBlendEnabled() != bHasBlend) + { + bHasBlend = state.isBlendEnabled(); + + if (bHasBlend) + { + gapi->glEnable(GL_BLEND); + + // Set blend mode based on your enum values + switch (eBlendMode) + { + case gamelib::mat::MATBlendMode::BM_TRANS: + gapi->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + break; + case gamelib::mat::MATBlendMode::BM_TRANS_ON_OPAQUE: + gapi->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + break; + case gamelib::mat::MATBlendMode::BM_TRANSADD_ON_OPAQUE: + gapi->glBlendFunc(GL_SRC_ALPHA, GL_ONE); + break; + case gamelib::mat::MATBlendMode::BM_ADD_BEFORE_TRANS: + gapi->glBlendFunc(GL_ONE, GL_ONE); + break; + case gamelib::mat::MATBlendMode::BM_ADD_ON_OPAQUE: + gapi->glBlendFunc(GL_ONE, GL_ONE); + break; + case gamelib::mat::MATBlendMode::BM_ADD: + gapi->glBlendFunc(GL_ONE, GL_ONE); + gapi->glEnable(GL_BLEND); + break; + default: + // Do nothing + break; + } + } else { + gapi->glDisable(GL_BLEND); + } + } + + if (bHasAlphaTest != state.isAlphaTestEnabled()) + { + bHasAlphaTest = state.isAlphaTestEnabled(); + + if (bHasAlphaTest) { + gapi->glEnable(GL_ALPHA_TEST); + } else { + gapi->glDisable(GL_ALPHA_TEST); + } + } + + if (bHasFog != state.isFogEnabled()) + { + bHasFog = state.isFogEnabled(); + + if (bHasFog) { + gapi->glEnable(GL_FOG); + } else { + gapi->glDisable(GL_FOG); + } + } + + if (eCullMode != state.getCullMode()) + { + eCullMode = state.getCullMode(); + + switch (eCullMode) + { + case gamelib::mat::MATCullMode::CM_OneSided: + gapi->glCullFace(GL_BACK); + break; + case gamelib::mat::MATCullMode::CM_DontCare: + case gamelib::mat::MATCullMode::CM_TwoSided: + // please complete + gapi->glDisable(GL_CULL_FACE); + break; + } + } + } + + void RenderState::apply(QOpenGLFunctions_3_3_Core* gapi) const + { + if (bHasBlend) + { + gapi->glEnable(GL_BLEND); + + // Set blend mode based on your enum values + switch (eBlendMode) + { + case gamelib::mat::MATBlendMode::BM_TRANS: + gapi->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + break; + case gamelib::mat::MATBlendMode::BM_TRANS_ON_OPAQUE: + gapi->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + break; + case gamelib::mat::MATBlendMode::BM_TRANSADD_ON_OPAQUE: + gapi->glBlendFunc(GL_SRC_ALPHA, GL_ONE); + break; + case gamelib::mat::MATBlendMode::BM_ADD_BEFORE_TRANS: + gapi->glBlendFunc(GL_ONE, GL_ONE); + break; + case gamelib::mat::MATBlendMode::BM_ADD_ON_OPAQUE: + gapi->glBlendFunc(GL_ONE, GL_ONE); + break; + case gamelib::mat::MATBlendMode::BM_ADD: + gapi->glBlendFunc(GL_ONE, GL_ONE); + gapi->glEnable(GL_BLEND); + break; + default: + // Do nothing + break; + } + } else { + gapi->glDisable(GL_BLEND); + } + + // Enable or disable alpha testing + if (bHasAlphaTest) { + gapi->glEnable(GL_ALPHA_TEST); + } else { + gapi->glDisable(GL_ALPHA_TEST); + } + + // Enable or disable fog + if (bHasFog) { + gapi->glEnable(GL_FOG); + } else { + gapi->glDisable(GL_FOG); + } + +#if 0 + // Enable or disable depth offset (Z bias) + if (renderState.hasZBias()) { + gapi->glEnable(GL_POLYGON_OFFSET_FILL); + gapi->glPolygonOffset(2.0f, renderState.getZOffset()); + } else { + gapi->glDisable(GL_POLYGON_OFFSET_FILL); + } +#endif + + // Set cull mode based on your enum values + switch (eCullMode) + { + case gamelib::mat::MATCullMode::CM_OneSided: + gapi->glCullFace(GL_BACK); + break; + case gamelib::mat::MATCullMode::CM_DontCare: + case gamelib::mat::MATCullMode::CM_TwoSided: + // please complete + gapi->glDisable(GL_CULL_FACE); + break; + } + } +} \ No newline at end of file diff --git a/BMEdit/Editor/Source/Widgets/TypePropertyWidget.cpp b/BMEdit/Editor/Source/Widgets/TypePropertyWidget.cpp index fd84680..47275fc 100644 --- a/BMEdit/Editor/Source/Widgets/TypePropertyWidget.cpp +++ b/BMEdit/Editor/Source/Widgets/TypePropertyWidget.cpp @@ -29,6 +29,11 @@ namespace widgets { return m_value; } + bool TypePropertyWidget::canHookFocus() const + { + return false; + } + bool TypePropertyWidget::areSame(const types::QGlacierValue ¤t, const types::QGlacierValue &value) { if (current.instructions.size() != value.instructions.size()) return false; diff --git a/BMEdit/Editor/Source/Widgets/TypeSimplePropertyWidget.cpp b/BMEdit/Editor/Source/Widgets/TypeSimplePropertyWidget.cpp index 9316aa2..d9666cc 100644 --- a/BMEdit/Editor/Source/Widgets/TypeSimplePropertyWidget.cpp +++ b/BMEdit/Editor/Source/Widgets/TypeSimplePropertyWidget.cpp @@ -1,9 +1,11 @@ #include #include #include +#include #include #include +#include // Layout #include diff --git a/BMEdit/Editor/ThirdParty/RenderDoc/CMakeLists.txt b/BMEdit/Editor/ThirdParty/RenderDoc/CMakeLists.txt new file mode 100644 index 0000000..0eb5b67 --- /dev/null +++ b/BMEdit/Editor/ThirdParty/RenderDoc/CMakeLists.txt @@ -0,0 +1,6 @@ +project(RenderDoc) + +add_library(RenderDoc_AppInterface INTERFACE) +add_library(RenderDoc::AppInterface ALIAS RenderDoc_AppInterface) + +target_include_directories(RenderDoc_AppInterface INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) \ No newline at end of file diff --git a/BMEdit/Editor/ThirdParty/RenderDoc/include/RenderDoc/renderdoc_app.h b/BMEdit/Editor/ThirdParty/RenderDoc/include/RenderDoc/renderdoc_app.h new file mode 100644 index 0000000..31632be --- /dev/null +++ b/BMEdit/Editor/ThirdParty/RenderDoc/include/RenderDoc/renderdoc_app.h @@ -0,0 +1,741 @@ +/****************************************************************************** +* The MIT License (MIT) +* +* Copyright (c) 2019-2024 Baldur Karlsson +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +******************************************************************************/ + +#pragma once + +////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Documentation for the API is available at https://renderdoc.org/docs/in_application_api.html +// + +#if !defined(RENDERDOC_NO_STDINT) + #include +#endif + +#if defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER) + #define RENDERDOC_CC __cdecl +#elif defined(__linux__) || defined(__FreeBSD__) + #define RENDERDOC_CC +#elif defined(__APPLE__) + #define RENDERDOC_CC +#else + #error "Unknown platform" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Constants not used directly in below API + +// This is a GUID/magic value used for when applications pass a path where shader debug +// information can be found to match up with a stripped shader. +// the define can be used like so: const GUID RENDERDOC_ShaderDebugMagicValue = +// RENDERDOC_ShaderDebugMagicValue_value +#define RENDERDOC_ShaderDebugMagicValue_struct \ + { \ + 0xeab25520, 0x6670, 0x4865, 0x84, 0x29, 0x6c, 0x8, 0x51, 0x54, 0x00, 0xff \ + } + +// as an alternative when you want a byte array (assuming x86 endianness): +#define RENDERDOC_ShaderDebugMagicValue_bytearray \ + { \ + 0x20, 0x55, 0xb2, 0xea, 0x70, 0x66, 0x65, 0x48, 0x84, 0x29, 0x6c, 0x8, 0x51, 0x54, 0x00, 0xff \ + } + +// truncated version when only a uint64_t is available (e.g. Vulkan tags): +#define RENDERDOC_ShaderDebugMagicValue_truncated 0x48656670eab25520ULL + +////////////////////////////////////////////////////////////////////////////////////////////////// +// RenderDoc capture options +// + +typedef enum RENDERDOC_CaptureOption +{ + // Allow the application to enable vsync + // + // Default - enabled + // + // 1 - The application can enable or disable vsync at will + // 0 - vsync is force disabled + eRENDERDOC_Option_AllowVSync = 0, + + // Allow the application to enable fullscreen + // + // Default - enabled + // + // 1 - The application can enable or disable fullscreen at will + // 0 - fullscreen is force disabled + eRENDERDOC_Option_AllowFullscreen = 1, + + // Record API debugging events and messages + // + // Default - disabled + // + // 1 - Enable built-in API debugging features and records the results into + // the capture, which is matched up with events on replay + // 0 - no API debugging is forcibly enabled + eRENDERDOC_Option_APIValidation = 2, + eRENDERDOC_Option_DebugDeviceMode = 2, // deprecated name of this enum + + // Capture CPU callstacks for API events + // + // Default - disabled + // + // 1 - Enables capturing of callstacks + // 0 - no callstacks are captured + eRENDERDOC_Option_CaptureCallstacks = 3, + + // When capturing CPU callstacks, only capture them from actions. + // This option does nothing without the above option being enabled + // + // Default - disabled + // + // 1 - Only captures callstacks for actions. + // Ignored if CaptureCallstacks is disabled + // 0 - Callstacks, if enabled, are captured for every event. + eRENDERDOC_Option_CaptureCallstacksOnlyDraws = 4, + eRENDERDOC_Option_CaptureCallstacksOnlyActions = 4, + + // Specify a delay in seconds to wait for a debugger to attach, after + // creating or injecting into a process, before continuing to allow it to run. + // + // 0 indicates no delay, and the process will run immediately after injection + // + // Default - 0 seconds + // + eRENDERDOC_Option_DelayForDebugger = 5, + + // Verify buffer access. This includes checking the memory returned by a Map() call to + // detect any out-of-bounds modification, as well as initialising buffers with undefined contents + // to a marker value to catch use of uninitialised memory. + // + // NOTE: This option is only valid for OpenGL and D3D11. Explicit APIs such as D3D12 and Vulkan do + // not do the same kind of interception & checking and undefined contents are really undefined. + // + // Default - disabled + // + // 1 - Verify buffer access + // 0 - No verification is performed, and overwriting bounds may cause crashes or corruption in + // RenderDoc. + eRENDERDOC_Option_VerifyBufferAccess = 6, + + // The old name for eRENDERDOC_Option_VerifyBufferAccess was eRENDERDOC_Option_VerifyMapWrites. + // This option now controls the filling of uninitialised buffers with 0xdddddddd which was + // previously always enabled + eRENDERDOC_Option_VerifyMapWrites = eRENDERDOC_Option_VerifyBufferAccess, + + // Hooks any system API calls that create child processes, and injects + // RenderDoc into them recursively with the same options. + // + // Default - disabled + // + // 1 - Hooks into spawned child processes + // 0 - Child processes are not hooked by RenderDoc + eRENDERDOC_Option_HookIntoChildren = 7, + + // By default RenderDoc only includes resources in the final capture necessary + // for that frame, this allows you to override that behaviour. + // + // Default - disabled + // + // 1 - all live resources at the time of capture are included in the capture + // and available for inspection + // 0 - only the resources referenced by the captured frame are included + eRENDERDOC_Option_RefAllResources = 8, + + // **NOTE**: As of RenderDoc v1.1 this option has been deprecated. Setting or + // getting it will be ignored, to allow compatibility with older versions. + // In v1.1 the option acts as if it's always enabled. + // + // By default RenderDoc skips saving initial states for resources where the + // previous contents don't appear to be used, assuming that writes before + // reads indicate previous contents aren't used. + // + // Default - disabled + // + // 1 - initial contents at the start of each captured frame are saved, even if + // they are later overwritten or cleared before being used. + // 0 - unless a read is detected, initial contents will not be saved and will + // appear as black or empty data. + eRENDERDOC_Option_SaveAllInitials = 9, + + // In APIs that allow for the recording of command lists to be replayed later, + // RenderDoc may choose to not capture command lists before a frame capture is + // triggered, to reduce overheads. This means any command lists recorded once + // and replayed many times will not be available and may cause a failure to + // capture. + // + // NOTE: This is only true for APIs where multithreading is difficult or + // discouraged. Newer APIs like Vulkan and D3D12 will ignore this option + // and always capture all command lists since the API is heavily oriented + // around it and the overheads have been reduced by API design. + // + // 1 - All command lists are captured from the start of the application + // 0 - Command lists are only captured if their recording begins during + // the period when a frame capture is in progress. + eRENDERDOC_Option_CaptureAllCmdLists = 10, + + // Mute API debugging output when the API validation mode option is enabled + // + // Default - enabled + // + // 1 - Mute any API debug messages from being displayed or passed through + // 0 - API debugging is displayed as normal + eRENDERDOC_Option_DebugOutputMute = 11, + + // Option to allow vendor extensions to be used even when they may be + // incompatible with RenderDoc and cause corrupted replays or crashes. + // + // Default - inactive + // + // No values are documented, this option should only be used when absolutely + // necessary as directed by a RenderDoc developer. + eRENDERDOC_Option_AllowUnsupportedVendorExtensions = 12, + + // Define a soft memory limit which some APIs may aim to keep overhead under where + // possible. Anything above this limit will where possible be saved directly to disk during + // capture. + // This will cause increased disk space use (which may cause a capture to fail if disk space is + // exhausted) as well as slower capture times. + // + // Not all memory allocations may be deferred like this so it is not a guarantee of a memory + // limit. + // + // Units are in MBs, suggested values would range from 200MB to 1000MB. + // + // Default - 0 Megabytes + eRENDERDOC_Option_SoftMemoryLimit = 13, +} RENDERDOC_CaptureOption; + +// Sets an option that controls how RenderDoc behaves on capture. +// +// Returns 1 if the option and value are valid +// Returns 0 if either is invalid and the option is unchanged +typedef int(RENDERDOC_CC *pRENDERDOC_SetCaptureOptionU32)(RENDERDOC_CaptureOption opt, uint32_t val); +typedef int(RENDERDOC_CC *pRENDERDOC_SetCaptureOptionF32)(RENDERDOC_CaptureOption opt, float val); + +// Gets the current value of an option as a uint32_t +// +// If the option is invalid, 0xffffffff is returned +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetCaptureOptionU32)(RENDERDOC_CaptureOption opt); + +// Gets the current value of an option as a float +// +// If the option is invalid, -FLT_MAX is returned +typedef float(RENDERDOC_CC *pRENDERDOC_GetCaptureOptionF32)(RENDERDOC_CaptureOption opt); + +typedef enum RENDERDOC_InputButton +{ + // '0' - '9' matches ASCII values + eRENDERDOC_Key_0 = 0x30, + eRENDERDOC_Key_1 = 0x31, + eRENDERDOC_Key_2 = 0x32, + eRENDERDOC_Key_3 = 0x33, + eRENDERDOC_Key_4 = 0x34, + eRENDERDOC_Key_5 = 0x35, + eRENDERDOC_Key_6 = 0x36, + eRENDERDOC_Key_7 = 0x37, + eRENDERDOC_Key_8 = 0x38, + eRENDERDOC_Key_9 = 0x39, + + // 'A' - 'Z' matches ASCII values + eRENDERDOC_Key_A = 0x41, + eRENDERDOC_Key_B = 0x42, + eRENDERDOC_Key_C = 0x43, + eRENDERDOC_Key_D = 0x44, + eRENDERDOC_Key_E = 0x45, + eRENDERDOC_Key_F = 0x46, + eRENDERDOC_Key_G = 0x47, + eRENDERDOC_Key_H = 0x48, + eRENDERDOC_Key_I = 0x49, + eRENDERDOC_Key_J = 0x4A, + eRENDERDOC_Key_K = 0x4B, + eRENDERDOC_Key_L = 0x4C, + eRENDERDOC_Key_M = 0x4D, + eRENDERDOC_Key_N = 0x4E, + eRENDERDOC_Key_O = 0x4F, + eRENDERDOC_Key_P = 0x50, + eRENDERDOC_Key_Q = 0x51, + eRENDERDOC_Key_R = 0x52, + eRENDERDOC_Key_S = 0x53, + eRENDERDOC_Key_T = 0x54, + eRENDERDOC_Key_U = 0x55, + eRENDERDOC_Key_V = 0x56, + eRENDERDOC_Key_W = 0x57, + eRENDERDOC_Key_X = 0x58, + eRENDERDOC_Key_Y = 0x59, + eRENDERDOC_Key_Z = 0x5A, + + // leave the rest of the ASCII range free + // in case we want to use it later + eRENDERDOC_Key_NonPrintable = 0x100, + + eRENDERDOC_Key_Divide, + eRENDERDOC_Key_Multiply, + eRENDERDOC_Key_Subtract, + eRENDERDOC_Key_Plus, + + eRENDERDOC_Key_F1, + eRENDERDOC_Key_F2, + eRENDERDOC_Key_F3, + eRENDERDOC_Key_F4, + eRENDERDOC_Key_F5, + eRENDERDOC_Key_F6, + eRENDERDOC_Key_F7, + eRENDERDOC_Key_F8, + eRENDERDOC_Key_F9, + eRENDERDOC_Key_F10, + eRENDERDOC_Key_F11, + eRENDERDOC_Key_F12, + + eRENDERDOC_Key_Home, + eRENDERDOC_Key_End, + eRENDERDOC_Key_Insert, + eRENDERDOC_Key_Delete, + eRENDERDOC_Key_PageUp, + eRENDERDOC_Key_PageDn, + + eRENDERDOC_Key_Backspace, + eRENDERDOC_Key_Tab, + eRENDERDOC_Key_PrtScrn, + eRENDERDOC_Key_Pause, + + eRENDERDOC_Key_Max, +} RENDERDOC_InputButton; + +// Sets which key or keys can be used to toggle focus between multiple windows +// +// If keys is NULL or num is 0, toggle keys will be disabled +typedef void(RENDERDOC_CC *pRENDERDOC_SetFocusToggleKeys)(RENDERDOC_InputButton *keys, int num); + +// Sets which key or keys can be used to capture the next frame +// +// If keys is NULL or num is 0, captures keys will be disabled +typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureKeys)(RENDERDOC_InputButton *keys, int num); + +typedef enum RENDERDOC_OverlayBits +{ + // This single bit controls whether the overlay is enabled or disabled globally + eRENDERDOC_Overlay_Enabled = 0x1, + + // Show the average framerate over several seconds as well as min/max + eRENDERDOC_Overlay_FrameRate = 0x2, + + // Show the current frame number + eRENDERDOC_Overlay_FrameNumber = 0x4, + + // Show a list of recent captures, and how many captures have been made + eRENDERDOC_Overlay_CaptureList = 0x8, + + // Default values for the overlay mask + eRENDERDOC_Overlay_Default = (eRENDERDOC_Overlay_Enabled | eRENDERDOC_Overlay_FrameRate | + eRENDERDOC_Overlay_FrameNumber | eRENDERDOC_Overlay_CaptureList), + + // Enable all bits + eRENDERDOC_Overlay_All = ~0U, + + // Disable all bits + eRENDERDOC_Overlay_None = 0, +} RENDERDOC_OverlayBits; + +// returns the overlay bits that have been set +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetOverlayBits)(); +// sets the overlay bits with an and & or mask +typedef void(RENDERDOC_CC *pRENDERDOC_MaskOverlayBits)(uint32_t And, uint32_t Or); + +// this function will attempt to remove RenderDoc's hooks in the application. +// +// Note: that this can only work correctly if done immediately after +// the module is loaded, before any API work happens. RenderDoc will remove its +// injected hooks and shut down. Behaviour is undefined if this is called +// after any API functions have been called, and there is still no guarantee of +// success. +typedef void(RENDERDOC_CC *pRENDERDOC_RemoveHooks)(); + +// DEPRECATED: compatibility for code compiled against pre-1.4.1 headers. +typedef pRENDERDOC_RemoveHooks pRENDERDOC_Shutdown; + +// This function will unload RenderDoc's crash handler. +// +// If you use your own crash handler and don't want RenderDoc's handler to +// intercede, you can call this function to unload it and any unhandled +// exceptions will pass to the next handler. +typedef void(RENDERDOC_CC *pRENDERDOC_UnloadCrashHandler)(); + +// Sets the capture file path template +// +// pathtemplate is a UTF-8 string that gives a template for how captures will be named +// and where they will be saved. +// +// Any extension is stripped off the path, and captures are saved in the directory +// specified, and named with the filename and the frame number appended. If the +// directory does not exist it will be created, including any parent directories. +// +// If pathtemplate is NULL, the template will remain unchanged +// +// Example: +// +// SetCaptureFilePathTemplate("my_captures/example"); +// +// Capture #1 -> my_captures/example_frame123.rdc +// Capture #2 -> my_captures/example_frame456.rdc +typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureFilePathTemplate)(const char *pathtemplate); + +// returns the current capture path template, see SetCaptureFileTemplate above, as a UTF-8 string +typedef const char *(RENDERDOC_CC *pRENDERDOC_GetCaptureFilePathTemplate)(); + +// DEPRECATED: compatibility for code compiled against pre-1.1.2 headers. +typedef pRENDERDOC_SetCaptureFilePathTemplate pRENDERDOC_SetLogFilePathTemplate; +typedef pRENDERDOC_GetCaptureFilePathTemplate pRENDERDOC_GetLogFilePathTemplate; + +// returns the number of captures that have been made +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetNumCaptures)(); + +// This function returns the details of a capture, by index. New captures are added +// to the end of the list. +// +// filename will be filled with the absolute path to the capture file, as a UTF-8 string +// pathlength will be written with the length in bytes of the filename string +// timestamp will be written with the time of the capture, in seconds since the Unix epoch +// +// Any of the parameters can be NULL and they'll be skipped. +// +// The function will return 1 if the capture index is valid, or 0 if the index is invalid +// If the index is invalid, the values will be unchanged +// +// Note: when captures are deleted in the UI they will remain in this list, so the +// capture path may not exist anymore. +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetCapture)(uint32_t idx, char *filename, + uint32_t *pathlength, uint64_t *timestamp); + +// Sets the comments associated with a capture file. These comments are displayed in the +// UI program when opening. +// +// filePath should be a path to the capture file to add comments to. If set to NULL or "" +// the most recent capture file created made will be used instead. +// comments should be a NULL-terminated UTF-8 string to add as comments. +// +// Any existing comments will be overwritten. +typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureFileComments)(const char *filePath, + const char *comments); + +// returns 1 if the RenderDoc UI is connected to this application, 0 otherwise +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_IsTargetControlConnected)(); + +// DEPRECATED: compatibility for code compiled against pre-1.1.1 headers. +// This was renamed to IsTargetControlConnected in API 1.1.1, the old typedef is kept here for +// backwards compatibility with old code, it is castable either way since it's ABI compatible +// as the same function pointer type. +typedef pRENDERDOC_IsTargetControlConnected pRENDERDOC_IsRemoteAccessConnected; + +// This function will launch the Replay UI associated with the RenderDoc library injected +// into the running application. +// +// if connectTargetControl is 1, the Replay UI will be launched with a command line parameter +// to connect to this application +// cmdline is the rest of the command line, as a UTF-8 string. E.g. a captures to open +// if cmdline is NULL, the command line will be empty. +// +// returns the PID of the replay UI if successful, 0 if not successful. +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_LaunchReplayUI)(uint32_t connectTargetControl, + const char *cmdline); + +// RenderDoc can return a higher version than requested if it's backwards compatible, +// this function returns the actual version returned. If a parameter is NULL, it will be +// ignored and the others will be filled out. +typedef void(RENDERDOC_CC *pRENDERDOC_GetAPIVersion)(int *major, int *minor, int *patch); + +// Requests that the replay UI show itself (if hidden or not the current top window). This can be +// used in conjunction with IsTargetControlConnected and LaunchReplayUI to intelligently handle +// showing the UI after making a capture. +// +// This will return 1 if the request was successfully passed on, though it's not guaranteed that +// the UI will be on top in all cases depending on OS rules. It will return 0 if there is no current +// target control connection to make such a request, or if there was another error +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_ShowReplayUI)(); + +////////////////////////////////////////////////////////////////////////// +// Capturing functions +// + +// A device pointer is a pointer to the API's root handle. +// +// This would be an ID3D11Device, HGLRC/GLXContext, ID3D12Device, etc +typedef void *RENDERDOC_DevicePointer; + +// A window handle is the OS's native window handle +// +// This would be an HWND, GLXDrawable, etc +typedef void *RENDERDOC_WindowHandle; + +// A helper macro for Vulkan, where the device handle cannot be used directly. +// +// Passing the VkInstance to this macro will return the RENDERDOC_DevicePointer to use. +// +// Specifically, the value needed is the dispatch table pointer, which sits as the first +// pointer-sized object in the memory pointed to by the VkInstance. Thus we cast to a void** and +// indirect once. +#define RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE(inst) (*((void **)(inst))) + +// This sets the RenderDoc in-app overlay in the API/window pair as 'active' and it will +// respond to keypresses. Neither parameter can be NULL +typedef void(RENDERDOC_CC *pRENDERDOC_SetActiveWindow)(RENDERDOC_DevicePointer device, + RENDERDOC_WindowHandle wndHandle); + +// capture the next frame on whichever window and API is currently considered active +typedef void(RENDERDOC_CC *pRENDERDOC_TriggerCapture)(); + +// capture the next N frames on whichever window and API is currently considered active +typedef void(RENDERDOC_CC *pRENDERDOC_TriggerMultiFrameCapture)(uint32_t numFrames); + +// When choosing either a device pointer or a window handle to capture, you can pass NULL. +// Passing NULL specifies a 'wildcard' match against anything. This allows you to specify +// any API rendering to a specific window, or a specific API instance rendering to any window, +// or in the simplest case of one window and one API, you can just pass NULL for both. +// +// In either case, if there are two or more possible matching (device,window) pairs it +// is undefined which one will be captured. +// +// Note: for headless rendering you can pass NULL for the window handle and either specify +// a device pointer or leave it NULL as above. + +// Immediately starts capturing API calls on the specified device pointer and window handle. +// +// If there is no matching thing to capture (e.g. no supported API has been initialised), +// this will do nothing. +// +// The results are undefined (including crashes) if two captures are started overlapping, +// even on separate devices and/oror windows. +typedef void(RENDERDOC_CC *pRENDERDOC_StartFrameCapture)(RENDERDOC_DevicePointer device, + RENDERDOC_WindowHandle wndHandle); + +// Returns whether or not a frame capture is currently ongoing anywhere. +// +// This will return 1 if a capture is ongoing, and 0 if there is no capture running +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_IsFrameCapturing)(); + +// Ends capturing immediately. +// +// This will return 1 if the capture succeeded, and 0 if there was an error capturing. +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_EndFrameCapture)(RENDERDOC_DevicePointer device, + RENDERDOC_WindowHandle wndHandle); + +// Ends capturing immediately and discard any data stored without saving to disk. +// +// This will return 1 if the capture was discarded, and 0 if there was an error or no capture +// was in progress +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_DiscardFrameCapture)(RENDERDOC_DevicePointer device, + RENDERDOC_WindowHandle wndHandle); + +// Only valid to be called between a call to StartFrameCapture and EndFrameCapture. Gives a custom +// title to the capture produced which will be displayed in the UI. +// +// If multiple captures are ongoing, this title will be applied to the first capture to end after +// this call. The second capture to end will have no title, unless this function is called again. +// +// Calling this function has no effect if no capture is currently running, and if it is called +// multiple times only the last title will be used. +typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureTitle)(const char *title); + +////////////////////////////////////////////////////////////////////////////////////////////////// +// RenderDoc API versions +// + +// RenderDoc uses semantic versioning (http://semver.org/). +// +// MAJOR version is incremented when incompatible API changes happen. +// MINOR version is incremented when functionality is added in a backwards-compatible manner. +// PATCH version is incremented when backwards-compatible bug fixes happen. +// +// Note that this means the API returned can be higher than the one you might have requested. +// e.g. if you are running against a newer RenderDoc that supports 1.0.1, it will be returned +// instead of 1.0.0. You can check this with the GetAPIVersion entry point +typedef enum RENDERDOC_Version +{ + eRENDERDOC_API_Version_1_0_0 = 10000, // RENDERDOC_API_1_0_0 = 1 00 00 + eRENDERDOC_API_Version_1_0_1 = 10001, // RENDERDOC_API_1_0_1 = 1 00 01 + eRENDERDOC_API_Version_1_0_2 = 10002, // RENDERDOC_API_1_0_2 = 1 00 02 + eRENDERDOC_API_Version_1_1_0 = 10100, // RENDERDOC_API_1_1_0 = 1 01 00 + eRENDERDOC_API_Version_1_1_1 = 10101, // RENDERDOC_API_1_1_1 = 1 01 01 + eRENDERDOC_API_Version_1_1_2 = 10102, // RENDERDOC_API_1_1_2 = 1 01 02 + eRENDERDOC_API_Version_1_2_0 = 10200, // RENDERDOC_API_1_2_0 = 1 02 00 + eRENDERDOC_API_Version_1_3_0 = 10300, // RENDERDOC_API_1_3_0 = 1 03 00 + eRENDERDOC_API_Version_1_4_0 = 10400, // RENDERDOC_API_1_4_0 = 1 04 00 + eRENDERDOC_API_Version_1_4_1 = 10401, // RENDERDOC_API_1_4_1 = 1 04 01 + eRENDERDOC_API_Version_1_4_2 = 10402, // RENDERDOC_API_1_4_2 = 1 04 02 + eRENDERDOC_API_Version_1_5_0 = 10500, // RENDERDOC_API_1_5_0 = 1 05 00 + eRENDERDOC_API_Version_1_6_0 = 10600, // RENDERDOC_API_1_6_0 = 1 06 00 +} RENDERDOC_Version; + +// API version changelog: +// +// 1.0.0 - initial release +// 1.0.1 - Bugfix: IsFrameCapturing() was returning false for captures that were triggered +// by keypress or TriggerCapture, instead of Start/EndFrameCapture. +// 1.0.2 - Refactor: Renamed eRENDERDOC_Option_DebugDeviceMode to eRENDERDOC_Option_APIValidation +// 1.1.0 - Add feature: TriggerMultiFrameCapture(). Backwards compatible with 1.0.x since the new +// function pointer is added to the end of the struct, the original layout is identical +// 1.1.1 - Refactor: Renamed remote access to target control (to better disambiguate from remote +// replay/remote server concept in replay UI) +// 1.1.2 - Refactor: Renamed "log file" in function names to just capture, to clarify that these +// are captures and not debug logging files. This is the first API version in the v1.0 +// branch. +// 1.2.0 - Added feature: SetCaptureFileComments() to add comments to a capture file that will be +// displayed in the UI program on load. +// 1.3.0 - Added feature: New capture option eRENDERDOC_Option_AllowUnsupportedVendorExtensions +// which allows users to opt-in to allowing unsupported vendor extensions to function. +// Should be used at the user's own risk. +// Refactor: Renamed eRENDERDOC_Option_VerifyMapWrites to +// eRENDERDOC_Option_VerifyBufferAccess, which now also controls initialisation to +// 0xdddddddd of uninitialised buffer contents. +// 1.4.0 - Added feature: DiscardFrameCapture() to discard a frame capture in progress and stop +// capturing without saving anything to disk. +// 1.4.1 - Refactor: Renamed Shutdown to RemoveHooks to better clarify what is happening +// 1.4.2 - Refactor: Renamed 'draws' to 'actions' in callstack capture option. +// 1.5.0 - Added feature: ShowReplayUI() to request that the replay UI show itself if connected +// 1.6.0 - Added feature: SetCaptureTitle() which can be used to set a title for a +// capture made with StartFrameCapture() or EndFrameCapture() + +typedef struct RENDERDOC_API_1_6_0 +{ + pRENDERDOC_GetAPIVersion GetAPIVersion; + + pRENDERDOC_SetCaptureOptionU32 SetCaptureOptionU32; + pRENDERDOC_SetCaptureOptionF32 SetCaptureOptionF32; + + pRENDERDOC_GetCaptureOptionU32 GetCaptureOptionU32; + pRENDERDOC_GetCaptureOptionF32 GetCaptureOptionF32; + + pRENDERDOC_SetFocusToggleKeys SetFocusToggleKeys; + pRENDERDOC_SetCaptureKeys SetCaptureKeys; + + pRENDERDOC_GetOverlayBits GetOverlayBits; + pRENDERDOC_MaskOverlayBits MaskOverlayBits; + + // Shutdown was renamed to RemoveHooks in 1.4.1. + // These unions allow old code to continue compiling without changes + union + { + pRENDERDOC_Shutdown Shutdown; + pRENDERDOC_RemoveHooks RemoveHooks; + }; + pRENDERDOC_UnloadCrashHandler UnloadCrashHandler; + + // Get/SetLogFilePathTemplate was renamed to Get/SetCaptureFilePathTemplate in 1.1.2. + // These unions allow old code to continue compiling without changes + union + { + // deprecated name + pRENDERDOC_SetLogFilePathTemplate SetLogFilePathTemplate; + // current name + pRENDERDOC_SetCaptureFilePathTemplate SetCaptureFilePathTemplate; + }; + union + { + // deprecated name + pRENDERDOC_GetLogFilePathTemplate GetLogFilePathTemplate; + // current name + pRENDERDOC_GetCaptureFilePathTemplate GetCaptureFilePathTemplate; + }; + + pRENDERDOC_GetNumCaptures GetNumCaptures; + pRENDERDOC_GetCapture GetCapture; + + pRENDERDOC_TriggerCapture TriggerCapture; + + // IsRemoteAccessConnected was renamed to IsTargetControlConnected in 1.1.1. + // This union allows old code to continue compiling without changes + union + { + // deprecated name + pRENDERDOC_IsRemoteAccessConnected IsRemoteAccessConnected; + // current name + pRENDERDOC_IsTargetControlConnected IsTargetControlConnected; + }; + pRENDERDOC_LaunchReplayUI LaunchReplayUI; + + pRENDERDOC_SetActiveWindow SetActiveWindow; + + pRENDERDOC_StartFrameCapture StartFrameCapture; + pRENDERDOC_IsFrameCapturing IsFrameCapturing; + pRENDERDOC_EndFrameCapture EndFrameCapture; + + // new function in 1.1.0 + pRENDERDOC_TriggerMultiFrameCapture TriggerMultiFrameCapture; + + // new function in 1.2.0 + pRENDERDOC_SetCaptureFileComments SetCaptureFileComments; + + // new function in 1.4.0 + pRENDERDOC_DiscardFrameCapture DiscardFrameCapture; + + // new function in 1.5.0 + pRENDERDOC_ShowReplayUI ShowReplayUI; + + // new function in 1.6.0 + pRENDERDOC_SetCaptureTitle SetCaptureTitle; +} RENDERDOC_API_1_6_0; + +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_0_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_0_1; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_0_2; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_1_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_1_1; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_1_2; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_2_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_3_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_4_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_4_1; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_4_2; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_5_0; + +////////////////////////////////////////////////////////////////////////////////////////////////// +// RenderDoc API entry point +// +// This entry point can be obtained via GetProcAddress/dlsym if RenderDoc is available. +// +// The name is the same as the typedef - "RENDERDOC_GetAPI" +// +// This function is not thread safe, and should not be called on multiple threads at once. +// Ideally, call this once as early as possible in your application's startup, before doing +// any API work, since some configuration functionality etc has to be done also before +// initialising any APIs. +// +// Parameters: +// version is a single value from the RENDERDOC_Version above. +// +// outAPIPointers will be filled out with a pointer to the corresponding struct of function +// pointers. +// +// Returns: +// 1 - if the outAPIPointers has been filled with a pointer to the API struct requested +// 0 - if the requested version is not supported or the arguments are invalid. +// +typedef int(RENDERDOC_CC *pRENDERDOC_GetAPI)(RENDERDOC_Version version, void **outAPIPointers); + +#ifdef __cplusplus +} // extern "C" +#endif \ No newline at end of file diff --git a/BMEdit/Editor/UI/Include/BMEditMainWindow.h b/BMEdit/Editor/UI/Include/BMEditMainWindow.h index 2478566..d0f917a 100644 --- a/BMEdit/Editor/UI/Include/BMEditMainWindow.h +++ b/BMEdit/Editor/UI/Include/BMEditMainWindow.h @@ -6,11 +6,15 @@ #include #include #include +#include #include #include +#include + #include "LoadSceneProgressDialog.h" +#include "ViewTexturesDialog.h" namespace Ui { @@ -22,9 +26,8 @@ namespace models class SceneObjectPropertiesModel; class SceneObjectsTreeModel; class ScenePropertiesModel; - class ScenePrimitivesModel; - class ScenePrimitivesFilterModel; class SceneFilterModel; + class SceneTexturesModel; } namespace delegates @@ -58,9 +61,8 @@ class BMEditMainWindow : public QMainWindow void initProperties(); void initSceneProperties(); void initControllers(); - void initScenePrimitives(); void initSceneLoadingDialog(); - void resetPrimitivesFilter(); + void initViewTexturesDialog(); public slots: void onExit(); @@ -78,8 +80,18 @@ public slots: void onAssetExportFailed(const QString &reason); void onCloseLevel(); void onExportPRP(); + void onExportLOC(); + void onShowTexturesDialog(); void onContextMenuRequestedForSceneTreeNode(const QPoint& point); - void onContextMenuRequestedForPrimitivesTableHeader(const QPoint& point); + void onLevelAssetsLoaded(); + void onLevelAssetsLoadFailed(const QString& reason); + void onSceneObjectPropertyChanged(const gamelib::scene::SceneObject* geom); + void onTextureChanged(uint32_t textureIndex); + void onSceneFramePresented(const widgets::RenderStats& stats); + +protected: + void dragEnterEvent(QDragEnterEvent* pEvent) override; + void dropEvent(QDropEvent* pEvent) override; private: // UI @@ -88,16 +100,15 @@ public slots: // Custom QLabel* m_operationLabel; QLabel* m_operationCommentLabel; + QLabel* m_renderStatsLabel; QProgressBar* m_operationProgress; // Models QStringListModel *m_geomTypesModel { nullptr }; - models::SceneObjectsTreeModel *m_sceneTreeModel { nullptr }; models::SceneFilterModel* m_sceneTreeFilterModel { nullptr }; models::SceneObjectPropertiesModel *m_sceneObjectPropertiesModel { nullptr }; models::ScenePropertiesModel *m_scenePropertiesModel { nullptr }; - models::ScenePrimitivesModel *m_scenePrimitivesModel { nullptr }; - models::ScenePrimitivesFilterModel *m_scenePrimitivesFilterModel { nullptr }; + QScopedPointer m_sceneTexturesModel { nullptr }; // Delegates delegates::TypePropertyItemDelegate *m_typePropertyItemDelegate { nullptr }; @@ -105,6 +116,7 @@ public slots: // Dialogs LoadSceneProgressDialog m_loadSceneDialog; + ViewTexturesDialog m_viewTexturesDialog; // Selection std::optional m_selectedPrimitiveToPreview; diff --git a/BMEdit/Editor/UI/Include/ImportTextureDialog.h b/BMEdit/Editor/UI/Include/ImportTextureDialog.h new file mode 100644 index 0000000..08879d9 --- /dev/null +++ b/BMEdit/Editor/UI/Include/ImportTextureDialog.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include + + +namespace Ui { +class ImportTextureDialog; +} + +class ImportTextureDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ImportTextureDialog(QWidget *parent = nullptr); + ~ImportTextureDialog(); + + // Setters + void resetState(); + void setSourcePath(const QString& sourcePath); + void setTextureName(const QString& textureName); + void setMIPLevelsCount(uint8_t count); + + // Getters + [[nodiscard]] const QString& getSourceFilePath() const; + [[nodiscard]] const QString& getTextureName() const; + [[nodiscard]] gamelib::tex::TEXEntryType getTargetFormat() const; + [[nodiscard]] uint8_t getTargetMIPLevels() const; + +private: + Ui::ImportTextureDialog *ui; + + // State + QString m_sourcePath {}; + QString m_textureName {}; + gamelib::tex::TEXEntryType m_entryType { gamelib::tex::TEXEntryType::ET_BITMAP_32 }; + uint8_t m_mipLevels { 1u }; +}; diff --git a/BMEdit/Editor/UI/Include/SelectLocalizationTool.h b/BMEdit/Editor/UI/Include/SelectLocalizationTool.h new file mode 100644 index 0000000..469adf5 --- /dev/null +++ b/BMEdit/Editor/UI/Include/SelectLocalizationTool.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + + +class Frontend_SelectLocalizationTool final : public widgets::TypePropertyWidget +{ + Q_OBJECT +public: + Frontend_SelectLocalizationTool(QWidget* parent, widgets::TypePropertyWidget* pTarget); + ~Frontend_SelectLocalizationTool() override; + + void setValue(const types::QGlacierValue &value) override; + [[nodiscard]] const types::QGlacierValue &getValue() const override; + + bool canHookFocus() const override; + +private slots: + void onTargetValueChanged(); + void onTargetEditFinished(); + +private: + void commitValue(const types::QGlacierValue &value); + +private: + widgets::TypePropertyWidget* m_pTarget { nullptr }; + QLabel* m_pLabel { nullptr }; +}; + + +namespace Ui { + class SelectLocalizationTool; +} + +class SelectLocalizationTool : public widgets::TypePropertyWidget +{ + Q_OBJECT + +public: + explicit SelectLocalizationTool(QWidget *parent = nullptr); + ~SelectLocalizationTool(); + + static Frontend_SelectLocalizationTool* Create(QWidget* parent); + + void setValue(const types::QGlacierValue &value) override; + +protected: + // buildLayout and updateLayout not implemented because no dynamic layout here. I'm just handling setValue + void closeEvent(QCloseEvent* pEvent) override; + +private: + void disableAcceptButton(); + void enableAcceptButton(); + void selectByPath(const QString& path); + +private slots: + void onAccepted(); + void onRejected(); + void onLocaleSelected(const QItemSelection &selected, const QItemSelection &deselected); + +private: + Ui::SelectLocalizationTool *m_ui; +}; \ No newline at end of file diff --git a/BMEdit/Editor/UI/Include/SelectSceneObjectTool.h b/BMEdit/Editor/UI/Include/SelectSceneObjectTool.h new file mode 100644 index 0000000..3e849c7 --- /dev/null +++ b/BMEdit/Editor/UI/Include/SelectSceneObjectTool.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + + +class Frontend_SelectSceneObjectTool final : public widgets::TypePropertyWidget +{ + Q_OBJECT +public: + Frontend_SelectSceneObjectTool(QWidget* parent, widgets::TypePropertyWidget* pTarget); + ~Frontend_SelectSceneObjectTool() override; + + void setValue(const types::QGlacierValue &value) override; + [[nodiscard]] const types::QGlacierValue &getValue() const override; + + bool canHookFocus() const override; + +private: + void commitValue(const types::QGlacierValue &value); + +private slots: + void onTargetValueChanged(); + void onTargetEditFinished(); + +private: + widgets::TypePropertyWidget* m_pTarget { nullptr }; + QLabel* m_pLabel { nullptr }; +}; + + +namespace Ui { + class SelectSceneObjectTool; +} + +class SelectSceneObjectTool final : public widgets::TypePropertyWidget +{ + Q_OBJECT +public: + explicit SelectSceneObjectTool(QWidget* parent = nullptr); + ~SelectSceneObjectTool() override; + + void setValue(const types::QGlacierValue &value) override; + + static Frontend_SelectSceneObjectTool* Create(QWidget* parent); + +private: + void selectByPath(const QString& path); + +private slots: + void onAccepted(); + +protected: + // buildLayout and updateLayout not implemented because no dynamic layout here. I'm just handling setValue + void closeEvent(QCloseEvent* pEvent) override; + +private: + Ui::SelectSceneObjectTool *m_ui; +}; \ No newline at end of file diff --git a/BMEdit/Editor/UI/Include/SelectScriptTool.h b/BMEdit/Editor/UI/Include/SelectScriptTool.h new file mode 100644 index 0000000..7e32768 --- /dev/null +++ b/BMEdit/Editor/UI/Include/SelectScriptTool.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + + +class Frontend_SelectScriptTool final : public widgets::TypePropertyWidget +{ + Q_OBJECT +public: + Frontend_SelectScriptTool(QWidget* parent, widgets::TypePropertyWidget* pTarget); + ~Frontend_SelectScriptTool() override; + + void setValue(const types::QGlacierValue &value) override; + [[nodiscard]] const types::QGlacierValue &getValue() const override; + + bool canHookFocus() const override; + +private slots: + void onTargetValueChanged(); + void onTargetEditFinished(); + +private: + void commitValue(const types::QGlacierValue &value); + +private: + widgets::TypePropertyWidget* m_pTarget { nullptr }; + QLabel* m_pLabel { nullptr }; +}; + + +namespace Ui { + class SelectScriptTool; +} + +class SelectScriptTool : public widgets::TypePropertyWidget +{ + Q_OBJECT + +public: + explicit SelectScriptTool(QWidget *parent = nullptr); + ~SelectScriptTool(); + + static Frontend_SelectScriptTool* Create(QWidget* parent); + + void setValue(const types::QGlacierValue &value) override; + +protected: + // buildLayout and updateLayout not implemented because no dynamic layout here. I'm just handling setValue + void closeEvent(QCloseEvent* pEvent) override; + +private: + void disableAcceptButton(); + void enableAcceptButton(); + void selectByPath(const QString& path); + +private slots: + void onAccepted(); + void onRejected(); + void onScriptSelected(const QItemSelection &selected, const QItemSelection &deselected); + +private: + Ui::SelectScriptTool *m_ui; +}; \ No newline at end of file diff --git a/BMEdit/Editor/UI/Include/ViewTexturesDialog.h b/BMEdit/Editor/UI/Include/ViewTexturesDialog.h new file mode 100644 index 0000000..3521962 --- /dev/null +++ b/BMEdit/Editor/UI/Include/ViewTexturesDialog.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace Ui { +class ViewTexturesDialog; +} + +namespace models +{ +class SceneTexturesModel; +class SceneTextureFilterModel; +} + +class ImportTextureDialog; + + +class ViewTexturesDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ViewTexturesDialog(QWidget *parent = nullptr); + ~ViewTexturesDialog(); + + void setTexturesSource(models::SceneTexturesModel *model); + +signals: + void textureChanged(uint32_t textureIndex); + +protected: + void showEvent(QShowEvent *event) override; + +private slots: + void onCurrentMipLevelChanged(int mipIndex); + void onExportTEXFile(); + void onExportCurrentTextureToFile(); + void onReplaceCurrentTexture(); + void onTextureToImportSpecified(); + +private: + void setPreview(uint32_t textureIndex, const std::optional& mipLevel = std::nullopt); + void setPreview(QPixmap &&image); + void resetPreview(); + void resetAvailableMIPs(uint32_t textureIndex); + void clearAvailableMIPs(); + +private: + [[nodiscard]] std::optional getActiveTexture() const; + [[nodiscard]] std::optional getActiveMIPLevel() const; + +private: + Ui::ViewTexturesDialog *ui; + QScopedPointer m_filterModel; + QScopedPointer m_texturePreviewWidgetContextMenu; + QScopedPointer m_importDialog; +}; diff --git a/BMEdit/Editor/UI/Source/BMEditMainWindow.cpp b/BMEdit/Editor/UI/Source/BMEditMainWindow.cpp index ff9ae6e..64d3d52 100644 --- a/BMEdit/Editor/UI/Source/BMEditMainWindow.cpp +++ b/BMEdit/Editor/UI/Source/BMEditMainWindow.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include #include @@ -18,9 +21,10 @@ #include #include #include -#include -#include #include +#include +#include +#include #include #include @@ -46,7 +50,8 @@ enum OperationToProgress : int BMEditMainWindow::BMEditMainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::BMEditMainWindow), - m_loadSceneDialog(this) + m_loadSceneDialog(this), + m_viewTexturesDialog(this) { ui->setupUi(this); @@ -54,14 +59,17 @@ BMEditMainWindow::BMEditMainWindow(QWidget *parent) : initProperties(); initControllers(); initSceneProperties(); - initScenePrimitives(); initSceneLoadingDialog(); + initViewTexturesDialog(); initStatusBar(); initSearchInput(); connectActions(); connectDockWidgetActions(); connectEditorSignals(); loadTypesDataBase(); + + // Drag & Drop for levels + setAcceptDrops(true); } BMEditMainWindow::~BMEditMainWindow() @@ -69,9 +77,10 @@ BMEditMainWindow::~BMEditMainWindow() delete m_geomTypesModel; delete m_typePropertyItemDelegate; delete m_sceneTreeFilterModel; - delete m_sceneTreeModel; + models::ModelsLocator::s_SceneTreeModel = nullptr; // reset me + models::ModelsLocator::s_GameScriptsTreeModel = nullptr; // reset me + models::ModelsLocator::s_LocalizationTreeModel = nullptr; // reset me delete m_sceneObjectPropertiesModel; - delete m_scenePrimitivesModel; delete m_operationProgress; delete m_operationLabel; @@ -84,12 +93,14 @@ void BMEditMainWindow::initStatusBar() m_operationProgress = new QProgressBar(statusBar()); m_operationLabel = new QLabel(statusBar()); m_operationCommentLabel = new QLabel(statusBar()); + m_renderStatsLabel = new QLabel(statusBar()); resetStatusToDefault(); statusBar()->insertWidget(0, m_operationLabel); statusBar()->insertWidget(1, m_operationProgress); statusBar()->insertWidget(2, m_operationCommentLabel); + statusBar()->insertWidget(3, m_renderStatsLabel); } void BMEditMainWindow::initSearchInput() @@ -99,12 +110,47 @@ void BMEditMainWindow::initSearchInput() void BMEditMainWindow::connectActions() { - connect(ui->actionExit, &QAction::triggered, [=]() { onExit(); }); - connect(ui->actionOpen_level, &QAction::triggered, [=]() { onOpenLevel(); }); - connect(ui->actionRestore_layout, &QAction::triggered, [=]() { onRestoreLayout(); }); - connect(ui->actionTypes_Viewer, &QAction::triggered, [=]() { onShowTypesViewer(); }); - connect(ui->actionSave_properties, &QAction::triggered, [=]() { onExportProperties(); }); - connect(ui->actionExport_PRP_properties, &QAction::triggered, [=]() { onExportPRP(); }); + connect(ui->actionExit, &QAction::triggered, this, &BMEditMainWindow::onExit); + connect(ui->actionOpen_level, &QAction::triggered, this, &BMEditMainWindow::onOpenLevel); + connect(ui->actionRestore_layout, &QAction::triggered, this, &BMEditMainWindow::onRestoreLayout); + connect(ui->actionTypes_Viewer, &QAction::triggered, this, &BMEditMainWindow::onShowTypesViewer); + connect(ui->actionSave_properties, &QAction::triggered, this, &BMEditMainWindow::onExportProperties); + connect(ui->actionExport_PRP_properties, &QAction::triggered, this, &BMEditMainWindow::onExportPRP); + connect(ui->actionExport_LOC_localization, &QAction::triggered, this, &BMEditMainWindow::onExportLOC); + connect(ui->actionTextures, &QAction::triggered, this, &BMEditMainWindow::onShowTexturesDialog); + connect(ui->actionView_whole_scene, &QAction::triggered, this, [this] { ui->sceneGLView->setWorldViewMode(); }); + + // Modes + connect(ui->actionRenderMode_Texture, &QAction::toggled, this, [this](bool val) { + widgets::RenderModeFlags newMode = ui->sceneGLView->getRenderMode(); + + if (val) + newMode |= widgets::RenderMode::RM_TEXTURE; + else + newMode &= ~widgets::RenderMode::RM_TEXTURE; + + ui->sceneGLView->setRenderMode(newMode); + }); + connect(ui->actionRenderMode_Wireframe, &QAction::toggled, this, [this](bool val) { + widgets::RenderModeFlags newMode = ui->sceneGLView->getRenderMode(); + + if (val) + newMode |= widgets::RenderMode::RM_WIREFRAME; + else + newMode &= ~widgets::RenderMode::RM_WIREFRAME; + + ui->sceneGLView->setRenderMode(newMode); + }); + connect(ui->actionRenderMode_Portals, &QAction::toggled, this, [this](bool val) { + ui->sceneGLView->setShouldRenderPortals(val); + }); + connect(ui->actionRenderMode_RenderRoomBoundingBoxes, &QAction::toggled, this, [this](bool val) { + ui->sceneGLView->setShouldRenderRoomBoundingBox(val); + }); + connect(ui->sceneGLView, &widgets::SceneRenderWidget::worldSelectionChanged, this, [this](const std::vector& hitList) { + // On hit performed we need to react somehow + // TODO: Do something here + }); } void BMEditMainWindow::connectDockWidgetActions() @@ -140,7 +186,7 @@ void BMEditMainWindow::connectDockWidgetActions() }); // Properties - connect(ui->propertiesDock, &QDockWidget::visibilityChanged, [=](bool visibility) + connect(ui->gameObjectDock, &QDockWidget::visibilityChanged, [=](bool visibility) { QSignalBlocker actionPropertiesBlocker{ui->actionProperties}; @@ -149,9 +195,9 @@ void BMEditMainWindow::connectDockWidgetActions() connect(ui->actionProperties, &QAction::triggered, [=](bool checked) { - QSignalBlocker propertiesDockBlocker{ui->propertiesDock}; + QSignalBlocker propertiesDockBlocker{ui->gameObjectDock}; - ui->propertiesDock->setVisible(checked); + ui->gameObjectDock->setVisible(checked); }); } @@ -192,9 +238,9 @@ void BMEditMainWindow::onOpenLevel() } void BMEditMainWindow::onRestoreLayout() { - ui->propertiesDock->setVisible(true); + ui->gameObjectDock->setVisible(true); ui->sceneDock->setVisible(true); - ui->propertiesDock->setVisible(true); + ui->gameObjectDock->setVisible(true); } void BMEditMainWindow::onShowTypesViewer() @@ -207,16 +253,37 @@ void BMEditMainWindow::onShowTypesViewer() void BMEditMainWindow::onLevelLoadSuccess() { auto currentLevel = editor::EditorInstance::getInstance().getActiveLevel(); - setWindowTitle(QString("BMEdit - %1").arg(QString::fromStdString(currentLevel->getLevelName()))); + setWindowTitle(QString("BMEdit - %1 [loading view...]").arg(QString::fromStdString(currentLevel->getLevelName()))); resetStatusToDefault(); // Level loaded, show objects tree ui->searchInputField->clear(); + ui->actionView_whole_scene->setEnabled(true); + ui->actionView_whole_scene->setChecked(true); + ui->actionRenderMode_Texture->setEnabled(true); + ui->actionRenderMode_Texture->setChecked(true); + ui->actionRenderMode_Wireframe->setEnabled(true); + ui->actionRenderMode_Wireframe->setChecked(false); + ui->actionRenderMode_Portals->setEnabled(true); + ui->actionRenderMode_Portals->setChecked(ui->sceneGLView->shouldRenderPortals()); + ui->actionRenderMode_RenderRoomBoundingBoxes->setEnabled(true); + ui->actionRenderMode_RenderRoomBoundingBoxes->setChecked(ui->sceneGLView->shouldRenderRoomBoundingBox()); + + // Setup models + if (models::ModelsLocator::s_SceneTreeModel) + { + models::ModelsLocator::s_SceneTreeModel->setLevel(currentLevel); + } - if (m_sceneTreeModel) + if (models::ModelsLocator::s_LocalizationTreeModel) { - m_sceneTreeModel->setLevel(currentLevel); + models::ModelsLocator::s_LocalizationTreeModel->setLevel(currentLevel); + } + + if (m_sceneTexturesModel) + { + m_sceneTexturesModel->setLevel(currentLevel); } if (m_sceneObjectPropertiesModel) @@ -229,24 +296,18 @@ void BMEditMainWindow::onLevelLoadSuccess() m_scenePropertiesModel->setLevel(const_cast(currentLevel)); } - if (m_scenePrimitivesModel) - { - m_scenePrimitivesModel->setLevel(const_cast(currentLevel)); - ui->primitivesCountLabel->setText(QString("%1").arg(currentLevel->getLevelGeometry()->header.countOfPrimitives)); - } - - // Reset primitive filters - resetPrimitivesFilter(); - - // Setup preview - ui->scenePrimitivePreview->setLevel(const_cast(currentLevel)); + // Show game scene (start loading process) + ui->sceneGLView->setLevel(const_cast(currentLevel)); // Load controllers index - ui->geomControllers->switchToDefaults(); + ui->geomControllers->resetGeom(); // FIRST: Reset geom, save or revert changes + ui->geomControllers->switchToDefaults(); // SECOND: Drop value // Export action ui->menuExport->setEnabled(true); ui->actionExport_PRP_properties->setEnabled(true); + ui->actionExport_LOC_localization->setEnabled(true); + ui->actionTextures->setEnabled(true); //ui->actionSave_properties->setEnabled(true); //TODO: Uncomment when exporter to ZIP will be done ui->searchInputField->setEnabled(true); @@ -260,6 +321,7 @@ void BMEditMainWindow::onLevelLoadFailed(const QString &reason) QMessageBox::warning(this, QString("Failed to load level"), QString("Error occurred during level load process:\n%1").arg(reason)); m_operationCommentLabel->setText(QString("Failed to open level '%1'").arg(reason)); m_operationProgress->setValue(0); + //TODO: Need to reset global state properly! } void BMEditMainWindow::onLevelLoadProgressChanged(int totalPercentsProgress, const QString ¤tOperationTag) @@ -301,15 +363,37 @@ void BMEditMainWindow::onSelectedSceneObject(const gamelib::scene::SceneObject* ui->sceneObjectName->setText(QString::fromStdString(selectedSceneObject->getName())); ui->sceneObjectTypeCombo->setEnabled(true); ui->sceneObjectTypeCombo->setCurrentText(QString::fromStdString(selectedSceneObject->getType()->getName())); + ui->geomInstanceId->setText(QString("%1").arg(selectedSceneObject->getGeomInfo().getInstanceId())); + + ui->sceneGLView->setSelectedObject(const_cast(selectedSceneObject)); m_sceneObjectPropertiesModel->setGeom(const_cast(selectedSceneObject)); ui->geomControllers->setGeom(const_cast(selectedSceneObject)); ui->geomControllers->switchToFirstController(); + + // Show coli bits and other ZGEOM stuff + ui->coliBitsRepr->setPossibleValues({ + { "Bit 0", 0 }, + { "Bit 1", 1 }, + { "Bit 2", 2 }, + { "Bit 3", 3 }, + { "Bit 4", 4 }, + { "Bit 5", 5 }, + { "Bit 6", 6 }, + { "Bit 7", 7 } + }); + + ui->coliBitsRepr->setValue(selectedSceneObject->getGeomInfo().getColiBits()); + ui->coliBitsRepr->setEnabled(true); } void BMEditMainWindow::onDeselectedSceneObject() { + ui->sceneGLView->resetSelectedObject(); + ui->coliBitsRepr->reset(); + ui->coliBitsRepr->setEnabled(false); + if (!m_sceneObjectPropertiesModel) { return; @@ -317,6 +401,7 @@ void BMEditMainWindow::onDeselectedSceneObject() ui->sceneObjectTypeCombo->setEnabled(false); ui->sceneObjectName->clear(); + ui->geomInstanceId->setText("0"); ui->geomControllers->resetGeom(); m_sceneObjectPropertiesModel->resetGeom(); @@ -344,16 +429,14 @@ void BMEditMainWindow::onAssetExportFailed(const QString &reason) void BMEditMainWindow::onCloseLevel() { // Cleanup models - if (m_sceneTreeModel) m_sceneTreeModel->resetLevel(); + if (models::ModelsLocator::s_SceneTreeModel) models::ModelsLocator::s_SceneTreeModel->resetLevel(); + if (models::ModelsLocator::s_LocalizationTreeModel) models::ModelsLocator::s_LocalizationTreeModel->resetLevel(); if (m_sceneObjectPropertiesModel) m_sceneObjectPropertiesModel->resetLevel(); if (m_scenePropertiesModel) m_scenePropertiesModel->resetLevel(); - if (m_scenePrimitivesModel) m_scenePrimitivesModel->resetLevel(); - - // Reset filters - resetPrimitivesFilter(); + if (m_sceneTexturesModel) m_sceneTexturesModel->resetLevel(); - // Reset - ui->scenePrimitivePreview->resetLevel(); + // Unload resources + ui->sceneGLView->resetLevel(); // Reset widget states ui->geomControllers->resetGeom(); @@ -361,9 +444,20 @@ void BMEditMainWindow::onCloseLevel() // Reset export menu ui->menuExport->setEnabled(false); ui->actionExport_PRP_properties->setEnabled(false); - - // Reset primitives counter - ui->primitivesCountLabel->setText("0"); + ui->actionExport_LOC_localization->setEnabled(false); + ui->actionTextures->setEnabled(false); + + // Reset world view mode + ui->actionView_whole_scene->setEnabled(false); + ui->actionView_whole_scene->setChecked(true); + ui->actionRenderMode_Texture->setEnabled(false); + ui->actionRenderMode_Texture->setChecked(true); + ui->actionRenderMode_Wireframe->setEnabled(false); + ui->actionRenderMode_Wireframe->setChecked(true); + ui->actionRenderMode_Portals->setEnabled(false); + ui->actionRenderMode_Portals->setChecked(ui->sceneGLView->shouldRenderPortals()); + ui->actionRenderMode_RenderRoomBoundingBoxes->setEnabled(false); + ui->actionRenderMode_RenderRoomBoundingBoxes->setChecked(ui->sceneGLView->shouldRenderRoomBoundingBox()); // Disable filtering QSignalBlocker blocker { ui->searchInputField }; @@ -394,9 +488,37 @@ void BMEditMainWindow::onExportPRP() QMessageBox::information(this, "Export PRP", QString("PRP file exported successfully to %1").arg(saveAsPath)); } +void BMEditMainWindow::onExportLOC() +{ + QFileDialog saveLOCDialog(this, QString("Save LOC"), QString(), QString("Localization (*.LOC)")); + saveLOCDialog.setViewMode(QFileDialog::ViewMode::Detail); + saveLOCDialog.setFileMode(QFileDialog::FileMode::AnyFile); + saveLOCDialog.setAcceptMode(QFileDialog::AcceptMode::AcceptSave); + saveLOCDialog.selectFile(QString("%1.LOC").arg(QString::fromStdString(editor::EditorInstance::getInstance().getActiveLevel()->getLevelName()))); + if (!saveLOCDialog.exec()) + { + return; + } + + if (saveLOCDialog.selectedFiles().empty()) + { + return; + } + + const auto saveAsPath = saveLOCDialog.selectedFiles().first(); + editor::EditorInstance::getInstance().exportLOC(saveAsPath); + + QMessageBox::information(this, "Export LOC", QString("LOC file exported successfully to %1").arg(saveAsPath)); +} + +void BMEditMainWindow::onShowTexturesDialog() +{ + m_viewTexturesDialog.show(); +} + void BMEditMainWindow::onContextMenuRequestedForSceneTreeNode(const QPoint& point) { - if (!m_sceneTreeModel) + if (!models::ModelsLocator::s_SceneTreeModel) { return; } @@ -433,40 +555,111 @@ void BMEditMainWindow::onContextMenuRequestedForSceneTreeNode(const QPoint& poin QGuiApplication::clipboard()->setText(finalPath); }; + auto implMoveCameraToGeom = [this](gamelib::scene::SceneObject* sceneObject) + { + const glm::vec3 vPosition { + sceneObject->getProperties()["Position"][1].getOperand().get(), + sceneObject->getProperties()["Position"][2].getOperand().get(), + sceneObject->getProperties()["Position"][3].getOperand().get() + }; + + ui->sceneGLView->moveCameraTo(vPosition); + }; + + auto implShowSelectedGeom = [this](gamelib::scene::SceneObject* sceneObject) + { + ui->actionView_whole_scene->setChecked(false); + ui->sceneGLView->setGeomViewMode(sceneObject); + }; + contextMenu.addAction(QString("Object: '%1'").arg(QString::fromStdString(selectedGeom->getName())))->setDisabled(true); contextMenu.addAction(QString("Type: '%1'").arg(QString::fromStdString(selectedGeom->getType()->getName())))->setDisabled(true); + contextMenu.addSeparator(); contextMenu.addAction("Copy path", [implCopyPathToGeom, selectedGeom] { implCopyPathToGeom(selectedGeom, false); }); contextMenu.addAction("Copy path (ignore ROOT)", [implCopyPathToGeom, selectedGeom] { implCopyPathToGeom(selectedGeom, true); }); + contextMenu.addSeparator(); + contextMenu.addAction("Move camera to this object", [implMoveCameraToGeom, selectedGeom] { implMoveCameraToGeom(const_cast(selectedGeom)); }); + contextMenu.addAction("Show only this geom", [implShowSelectedGeom, selectedGeom] { implShowSelectedGeom(const_cast(selectedGeom)); }); + contextMenu.exec(ui->sceneTreeView->viewport()->mapToGlobal(point)); } } -void BMEditMainWindow::onContextMenuRequestedForPrimitivesTableHeader(const QPoint &point) +void BMEditMainWindow::onLevelAssetsLoaded() { - QMenu contextMenu; + auto currentLevel = editor::EditorInstance::getInstance().getActiveLevel(); + setWindowTitle(QString("BMEdit - %1 [DONE]").arg(QString::fromStdString(currentLevel->getLevelName()))); +} -#define BE_CONFIGURE_ACTION(actName, actFmt) \ - { \ - auto action = contextMenu.addAction(actName); \ - action->setCheckable(true); \ - action->setChecked(m_scenePrimitivesFilterModel->isVertexFormatAllowed(actFmt)); \ - connect(action, &QAction::toggled, [this](bool val) { m_scenePrimitivesFilterModel->setVertexFormatAllowed(actFmt, val); }); \ +void BMEditMainWindow::onLevelAssetsLoadFailed(const QString& reason) +{ + auto currentLevel = editor::EditorInstance::getInstance().getActiveLevel(); + setWindowTitle(QString("BMEdit - %1 [!!!ERROR!!!]").arg(QString::fromStdString(currentLevel->getLevelName()))); + + QMessageBox::critical(this, QString("Scene render failed :("), QString("An error occurred while loading scene assets:\n%1").arg(reason)); +} + +void BMEditMainWindow::onSceneObjectPropertyChanged(const gamelib::scene::SceneObject* geom) +{ + ui->sceneGLView->onObjectMoved(const_cast(geom)); +} + +void BMEditMainWindow::onTextureChanged(uint32_t textureIndex) +{ + ui->sceneGLView->reloadTexture(textureIndex); +} + +void BMEditMainWindow::onSceneFramePresented(const widgets::RenderStats& stats) +{ + const int iApproxFPS = static_cast(std::floorf(1.f / stats.fFrameTime)); + + m_renderStatsLabel->setText(QString("ROOM: %1 | Visible objects: %2 | Rejected objects: %3 | FPS: %4") + .arg(stats.currentRoom) + .arg(stats.allowedObjects) + .arg(stats.rejectedObjects) + .arg(iApproxFPS)); +} + +void BMEditMainWindow::dragEnterEvent(QDragEnterEvent *pEvent) +{ + if (pEvent->mimeData()->hasUrls()) + { + for (const QUrl& url : pEvent->mimeData()->urls()) + { + if (QFileInfo(url.toLocalFile()).suffix().toLower() == "zip") + { + pEvent->acceptProposedAction(); + return; + } + } } +} - BE_CONFIGURE_ACTION("Vertex Format 10", gamelib::prm::PRMVertexBufferFormat::VBF_VERTEX_10); - BE_CONFIGURE_ACTION("Vertex Format 24", gamelib::prm::PRMVertexBufferFormat::VBF_VERTEX_24); - BE_CONFIGURE_ACTION("Vertex Format 28", gamelib::prm::PRMVertexBufferFormat::VBF_VERTEX_28); - BE_CONFIGURE_ACTION("Vertex Format 34", gamelib::prm::PRMVertexBufferFormat::VBF_VERTEX_34); +void BMEditMainWindow::dropEvent(QDropEvent *pEvent) +{ + if (pEvent->mimeData()->hasUrls()) + { + QString info; - contextMenu.exec(ui->scenePrimitivesTable->horizontalHeader()->viewport()->mapToGlobal(point)); + for (const QUrl& url : pEvent->mimeData()->urls()) + { + QString fileName = url.toLocalFile(); + if (QFileInfo(fileName).suffix().toLower() == "zip") + { + editor::EditorInstance::getInstance().openLevelFromZIP(fileName.toStdString()); + return; + } + } + } } void BMEditMainWindow::loadTypesDataBase() { m_operationProgress->setValue(OperationToProgress::DISCOVER_TYPES_DATABASE); + // TODO: Move this code to another place!!! gamelib::TypeRegistry::getInstance().reset(); QFile typeRegistryFile("TypesRegistry.json"); @@ -488,7 +681,7 @@ void BMEditMainWindow::loadTypesDataBase() m_operationProgress->setValue(OperationToProgress::DATABASE_PARSED); - if (!registryFile.contains("inc") || !registryFile.contains("db")) + if (!registryFile.contains("inc") || !registryFile.contains("db") || !registryFile.contains("script_incs")) { m_operationCommentLabel->setText("Invalid types database format"); return; @@ -504,6 +697,7 @@ void BMEditMainWindow::loadTypesDataBase() } const auto incPath = registryFile["inc"].get(); + const auto scriptsPath = registryFile["script_incs"].get(); m_operationProgress->setValue(OperationToProgress::LOADING_TYPE_DESCRIPTORS); m_operationCommentLabel->setText(QString("Hash indices loaded (%1), loading types from '%2' folder").arg(typesToHashes.size()).arg(QString::fromStdString(incPath))); @@ -536,13 +730,56 @@ void BMEditMainWindow::loadTypesDataBase() } } + // Here we need to lookup for script declarations and parse them + std::unordered_map scriptInfos; + QDirIterator scriptInfoFolderIterator(QString::fromStdString(scriptsPath), { "*.json" }, QDir::Files); + while (scriptInfoFolderIterator.hasNext()) + { + auto path = scriptInfoFolderIterator.next(); + + QFile scriptDescriptionFile(path); + if (!scriptDescriptionFile.open(QIODevice::ReadOnly)) + { + m_operationCommentLabel->setText(QString("ERROR: Failed to open file '%1'").arg(path)); + return; + } + + auto scriptInfoContents = scriptDescriptionFile.readAll().toStdString(); + scriptDescriptionFile.close(); + + nlohmann::json jContents = nlohmann::json::parse(scriptInfoContents, nullptr, false, true); + if (jContents.is_discarded()) + { + qWarning() << "Failed to parse " << path << " (script def)"; + continue; + } + + for (const auto& [key, data] : jContents.items()) + { + if (scriptInfos.contains(key)) + { + qWarning() << "Duplicate script name " << QString::fromStdString(key) << " in " << path << " (script def)"; + continue; + } + + scriptInfos[key] = data; + } + } + try { + // register common types registry.registerTypes(std::move(typeInfos), std::move(typesToHashes)); + // register script extensions (extra types) + registry.registerScripts(std::move(scriptInfos)); + QStringList allAvailableTypes; gamelib::TypeRegistry::getInstance().forEachType([&allAvailableTypes](const gamelib::Type *type) { allAvailableTypes.push_back(QString::fromStdString(type->getName())); }); + // Runtime types + models::ModelsLocator::s_GameScriptsTreeModel = std::make_unique(this); + delete m_geomTypesModel; m_geomTypesModel = new QStringListModel(allAvailableTypes, this); ui->sceneObjectTypeCombo->setModel(m_geomTypesModel); @@ -566,16 +803,23 @@ void BMEditMainWindow::resetStatusToDefault() { m_operationLabel->setText("Progress: "); m_operationCommentLabel->setText("(No active operation)"); + m_renderStatsLabel->setText("[No render stats]"); m_operationProgress->setValue(0); } void BMEditMainWindow::initSceneTree() { // Main model - m_sceneTreeModel = new models::SceneObjectsTreeModel(this); + models::ModelsLocator::s_SceneTreeModel = std::make_unique(this); + models::ModelsLocator::s_LocalizationTreeModel = std::make_unique(this); m_sceneTreeFilterModel = new models::SceneFilterModel(this); - m_sceneTreeFilterModel->setSourceModel(m_sceneTreeModel); + m_sceneTreeFilterModel->setSourceModel(models::ModelsLocator::s_SceneTreeModel.get()); + + // Fill LocalizationTree view + ui->localizationTree->setModel(models::ModelsLocator::s_LocalizationTreeModel.get()); + ui->localizationTree->setSelectionMode(QAbstractItemView::SingleSelection); + // Fill SceneTree view ui->sceneTreeView->header()->setSectionResizeMode(QHeaderView::Stretch); ui->sceneTreeView->setModel(m_sceneTreeFilterModel); ui->sceneTreeView->setContextMenuPolicy(Qt::CustomContextMenu); @@ -599,6 +843,10 @@ void BMEditMainWindow::initSceneTree() }); connect(ui->sceneTreeView, &QTreeView::customContextMenuRequested, this, &BMEditMainWindow::onContextMenuRequestedForSceneTreeNode); + + connect(ui->sceneGLView, &widgets::SceneRenderWidget::resourcesReady, this, &BMEditMainWindow::onLevelAssetsLoaded); + connect(ui->sceneGLView, &widgets::SceneRenderWidget::resourceLoadFailed, this, &BMEditMainWindow::onLevelAssetsLoadFailed); + connect(ui->sceneGLView, &widgets::SceneRenderWidget::frameReady, this, &BMEditMainWindow::onSceneFramePresented); } void BMEditMainWindow::initProperties() @@ -610,6 +858,8 @@ void BMEditMainWindow::initProperties() ui->propertiesView->setItemDelegateForColumn(1, m_typePropertyItemDelegate); ui->propertiesView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); ui->propertiesView->verticalHeader()->setSectionResizeMode(QHeaderView::Interactive); + + connect(m_sceneObjectPropertiesModel, &models::SceneObjectPropertiesModel::objectPropertiesChanged, this, &BMEditMainWindow::onSceneObjectPropertyChanged); } void BMEditMainWindow::initSceneProperties() @@ -629,60 +879,6 @@ void BMEditMainWindow::initControllers() //TODO: Init this } -void BMEditMainWindow::initScenePrimitives() -{ - m_scenePrimitivesModel = new models::ScenePrimitivesModel(this); - m_scenePrimitivesFilterModel = new models::ScenePrimitivesFilterModel(this); - - m_scenePrimitivesFilterModel->setSourceModel(m_scenePrimitivesModel); - ui->scenePrimitivesTable->setModel(m_scenePrimitivesFilterModel); - ui->scenePrimitivesTable->setSelectionBehavior(QAbstractItemView::SelectItems); - ui->scenePrimitivesTable->setSelectionMode(QAbstractItemView::SingleSelection); - ui->scenePrimitivesTable->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); - ui->scenePrimitivesTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Stretch); - - connect(ui->scenePrimitivesTable->selectionModel(), &QItemSelectionModel::selectionChanged, [=](const QItemSelection &selected, const QItemSelection &deselected) { - if ((selected.indexes().size() == 1 && selected.indexes().at(0).row() != 0) || (!selected.indexes().empty())) - { - m_selectedPrimitiveToPreview.emplace(static_cast(selected.indexes().first().data(types::kChunkIndexRole).value())); - ui->scenePrimitivePreview->setPrimitiveIndex(*m_selectedPrimitiveToPreview); - } - else if (!deselected.indexes().isEmpty()) - { - m_selectedPrimitiveToPreview = std::nullopt; - } - - ui->exportChunk->setEnabled(m_selectedPrimitiveToPreview.has_value()); - }); - - connect(ui->exportChunk, &QPushButton::clicked, [=]() { - if (m_selectedPrimitiveToPreview.has_value()) - { - //TODO: Impl me - } - }); - - connect(ui->scenePrimitivesTable->horizontalHeader(), &QHeaderView::customContextMenuRequested, this, &BMEditMainWindow::onContextMenuRequestedForPrimitivesTableHeader); - - auto sendChangesToFilterModel = [=](models::ScenePrimitivesFilterEntry entry, int newState) - { - if (newState) - { - m_scenePrimitivesFilterModel->addFilterEntry(entry); - } - else - { - m_scenePrimitivesFilterModel->removeFilterEntry(entry); - } - }; - - connect(ui->primitivesFilter_UnknownPrimType, &QCheckBox::stateChanged, [=](int newState) { sendChangesToFilterModel(models::ScenePrimitivesFilterEntry::FilterAllow_Unknown, newState); }); - connect(ui->primitivesFilter_ZeroBufferPrimType, &QCheckBox::stateChanged, [=](int newState) { sendChangesToFilterModel(models::ScenePrimitivesFilterEntry::FilterAllow_Zero, newState); }); - connect(ui->primitivesFilter_DescriptionPrimType, &QCheckBox::stateChanged, [=](int newState) { sendChangesToFilterModel(models::ScenePrimitivesFilterEntry::FilterAllow_Description, newState); }); - connect(ui->primitivesFilter_IndexBufferPrimType, &QCheckBox::stateChanged, [=](int newState) { sendChangesToFilterModel(models::ScenePrimitivesFilterEntry::FilterAllow_Index, newState); }); - connect(ui->primitivesFilter_VertexBufferPrimType, &QCheckBox::stateChanged, [=](int newState) { sendChangesToFilterModel(models::ScenePrimitivesFilterEntry::FilterAllow_Vertex, newState); }); -} - void BMEditMainWindow::initSceneLoadingDialog() { m_loadSceneDialog.setFixedSize(m_loadSceneDialog.size()); @@ -690,24 +886,11 @@ void BMEditMainWindow::initSceneLoadingDialog() m_loadSceneDialog.setModal(true); } -void BMEditMainWindow::resetPrimitivesFilter() +void BMEditMainWindow::initViewTexturesDialog() { - if (m_scenePrimitivesFilterModel) - { - m_scenePrimitivesFilterModel->resetToDefaults(); - } - -#define BE_RESET_CHECK_BOX(x) \ - { \ - QSignalBlocker blk{x}; \ - x->setChecked(true); \ - } - - BE_RESET_CHECK_BOX(ui->primitivesFilter_UnknownPrimType); - BE_RESET_CHECK_BOX(ui->primitivesFilter_ZeroBufferPrimType); - BE_RESET_CHECK_BOX(ui->primitivesFilter_DescriptionPrimType); - BE_RESET_CHECK_BOX(ui->primitivesFilter_IndexBufferPrimType); - BE_RESET_CHECK_BOX(ui->primitivesFilter_VertexBufferPrimType); + m_sceneTexturesModel.reset(new models::SceneTexturesModel(this)); + m_viewTexturesDialog.setModal(true); + m_viewTexturesDialog.setTexturesSource(m_sceneTexturesModel.get()); -#undef BE_RESET_CHECK_BOX + connect(&m_viewTexturesDialog, &ViewTexturesDialog::textureChanged, this, &BMEditMainWindow::onTextureChanged); } \ No newline at end of file diff --git a/BMEdit/Editor/UI/Source/ImportTextureDialog.cpp b/BMEdit/Editor/UI/Source/ImportTextureDialog.cpp new file mode 100644 index 0000000..82c4645 --- /dev/null +++ b/BMEdit/Editor/UI/Source/ImportTextureDialog.cpp @@ -0,0 +1,161 @@ +#include "ImportTextureDialog.h" +#include "ui_ImportTextureDialog.h" +#include +#include +#include +#include + + +ImportTextureDialog::ImportTextureDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ImportTextureDialog) +{ + ui->setupUi(this); + + resetState(); + + connect(ui->selectSourceTextureButton, &QPushButton::clicked, [this]() { + QFileDialog openTextureDialog(this, QString("Select Texture...")); + openTextureDialog.setViewMode(QFileDialog::ViewMode::Detail); + openTextureDialog.setFileMode(QFileDialog::FileMode::AnyFile); + openTextureDialog.setAcceptMode(QFileDialog::AcceptMode::AcceptOpen); + openTextureDialog.setNameFilters({ "PNG Texture (*.png)", "JPG Texture (*.jpg)" }); + + if (!openTextureDialog.exec()) + { + return; + } + + if (openTextureDialog.selectedFiles().empty()) + { + return; + } + + QString textureToImport = openTextureDialog.selectedFiles().first(); + + // Try update preview + QImage image { textureToImport }; + if (image.isNull()) + { + // Bruh + QMessageBox::warning(this, "Import texture", QString("Failed to import texture '%1'. Unsupported format!").arg(textureToImport)); + return; + } + + ui->previewBox->setTitle(QString("Preview (%1;%2):").arg(image.width()).arg(image.height())); + ui->previewWidget->setPixmap(QPixmap::fromImage(std::move(image))); + + // And set up + setSourcePath(openTextureDialog.selectedFiles().first()); + }); + + connect(ui->importButton, &QPushButton::clicked, [this]() { + if (!m_sourcePath.isEmpty()) + { + accept(); + } + else + { + QMessageBox::warning(this, "Import texture", "You must specify source texture path before import!"); + } + }); + + connect(ui->destinationFormatCombo, &QComboBox::currentIndexChanged, [this](int index) { + m_entryType = static_cast(ui->destinationFormatCombo->itemData(index, Qt::UserRole).value()); + }); + + connect(ui->mipLevels, &QSpinBox::valueChanged, [this](int value) { + m_mipLevels = static_cast(value); + }); + + connect(ui->textureName, &QLineEdit::textChanged, [this](const QString& newName) { + m_textureName = newName; + }); +} + +ImportTextureDialog::~ImportTextureDialog() +{ + delete ui; +} + +void ImportTextureDialog::resetState() +{ + // Clear data + m_sourcePath = {}; + m_textureName = {}; + m_entryType = gamelib::tex::TEXEntryType::ET_BITMAP_32; + m_mipLevels = 1u; + + // Clear path + { + QSignalBlocker blocker { ui->sourcePathEdit }; + ui->sourcePathEdit->clear(); + } + + // Clear name + { + QSignalBlocker blocker { ui->textureName }; + ui->textureName->clear(); + } + + // Fill known and supported formats + { + QSignalBlocker blocker { ui->destinationFormatCombo }; + ui->destinationFormatCombo->clear(); + ui->destinationFormatCombo->addItem("RGBA", static_cast(gamelib::tex::TEXEntryType::ET_BITMAP_32)); + ui->destinationFormatCombo->addItem("I8", static_cast(gamelib::tex::TEXEntryType::ET_BITMAP_I8)); + ui->destinationFormatCombo->addItem("PALN", static_cast(gamelib::tex::TEXEntryType::ET_BITMAP_PAL)); + ui->destinationFormatCombo->addItem("U8V8", static_cast(gamelib::tex::TEXEntryType::ET_BITMAP_U8V8)); + ui->destinationFormatCombo->addItem("DXT1", static_cast(gamelib::tex::TEXEntryType::ET_BITMAP_DXT1)); + ui->destinationFormatCombo->addItem("DXT3", static_cast(gamelib::tex::TEXEntryType::ET_BITMAP_DXT3)); + } + + // Reset mip level + { + QSignalBlocker blocker { ui->mipLevels }; + ui->mipLevels->setValue(1); + } + + // Reset preview + ui->previewWidget->setPixmap(QPixmap()); + ui->previewBox->setTitle("Preview:"); +} + +void ImportTextureDialog::setSourcePath(const QString& sourcePath) +{ + m_sourcePath = sourcePath; + ui->sourcePathEdit->setText(m_sourcePath); +} + +void ImportTextureDialog::setTextureName(const QString& textureName) +{ + m_textureName = textureName; + ui->textureName->setText(textureName); +} + +void ImportTextureDialog::setMIPLevelsCount(uint8_t count) +{ + if (count > 0) + { + m_mipLevels = count; + ui->mipLevels->setValue(count); + } +} + +const QString& ImportTextureDialog::getSourceFilePath() const +{ + return m_sourcePath; +} + +const QString &ImportTextureDialog::getTextureName() const +{ + return m_textureName; +} + +gamelib::tex::TEXEntryType ImportTextureDialog::getTargetFormat() const +{ + return m_entryType; +} + +uint8_t ImportTextureDialog::getTargetMIPLevels() const +{ + return m_mipLevels; +} diff --git a/BMEdit/Editor/UI/Source/SelectLocalizationTool.cpp b/BMEdit/Editor/UI/Source/SelectLocalizationTool.cpp new file mode 100644 index 0000000..b0cee6a --- /dev/null +++ b/BMEdit/Editor/UI/Source/SelectLocalizationTool.cpp @@ -0,0 +1,267 @@ +#include "ui_SelectLocalizationTool.h" +#include +#include +#include +#include +#include +#include +#include + + +Frontend_SelectLocalizationTool::Frontend_SelectLocalizationTool(QWidget *parent, widgets::TypePropertyWidget* pTarget) + : widgets::TypePropertyWidget(parent) + , m_pTarget(pTarget) +{ + if (m_pTarget) + { + connect(m_pTarget, &widgets::TypePropertyWidget::valueChanged, this, &Frontend_SelectLocalizationTool::onTargetEditFinished); + connect(m_pTarget, &widgets::TypePropertyWidget::editFinished, this, &Frontend_SelectLocalizationTool::onTargetValueChanged); + + m_pTarget->setWindowModality(Qt::WindowModality::ApplicationModal); + m_pTarget->show(); + } + + // And build layout + auto* pLayout = new QHBoxLayout(this); + m_pLabel = new QLabel(this); + m_pLabel->setText("MAYBE"); + pLayout->addWidget(m_pLabel); + setLayout(pLayout); +} + +Frontend_SelectLocalizationTool::~Frontend_SelectLocalizationTool() +{ + m_pTarget = nullptr; +} + +void Frontend_SelectLocalizationTool::setValue(const types::QGlacierValue &value) +{ + if (m_pLabel && !value.instructions.empty() && value.instructions[0].isString()) + { + m_pLabel->setText(QString::fromStdString(value.instructions[0].getOperand().str)); + } + + if (m_pTarget) + { + m_pTarget->setValue(value); + } + + commitValue(value); +} + +const types::QGlacierValue &Frontend_SelectLocalizationTool::getValue() const +{ + static const types::QGlacierValue s_Invalid {}; + if (!m_pTarget) return s_Invalid; + return m_pTarget->getValue(); +} + +bool Frontend_SelectLocalizationTool::canHookFocus() const +{ + // Yep it can in this case + return true; +} + +void Frontend_SelectLocalizationTool::commitValue(const types::QGlacierValue &value) +{ + widgets::TypePropertyWidget::setValue(value); +} + +void Frontend_SelectLocalizationTool::onTargetValueChanged() +{ + if (m_pTarget) + { + commitValue(m_pTarget->getValue()); + emit editFinished(); + } +} + +void Frontend_SelectLocalizationTool::onTargetEditFinished() +{ + if (m_pTarget) + { + commitValue(m_pTarget->getValue()); + emit valueChanged(); + } +} + +SelectLocalizationTool::SelectLocalizationTool(QWidget* parent) : widgets::TypePropertyWidget(parent), m_ui(new Ui::SelectLocalizationTool) +{ + m_ui->setupUi(this); + + // Set model + m_ui->localizedStrings->setModel(models::ModelsLocator::s_LocalizationTreeModel.get()); + m_ui->localizedStrings->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); + + // Connect signals + connect(m_ui->buttonBox, &QDialogButtonBox::rejected, [this]() { + emit editFinished(); + close(); + }); + + connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectLocalizationTool::onAccepted); +} + +SelectLocalizationTool::~SelectLocalizationTool() +{ + delete m_ui; +} + +Frontend_SelectLocalizationTool *SelectLocalizationTool::Create(QWidget *parent) +{ + auto* pEditor = new SelectLocalizationTool(nullptr); + auto* pFrontend = new Frontend_SelectLocalizationTool(parent, pEditor); + return pFrontend; +} + +void SelectLocalizationTool::setValue(const types::QGlacierValue &value) +{ + // call for base + widgets::TypePropertyWidget::setValue(value); + + if (value.instructions.size() == 1 && value.instructions[0].isString()) + { + // Need to parse path + selectByPath(QString::fromStdString(value.instructions[0].getOperand().str)); + } +} + +void SelectLocalizationTool::disableAcceptButton() +{ + if (QPushButton* pOkButton = m_ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) + { + pOkButton->setEnabled(false); + } +} + +void SelectLocalizationTool::enableAcceptButton() +{ + if (QPushButton* pOkButton = m_ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) + { + pOkButton->setEnabled(true); + } +} + +void SelectLocalizationTool::selectByPath(const QString &path) +{ + QStringList pathParts = path.split('/'); + if (pathParts.empty()) return; + + if (pathParts[0] == "ROOT") + { + // remove ROOT because it literally does not exists :) + pathParts.removeFirst(); + } + + auto* model = qobject_cast(m_ui->localizedStrings->model()); + if (!model) + { + return; + } + + QModelIndex currentIndex = QModelIndex(); + + foreach (const QString& part, pathParts) + { + bool found = false; + int rows = model->rowCount(currentIndex); + + for (int i = 0; i < rows; ++i) + { + QModelIndex childIndex = model->index(i, 0, currentIndex); + QString entryName = model->data(childIndex, Qt::DisplayRole).toString(); + + if (entryName == part) + { + currentIndex = childIndex; + found = true; + m_ui->localizedStrings->expand(currentIndex); + m_ui->localizedStrings->scrollTo(currentIndex); + break; + } + } + + if (!found) + { + return; + } + } + + m_ui->localizedStrings->selectionModel()->select(currentIndex, QItemSelectionModel::Select | QItemSelectionModel::Rows); +} + +void SelectLocalizationTool::onAccepted() +{ + // need to set value + const QModelIndexList selectedIndexes = m_ui->localizedStrings->selectionModel()->selectedIndexes(); + if (selectedIndexes.isEmpty()) + { + // just do nothing + emit editFinished(); + return; + } + + QModelIndex currentIndex = selectedIndexes.first(); + QStringList pathParts {}; + while (currentIndex.isValid()) + { + pathParts.prepend(currentIndex.data().toString()); + currentIndex = currentIndex.parent(); + } + + const auto fullPath = pathParts.join('/').toStdString(); + auto newVal = getValue(); + if (!newVal.instructions.empty()) + { + // weird but ok + newVal.instructions[0] = gamelib::prp::PRPInstruction(newVal.instructions[0].getOpCode(), gamelib::prp::PRPOperandVal(fullPath)); + + // use base to avoid of extra selector iteration + widgets::TypePropertyWidget::setValue(newVal); + emit editFinished(); + } + + // and close us + close(); +} + +void SelectLocalizationTool::onRejected() +{ + emit editFinished(); + close(); +} + +void SelectLocalizationTool::onLocaleSelected(const QItemSelection &selected, const QItemSelection &deselected) +{ + if (selected.empty()) + { + disableAcceptButton(); + return; + } + + QModelIndex index = selected.first().indexes()[0]; + auto* node = reinterpret_cast(index.internalPointer()); + + if (!node) + { + disableAcceptButton(); + } + else + { + types::QGlacierValue temp = getValue(); + if (temp.instructions.empty()) + { + disableAcceptButton(); + } + else + { + enableAcceptButton(); + } + } +} + +void SelectLocalizationTool::closeEvent(QCloseEvent *pEvent) +{ + emit editFinished(); + QWidget::closeEvent(pEvent); +} \ No newline at end of file diff --git a/BMEdit/Editor/UI/Source/SelectSceneObjectTool.cpp b/BMEdit/Editor/UI/Source/SelectSceneObjectTool.cpp new file mode 100644 index 0000000..68d268e --- /dev/null +++ b/BMEdit/Editor/UI/Source/SelectSceneObjectTool.cpp @@ -0,0 +1,218 @@ +#include "ui_SelectSceneObjectTool.h" +#include +#include +#include +#include +#include +#include + + +Frontend_SelectSceneObjectTool::Frontend_SelectSceneObjectTool(QWidget *parent, widgets::TypePropertyWidget* pTarget) + : widgets::TypePropertyWidget(parent) + , m_pTarget(pTarget) +{ + if (m_pTarget) + { + connect(m_pTarget, &widgets::TypePropertyWidget::valueChanged, this, &Frontend_SelectSceneObjectTool::onTargetEditFinished); + connect(m_pTarget, &widgets::TypePropertyWidget::editFinished, this, &Frontend_SelectSceneObjectTool::onTargetValueChanged); + + m_pTarget->setWindowModality(Qt::WindowModality::ApplicationModal); + m_pTarget->show(); + } + + // And build layout + auto* pLayout = new QHBoxLayout(this); + m_pLabel = new QLabel(this); + m_pLabel->setText("MAYBE"); + pLayout->addWidget(m_pLabel); + setLayout(pLayout); +} + +Frontend_SelectSceneObjectTool::~Frontend_SelectSceneObjectTool() +{ + m_pTarget = nullptr; +} + +void Frontend_SelectSceneObjectTool::setValue(const types::QGlacierValue &value) +{ + if (m_pLabel && !value.instructions.empty() && value.instructions[0].isString()) + { + m_pLabel->setText(QString::fromStdString(value.instructions[0].getOperand().str)); + } + + if (m_pTarget) + { + m_pTarget->setValue(value); + } + + commitValue(value); +} + +const types::QGlacierValue &Frontend_SelectSceneObjectTool::getValue() const +{ + static const types::QGlacierValue s_Invalid {}; + if (!m_pTarget) return s_Invalid; + return m_pTarget->getValue(); +} + +bool Frontend_SelectSceneObjectTool::canHookFocus() const +{ + // Yep it can in this case + return true; +} + +void Frontend_SelectSceneObjectTool::commitValue(const types::QGlacierValue &value) +{ + widgets::TypePropertyWidget::setValue(value); +} + +void Frontend_SelectSceneObjectTool::onTargetValueChanged() +{ + if (m_pTarget) + { + commitValue(m_pTarget->getValue()); + emit editFinished(); + } +} + +void Frontend_SelectSceneObjectTool::onTargetEditFinished() +{ + if (m_pTarget) + { + commitValue(m_pTarget->getValue()); + emit valueChanged(); + } +} + +SelectSceneObjectTool::SelectSceneObjectTool(QWidget* parent) : widgets::TypePropertyWidget(parent), m_ui(new Ui::SelectSceneObjectTool) +{ + m_ui->setupUi(this); + + // Set model + m_ui->objectsTree->setModel(models::ModelsLocator::s_SceneTreeModel.get()); + m_ui->objectsTree->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); + + // Connect signals + connect(m_ui->buttonBox, &QDialogButtonBox::rejected, [this]() { + emit editFinished(); + close(); + }); + + connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectSceneObjectTool::onAccepted); +} + +SelectSceneObjectTool::~SelectSceneObjectTool() +{ + delete m_ui; +} + +Frontend_SelectSceneObjectTool *SelectSceneObjectTool::Create(QWidget *parent) +{ + auto* pEditor = new SelectSceneObjectTool(nullptr); + auto* pFrontend = new Frontend_SelectSceneObjectTool(parent, pEditor); + return pFrontend; +} + +void SelectSceneObjectTool::setValue(const types::QGlacierValue &value) +{ + // call for base + widgets::TypePropertyWidget::setValue(value); + + if (value.instructions.size() == 1 && value.instructions[0].isString()) + { + // Need to parse path + selectByPath(QString::fromStdString(value.instructions[0].getOperand().str)); + } +} + +void SelectSceneObjectTool::selectByPath(const QString &path) +{ + QStringList pathParts = path.split('\\'); + if (pathParts.empty()) return; + + if (pathParts[0] == "ROOT") + { + // remove ROOT because it literally does not exists :) + pathParts.removeFirst(); + } + + auto* model = qobject_cast(m_ui->objectsTree->model()); + if (!model) + { + return; + } + + QModelIndex currentIndex = QModelIndex(); + + foreach (const QString& part, pathParts) + { + bool found = false; + int rows = model->rowCount(currentIndex); + + for (int i = 0; i < rows; ++i) + { + QModelIndex childIndex = model->index(i, 0, currentIndex); + QString entryName = model->data(childIndex, Qt::DisplayRole).toString(); + + if (entryName == part) + { + currentIndex = childIndex; + found = true; + m_ui->objectsTree->expand(currentIndex); + m_ui->objectsTree->scrollTo(currentIndex); + break; + } + } + + if (!found) + { + return; + } + } + + m_ui->objectsTree->selectionModel()->select(currentIndex, QItemSelectionModel::Select | QItemSelectionModel::Rows); +} + +void SelectSceneObjectTool::onAccepted() +{ + // need to set value + const QModelIndexList selectedIndexes = m_ui->objectsTree->selectionModel()->selectedIndexes(); + if (selectedIndexes.isEmpty()) + { + // just do nothing + emit editFinished(); + return; + } + + QModelIndex currentIndex = selectedIndexes.first(); + QStringList pathParts {}; + while (currentIndex.isValid()) + { + pathParts.prepend(currentIndex.data().toString()); + currentIndex = currentIndex.parent(); + } + + // Always include ROOT subject here + pathParts.prepend("ROOT"); + + const auto fullPath = pathParts.join('\\').toStdString(); + auto newVal = getValue(); + if (!newVal.instructions.empty()) + { + // weird but ok + newVal.instructions[0] = gamelib::prp::PRPInstruction(newVal.instructions[0].getOpCode(), gamelib::prp::PRPOperandVal(fullPath)); + + // use base to avoid of extra selector iteration + widgets::TypePropertyWidget::setValue(newVal); + emit editFinished(); + } + + // and close us + close(); +} + +void SelectSceneObjectTool::closeEvent(QCloseEvent *pEvent) +{ + emit editFinished(); + QWidget::closeEvent(pEvent); +} \ No newline at end of file diff --git a/BMEdit/Editor/UI/Source/SelectScriptTool.cpp b/BMEdit/Editor/UI/Source/SelectScriptTool.cpp new file mode 100644 index 0000000..4f450dd --- /dev/null +++ b/BMEdit/Editor/UI/Source/SelectScriptTool.cpp @@ -0,0 +1,252 @@ +#include +#include +#include +#include +#include "ui_SelectScriptTool.h" + + +Frontend_SelectScriptTool::Frontend_SelectScriptTool(QWidget *parent, widgets::TypePropertyWidget *pTarget) + : widgets::TypePropertyWidget(parent) + , m_pTarget(pTarget) +{ + if (m_pTarget) + { + connect(m_pTarget, &widgets::TypePropertyWidget::valueChanged, this, &Frontend_SelectScriptTool::onTargetValueChanged); + connect(m_pTarget, &widgets::TypePropertyWidget::editFinished, this, &Frontend_SelectScriptTool::onTargetEditFinished); + + m_pTarget->setWindowModality(Qt::WindowModality::ApplicationModal); + m_pTarget->show(); + } + + // And build layout + auto* pLayout = new QHBoxLayout(this); + m_pLabel = new QLabel(this); + m_pLabel->setText("MAYBE"); + pLayout->addWidget(m_pLabel); + setLayout(pLayout); +} + +Frontend_SelectScriptTool::~Frontend_SelectScriptTool() +{ + m_pTarget = nullptr; +} + +void Frontend_SelectScriptTool::onTargetValueChanged() +{ + if (m_pTarget) + { + commitValue(m_pTarget->getValue()); + emit valueChanged(); + } +} + +void Frontend_SelectScriptTool::onTargetEditFinished() +{ + if (m_pTarget) + { + commitValue(m_pTarget->getValue()); + emit editFinished(); + } +} + +void Frontend_SelectScriptTool::setValue(const types::QGlacierValue &value) +{ + if (m_pLabel && !value.instructions.empty() && value.instructions[0].isString()) + { + m_pLabel->setText(QString::fromStdString(value.instructions[0].getOperand().str)); + } + + if (m_pTarget) + { + m_pTarget->setValue(value); + } + + commitValue(value); +} + +const types::QGlacierValue &Frontend_SelectScriptTool::getValue() const +{ + static const types::QGlacierValue s_Invalid {}; + if (!m_pTarget) return s_Invalid; + return m_pTarget->getValue(); +} + +bool Frontend_SelectScriptTool::canHookFocus() const +{ + // Yep it can in this case + return true; +} + +void Frontend_SelectScriptTool::commitValue(const types::QGlacierValue &value) +{ + widgets::TypePropertyWidget::setValue(value); +} + + +SelectScriptTool::SelectScriptTool(QWidget *parent) + : widgets::TypePropertyWidget(parent) + , m_ui(new Ui::SelectScriptTool) +{ + m_ui->setupUi(this); + + disableAcceptButton(); + + connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectScriptTool::onAccepted); + connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectScriptTool::onRejected); + + // assign model and connect signals + m_ui->gameScripts->setModel(models::ModelsLocator::s_GameScriptsTreeModel.get()); + m_ui->gameScripts->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); + connect(m_ui->gameScripts->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SelectScriptTool::onScriptSelected); +} + +SelectScriptTool::~SelectScriptTool() +{ + delete m_ui; +} + +void SelectScriptTool::closeEvent(QCloseEvent *pEvent) +{ + emit editFinished(); + QWidget::closeEvent(pEvent); +} + +void SelectScriptTool::disableAcceptButton() +{ + if (QPushButton* pOkButton = m_ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) + { + pOkButton->setEnabled(false); + } +} + +void SelectScriptTool::enableAcceptButton() +{ + if (QPushButton* pOkButton = m_ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)) + { + pOkButton->setEnabled(true); + } +} + +void SelectScriptTool::selectByPath(const QString &path) +{ + QSignalBlocker blocker { m_ui->gameScripts->selectionModel() }; + + QStringList pathParts = path.split('\\'); + if (pathParts.empty()) return; + + auto* model = qobject_cast(m_ui->gameScripts->model()); + if (!model) + { + return; + } + + QModelIndex currentIndex = QModelIndex(); + + foreach (const QString& part, pathParts) + { + bool found = false; + int rows = model->rowCount(currentIndex); + + for (int i = 0; i < rows; ++i) + { + QModelIndex childIndex = model->index(i, 0, currentIndex); + QString entryName = model->data(childIndex, Qt::DisplayRole).toString(); + + if (entryName == part) + { + currentIndex = childIndex; + found = true; + m_ui->gameScripts->expand(currentIndex); + m_ui->gameScripts->scrollTo(currentIndex); + break; + } + } + + if (!found) + { + return; + } + } + + m_ui->gameScripts->selectionModel()->select(currentIndex, QItemSelectionModel::Select | QItemSelectionModel::Rows); +} + +void SelectScriptTool::onAccepted() +{ + // need to set value + const QModelIndexList selectedIndexes = m_ui->gameScripts->selectionModel()->selectedIndexes(); + if (selectedIndexes.isEmpty()) + { + // just do nothing + return; + } + + QModelIndex currentIndex = selectedIndexes.first(); + auto* pScript = reinterpret_cast(currentIndex.internalPointer()); + if (!pScript || pScript->type != models::GameScriptsTreeModel::ScripTreeNode::NodeType::STN_SCRIPT || m_value.instructions.empty()) + { + // Just do nothing + return; + } + + auto newVal = getValue(); + newVal.instructions[0] = gamelib::prp::PRPInstruction(newVal.instructions[0].getOpCode(), gamelib::prp::PRPOperandVal(pScript->fullPath.toStdString())); + widgets::TypePropertyWidget::setValue(newVal); + emit editFinished(); + + close(); +} + +void SelectScriptTool::onRejected() +{ + emit editFinished(); + close(); +} + +void SelectScriptTool::onScriptSelected(const QItemSelection &selected, const QItemSelection &deselected) +{ + if (selected.empty()) + { + disableAcceptButton(); + return; + } + + QModelIndex index = selected.first().indexes()[0]; + auto* pScript = reinterpret_cast(index.internalPointer()); + + if (!pScript || pScript->type != models::GameScriptsTreeModel::ScripTreeNode::NodeType::STN_SCRIPT) + { + disableAcceptButton(); + } + else + { + types::QGlacierValue temp = getValue(); + if (temp.instructions.empty()) + { + disableAcceptButton(); + } + else + { + enableAcceptButton(); + } + } +} + +void SelectScriptTool::setValue(const types::QGlacierValue &value) +{ + // call for base + widgets::TypePropertyWidget::setValue(value); + + if (value.instructions.size() == 1 && value.instructions[0].isString()) + { + // Need to parse path + selectByPath(QString::fromStdString(value.instructions[0].getOperand().str)); + } +} + +Frontend_SelectScriptTool *SelectScriptTool::Create(QWidget *parent) +{ + auto* pEditor = new SelectScriptTool(nullptr); + auto* pFrontend = new Frontend_SelectScriptTool(parent, pEditor); + return pFrontend; +} \ No newline at end of file diff --git a/BMEdit/Editor/UI/Source/ViewTexturesDialog.cpp b/BMEdit/Editor/UI/Source/ViewTexturesDialog.cpp new file mode 100644 index 0000000..c2052eb --- /dev/null +++ b/BMEdit/Editor/UI/Source/ViewTexturesDialog.cpp @@ -0,0 +1,447 @@ +#include "ViewTexturesDialog.h" +#include +#include "ui_ViewTexturesDialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +static constexpr int kDefaultMIP = 0; +static constexpr const char* kPNGFilter = "PNG image (*.png)"; + + +static QString convertTextureTypeToQString(gamelib::tex::TEXEntryType entry) +{ + switch (entry) + { + case gamelib::tex::TEXEntryType::ET_BITMAP_I8: return "I8"; + case gamelib::tex::TEXEntryType::ET_BITMAP_EMBM: return "EMBM"; + case gamelib::tex::TEXEntryType::ET_BITMAP_DOT3: return "DOT3"; + case gamelib::tex::TEXEntryType::ET_BITMAP_CUBE: return "CUBE"; + case gamelib::tex::TEXEntryType::ET_BITMAP_DMAP: return "DMAP"; + case gamelib::tex::TEXEntryType::ET_BITMAP_PAL: return "PAL (Neg)"; + case gamelib::tex::TEXEntryType::ET_BITMAP_PAL_OPAC: return "PAL (Opac)"; + case gamelib::tex::TEXEntryType::ET_BITMAP_32: return "RGBA"; + case gamelib::tex::TEXEntryType::ET_BITMAP_U8V8: return "U8V8"; + case gamelib::tex::TEXEntryType::ET_BITMAP_DXT1: return "DXT1"; + case gamelib::tex::TEXEntryType::ET_BITMAP_DXT3: return "DXT3"; + } + + return "Unknown"; +} + +ViewTexturesDialog::ViewTexturesDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::ViewTexturesDialog) +{ + ui->setupUi(this); + ui->texturesTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Interactive); + ui->texturesTableView->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); + ui->texturesTableView->verticalHeader()->hide(); + + m_importDialog.reset(new ImportTextureDialog(this)); + m_filterModel.reset(new models::SceneTextureFilterModel(this)); + ui->texturesTableView->setModel(m_filterModel.get()); + + // Connect common signals + connect(ui->mipLevelCombo, &QComboBox::currentIndexChanged, this, &ViewTexturesDialog::onCurrentMipLevelChanged); + connect(ui->exportTEXButton, &QPushButton::clicked, this, &ViewTexturesDialog::onExportTEXFile); + connect(ui->exportTextureButton, &QPushButton::clicked, this, &ViewTexturesDialog::onExportCurrentTextureToFile); + connect(ui->replaceTextureButton, &QPushButton::clicked, this, &ViewTexturesDialog::onReplaceCurrentTexture); + connect(ui->searchTextureByNameInput, &QLineEdit::textChanged, [this](const QString& query) { + m_filterModel->setTextureNameFilter(query); + }); + connect(m_importDialog.get(), &QDialog::accepted, this, &ViewTexturesDialog::onTextureToImportSpecified); +} + +ViewTexturesDialog::~ViewTexturesDialog() +{ + delete ui; +} + +void ViewTexturesDialog::setTexturesSource(models::SceneTexturesModel* model) +{ + { + QSignalBlocker blocker { ui->searchTextureByNameInput }; + ui->searchTextureByNameInput->clear(); + m_filterModel->setTextureNameFilter({}); + } + + m_filterModel->setSourceModel(model); + ui->mipLevelCombo->clear(); + ui->texturesTableView->resizeColumnsToContents(); + + connect(ui->texturesTableView->selectionModel(), &QItemSelectionModel::selectionChanged, [this](const QItemSelection &selected, const QItemSelection &deselected) { + const bool somethingSelected = !selected.indexes().isEmpty(); + + if (somethingSelected) + { + const QModelIndex mappedSelection = m_filterModel->mapToSource(selected.indexes().first()); + const auto textureRef = mappedSelection.data(models::SceneTexturesModel::Roles::R_TEXTURE_REF).value(); + + if (!textureRef.textureIndex) + return; + + const auto textureIndex = textureRef.textureIndex; + + // Show available MIP levels + resetAvailableMIPs(textureIndex); + setPreview(textureIndex, std::nullopt); + } + else if (!deselected.indexes().isEmpty()) + { + ui->exportTextureButton->setEnabled(false); + ui->replaceTextureButton->setEnabled(false); + clearAvailableMIPs(); + resetPreview(); + } + }); +} + +void ViewTexturesDialog::showEvent(QShowEvent *event) +{ + ui->mipLevelCombo->clear(); + ui->texturesTableView->selectionModel()->reset(); + ui->searchTextureByNameInput->setText(QString()); + resetPreview(); + + QDialog::showEvent(event); +} + +void ViewTexturesDialog::onCurrentMipLevelChanged(int mipIndex) +{ + auto textureRefOptional = getActiveTexture(); + if (!textureRefOptional.has_value()) + return; + + auto currentMIPOptional = getActiveMIPLevel(); + if (!currentMIPOptional.has_value()) + return; + + const auto& textureREF = textureRefOptional.value(); + const auto textureIndex = textureREF.textureIndex; + + setPreview(textureIndex, currentMIPOptional.value()); +} + +void ViewTexturesDialog::onExportTEXFile() +{ + auto model = dynamic_cast(m_filterModel->sourceModel()); + if (!model) + return; + + QFileDialog saveTEXDialog(this, QString("Save TEX"), QString(), QString("Glacier Texture pack (*.TEX)")); + saveTEXDialog.setViewMode(QFileDialog::ViewMode::Detail); + saveTEXDialog.setFileMode(QFileDialog::FileMode::AnyFile); + saveTEXDialog.setAcceptMode(QFileDialog::AcceptMode::AcceptSave); + saveTEXDialog.selectFile(QString("%1.TEX").arg(QString::fromStdString(model->getLevel()->getLevelName()))); + if (!saveTEXDialog.exec()) + { + return; + } + + if (saveTEXDialog.selectedFiles().empty()) + { + return; + } + + const auto saveAsPath = saveTEXDialog.selectedFiles().first(); + const auto sceneTextures = model->getLevel()->getSceneTextures(); + + // Build TEX into buffer + std::vector texBuffer; + gamelib::tex::TEXWriter::write(sceneTextures->header, sceneTextures->entries, texBuffer); + + QFile texFile(saveAsPath); + if (!texFile.open(QIODeviceBase::OpenModeFlag::WriteOnly | QIODeviceBase::OpenModeFlag::Truncate | QIODeviceBase::OpenModeFlag::Unbuffered)) + { + QMessageBox::warning(this, "Export TEX", QString("Failed to export TEX file. Unable to open file '%1' to write").arg(saveAsPath)); + return; + } + + texFile.write(QByteArray::fromRawData(reinterpret_cast(texBuffer.data()), static_cast(texBuffer.size()))); + QMessageBox::information(this, "Export TEX", QString("TEX file '%1' exported successfully!").arg(saveAsPath)); +} + +void ViewTexturesDialog::onExportCurrentTextureToFile() +{ + auto textureRefOptional = getActiveTexture(); + if (!textureRefOptional.has_value()) + return; + + const auto& textureREF = textureRefOptional.value(); + + const auto textureIndex = textureREF.textureIndex; + const auto& entries = textureREF.ownerModel->getLevel()->getSceneTextures()->entries; + auto foundIt = std::find_if(entries.begin(), entries.end(), [idx = textureIndex](const gamelib::tex::TEXEntry& entry) -> bool { + return entry.m_index == idx; + }); + + if (foundIt == entries.end()) + return; + + QString relatedFileName; + const gamelib::tex::TEXEntry& textureToSave = *foundIt; + + // Predict name + if (textureToSave.m_fileName.has_value()) + { + relatedFileName = QString::fromStdString(textureToSave.m_fileName.value()); + } + else + { + relatedFileName = QString("Texture_%1").arg(textureToSave.m_index); + } + + // Show dialog + QFileDialog saveTEXDialog(this, QString("Export texture")); + saveTEXDialog.setNameFilters({ kPNGFilter }); + saveTEXDialog.setViewMode(QFileDialog::ViewMode::Detail); + saveTEXDialog.setFileMode(QFileDialog::FileMode::AnyFile); + saveTEXDialog.setAcceptMode(QFileDialog::AcceptMode::AcceptSave); + saveTEXDialog.selectFile(relatedFileName); + if (!saveTEXDialog.exec()) + { + return; + } + + if (saveTEXDialog.selectedFiles().empty()) + { + return; + } + + const auto savePath = saveTEXDialog.selectedFiles().first(); + const auto saveExt = saveTEXDialog.selectedNameFilter(); + + bool exportResult = false; + if (saveExt == kPNGFilter) + { + exportResult = editor::TextureProcessor::exportTEXEntryAsPNG(textureToSave, savePath.toStdString()); + } + //TODO: Add support for other formats + + if (exportResult) + { + QMessageBox::information(this, "Export texture", QString("Texture exported to file %1 successfully!").arg(savePath)); + } + else + { + QMessageBox::warning(this, "Export texture", "Failed to export texture. Unsupported format or internal error"); + } +} + +void ViewTexturesDialog::onReplaceCurrentTexture() +{ + auto textureRefOptional = getActiveTexture(); + if (!textureRefOptional.has_value()) + return; + + const auto& textureREF = textureRefOptional.value(); + + const auto textureIndex = textureREF.textureIndex; + auto& entries = textureREF.ownerModel->getLevel()->getSceneTextures()->entries; + auto foundIt = std::find_if(entries.begin(), entries.end(), [idx = textureIndex](const gamelib::tex::TEXEntry& entry) -> bool { + return entry.m_index == idx; + }); + + // Cleanup + m_importDialog->resetState(); + + // Update state + if (foundIt->m_fileName.has_value()) + { + m_importDialog->setTextureName(QString::fromStdString(foundIt->m_fileName.value())); + } + m_importDialog->setMIPLevelsCount(foundIt->m_mipLevels.size()); + + // And open dialog again + m_importDialog->setModal(true); + m_importDialog->open(); +} + +void ViewTexturesDialog::onTextureToImportSpecified() +{ + if (ui->texturesTableView->selectionModel()->selectedIndexes().isEmpty()) + return; + + auto model = dynamic_cast(m_filterModel->sourceModel()); + if (!model) + return; + + // Extract data & select texture to replace + const auto& sourceFile = m_importDialog->getSourceFilePath(); + const auto& textureName = m_importDialog->getTextureName(); + const auto targetFormat = m_importDialog->getTargetFormat(); + const auto mipLevelsNr = m_importDialog->getTargetMIPLevels(); + + // Check that format supported + if (targetFormat == gamelib::tex::TEXEntryType::ET_BITMAP_PAL_OPAC || // Maybe will support in future + targetFormat == gamelib::tex::TEXEntryType::ET_BITMAP_EMBM || // DirectX 9 stuff, no support from us + targetFormat == gamelib::tex::TEXEntryType::ET_BITMAP_DOT3 || // DirectX 9 stuff, no support from us + targetFormat == gamelib::tex::TEXEntryType::ET_BITMAP_CUBE || // DirectX 9 stuff, no support from us + targetFormat == gamelib::tex::TEXEntryType::ET_BITMAP_DMAP) // DirectX 9 stuff, no support from us + { + QMessageBox::warning(this, "Import texture", "Required target format not supported yet"); + return; + } + + auto textureRefOptional = getActiveTexture(); + if (!textureRefOptional.has_value()) + return; + + const auto& textureREF = textureRefOptional.value(); + + const auto textureIndex = textureREF.textureIndex; + auto& entries = const_cast(textureREF.ownerModel->getLevel())->getSceneTextures()->entries; + auto foundIt = std::find_if(entries.begin(), entries.end(), [idx = textureIndex](const gamelib::tex::TEXEntry& entry) -> bool { + return entry.m_index == idx; + }); + + if (!editor::TextureProcessor::importTextureToEntry(*foundIt, sourceFile, textureName, targetFormat, mipLevelsNr)) + { + QMessageBox::warning(this, "Import texture", "Failed to import texture! Invalid or unsupported format"); + return; + } + + // And it's done! Update our preview! + resetPreview(); + setPreview(textureIndex, std::nullopt); + resetAvailableMIPs(textureIndex); + + // Notify everybody about changes + emit textureChanged(textureREF.textureIndex); +} + +void ViewTexturesDialog::setPreview(uint32_t textureIndex, const std::optional& mipLevel) +{ + auto model = dynamic_cast(m_filterModel->sourceModel()); + if (!model) + return; + + auto& entries = const_cast(model->getLevel())->getSceneTextures()->entries; + auto foundIt = std::find_if(entries.begin(), entries.end(), [textureIndex](const gamelib::tex::TEXEntry& entry) -> bool { + return entry.m_index == textureIndex; + }); + + const auto& textureEntry = *foundIt; + + // Select MIP level + int requiredMipLevel = mipLevel.has_value() ? mipLevel.value() : kDefaultMIP; + + if (requiredMipLevel < 0 || requiredMipLevel >= textureEntry.m_mipLevels.size()) + { + if (textureEntry.m_mipLevels.empty()) + { + qDebug() << "No mip levels in texture #" << textureIndex; + return; + } + + qDebug() << "Bad mip level. Use #0"; + requiredMipLevel = 0; + } + + uint16_t width = 0; + uint16_t height = 0; + std::unique_ptr decompressedMemBlk = editor::TextureProcessor::decompressRGBA(textureEntry, width, height, requiredMipLevel); + + // Save to QPixmap + if (decompressedMemBlk) + { + QImage image(decompressedMemBlk.get(), width, height, QImage::Format::Format_RGBA8888); + QPixmap pixmap = QPixmap::fromImage(std::move(image)); + setPreview(std::move(pixmap)); + + ui->formatLabel->setText(convertTextureTypeToQString(textureEntry.m_type1)); + + ui->exportTextureButton->setEnabled(true); + ui->replaceTextureButton->setEnabled(true); + } + else + { + qDebug() << "Unsupported format"; + resetPreview(); + } +} + +void ViewTexturesDialog::setPreview(QPixmap &&image) +{ + resetPreview(); + ui->textureViewWidget->setPixmap(image); +} + +void ViewTexturesDialog::resetPreview() +{ + ui->textureViewWidget->setPixmap(QPixmap()); + ui->formatLabel->setText("Unknown"); + ui->exportTextureButton->setEnabled(false); + ui->replaceTextureButton->setEnabled(false); +} + +void ViewTexturesDialog::resetAvailableMIPs(uint32_t textureIndex) +{ + QSignalBlocker blocker { ui->mipLevelCombo }; + ui->mipLevelCombo->clear(); + + auto model = dynamic_cast(m_filterModel->sourceModel()); + if (!model) + return; + + auto& entries = const_cast(model->getLevel())->getSceneTextures()->entries; + auto foundIt = std::find_if(entries.begin(), entries.end(), [idx = textureIndex](const gamelib::tex::TEXEntry& entry) -> bool { + return entry.m_index == idx; + }); + + const auto& textureEntry = *foundIt; + + for (uint32_t mip = 0; mip < textureEntry.m_numOfMipMaps; ++mip) + { + ui->mipLevelCombo->addItem(QString("MIP #%1").arg(mip), mip); + } + + ui->mipLevelCombo->setEnabled(textureEntry.m_numOfMipMaps > 1); +} + +void ViewTexturesDialog::clearAvailableMIPs() +{ + QSignalBlocker blocker { ui->mipLevelCombo }; + ui->mipLevelCombo->clear(); +} + +std::optional ViewTexturesDialog::getActiveTexture() const +{ + auto model = dynamic_cast(m_filterModel->sourceModel()); + if (!model) + return std::nullopt; + + if (ui->texturesTableView->selectionModel()->selectedIndexes().empty()) + return std::nullopt; + + QModelIndex mappedSelection = m_filterModel->mapToSource(ui->texturesTableView->selectionModel()->selectedIndexes().first()); + + auto textureREF = mappedSelection.data(models::SceneTexturesModel::Roles::R_TEXTURE_REF).value(); + if (!textureREF.textureIndex) + return std::nullopt; + + return textureREF; +} + +std::optional ViewTexturesDialog::getActiveMIPLevel() const +{ + auto model = dynamic_cast(m_filterModel->sourceModel()); + if (!model) + return std::nullopt; + + if (ui->texturesTableView->selectionModel()->selectedIndexes().empty()) + return std::nullopt; + + if (!ui->mipLevelCombo->isEnabled()) + return kDefaultMIP; + + return static_cast(ui->mipLevelCombo->itemData(ui->mipLevelCombo->currentIndex(), Qt::UserRole).value()); +} \ No newline at end of file diff --git a/BMEdit/Editor/UI/UI/BMEditMainWindow.ui b/BMEdit/Editor/UI/UI/BMEditMainWindow.ui index a925327..e31f93a 100644 --- a/BMEdit/Editor/UI/UI/BMEditMainWindow.ui +++ b/BMEdit/Editor/UI/UI/BMEditMainWindow.ui @@ -6,21 +6,21 @@ 0 0 - 1548 - 790 + 1499 + 979 BMEdit - + - - - - - + + + Qt::ClickFocus + + @@ -29,8 +29,8 @@ 0 0 - 1548 - 21 + 1499 + 22 @@ -42,6 +42,7 @@ Export + @@ -52,6 +53,8 @@ Edit + + @@ -69,7 +72,19 @@ Scene + + + Render Mode + + + + + + + + + @@ -81,7 +96,7 @@ 120 - 35 + 40 @@ -102,7 +117,7 @@ 260 - 189 + 191 @@ -187,7 +202,7 @@ - + 260 @@ -201,7 +216,7 @@ QDockWidget::DockWidgetClosable|QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable - Properties + Game Object 2 @@ -209,7 +224,7 @@ - + 0 @@ -258,19 +273,51 @@ - - - Properties: - - + + + + + Instance ID: + + + + + + + 0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + - - - QAbstractItemView::SingleSelection + + + Qt::Horizontal + + + + + + Properties: + + + + + + + QAbstractItemView::SingleSelection + + + + + @@ -283,156 +330,77 @@ - - - - - - - - QDockWidget::DockWidgetFeatureMask - - - Primitives - - - 8 - - - - - - - Qt::Horizontal - - - + + + ZGEOM + + - - - - - Primitives count: - - - - - - - 0 - - - - - - - - - false + + + + 16777215 + 20 + - Export chunk + Coli Bits (8): - - - Qt::Vertical + + + false - + - 20 - 40 + 100 + 100 - - - - - - - - - - - - Filter by: - - - - - - - Unknown - - - - - - - Zero buffer - - - - - - - Description Buffer - - - - - - - Index Buffer - - - - - - - Vertex Buffer - - - - - - - + - - - Primitive Preview: - - - - - - - 100 - 100 - - - - - - + + + Localization + + + 1 + + + + + + + Search... + + + + + + + + + Open level + + Ctrl+O + @@ -494,6 +462,90 @@ Export PRP (properties) + + + false + + + Textures + + + + + false + + + Localization + + + + + true + + + true + + + false + + + View whole scene + + + Alt+V + + + + + true + + + true + + + Texture + + + + + true + + + Wireframe + + + + + Texture & Wireframe + + + + + true + + + Render Portals + + + Render Portals + + + + + true + + + Render Room Bounding Boxes + + + + + false + + + Export LOC (localization) + + @@ -508,9 +560,15 @@
Widgets/SceneTreeView.h
- widgets::PrimitivePreviewWidget + widgets::SceneRenderWidget QOpenGLWidget -
Widgets/PrimitivePreviewWidget.h
+
Widgets/SceneRenderWidget.h
+
+ + widgets::BitSetViewWidget + QWidget +
Widgets/BitSetViewWidget.h
+ 1
diff --git a/BMEdit/Editor/UI/UI/GeomControllersWidget.ui b/BMEdit/Editor/UI/UI/GeomControllersWidget.ui index 4619a8d..db10951 100644 --- a/BMEdit/Editor/UI/UI/GeomControllersWidget.ui +++ b/BMEdit/Editor/UI/UI/GeomControllersWidget.ui @@ -6,36 +6,111 @@ 0 0 - 400 - 300 + 300 + 409
Form - + - - - - - - - - - 65 - 16777215 - - - - ... - - - - - - - + + + 0 + + + + Controllers + + + + + + + + + + + false + + + + 25 + 16777215 + + + + X + + + + + + + + + + + + + New Controller + + + + + + Search... + + + + + + + QAbstractItemView::NoEditTriggers + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Add controller + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + diff --git a/BMEdit/Editor/UI/UI/ImportTextureDialog.ui b/BMEdit/Editor/UI/UI/ImportTextureDialog.ui new file mode 100644 index 0000000..9e334fb --- /dev/null +++ b/BMEdit/Editor/UI/UI/ImportTextureDialog.ui @@ -0,0 +1,132 @@ + + + ImportTextureDialog + + + + 0 + 0 + 874 + 473 + + + + Import Texture + + + + + + Qt::Horizontal + + + + + + + + + Source file: + + + + + + + + + false + + + + + + + ... + + + + + + + + + Name: + + + + + + + 512 + + + + + + + Destination format: + + + + + + + + + + MIP Levels: + + + + + + + 1 + + + 8 + + + + + + + + + Import + + + + + + + + Preview: + + + + + + + 256 + 256 + + + + + + + Qt::AlignCenter + + + + + + + + + + + + diff --git a/BMEdit/Editor/UI/UI/SelectLocalizationTool.ui b/BMEdit/Editor/UI/UI/SelectLocalizationTool.ui new file mode 100644 index 0000000..1b57134 --- /dev/null +++ b/BMEdit/Editor/UI/UI/SelectLocalizationTool.ui @@ -0,0 +1,31 @@ + + + SelectLocalizationTool + + + + 0 + 0 + 490 + 890 + + + + Select localized string... + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/BMEdit/Editor/UI/UI/SelectSceneObjectTool.ui b/BMEdit/Editor/UI/UI/SelectSceneObjectTool.ui new file mode 100644 index 0000000..b8857d3 --- /dev/null +++ b/BMEdit/Editor/UI/UI/SelectSceneObjectTool.ui @@ -0,0 +1,34 @@ + + + SelectSceneObjectTool + + + Qt::NonModal + + + + 0 + 0 + 360 + 700 + + + + Select Scene Object... + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/BMEdit/Editor/UI/UI/SelectScriptTool.ui b/BMEdit/Editor/UI/UI/SelectScriptTool.ui new file mode 100644 index 0000000..d75e316 --- /dev/null +++ b/BMEdit/Editor/UI/UI/SelectScriptTool.ui @@ -0,0 +1,31 @@ + + + SelectScriptTool + + + + 0 + 0 + 490 + 890 + + + + Select Game Script... + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/BMEdit/Editor/UI/UI/ViewTexturesDialog.ui b/BMEdit/Editor/UI/UI/ViewTexturesDialog.ui new file mode 100644 index 0000000..c17de3d --- /dev/null +++ b/BMEdit/Editor/UI/UI/ViewTexturesDialog.ui @@ -0,0 +1,196 @@ + + + ViewTexturesDialog + + + + 0 + 0 + 1188 + 791 + + + + + 900 + 400 + + + + Texture Viewer + + + + + + + + + Qt::Horizontal + + + + + + + + + Search: + + + + + + + Texture name... + + + + + + + + + + 0 + 0 + + + + + + + + + + New texture... + + + + + + + Export TEX + + + + + + + + + + Texture: + + + + + + + + + + LOD: + + + + + + + + + + + + + + Format: + + + + + + + Unkown + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + true + + + 0 + + + + Texture + + + + + + + 512 + 512 + + + + true + + + + + + + + + Qt::AlignCenter + + + + + + + + + + + + + false + + + REPLACE + + + + + + + false + + + EXPORT + + + + + + + + + + + + + + diff --git a/BMEdit/GameLib/CMakeLists.txt b/BMEdit/GameLib/CMakeLists.txt index 37d8307..9b8d0b6 100644 --- a/BMEdit/GameLib/CMakeLists.txt +++ b/BMEdit/GameLib/CMakeLists.txt @@ -18,8 +18,9 @@ target_include_directories(GameLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/Include) # --- Dependencies target_link_libraries(GameLib PRIVATE ZBinaryReader) # Private libs -target_link_libraries(GameLib PUBLIC nlohmann_json::nlohmann_json fmt::fmt-header-only) # Public library to work with json -target_link_libraries(GameLib PUBLIC zlib) # Public library to work with compressed streams +target_link_libraries(GameLib PUBLIC nlohmann_json::nlohmann_json fmt::fmt-header-only glm::glm) +target_link_libraries(GameLib PUBLIC ${ZLIB_LIBRARIES}) # Public library to work with compressed streams +target_include_directories(GameLib PRIVATE ${ZLIB_INCLUDE_DIRS}) # --- Tests (temporary disabled) #add_subdirectory(ThirdParty/gtest) diff --git a/BMEdit/GameLib/Include/GameLib/BoundingBox.h b/BMEdit/GameLib/Include/GameLib/BoundingBox.h index a0821ae..dfa2449 100644 --- a/BMEdit/GameLib/Include/GameLib/BoundingBox.h +++ b/BMEdit/GameLib/Include/GameLib/BoundingBox.h @@ -1,18 +1,65 @@ #pragma once -#include +#include +#include +#include +#include + +#include namespace gamelib { struct BoundingBox { - Vector3 min; - Vector3 max; + glm::vec3 min; + glm::vec3 max; BoundingBox() = default; - BoundingBox(const Vector3 &vMin, const Vector3 &vMax); + BoundingBox(const glm::vec3 &vMin, const glm::vec3 &vMax); + + glm::vec3 getCenter() const; + + void expand(const BoundingBox& another); + void expand(const glm::vec3& vPoint); + bool contains(const glm::vec3& vPoint) const; + bool intersect(const BoundingBox& another) const; + + double getVolume() const; + std::tuple getDimensions() const; + + static BoundingBox toWorld(const BoundingBox& source, const glm::mat4& mTransform); + + template + void toLines(TOutVertexIterator outVertexIt, TOutIndexIterator outIndexIt) + { + // Compute vertices + std::array aVertices { + glm::vec3{min.x, min.y, min.z}, glm::vec3{min.x, min.y, max.z}, + glm::vec3{min.x, max.y, min.z}, glm::vec3{min.x, max.y, max.z}, + glm::vec3{max.x, min.y, min.z}, glm::vec3{max.x, min.y, max.z}, + glm::vec3{max.x, max.y, min.z}, glm::vec3{max.x, max.y, max.z} + }; + + // Compute indices + std::array aIndices { + 0, 1, 1, 3, 3, 2, 2, 0, + 4, 5, 5, 7, 7, 6, 6, 4, + 0, 4, 1, 5, 2, 6, 3, 7 + }; + + for (const auto& vVertex : aVertices) + { + using TP = typename TOutVertexIterator::container_type::value_type; + TP vProxy; + vProxy.vPos = vVertex; + (*outVertexIt++) = std::move(vProxy); + } - Vector3 getCenter() const; + for (const auto& iIndex : aIndices) + { + (*outIndexIt++) = iIndex; + } + } }; } \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/GMS/GMSGeomEntity.h b/BMEdit/GameLib/Include/GameLib/GMS/GMSGeomEntity.h index ffe0027..b012030 100644 --- a/BMEdit/GameLib/Include/GameLib/GMS/GMSGeomEntity.h +++ b/BMEdit/GameLib/Include/GameLib/GMS/GMSGeomEntity.h @@ -11,6 +11,19 @@ namespace ZBio::ZBinaryReader namespace gamelib::gms { + enum ECollisionMask : uint32_t + { + COLIMASK_All = 1, + COLIMASK_Background = 2, + COLIMASK_Shot = 4, + COLIMASK_WaterGlass = 8, + COLIMASK_NoWalk = 16, + COLIMASK_Sight = 32, + COLIMASK_Hero = 64, + COLIMASK_Camera = 128, + COLIMASK_NPC = 256 + }; + class GMSGeomEntity { ///---------- @@ -32,6 +45,7 @@ namespace gamelib::gms [[nodiscard]] bool isInheritedOfGeom() const; [[nodiscard]] bool isRootOfGroup() const; [[nodiscard]] uint32_t getRelativeDepthLevel() const; + [[nodiscard]] uint32_t getGeomFlags() const; static void deserialize(GMSGeomEntity &entity, ZBio::ZBinaryReader::BinaryReader *gmsBinaryReader, ZBio::ZBinaryReader::BinaryReader *bufBinaryReader); @@ -52,7 +66,7 @@ namespace gamelib::gms uint32_t m_unk10 { }; uint32_t m_typeId { }; uint32_t m_unk18 { }; - uint32_t m_coliBits { }; + uint32_t m_coliBits {}; // +1C. NOTE: ECollisionMask contains all possible & expected values uint32_t m_unk20 { }; uint32_t m_unk24 { }; uint32_t m_unk28 { }; diff --git a/BMEdit/GameLib/Include/GameLib/GMS/Room/ZRoomDefs.h b/BMEdit/GameLib/Include/GameLib/GMS/Room/ZRoomDefs.h new file mode 100644 index 0000000..35d43d9 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/GMS/Room/ZRoomDefs.h @@ -0,0 +1,62 @@ +/** +* This file contains definition of internal structure which defines information about room "eXit" segments +*/ +#pragma once + +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::gms::room +{ +#pragma pack(push, 1) + /** + * @struct ZRoomExit + * @brief This structure represents all information about single room exit. See ZROOM.json properties iExitsCount and ExitOffsets + * @note Total size of this structure must be 0x38 bytes for PC version (main supported game version) + */ + struct ZRoomExit + { + // Plane vertices + glm::vec3 v0; + glm::vec3 v1; + glm::vec3 v2; + glm::vec3 v3; + + // Other data + uint32_t iRoomREF; // Instance ID, just lookup over entities on scene to locate it + uint8_t unk1C; + uint8_t unk1D; + uint8_t unkFlags; // bit#2 - always required, bit#4 - means processed remap or not (setup by engine in ZROOM::RemapRefs) + uint8_t unk1F; + + static void deserialize(ZRoomExit& eXit, ZBio::ZBinaryReader::BinaryReader *bufBinaryReader); + static void deserialize(ZRoomExit& eXit, const Span& byteBufferSpan); + }; +#pragma pack(pop) + +#pragma pack(push, 1) + /** + * @struct ZRoomNeighbor + * @brief Describes room neighbor. Declared in BUF file. See ZROOM.json properties iNeighboursCount and NeighborsOffset + */ + struct ZRoomNeighbor + { + uint32_t rRoomREF; // Instance ID (not owner) + uint32_t unk4; // Means some 'amount of objects'. Maybe amount of 'neighbors'? + uint32_t unk8; // Some offset inside BUF. Maybe 'neighbors'? + + static void deserialize(ZRoomNeighbor& neighbor, ZBio::ZBinaryReader::BinaryReader *bufBinaryReader); + static void deserialize(ZRoomNeighbor& neighbor, const Span& byteBufferSpan); + }; +#pragma pack(pop) + + + static_assert(sizeof(ZRoomExit) == 0x38); +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/LOC/LOC.h b/BMEdit/GameLib/Include/GameLib/LOC/LOC.h new file mode 100644 index 0000000..e7347bf --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/LOC/LOC.h @@ -0,0 +1,5 @@ +#pragma once + +#include +#include +#include \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/LOC/LOCReader.h b/BMEdit/GameLib/Include/GameLib/LOC/LOCReader.h new file mode 100644 index 0000000..df90062 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/LOC/LOCReader.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + + +namespace gamelib::loc +{ + class LOCReader + { + public: + LOCReader(); + + bool parse(const uint8_t *locFileBuffer, int64_t locFileSize); + + [[nodiscard]] const LOCTreeNode::Ptr& getRoot() const; + + private: + LOCTreeNode::Ptr m_pRoot { nullptr }; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/LOC/LOCTreeNode.h b/BMEdit/GameLib/Include/GameLib/LOC/LOCTreeNode.h new file mode 100644 index 0000000..d1665dd --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/LOC/LOCTreeNode.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace ZBio::ZBinaryWriter +{ + class BinaryWriter; +} + +namespace gamelib::loc +{ + enum LOCTreeNodeType : int8_t + { + EMPTY_BLOCK = 0x8, ///< Empty chunk, no data at all + LOCALIZED_STRING = 0x9, ///< Key value (string to aligned string) + CHILDREN = 0x10, ///< Container (amount & list of offsets) + SUBTITLES_HINT_WITH_COMMENT = 0x0B, ///< Some subtitle bullshit with comment (key, value string, comment aligned CString) + SUBTITLES_FIN = 0x28, ///< Subtitles finish string + SUBTITLES = 0x29, ///< Subtitles value (long text with extra parameters) | 0x20 mask means that extra data exists + SUBTITLES_HINT = 0x2B ///< Another subtitles data with extra string hint + }; + + enum LOCMissionObjectiveType : char + { + HBM_Target = 'T', + HBM_Retrieve = 'R', + HBM_Escape = 'E', + HBM_Dispose = 'D', + HBM_Protect = 'P', + HBM_Optional = 'O' + }; + + /** + * Notes: + * + * 1. Decompiler ref: https://github.com/ReGlacier/ReHitmanTools/blob/main/Tools/LOCC/source/Decompiler.cpp + * 2. Format notes + * File format; + * 1. Root node name omitted, starts from amount of nodes [u8] + * 2. If amount more than 1 - offsets segment starts there + * + * Node format: + * Name - CString + * +0x0 - kind of node (TreeNodeType : 0x0 - value or data (leaf), 0x10 - node with children (bone) + * +0x1 - how much child nodes coming (but +1) + * Then going list of offsets by 0x4 bytes each. Each offset starts after offsets block + * + * We should read + */ + struct LOCTreeNode + { + using Ptr = std::shared_ptr; + using Ref = std::weak_ptr; + + std::vector children {}; // When NODE_WITH_CHILDREN + LOCTreeNode::Ref parent {}; + LOCTreeNodeType type { LOCTreeNodeType::LOCALIZED_STRING }; + std::string name {}; + std::string value {}; // When SIMPLE_VALUE or PARAGRAPH_VALUE + struct SubtitleData + { + uint8_t unkData[8]; + std::string extraHint {}; + } subtitle; + + [[nodiscard]] bool canHaveValue() const; + [[nodiscard]] bool canHaveChildren() const; + + static void deserialize(const LOCTreeNode::Ptr &node, ZBio::ZBinaryReader::BinaryReader* binaryReader); + static void serialize(const LOCTreeNode::Ptr& node, ZBio::ZBinaryWriter::BinaryWriter* binaryWriter, std::vector>& replacement); + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/LOC/LOCWriter.h b/BMEdit/GameLib/Include/GameLib/LOC/LOCWriter.h new file mode 100644 index 0000000..ce06d38 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/LOC/LOCWriter.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include +#include + + +namespace gamelib::loc +{ + class LOCWriter + { + public: + static void write(const LOCTreeNode::Ptr& root, std::vector &outBuffer); + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/Level.h b/BMEdit/GameLib/Include/GameLib/Level.h index eb8460b..2d7f436 100644 --- a/BMEdit/GameLib/Include/GameLib/Level.h +++ b/BMEdit/GameLib/Include/GameLib/Level.h @@ -2,10 +2,15 @@ #include #include -#include #include #include +#include +#include +#include +#include +#include +#include #include #include #include @@ -21,11 +26,13 @@ namespace gamelib uint32_t objectsCount; }; - struct LevelGeometry + struct LevelTextures { - prm::PRMHeader header; - std::vector chunkDescriptors; - std::vector chunks; + tex::TEXHeader header; + std::vector entries; + tex::OffsetsPool table1Offsets { 0u }; + tex::OffsetsPool table2Offsets { 0u }; + uint32_t countOfEmptyOffsets { 0u }; }; struct SceneProperties @@ -33,6 +40,40 @@ namespace gamelib gms::GMSHeader header; }; + struct LevelGeometry + { + prm::PrmFile primitives; + }; + + struct LevelMaterials + { + mat::MATHeader header; + std::vector materialClasses; + std::vector materialInstances; + }; + + struct LevelRooms + { + struct RoomGroup + { + oct::OCTHeader header {}; + std::vector nodes{}; + std::vector objects{}; + std::vector ubs{}; + + [[nodiscard]] glm::i16vec3 worldToRoom(const glm::vec3& vWorld) const; + [[nodiscard]] glm::vec3 roomToWorld(const glm::i16vec3& vTree) const; + }; + + RoomGroup outside {}; + RoomGroup inside {}; + }; + + struct LevelLocalization + { + loc::LOCTreeNode::Ptr localizationRoot { nullptr }; + }; + class Level { public: @@ -44,17 +85,38 @@ namespace gamelib [[nodiscard]] const LevelProperties *getLevelProperties() const; [[nodiscard]] LevelProperties *getLevelProperties(); [[nodiscard]] const SceneProperties *getSceneProperties() const; + [[nodiscard]] const LevelTextures* getSceneTextures() const; + [[nodiscard]] LevelTextures* getSceneTextures(); [[nodiscard]] const LevelGeometry* getLevelGeometry() const; [[nodiscard]] LevelGeometry* getLevelGeometry(); + [[nodiscard]] const LevelMaterials* getLevelMaterials() const; + [[nodiscard]] LevelMaterials* getLevelMaterials(); + [[nodiscard]] const LevelRooms* getLevelRooms() const; + [[nodiscard]] LevelRooms* getLevelRooms(); + [[nodiscard]] const LevelLocalization* getLevelLocalization() const; + [[nodiscard]] LevelLocalization* getLevelLocalization(); - [[nodiscard]] const std::vector &getSceneObjects() const; + [[nodiscard]] const std::vector& getSceneObjects() const; + + [[nodiscard]] scene::SceneObject::Ptr getSceneObjectByGEOMREF(const std::string& path) const; + + [[nodiscard]] scene::SceneObject::Ptr getSceneObjectByInstanceID(std::uint32_t instanceID) const; + + [[nodiscard]] Span getStaticBuffer() const; void dumpAsset(io::AssetKind assetKind, std::vector &outBuffer) const; + void forEachObjectOfType(const std::string& objectTypeName, const std::function& pred) const; + void forEachObjectOfTypeWithInheritance(const std::string& objectBaseType, const std::function& pred) const; + private: bool loadLevelProperties(); bool loadLevelScene(); bool loadLevelPrimitives(); + bool loadLevelTextures(); + bool loadLevelMaterials(); + bool loadLevelRooms(); + bool loadLevelLocalization(); private: // Core @@ -64,7 +126,17 @@ namespace gamelib // Raw data LevelProperties m_levelProperties; SceneProperties m_sceneProperties; + LevelTextures m_levelTextures; LevelGeometry m_levelGeometry; + LevelMaterials m_levelMaterials; + LevelRooms m_levelRooms; + LevelLocalization m_levelLocalization; + + struct BUF + { + std::unique_ptr data { nullptr }; + std::int64_t size{ 0 }; + } m_buf; // Managed objects std::vector m_sceneObjects {}; diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MAT.h b/BMEdit/GameLib/Include/GameLib/MAT/MAT.h new file mode 100644 index 0000000..ff5ac77 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MAT.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + + +namespace gamelib::mat +{ +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATBind.h b/BMEdit/GameLib/Include/GameLib/MAT/MATBind.h new file mode 100644 index 0000000..6627321 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATBind.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + struct MATBind + { + MATBind(); + + static MATBind makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount); + + // Collected things + std::string name {}; /// I assume, that we able to have no name or only 1 name... + std::vector renderStates; + std::vector textures; + std::vector colorChannels; + std::vector sprites; + std::vector options; + std::vector scrolls; + std::vector floats; + /// ... another fields? + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATBlendMode.h b/BMEdit/GameLib/Include/GameLib/MAT/MATBlendMode.h new file mode 100644 index 0000000..5689c05 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATBlendMode.h @@ -0,0 +1,19 @@ +#pragma once + +#include + + +namespace gamelib::mat +{ + enum class MATBlendMode : uint8_t + { + BM_TRANS, + BM_TRANS_ON_OPAQUE, + BM_TRANSADD_ON_OPAQUE, + BM_ADD_BEFORE_TRANS, + BM_ADD_ON_OPAQUE, + BM_ADD, + BM_SHADOW, + BM_STATICSHADOW + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATClass.h b/BMEdit/GameLib/Include/GameLib/MAT/MATClass.h new file mode 100644 index 0000000..34dd8e0 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATClass.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include + + +namespace gamelib::mat +{ + class MATClass + { + public: + MATClass(std::string clasName, std::string parentClass, std::vector&& properties, std::vector&& subClasses); + + [[nodiscard]] const std::string& getName() const; + [[nodiscard]] const std::string& getParentClassName() const; + [[nodiscard]] const std::vector& getSubClasses() const; + + private: + std::string m_name; + std::string m_parentClass; + std::vector m_properties; + std::vector m_subClasses; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATColorChannel.h b/BMEdit/GameLib/Include/GameLib/MAT/MATColorChannel.h new file mode 100644 index 0000000..7f39c36 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATColorChannel.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + /** + * @brief Present option for color channel (able to pass rgba as float & int values) + */ + class MATColorChannel + { + public: + MATColorChannel(std::string name, bool bEnabled, MATValU&& color); + + static MATColorChannel makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount); + + [[nodiscard]] const std::string& getName() const; + [[nodiscard]] bool isEnabled() const; + [[nodiscard]] const MATValU& getColor() const; + + private: + std::string m_name {}; + bool m_bEnabled { false }; + MATValU m_color {}; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATCullMode.h b/BMEdit/GameLib/Include/GameLib/MAT/MATCullMode.h new file mode 100644 index 0000000..c52c951 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATCullMode.h @@ -0,0 +1,14 @@ +#pragma once + +#include + + +namespace gamelib::mat +{ + enum class MATCullMode : uint8_t + { + CM_DontCare, + CM_OneSided, + CM_TwoSided + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATEntries.h b/BMEdit/GameLib/Include/GameLib/MAT/MATEntries.h new file mode 100644 index 0000000..c491d83 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATEntries.h @@ -0,0 +1,176 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + /** + * Describe type of operand + * @note See sub_471820 for details + */ + enum class MATValueType + { + PT_FLOAT = 0, + PT_CHAR = 1, + PT_UINT32 = 2, + PT_LIST = 3, + PT_UNKNOWN + }; + + /** + * @brief All (almost) possible material system properties from Glacier 1. There are divided by few groups: + * 1) STRREF - reference to string + * 2) TRIVIAL - trivial value (int, float, bool) + * 3) MATLayer - layer + * 4) MATClass - material class + * 5) MATSubClass - material subclass + * 6) MATSprite - material sprite + * 7) MATBind - some binding + * 8) MATInstance - material instance + * 9) MATRenderState - render state + * 10) MATTexture - texture description + * 11) MATColorChannel - color channel description + * 12) MATOption - some boolean option with ref to shader param + * @note See sub_471D70 (ZRenderMaterialBinderParser::ParseMaterialProperties) for details + */ + enum class MATPropertyKind : uint32_t + { + PK_NULL_PROPERTY = 0u, ///< Nothing, used only as 'error' value. Not used by game! + PK_NAME = 0x4E414D45u, ///< [STRREF] Reference to string represents name of something + PK_TYPE = 0x54595045u, ///< [STRREF] Reference to string represents type of object + PK_PATH = 0x50415448u, ///< [STRREF] Reference to string with part to something (asset, scene object, texture name, etc) + PK_IDEN = 0x4944454Eu, ///< [STRREF] Reference to string represents some kind of id + PK_OTYP = 0x4F545950u, ///< [STRREF] Reference to string, idk what represents + PK_STYP = 0x53545950u, ///< [STRREF] Reference to string, idk what represents + PK_LAYER = 0x4C415945u, ///< [MATLayer] Reference to list of properties represents 1 layer. Contains PK_PATH block with path to shader program (with extension) + PK_CLASS = 0x434C4153u, ///< [MATClass] Reference to list of properties represents material class + PK_SUB_CLASS = 0x53554243u, ///< [MATSubClass] Reference to list of properties represents material subclass + PK_ENABLE = 0x454E4142u, ///< [TRIVIAL] Hold boolean value, in `reference` stored value (0 or 1) + PK_SPRITE = 0x53505249u, ///< [MATSprite] Reference to list of properties represents sprite. Contains PK_NAME block (name of sprite?) and one or more enable blocks. @note Need investigate this place more carefully + PK_BIND = 0x42494E44u, ///< [MATBind] Reference to list of properties + PK_INSTANCE = 0x494E5354u, ///< [MATInstance] Reference to list of properties represents material instance on scene (scene object may refs to instance) + PK_BLEND_ENABLE = 0x42454E41u, ///< [TRIVIAL] Hold boolean value, in `reference` stored value (0 or 1 - disable or enable blending) + /** + * @brief [STRREF] Reference to string contains blend mode string repr (part of enum?). + * Possible values: TRANS, TRANS_ON_OPAQUE, TRANSADD_ON_OPAQUE, ADD_BEFORE_TRANS, ADD_ON_OPAQUE, ADD, SHADOW, STATICSHADOW + */ + PK_BLEND_MODE = 0x424D4F44u, + PK_OPACITY = 0x4F504143u, ///< [TRIVIAL] Hold float value, in `reference` stored float (fp32) value represents opacity of something + PK_ALPHA_TEST = 0x41545354u, ///< [TRIVIAL] Hold boolean value, in `reference` stored bool (0 or 1) + PK_ALPHA_REFERENCE = 0x41524546u, ///< [TRIVIAL] Hold uint32 value, in `reference` stored uint32_t (u32) value represents some alpha reference scalar value + PK_FOG_ENABLED = 0x46454E41u, ///< [TRIVIAL] Hold boolean value, in `reference` stored bool (0 or 1). Value represents fog target state + PK_CULL_MODE = 0x43554C4Cu, ///< [STRREF] Reference to string represents culling mode. Possible values: DontCare, OneSided, TwoSided. @note I'm not sure that game uses this values. Be careful! + PK_Z_BIAS = 0x5A424941u, ///< [TRIVIAL] Hold boolean value, in `reference` stored bool (0 or 1) + PK_Z_OFFSET = 0x5A4F4646u, ///< [TRIVIAL] Hold float value (fp32) in `reference`, represents Z offset + PK_TEXTURE_ID = 0x54584944u, ///< [TRIVIAL] Hold uint32 value, in `reference` stored ID of texture (TEXEntry id, see TEX parser for details) + PK_TILINIG_U = 0x54494C55u, ///< [STRREF] Reference to string represents U tiling mode. Possible values: NONE, TILED. See MATTilingMode enum for details + PK_TILINIG_V = 0x54494C56u, ///< [STRREF] Reference to string represents V tiling mode. Possible values: NONE, TILED + PK_TILINIG_W = 0x54494C57u, ///< [STRREF] Reference to string represents W tiling mode. Possible values are unknown. Probably unused, but declared in Glacier1 code. + PK_VAL_I = 0x56414C49u, ///< [STRREF] Reference to string. I guess it's something about 'set boolean parameter by string literal'. `ReflectionEnabled` as example. + PK_VAL_U = 0x56414C55u, ///< [TRIVIAL] Hold float or int value + PK_VAL_F = 0x554C4156u, ///< Idk, unused + PK_RENDER_STATE = 0x52535441u, ///< [MATRenderState] Reference to list of properties which represents render state. + PK_TEXTURE = 0x54455854u, ///< [MATTexture] Reference to list of properties represents texture (PK_PATH - path to texture/PK_TEXTURE_ID - index of texture) + PK_COLOR = 0x434F4C4Fu, ///< [MATColorChannel] Reference to list of properties represents usage of color channel (as example v4IlluminationColor, presented via 2 properties: PK_NAME (name of channel) and PK_ENABLED) + PK_BOOLEAN = 0x424F4F4Cu, ///< [MATOption] Reference to list of properties represents boolean flag. Presented via 3 properties: PK_NAME: AlphaFadeEnabled, PK_ENABLED - use parameter or not and PK_VAL_U - value of flag + PK_FLOAT_VALUE = 0x464C5456u, ///< [MATFloat] Reference to list of properties represents some float argument (or group of floats) + PK_DMAP = 0x50414D44u, ///< Idk, unused (Glacier supports, but no usage) + PK_FMIN = 0x4E494D46u, ///< Min filter (Idk, looks like legacy from OpenGL times) + PK_FMAG = 0x47414D46u, ///< Mag filter (Idk, looks like legacy from OpenGL times) + PK_FMIP = 0x50494D46u, ///< Idk, unused + PK_SCROLL = 0x5343524Cu, ///< [MATScroll] Some scrollable something... + PK_SCROLL_SPEED = 0x53504544u, ///< Idk, unused + PK_ENUM = 0x4D554E45u ///< Idk, unused + }; + + struct MATHeader + { + uint32_t classListOffset { 0 }; + uint32_t instancesListOffset { 0 }; + uint32_t zeroed { 0 }; + uint32_t unknownTableOffset { 0 }; + + static void deserialize(MATHeader& header, ZBio::ZBinaryReader::BinaryReader* binaryReader); + }; + + struct MATPropertyEntry + { + /** + * @brief Kind of this entry + * @note When PK_NULL_PROPERTY all other data will be invalid! + */ + MATPropertyKind kind { MATPropertyKind::PK_NULL_PROPERTY }; + + /** + * @brief Reference to another block + */ + uint32_t reference { 0 }; + + /** + * @brief Represents how much elements will be captured inside reference. + * For int & float - always 1 + * For str - length of string + * For list - elements count in list + */ + uint32_t containerCapacity { 0u }; + + /** + * @brief Represents type of value inside reference (if reference value or not, idk) + */ + MATValueType valueType { MATValueType::PT_UNKNOWN }; + + /** + * @brief Deserialize binary stream into object + * @param entry + * @param binaryReader + */ + static void deserialize(MATPropertyEntry& entry, ZBio::ZBinaryReader::BinaryReader* binaryReader); + }; + + struct MATClassDescription + { + std::string parentClass {}; // Name of parent class (a reference to PK_NAME property block) + uint32_t unk4 { 0 }; + uint32_t unk8 { 0 }; + uint32_t unkC { 0 }; + uint32_t unk10 { 0 }; + uint32_t unk14 { 0 }; + uint32_t unk18 { 0 }; + uint32_t classDeclarationOffset {0}; // Class declaration offset + uint32_t unk20 { 0 }; + uint32_t unk24 { 0 }; + uint32_t unk28 { 0 }; + uint32_t unk2C { 0 }; + + static void deserialize(MATClassDescription& classDescription, ZBio::ZBinaryReader::BinaryReader* binaryReader); + }; + + struct MATInstanceDescription + { + std::string instanceParentClassName {}; // Name of instance class (a reference to PK_NAME property block) + uint32_t unk4 { 0 }; // Usually 0x1 + uint32_t unk8 { 0 }; // 0x0 + uint32_t unkC { 0 }; // Usually 0x77 + uint32_t unk10 { 0 }; // 0x2 + uint32_t unk14 { 0 }; // 0x1 + uint32_t unk18 { 0 }; // 0x1 + uint32_t instanceDeclarationOffset {0}; // Material instance declaration offset + uint32_t unk20 { 0 }; // 0x2 + uint32_t unk24 { 0 }; // 0x0 + uint32_t unk28 { 0 }; // 0x0 + uint32_t unk2C { 0 }; // 0x0 + + static void deserialize(MATInstanceDescription& instanceDescription, ZBio::ZBinaryReader::BinaryReader* binaryReader); + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATFloat.h b/BMEdit/GameLib/Include/GameLib/MAT/MATFloat.h new file mode 100644 index 0000000..8ed449a --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATFloat.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + using IntOrFloat = std::variant; + + class MATFloat + { + public: + MATFloat(std::string name, bool bEnabled, MATValU&& valU); + + static MATFloat makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount); + + [[nodiscard]] const std::string& getName() const { return m_name; } + [[nodiscard]] bool isEnabled() const { return m_bEnabled; } + [[nodiscard]] const MATValU& getValU() const { return m_valU; } + + private: + std::string m_name {}; + bool m_bEnabled { false }; + MATValU m_valU; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATInstance.h b/BMEdit/GameLib/Include/GameLib/MAT/MATInstance.h new file mode 100644 index 0000000..023bd2c --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATInstance.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include + + +namespace gamelib::mat +{ + class MATInstance + { + public: + MATInstance(std::string instanceName, std::string parentClassName, std::vector&& properties, std::vector&& binders); + + [[nodiscard]] const std::string& getName() const; + [[nodiscard]] const std::string& getParentName() const; + [[nodiscard]] const std::vector& getBinders() const; + + private: + std::string m_name {}; + std::string m_parentName {}; + std::vector m_properties {}; + std::vector m_binders {}; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATLayer.h b/BMEdit/GameLib/Include/GameLib/MAT/MATLayer.h new file mode 100644 index 0000000..5f8f726 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATLayer.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + /** + * @brief Present layer from Glacier material system + */ + class MATLayer + { + public: + MATLayer(std::string name, std::string type, std::string shaderPath, std::string identity, std::string val_i); + + static MATLayer makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount); + + [[nodiscard]] const std::string& getName() const; + [[nodiscard]] const std::string& getType() const; + [[nodiscard]] const std::string& getShaderProgramName() const; + [[nodiscard]] const std::string& getIdentity() const; + private: + std::string m_name; + std::string m_type; + std::string m_shaderPath; + std::string m_identity; + std::string m_valI; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATOption.h b/BMEdit/GameLib/Include/GameLib/MAT/MATOption.h new file mode 100644 index 0000000..d4e8030 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATOption.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + class MATOption + { + public: + MATOption(std::string name, bool bEnabled, MATValU&& valU); + + static MATOption makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount); + + [[nodiscard]] const std::string& getName() const; + [[nodiscard]] bool isEnabled() const; + [[nodiscard]] const MATValU& getValU() const; + + + private: + std::string m_name{}; + bool m_bEnabled {}; + MATValU m_valU {}; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATReader.h b/BMEdit/GameLib/Include/GameLib/MAT/MATReader.h new file mode 100644 index 0000000..cdbbc44 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATReader.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + class MATReader + { + public: + MATReader(); + + bool parse(const uint8_t* pMatBuffer, size_t iMatBufferSize); + + [[nodiscard]] const MATHeader& getHeader() const { return m_header; } + [[nodiscard]] std::vector&& takeClasses() { return std::move(m_classes); } + [[nodiscard]] std::vector&& takeInstances() { return std::move(m_instances); } + + private: + bool collectMaterialClasses(ZBio::ZBinaryReader::BinaryReader* matReader); + bool collectMaterialInstances(ZBio::ZBinaryReader::BinaryReader* matReader); + + private: + MATHeader m_header; + std::vector m_constantTable; + std::vector m_classes; + std::vector m_instances; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATRenderState.h b/BMEdit/GameLib/Include/GameLib/MAT/MATRenderState.h new file mode 100644 index 0000000..ba24c41 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATRenderState.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + class MATRenderState + { + public: + MATRenderState() = default; + + MATRenderState(std::string name, + bool bEnabled, bool bBlendEnabled, bool bAlphaTest, bool bFogEnabled, bool bZBias, + float fOpacity, float fZOffset, + uint32_t iAlphaReference, + MATCullMode cullMode, MATBlendMode blendMode, + MATValU&& valU): + m_name(std::move(name)), + m_bEnabled(bEnabled), m_bEnableBlend(bBlendEnabled), m_bAlphaTest(bAlphaTest), m_bFogEnabled(bFogEnabled), m_bZBias(bZBias), + m_fOpacity(fOpacity), m_fZOffset(fZOffset), + m_iAlphaReference(iAlphaReference), + m_eCullMode(cullMode), m_eBlendMode(blendMode), + m_valU(std::move(valU)) + { + } + + static MATRenderState makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount); + + [[nodiscard]] const std::string& getName() const { return m_name; } + [[nodiscard]] bool isEnabled() const { return m_bEnabled; } + [[nodiscard]] bool isBlendEnabled() const { return m_bEnableBlend; } + [[nodiscard]] bool isAlphaTestEnabled() const { return m_bAlphaTest; } + [[nodiscard]] bool isFogEnabled() const { return m_bFogEnabled; } + [[nodiscard]] bool hasZBias() const { return m_bZBias; } + [[nodiscard]] uint32_t getAlphaReference() const { return m_iAlphaReference; } + [[nodiscard]] float getOpacity() const { return m_fOpacity; } + [[nodiscard]] float getZOffset() const { return m_fZOffset; } + [[nodiscard]] MATCullMode getCullMode() const { return m_eCullMode; } + [[nodiscard]] MATBlendMode getBlendMode() const { return m_eBlendMode; } + + private: + std::string m_name {}; + bool m_bEnabled { false }; + bool m_bEnableBlend { false }; + bool m_bAlphaTest { false }; + bool m_bFogEnabled { false }; + bool m_bZBias { false }; + MATCullMode m_eCullMode { MATCullMode::CM_DontCare }; + MATBlendMode m_eBlendMode { MATBlendMode::BM_TRANS }; + uint32_t m_iAlphaReference { 0u }; + float m_fOpacity { 1.f }; + float m_fZOffset { .0f }; + MATValU m_valU {}; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATScroll.h b/BMEdit/GameLib/Include/GameLib/MAT/MATScroll.h new file mode 100644 index 0000000..1ac2f7e --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATScroll.h @@ -0,0 +1,29 @@ +#pragma once + +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + class MATScroll + { + public: + MATScroll(std::string name, bool bEnabled, std::vector&& speedVector); + + static MATScroll makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount); + + [[nodiscard]] const std::string& getName() const { return m_name; } + [[nodiscard]] bool isEnabled() const { return m_bEnabled; } + [[nodiscard]] const std::vector& getSpeedVec() const { return m_vfSpeed; } + + private: + std::string m_name {}; + bool m_bEnabled { false }; + std::vector m_vfSpeed {}; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATSprite.h b/BMEdit/GameLib/Include/GameLib/MAT/MATSprite.h new file mode 100644 index 0000000..9861cc7 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATSprite.h @@ -0,0 +1,29 @@ +#pragma once + +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + class MATSprite + { + public: + MATSprite(std::string name, bool bUnk0, bool bUnk1); + + static MATSprite makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount); + + [[nodiscard]] const std::string& getName() const; + [[nodiscard]] bool getUnk0() const; + [[nodiscard]] bool getUnk1() const; + + private: + std::string m_name {}; + bool m_bUnk0 { false }; + bool m_bUnk1 { false }; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATSubClass.h b/BMEdit/GameLib/Include/GameLib/MAT/MATSubClass.h new file mode 100644 index 0000000..34eef57 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATSubClass.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + class MATSubClass + { + public: + MATSubClass(std::string name, std::string oTyp, std::string sType, std::vector&& layers) + : m_name(std::move(name)), m_oTyp(std::move(oTyp)), m_sTyp(std::move(sType)), m_layers(std::move(layers)) + { + } + + static MATSubClass makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount); + + [[nodiscard]] const std::string& getName() const { return m_name; } + [[nodiscard]] const std::string& getOTyp() const { return m_oTyp; } + [[nodiscard]] const std::string& getSTyp() const { return m_sTyp; } + [[nodiscard]] const std::vector& getLayers() const { return m_layers; } + + private: + std::string m_name {}; + std::string m_oTyp {}; + std::string m_sTyp {}; + std::vector m_layers {}; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATTexture.h b/BMEdit/GameLib/Include/GameLib/MAT/MATTexture.h new file mode 100644 index 0000000..b3ad8e9 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATTexture.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + enum PresentedTextureSource : uint8_t + { + PTS_NOTHING, + PTS_TEXTURE_ID, + PTS_TEXTURE_PATH, + PTS_TEXTURE_ID_AND_PATH + }; + + class MATTexture + { + public: + MATTexture(std::string name, bool bEnabled, uint32_t textureId, std::string path, MATTilingMode tilingU, MATTilingMode tilingV, MATTilingMode tilingW); + + static MATTexture makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount); + + [[nodiscard]] const std::string& getName() const; + [[nodiscard]] bool isEnabled() const; + [[nodiscard]] uint32_t getTextureId() const; + [[nodiscard]] const std::string& getTexturePath() const; + [[nodiscard]] MATTilingMode getTilingU() const; + [[nodiscard]] MATTilingMode getTilingV() const; + [[nodiscard]] MATTilingMode getTilingW() const; + [[nodiscard]] PresentedTextureSource getPresentedTextureSources() const; + + private: + std::string m_name {}; + std::string m_texturePath {}; + std::uint32_t m_iTextureId { 0u }; + bool m_bEnabled { false }; + MATTilingMode m_tileU { MATTilingMode::TM_NONE }; + MATTilingMode m_tileV { MATTilingMode::TM_NONE }; + MATTilingMode m_tileW { MATTilingMode::TM_NONE }; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATTilingMode.h b/BMEdit/GameLib/Include/GameLib/MAT/MATTilingMode.h new file mode 100644 index 0000000..f7f00d7 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATTilingMode.h @@ -0,0 +1,12 @@ +#pragma once + + +namespace gamelib::mat +{ + enum class MATTilingMode + { + TM_NONE, + TM_TILED, + TM_MIRRORED + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/MAT/MATValU.h b/BMEdit/GameLib/Include/GameLib/MAT/MATValU.h new file mode 100644 index 0000000..4a7ff28 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/MAT/MATValU.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::mat +{ + class MATValU + { + public: + using Value = std::variant; + + MATValU(); + explicit MATValU(std::vector&& values); + + MATValU& operator=(std::vector&& values) noexcept; + + static MATValU makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, const MATPropertyEntry& selfDecl); + + [[nodiscard]] const std::vector& getValues() const { return m_values; } + + private: + std::vector m_values {}; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/OCT/OCT.h b/BMEdit/GameLib/Include/GameLib/OCT/OCT.h new file mode 100644 index 0000000..192260c --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/OCT/OCT.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + + +namespace gamelib::oct +{ +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/OCT/OCTEntries.h b/BMEdit/GameLib/Include/GameLib/OCT/OCTEntries.h new file mode 100644 index 0000000..6c19f99 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/OCT/OCTEntries.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::oct +{ + struct OCTHeader + { + uint32_t objectsOffset { 0 }; + glm::vec3 vWorldOrigin { .0f }; + float fWorldScale { .0f }; + + static void deserialize(OCTHeader& header, ZBio::ZBinaryReader::BinaryReader* binaryReader); + }; + + struct OCTNode + { + union + { + struct + { + uint16_t unk3 : 3 { 0 }; // Idk + uint16_t childCount : 12 { 0 }; // Count of child objects + uint16_t unk1 : 1 { 0 }; // Idk + } uVal; + uint16_t iVal { 0 }; + } childCountData; + uint16_t childIndex { 0 }; // It's index of NODE + uint16_t objectIndex { 0 }; + + static_assert(sizeof(childCountData) == sizeof(uint16_t), "Bad size of childCountData"); + + [[nodiscard]] uint16_t getChildCount() const { return childCountData.uVal.childCount; } + + static void deserialize(OCTNode& node, ZBio::ZBinaryReader::BinaryReader* binaryReader); + }; + + struct OCTObject + { + uint32_t gameObjectREF { 0 }; + glm::i16vec3 vMin { 0 }; + glm::i16vec3 vMax { 0 }; + + static void deserialize(OCTObject& object, ZBio::ZBinaryReader::BinaryReader* binaryReader); + }; + + /** + * Idk what this block contains + * + * Note: first entry in most cases zeroed ni vUnk4, vUnk28, vUnk34, vUnk40, unk4C, unk50 + */ + struct OCTUnknownBlock + { + uint32_t unk0 { 0 }; // always 1? + + glm::mat3 vUnk4 {}; // in most cases identity matrix + + glm::vec3 vUnk28 {}; // some vector + glm::vec3 vUnk34 { 0.f }; // another vector, Z component bigger than vUnk28 + glm::vec3 vUnk40 {}; // vector, idk + + uint32_t unk4C { 0 }; // Looks like priority or flags. In hideout first 1114, then less and decreased by 1 since second entry + uint32_t unk50 { 0 }; // In eOUTSIDE tree always zeroed, in eINSIDE/eBOTH/eUNKNOWN in most cases 0 but sometimes > 0 + + static void deserialize(OCTUnknownBlock& block, ZBio::ZBinaryReader::BinaryReader* binaryReader); + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/OCT/OCTReader.h b/BMEdit/GameLib/Include/GameLib/OCT/OCTReader.h new file mode 100644 index 0000000..b6ff7f3 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/OCT/OCTReader.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + + +namespace gamelib::oct +{ + class OCTReader + { + public: + OCTReader(); + + bool parse(const uint8_t* pOCTBuffer, size_t iBufferSize); + + [[nodiscard]] const OCTHeader& getHeader() const { return m_header; } + [[nodiscard]] std::vector&& takeNodes() { return std::move(m_nodes); } + [[nodiscard]] std::vector&& takeObjects() { return std::move(m_objects); } + [[nodiscard]] std::vector&& takeUBS() { return std::move(m_unknownBlocks); } + + private: + OCTHeader m_header {}; + std::vector m_nodes {}; + std::vector m_objects {}; + std::vector m_unknownBlocks {}; // associated with m_objects + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRM.h b/BMEdit/GameLib/Include/GameLib/PRM/PRM.h index 58015fe..d616e69 100644 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRM.h +++ b/BMEdit/GameLib/Include/GameLib/PRM/PRM.h @@ -1,5 +1,4 @@ #pragma once -#include -#include -#include \ No newline at end of file +#include +#include \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMBadChunkException.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMBadChunkException.h deleted file mode 100644 index 6a38566..0000000 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRMBadChunkException.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include - - -namespace gamelib::prm -{ - class PRMBadChunkException : public PRMException - { - public: - explicit PRMBadChunkException(std::uint32_t chunkIndex); - - [[nodiscard]] char const* what() const override; - - private: - std::string m_errorMessage; - }; -} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMBadFile.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMBadFile.h deleted file mode 100644 index 4a27782..0000000 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRMBadFile.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - - -namespace gamelib::prm -{ - class PRMBadFile : public PRMException - { - public: - explicit PRMBadFile(const std::string& reason); - - [[nodiscard]] char const* what() const override; - private: - std::string m_message; - }; -} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMChunk.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMChunk.h deleted file mode 100644 index 42d0e38..0000000 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRMChunk.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - - -namespace gamelib::prm -{ - class PRMChunk - { - private: - struct NullData {}; - - std::uint32_t m_chunkIndex { 0u }; - std::unique_ptr m_buffer { nullptr }; - std::size_t m_bufferSize { 0 }; - PRMChunkRecognizedKind m_recognizedKind { PRMChunkRecognizedKind::CRK_UNKNOWN_BUFFER }; - std::variant m_data; - - public: - PRMChunk(); - PRMChunk(std::uint32_t chunkIndex, int totalChunksNr, std::unique_ptr &&buffer, std::size_t size); - - [[nodiscard]] std::uint32_t getIndex() const; - [[nodiscard]] Span getBuffer(); - [[nodiscard]] PRMChunkRecognizedKind getKind() const; - - [[nodiscard]] const PRMDescriptionChunkBaseHeader* getDescriptionBufferHeader() const; - [[nodiscard]] PRMDescriptionChunkBaseHeader* getDescriptionBufferHeader(); - - [[nodiscard]] const PRMIndexChunkHeader* getIndexBufferHeader() const; - [[nodiscard]] PRMIndexChunkHeader* getIndexBufferHeader(); - - [[nodiscard]] const PRMVertexBufferHeader* getVertexBufferHeader() const; - [[nodiscard]] PRMVertexBufferHeader* getVertexBufferHeader(); - - private: - void recognizeChunkKindAndSaveData(Span chunk, int totalChunksNr); - }; -} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMChunkDescriptor.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMChunkDescriptor.h deleted file mode 100644 index b9685f5..0000000 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRMChunkDescriptor.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include - - -namespace ZBio::ZBinaryReader -{ - class BinaryReader; -} - -namespace gamelib::prm -{ - struct PRMChunkDescriptor - { - uint32_t declarationOffset; // +0x0 - uint32_t declarationSize; // +0x4 (need to be confirmed! Source: https://github.com/HHCHunter/Hitman-BloodMoney/blob/master/TOOLS/PRMConverter/Source/PRMConvert.cpp#L23) - uint32_t declarationKind; // +0x8 - uint32_t unkC; // +0xC - - static constexpr int kDescriptorSize = 0x10; - - static void deserialize(PRMChunkDescriptor &descriptor, ZBio::ZBinaryReader::BinaryReader *binaryReader); - }; -} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMChunkRecognizedKind.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMChunkRecognizedKind.h deleted file mode 100644 index 86954d6..0000000 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRMChunkRecognizedKind.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - - -namespace gamelib::prm -{ - enum class PRMChunkRecognizedKind - { - CRK_ZERO_CHUNK, ///< Only for chunk #0 - CRK_INDEX_BUFFER, ///< For chunk with indices data - CRK_VERTEX_BUFFER, ///< For chunk with vertices data - CRK_DESCRIPTION_BUFFER, ///< For chunk with description - CRK_UNKNOWN_BUFFER ///< For unrecognized chunk, it may contains any kind of data - }; -} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMDescriptionChunkBaseHeader.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMDescriptionChunkBaseHeader.h deleted file mode 100644 index 1c12853..0000000 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRMDescriptionChunkBaseHeader.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include -#include - - -namespace ZBio::ZBinaryReader -{ - class BinaryReader; -} - -namespace gamelib::prm -{ - struct PRMDescriptionChunkBaseHeader - { - std::uint8_t boneDeclOffset { 0 }; - std::uint8_t primPackType { 0 }; - std::uint16_t kind { 0 }; - std::uint16_t textureId { 0 }; - std::uint16_t unk6 { 0 }; - std::uint32_t nextVariation { 0 }; - std::uint8_t unkC { 0 }; - std::uint8_t unkD { 0 }; - std::uint8_t unkE { 0 }; - std::uint8_t currentVariation { 0 }; - std::uint16_t ptrParts { 0 }; - std::uint16_t materialIdx { 0 }; - std::uint32_t totalVariations { 0 }; - std::uint16_t ptrObjects { 0 }; - std::uint16_t ptrObjects_HI { 0 }; - std::uint32_t unk3 { 0 }; - BoundingBox boundingBox {}; - - static void deserialize(PRMDescriptionChunkBaseHeader& header, ZBio::ZBinaryReader::BinaryReader *binaryReader); - }; -} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMEntries.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMEntries.h new file mode 100644 index 0000000..c5a1a4e --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/PRM/PRMEntries.h @@ -0,0 +1,117 @@ +/** + * Credits: + * * 2kpr - https://github.com/glacier-modding/io_scene_blood_money/blob/libraries/BMExport/src/Prm.hpp + */ +#pragma once + +#include +#include +#include +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::prm +{ + enum class VertexFormat : uint32_t { + VF_ERROR = 0, + VF_10 = 0x10, + VF_24 = 0x24, + VF_28 = 0x28, + VF_34 = 0x34 + }; + +#pragma pack(push, 1) // TODO: Need to use some sort of macro to make this place cross-compiler supportable + struct PrmFile; + + struct Index + { + uint16_t a = 0; + uint16_t b = 0; + uint16_t c = 0; + + static void deserialize(Index& index, ZBio::ZBinaryReader::BinaryReader* binaryReader); + }; + + struct BoundingBox + { + glm::vec3 vMin; + glm::vec3 vMax; + + static void deserialize(BoundingBox& boundingBox, ZBio::ZBinaryReader::BinaryReader* binaryReader); + }; + + struct Mesh + { + uint8_t boneDecl = 0; + uint8_t packType = 0; + uint16_t kind = 0; + uint16_t textureId = 0; + uint16_t unk6 = 0; + uint32_t nextVariation = 0; + uint8_t unkC = 0; + uint8_t unkD = 0; + uint8_t lod = 0; + uint16_t material_id = 0; + uint8_t variationId = 0; + int32_t diffuse_id = 0; + int32_t normal_id = 0; + int32_t specular_id = 0; + uint16_t trianglesCount = 0; + std::vector vertices {}; + std::vector indices {}; + std::vector uvs {}; + VertexFormat vertexFormat { VertexFormat::VF_ERROR }; + + static void deserialize(Mesh& mesh, ZBio::ZBinaryReader::BinaryReader* binaryReader, const PrmFile& prmFile); + }; + + struct Model + { + uint32_t chunk = 0; + BoundingBox boundingBox {}; + std::vector meshes {}; + }; + + struct Chunk + { + std::unique_ptr data { nullptr }; + bool is_model = false; + uint32_t model = 0; + }; + + struct Entry + { + uint32_t offset = 0; + uint32_t size = 0; + uint32_t type = 0; + uint32_t pad = 0; + + static void deserialize(Entry& entry, ZBio::ZBinaryReader::BinaryReader* binaryReader); + }; + + struct Header + { + uint32_t table_offset = 0; + uint32_t table_count = 0; + uint32_t table_offset2 = 0; + uint32_t zeroed = 0; + + static void deserialize(Header& header, ZBio::ZBinaryReader::BinaryReader* binaryReader); + }; + + struct PrmFile + { + Header header; + std::vector entries; + std::vector chunks; + std::vector models; + }; +#pragma pack(pop) +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMException.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMException.h deleted file mode 100644 index 114e018..0000000 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRMException.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include - - -namespace gamelib::prm -{ - class PRMException : public std::exception - { - public: - using std::exception::exception; - }; -} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMHeader.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMHeader.h deleted file mode 100644 index d724bc9..0000000 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRMHeader.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include - - - -namespace ZBio::ZBinaryReader -{ - class BinaryReader; -} - -namespace gamelib::prm -{ - struct PRMHeader - { - uint32_t chunkOffset { 0 }; // Header + 0x0: Offset of main chunk - uint32_t countOfPrimitives { 0 }; // Header + 0x4: Total geoms count on scene (max about 0x21000 things) - uint32_t chunkOffset2 {0}; // Duplicate of chunkOffset, maybe second chunk? Or ... LODs? - uint32_t zeroed {0}; // always zero (maybe used as 'checkpoint') - - static void deserialize(PRMHeader &header, ZBio::ZBinaryReader::BinaryReader *binaryReader); - }; -} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMIndexChunkHeader.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMIndexChunkHeader.h deleted file mode 100644 index 2617131..0000000 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRMIndexChunkHeader.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include - - -namespace ZBio::ZBinaryReader -{ - class BinaryReader; -} - -namespace gamelib::prm -{ - struct PRMIndexChunkHeader - { - std::uint16_t unk0; - std::uint16_t indicesCount; - - static void deserialize(PRMIndexChunkHeader& header, ZBio::ZBinaryReader::BinaryReader *binaryReader); - }; -} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMReader.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMReader.h index 0e57933..8d762b6 100644 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRMReader.h +++ b/BMEdit/GameLib/Include/GameLib/PRM/PRMReader.h @@ -1,31 +1,24 @@ #pragma once -#include -#include -#include -#include -#include -#include +#include -namespace gamelib::prm +namespace gamelib { + /** + * @note It's not full implementation of reader (it's doing partial read, but enough to visualize level geometry in editor) + * @todo Need to complete reverse engineering of this stuff and make writer + */ class PRMReader { public: - PRMReader() = delete; - PRMReader(PRMHeader &header, std::vector &chunkDescriptors, std::vector &chunks); + PRMReader(); - bool read(Span buffer); + bool parse(const std::uint8_t* pBuffer, std::size_t iBufferSize); - [[nodiscard]] const PRMHeader &getHeader() const; - [[nodiscard]] const std::vector &getChunkDescriptors() const; - [[nodiscard]] PRMChunk* getChunkAt(size_t chunkIndex); - [[nodiscard]] const PRMChunk* getChunkAt(size_t chunkIndex) const; + prm::PrmFile&& takePrimitives(); private: - PRMHeader& m_header; - std::vector& m_chunks; - std::vector& m_chunkDescriptors; + prm::PrmFile m_file {}; }; } \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMVertexBufferHeader.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMVertexBufferHeader.h deleted file mode 100644 index 0c804da..0000000 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRMVertexBufferHeader.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include - - -namespace gamelib::prm -{ - struct PRMVertexBufferHeader - { - PRMVertexBufferFormat vertexFormat { PRMVertexBufferFormat::VBF_UNKNOWN_VERTEX }; - }; -} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRM/PRMVertexFormat.h b/BMEdit/GameLib/Include/GameLib/PRM/PRMVertexFormat.h deleted file mode 100644 index b3ebb82..0000000 --- a/BMEdit/GameLib/Include/GameLib/PRM/PRMVertexFormat.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include - - -namespace gamelib::prm -{ - enum class PRMVertexBufferFormat : std::uint8_t - { - VBF_VERTEX_10 = 0x10, - VBF_VERTEX_24 = 0x24, - VBF_VERTEX_28 = 0x28, - VBF_VERTEX_34 = 0x34, - - VBF_SIMPLE_VERTEX = VBF_VERTEX_10, - VBF_UNKNOWN_VERTEX = 0x0 - }; -} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRP/PRPInstruction.h b/BMEdit/GameLib/Include/GameLib/PRP/PRPInstruction.h index a51ebb4..98fddbc 100644 --- a/BMEdit/GameLib/Include/GameLib/PRP/PRPInstruction.h +++ b/BMEdit/GameLib/Include/GameLib/PRP/PRPInstruction.h @@ -64,9 +64,15 @@ namespace gamelib::prp template <> int32_t get() const { return trivial.i32; } template <> float get() const { return trivial.f32; } template <> double get() const { return trivial.f64; } + template <> std::string_view get() const { return str; } template <> const std::string& get() const { return str; } template <> const RawData& get() const { return raw; } template <> const StringArray& get() const { return stringArray; } + + /** + * Initialised (isSet = true) empty operand. + */ + static const PRPOperandVal kInitedOperandValue; }; class PRPInstruction diff --git a/BMEdit/GameLib/Include/GameLib/PRP/PRPMathTypes.h b/BMEdit/GameLib/Include/GameLib/PRP/PRPMathTypes.h new file mode 100644 index 0000000..73f5241 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/PRP/PRPMathTypes.h @@ -0,0 +1,185 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + + +namespace gamelib +{ + template<> + struct TObjectExtractor + { + static bool extract(const Span& instructions) + { + if (instructions.size() == 1) + { + return instructions[0].getOperand().get(); + } + + assert(false && "Bad object"); + return {}; + } + }; + + template<> + struct TObjectExtractor + { + static std::int8_t extract(const Span& instructions) + { + if (instructions.size() == 1) + { + return instructions[0].getOperand().get(); + } + + assert(false && "Bad object"); + return {}; + } + }; + + template<> + struct TObjectExtractor + { + static std::int16_t extract(const Span& instructions) + { + if (instructions.size() == 1) + { + return instructions[0].getOperand().get(); + } + + assert(false && "Bad object"); + return {}; + } + }; + + template<> + struct TObjectExtractor + { + static std::int32_t extract(const Span& instructions) + { + if (instructions.size() == 1) + { + return instructions[0].getOperand().get(); + } + + assert(false && "Bad object"); + return {}; + } + }; + + template<> + struct TObjectExtractor + { + static float extract(const Span& instructions) + { + if (instructions.size() == 1) + { + return instructions[0].getOperand().get(); + } + + assert(false && "Bad object"); + return {}; + } + }; + + template<> + struct TObjectExtractor + { + static std::string extract(const Span& instructions) + { + if (instructions.size() == 1) + { + const std::string& v = instructions[0].getOperand().get(); + return v; + } + + assert(false && "Bad object"); + return {}; + } + }; + + template<> + struct TObjectExtractor + { + static glm::vec2 extract(const Span& instructions) + { + if (instructions.size() == 4) // [0] begin array, [-1] end array + { + return { + instructions[1].getOperand().get(), + instructions[2].getOperand().get() + }; + } + + assert(false && "Bad object"); + return {}; + } + }; + + template<> + struct TObjectExtractor + { + static glm::vec3 extract(const Span& instructions) + { + if (instructions.size() == 5) // [0] begin array, [-1] end array + { + return { + instructions[1].getOperand().get(), + instructions[2].getOperand().get(), + instructions[3].getOperand().get() + }; + } + + assert(false && "Bad object"); + return {}; + } + }; + + template<> + struct TObjectExtractor + { + static glm::vec4 extract(const Span& instructions) + { + if (instructions.size() == 6) // [0] begin array, [-1] end array + { + return { + instructions[1].getOperand().get(), + instructions[2].getOperand().get(), + instructions[3].getOperand().get(), + instructions[4].getOperand().get() + }; + } + + assert(false && "Bad object"); + return {}; + } + }; + + template<> + struct TObjectExtractor + { + static glm::mat3 extract(const Span& instructions) + { + if (instructions.size() == 11) // [0] begin array, [-1] end array + { + return { + instructions[1].getOperand().get(), + instructions[2].getOperand().get(), + instructions[3].getOperand().get(), + instructions[4].getOperand().get(), + instructions[5].getOperand().get(), + instructions[6].getOperand().get(), + instructions[7].getOperand().get(), + instructions[8].getOperand().get(), + instructions[9].getOperand().get() + }; + } + + assert(false && "Bad object"); + return {}; + } + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/PRP/PRPObjectExtractor.h b/BMEdit/GameLib/Include/GameLib/PRP/PRPObjectExtractor.h new file mode 100644 index 0000000..01eae31 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/PRP/PRPObjectExtractor.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + + +namespace gamelib +{ + class Value; + + template + struct TObjectExtractor + { + static T extract(const Span& instructions); + }; + + template + concept HasSpecializationTObjectExtractor = requires { + typename TObjectExtractor; + }; + +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/Plane.h b/BMEdit/GameLib/Include/GameLib/Plane.h new file mode 100644 index 0000000..7d2c726 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/Plane.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include + + +namespace gamelib +{ + template + concept THasPosition = requires(T t) + { + { t.vPos }; + }; + + class Plane + { + public: + Plane(); + Plane(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2, const glm::vec3& v3); + + /** + * @brief Calculates bounding box which cover plane + */ + BoundingBox makeBoundingBox() const; + + /** + * @brief Calculates normal vector of plane + * @note This method using easy calculation method, so it can return wrong result for hard planes + */ + glm::vec3 getNormal() const; + + /** + * @return -getNormal + * @note See comment to getNormal for details + */ + glm::vec3 getBiNormal() const; + + /** + * @return center of plane (point, not a vector) + */ + glm::vec3 getCenter() const; + + /** + * @return point of plane (0 to 3, other index will return (0;0;0) point) + * @param idx - index of point + */ + const glm::vec3& getPoint(size_t idx) const; + + /** + * @return Size of plane as maximum distance between points + */ + float getSize() const; + + template + void toTriangles(TOutVertexIterator outVertexIt, TOutIndexIterator outIndexIt) requires (THasPosition) + { + // Push vertices + for (const auto& v : m_aVertices) + { + using TP = typename TOutVertexIterator::container_type::value_type; + TP vProxy; + vProxy.vPos = v; + (*outVertexIt++) = std::move(vProxy); + } + + // Push indices + (*outIndexIt++) = 0; + (*outIndexIt++) = 1; + (*outIndexIt++) = 2; + (*outIndexIt++) = 0; + (*outIndexIt++) = 2; + (*outIndexIt++) = 3; + } + + private: + glm::vec3 m_aVertices[4]; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/Scene/SceneObject.h b/BMEdit/GameLib/Include/GameLib/Scene/SceneObject.h index c6d30e0..74c6714 100644 --- a/BMEdit/GameLib/Include/GameLib/Scene/SceneObject.h +++ b/BMEdit/GameLib/Include/GameLib/Scene/SceneObject.h @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -10,6 +11,7 @@ #include #include #include +#include #include @@ -22,10 +24,14 @@ namespace gamelib::scene using Ref = std::weak_ptr; using Instructions = std::vector; + /** + * Scene geom controller description (ZEventBase in Glacier) + */ struct Controller { - std::string name; - Value properties; + std::string name; /// Name of controller + Value properties; /// Properties pack + const Type* type { nullptr }; /// Type of controller (native type, see TypeRegistry for details) bool operator==(const std::string &controllerName) const; bool operator!=(const std::string &controllerName) const; @@ -49,13 +55,67 @@ namespace gamelib::scene [[nodiscard]] const Controllers &getControllers() const; [[nodiscard]] Controllers &getControllers(); [[nodiscard]] const Value &getProperties() const; - [[nodiscard]] Value &getProperties(); + void setProperties(const Value &v); [[nodiscard]] const gms::GMSGeomEntity &getGeomInfo() const; [[nodiscard]] gms::GMSGeomEntity &getGeomInfo(); [[nodiscard]] const SceneObject::Ref &getParent() const; [[nodiscard]] const std::vector &getChildren() const; [[nodiscard]] std::vector &getChildren(); + /** + * @param baseType + * @return return true if game object inherited of baseType + */ + [[nodiscard]] bool isInheritedOf(const std::string& baseType) const; + + /** + * @param type - name of type + * @return true if type equals to required 'type' + */ + [[nodiscard]] bool is(const std::string& type) const; + + /** + * @brief Calculate transform matrix for OpenGL and other render API buddies + * @note This function calculates matrix at runtime. So, it's not huge operation but avoid of frequency calling of this function, please. + * @return model matrix (scale, rotation and translate operations applied) + */ + [[nodiscard]] glm::mat4 getLocalTransform() const; + + /** + * @return Position object from ZGEOM or (0;0;0) + */ + [[nodiscard]] glm::vec3 getPosition() const; + + /** + * @note Unlike getLocalTransform this matrix created for DX9 renderer. Do not use this matrix without preparation! + * @note If object does not contains Matrix object this method will return identity matrix for OpenGL (specific of glm). To avoid of bugs use hasProperty before! + * @return Matrix object from ZGEOM or identity matrix + */ + [[nodiscard]] glm::mat3 getOriginalTransform() const; + + /** + * @brief Calculate world transform of object + * @note This method iterates over all parents and multiply all matrices into one combined matrix. It may take a while so use external caches (because SceneObject does not make any caches itself) + * @return World model matrix of object + */ + [[nodiscard]] glm::mat4 getWorldTransform() const; + + enum class EVisitResult + { + VR_CONTINUE, // Continue iterations inside + VR_STOP_ALL, // Stop all iterations, finish visitor + VR_NEXT // Go to next node on this level, do not go inside + }; + + /** + * @brief Visit scene tree from this object deep inside + * @param pred - predicate func + */ + void visitChildren(const std::function& pred) const; + + private: + EVisitResult internalVisitChildObjects(const std::function& pred) const; + private: std::string m_name {}; ///< Name of geom uint32_t m_typeId { 0u }; ///< Type ID of geom diff --git a/BMEdit/GameLib/Include/GameLib/Span.h b/BMEdit/GameLib/Include/GameLib/Span.h index 267cce8..d3832ab 100644 --- a/BMEdit/GameLib/Include/GameLib/Span.h +++ b/BMEdit/GameLib/Include/GameLib/Span.h @@ -49,6 +49,7 @@ namespace gamelib [[nodiscard]] bool empty() const { return m_size == 0; } [[nodiscard]] int64_t size() const { return m_size; } [[nodiscard]] T* data() { return const_cast(m_data); } + [[nodiscard]] const T* data() const { return m_data; } [[nodiscard]] explicit operator bool() const noexcept { return (m_data != nullptr); } diff --git a/BMEdit/GameLib/Include/GameLib/TEX/TEX.h b/BMEdit/GameLib/Include/GameLib/TEX/TEX.h new file mode 100644 index 0000000..82205a9 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/TEX/TEX.h @@ -0,0 +1,8 @@ +#pragma once + + +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/TEX/TEXEntry.h b/BMEdit/GameLib/Include/GameLib/TEX/TEXEntry.h new file mode 100644 index 0000000..d57370e --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/TEX/TEXEntry.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + + +namespace ZBio::ZBinaryWriter +{ + class BinaryWriter; +} + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::tex +{ + struct PALPalette + { + uint32_t m_size { 0 }; + std::unique_ptr m_data { nullptr }; + + public: + static void deserialize(PALPalette &palette, ZBio::ZBinaryReader::BinaryReader *binaryReader); + static void serialize(const PALPalette &palette, ZBio::ZBinaryWriter::BinaryWriter *writerStream); + }; + + struct TEXMipLevel + { + uint32_t m_mipLevelSize { 0u }; + std::unique_ptr m_buffer { nullptr }; + + public: + static void deserialize(TEXMipLevel &mipLevel, ZBio::ZBinaryReader::BinaryReader *binaryReader); + static void serialize(const TEXMipLevel &mipLevel, ZBio::ZBinaryWriter::BinaryWriter *writerStream); + }; + + struct TEXCubeMaps + { + uint32_t m_count { 0u }; + std::vector m_textureIndices {}; + + public: + static void deserialize(TEXCubeMaps &cubeMaps, ZBio::ZBinaryReader::BinaryReader *binaryReader); + static void serialize(const TEXCubeMaps &cubeMaps, ZBio::ZBinaryWriter::BinaryWriter *writerStream); + }; + + using TEXEntryFlag = uint32_t; + + enum TEXEntryFlags : TEXEntryFlag + { + TEX_EF_IsGUI = 0x2 + , TEX_EF_Unk40 = 0x40 + , TEX_EF_IsCubeMap = 0x400 + , TEX_EF_Unk1000 = 0x1000 + }; + + struct TEXEntry + { + TEXEntry() = default; + + static void deserialize(TEXEntry &header, ZBio::ZBinaryReader::BinaryReader *binaryReader); + static void serialize(const TEXEntry &header, ZBio::ZBinaryWriter::BinaryWriter *writerStream, uint32_t& entryOffset, uint32_t& cubeMapsDescOffset); + + [[nodiscard]] uint32_t calculateSize() const; + + public: + uint32_t m_offset { 0u }; + uint32_t m_fileSize { 0u }; + TEXEntryType m_type1 {}; + TEXEntryType m_type2 {}; + uint32_t m_index { 0u }; + uint16_t m_width { 0u }; + uint16_t m_height { 0u }; + uint32_t m_numOfMipMaps { 0u }; + TEXEntryFlag m_flags { 0u }; + uint32_t m_unk2 { 0u }; + uint32_t m_unk3 { 0u }; + std::optional m_fileName {}; + std::vector m_mipLevels {}; + std::optional m_palPalette; + TEXCubeMaps m_cubeMaps; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/TEX/TEXEntryType.h b/BMEdit/GameLib/Include/GameLib/TEX/TEXEntryType.h new file mode 100644 index 0000000..dc37efb --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/TEX/TEXEntryType.h @@ -0,0 +1,28 @@ +#pragma once + +#include + + +namespace gamelib::tex +{ + /** + * @enum TEXEntryType + * @brief Describes all supported by Glacier texture formats + * @note BMEdit supports I8, U8V8, DXT1, DXT3, RGBA (32) and PAL formats. PAL_OPAC not used by Glacier but supported in missions (not in Loader_Sequence!). + * EMBM, DOT3, CUBE and DMAP referred, supported (in theory) but we can operate it only via DirectX9 SDK, so not our case. + */ + enum class TEXEntryType : uint32_t + { + ET_BITMAP_I8 = 0x49382020u, //I8 + ET_BITMAP_EMBM = 0x454D424Du, //EMBM + ET_BITMAP_DOT3 = 0x444F5433u, //DOT3 + ET_BITMAP_CUBE = 0x43554245u, //CUBE + ET_BITMAP_DMAP = 0x444D4150u, //DMAP + ET_BITMAP_PAL = 0x50414C4Eu, //PALN + ET_BITMAP_PAL_OPAC = 0x50414C4Fu, //PALO + ET_BITMAP_32 = 0x52474241u, //RGBA + ET_BITMAP_U8V8 = 0x55385638u, //V8U8 + ET_BITMAP_DXT1 = 0x44585431u, //DXT1 + ET_BITMAP_DXT3 = 0x44585433u, //DXT3 + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/TEX/TEXHeader.h b/BMEdit/GameLib/Include/GameLib/TEX/TEXHeader.h new file mode 100644 index 0000000..9d9ea66 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/TEX/TEXHeader.h @@ -0,0 +1,32 @@ +#pragma once + +#include + + +namespace ZBio::ZBinaryWriter +{ + class BinaryWriter; +} + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::tex +{ + struct TEXHeader + { + TEXHeader() = default; + TEXHeader(uint32_t table1Offset, uint32_t table2Offset, uint32_t unk1, uint32_t unk2); + + static void deserialize(TEXHeader &header, ZBio::ZBinaryReader::BinaryReader *binaryReader); + static void serialize(const TEXHeader &header, ZBio::ZBinaryWriter::BinaryWriter *writerStream); + + public: + uint32_t m_texturesPoolOffset { 0u }; + uint32_t m_cubeMapsPoolOffset { 0u }; + uint32_t m_unk1 { 0u }; + uint32_t m_unk2 { 0u }; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/TEX/TEXReader.h b/BMEdit/GameLib/Include/GameLib/TEX/TEXReader.h new file mode 100644 index 0000000..ef47985 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/TEX/TEXReader.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + + +namespace ZBio::ZBinaryReader +{ + class BinaryReader; +} + +namespace gamelib::tex +{ + class TEXReader + { + public: + TEXReader() = default; + + bool parse(const uint8_t *texFileBuffer, int64_t texFileSize); + + public: + TEXHeader m_header; + std::vector m_entries; + OffsetsPool m_texturesPool { 0u }; + OffsetsPool m_cubeMapsPool { 0u }; + uint32_t m_countOfEmptyOffsets { 0u }; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/TEX/TEXTypes.h b/BMEdit/GameLib/Include/GameLib/TEX/TEXTypes.h new file mode 100644 index 0000000..43f60eb --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/TEX/TEXTypes.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + + +namespace gamelib::tex +{ + using OffsetsPool = std::array; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/TEX/TEXWriter.h b/BMEdit/GameLib/Include/GameLib/TEX/TEXWriter.h new file mode 100644 index 0000000..f7412bf --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/TEX/TEXWriter.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + + +namespace gamelib::tex +{ + struct TEXWriter + { + static void write(const TEXHeader &header, const std::vector &entries, std::vector &outBuffer); + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/Type.h b/BMEdit/GameLib/Include/GameLib/Type.h index 3d43aef..6e7a21f 100644 --- a/BMEdit/GameLib/Include/GameLib/Type.h +++ b/BMEdit/GameLib/Include/GameLib/Type.h @@ -43,6 +43,12 @@ namespace gamelib */ [[nodiscard]] virtual DataMappingResult map(const Span &instructions) const; + /** + * @fn makeDefaultPropertiesPack + * @return Value with pack of default constructed properties + */ + [[nodiscard]] virtual Value makeDefaultPropertiesPack() const; + private: std::string m_name {}; TypeKind m_kind { TypeKind::NONE }; diff --git a/BMEdit/GameLib/Include/GameLib/TypeAlias.h b/BMEdit/GameLib/Include/GameLib/TypeAlias.h index 11c5464..c58e611 100644 --- a/BMEdit/GameLib/Include/GameLib/TypeAlias.h +++ b/BMEdit/GameLib/Include/GameLib/TypeAlias.h @@ -17,10 +17,18 @@ namespace gamelib [[nodiscard]] VerificationResult verify(const Span& instructions) const override; [[nodiscard]] Type::DataMappingResult map(const Span &instructions) const override; + [[nodiscard]] Value makeDefaultPropertiesPack() const override; [[nodiscard]] const Type* getFinalType() const; [[nodiscard]] prp::PRPOpCode getFinalOpCode() const; + + public: // Additional & tooling + [[nodiscard]] bool hasToolHint() const; + [[nodiscard]] const std::string& getToolHint() const; + void setToolHint(const std::string& toolHint); + private: TypeReference m_resultTypeInfo; + std::string m_toolHint {}; }; } \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/TypeArray.h b/BMEdit/GameLib/Include/GameLib/TypeArray.h index c70d8e5..d6f488c 100644 --- a/BMEdit/GameLib/Include/GameLib/TypeArray.h +++ b/BMEdit/GameLib/Include/GameLib/TypeArray.h @@ -20,6 +20,7 @@ namespace gamelib [[nodiscard]] VerificationResult verify(const Span& instructions) const override; [[nodiscard]] Type::DataMappingResult map(const Span &instructions) const override; + [[nodiscard]] Value makeDefaultPropertiesPack() const override; private: prp::PRPOpCode m_entryType { prp::PRPOpCode::ERR_UNKNOWN }; uint32_t m_requiredCapacity { 0u }; diff --git a/BMEdit/GameLib/Include/GameLib/TypeBitfield.h b/BMEdit/GameLib/Include/GameLib/TypeBitfield.h index 4fcb8e7..ec094d5 100644 --- a/BMEdit/GameLib/Include/GameLib/TypeBitfield.h +++ b/BMEdit/GameLib/Include/GameLib/TypeBitfield.h @@ -18,6 +18,7 @@ namespace gamelib [[nodiscard]] VerificationResult verify(const Span& instructions) const override; [[nodiscard]] Type::DataMappingResult map(const Span &instructions) const override; + [[nodiscard]] Value makeDefaultPropertiesPack() const override; private: TypeBitfield::PossibleOptions m_possibleOptions; }; diff --git a/BMEdit/GameLib/Include/GameLib/TypeComplex.h b/BMEdit/GameLib/Include/GameLib/TypeComplex.h index dfc066f..9894d21 100644 --- a/BMEdit/GameLib/Include/GameLib/TypeComplex.h +++ b/BMEdit/GameLib/Include/GameLib/TypeComplex.h @@ -31,6 +31,7 @@ namespace gamelib [[nodiscard]] VerificationResult verify(const Span& instructions) const override; [[nodiscard]] Type::DataMappingResult map(const Span &instructions) const override; + [[nodiscard]] Value makeDefaultPropertiesPack() const override; private: GeomBasedTypeInfo &createGeomInfo(); diff --git a/BMEdit/GameLib/Include/GameLib/TypeContainer.h b/BMEdit/GameLib/Include/GameLib/TypeContainer.h index d7a2307..5b32ece 100644 --- a/BMEdit/GameLib/Include/GameLib/TypeContainer.h +++ b/BMEdit/GameLib/Include/GameLib/TypeContainer.h @@ -16,5 +16,6 @@ namespace gamelib [[nodiscard]] VerificationResult verify(const Span& instructions) const override; [[nodiscard]] Type::DataMappingResult map(const Span &instructions) const override; + [[nodiscard]] Value makeDefaultPropertiesPack() const; }; } \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/TypeEnum.h b/BMEdit/GameLib/Include/GameLib/TypeEnum.h index 16ccf8d..89d8e84 100644 --- a/BMEdit/GameLib/Include/GameLib/TypeEnum.h +++ b/BMEdit/GameLib/Include/GameLib/TypeEnum.h @@ -22,6 +22,7 @@ namespace gamelib [[nodiscard]] VerificationResult verify(const Span& instructions) const override; [[nodiscard]] Type::DataMappingResult map(const Span &instructions) const override; + [[nodiscard]] Value makeDefaultPropertiesPack() const override; [[nodiscard]] const Entries &getPossibleValues() const; private: diff --git a/BMEdit/GameLib/Include/GameLib/TypeRawData.h b/BMEdit/GameLib/Include/GameLib/TypeRawData.h index 1463fcd..074ca85 100644 --- a/BMEdit/GameLib/Include/GameLib/TypeRawData.h +++ b/BMEdit/GameLib/Include/GameLib/TypeRawData.h @@ -12,5 +12,6 @@ namespace gamelib [[nodiscard]] VerificationResult verify(const Span& instructions) const override; [[nodiscard]] Type::DataMappingResult map(const Span &instructions) const override; + [[nodiscard]] Value makeDefaultPropertiesPack() const override; }; } \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/TypeRegistry.h b/BMEdit/GameLib/Include/GameLib/TypeRegistry.h index 76bee7b..ebf4c7e 100644 --- a/BMEdit/GameLib/Include/GameLib/TypeRegistry.h +++ b/BMEdit/GameLib/Include/GameLib/TypeRegistry.h @@ -14,6 +14,13 @@ namespace gamelib { + struct ScriptInfo + { + std::string name; // name of scripts + std::vector entries; // entries in script + std::vector initialInstructions; // contains instructions to produce whole description in PRP + }; + class TypeRegistry { TypeRegistry(); @@ -32,12 +39,18 @@ namespace gamelib std::vector &&typeDeclarations, std::unordered_map &&typeToHash); + void registerScripts(std::unordered_map&& scriptInfoMap); + + [[nodiscard]] std::optional getScriptInfo(const std::string& scriptName); + [[nodiscard]] bool hasScriptInfo(const std::string& scriptName); + [[nodiscard]] const Type *findTypeByName(const std::string &typeName) const; [[nodiscard]] const Type *findTypeByHash(const std::string &hash) const; [[nodiscard]] const Type *findTypeByHash(std::size_t hash) const; [[nodiscard]] const Type *findTypeByShortName(const std::string &typeName) const; void forEachType(const std::function &predicate); + void forEachScript(const std::function &predicate); void linkTypes(); void addHashAssociation(std::size_t hash, const std::string &typeName); @@ -86,5 +99,6 @@ namespace gamelib std::vector> m_types; std::unordered_map m_typesByHash; std::unordered_map m_typesByName; + std::unordered_map m_scriptsByName; }; } \ No newline at end of file diff --git a/BMEdit/GameLib/Include/GameLib/Value.h b/BMEdit/GameLib/Include/GameLib/Value.h index f5036a5..427b11c 100644 --- a/BMEdit/GameLib/Include/GameLib/Value.h +++ b/BMEdit/GameLib/Include/GameLib/Value.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -73,13 +74,18 @@ namespace gamelib */ Value& operator+=(const std::pair& another); + /** + * Add a new view + */ + Value& operator+=(const ValueEntry& ent); + /** * Extract span of instructions which represents requested property * @param propertyName - name of the property * @return span of instructions * @note This function may throw an exception if requested propertyName not found. You should check your property via hasProperty before ask operator[] */ - Span operator[](const char* propertyName); + Span operator[](const char* propertyName) const; [[nodiscard]] bool operator==(const Value &other) const; [[nodiscard]] bool operator!=(const Value &other) const; @@ -93,6 +99,23 @@ namespace gamelib bool hasProperty(const char* propertyName) const; + // Extractor + template + T getObject(const std::string& objectName, T def = T()) const requires (HasSpecializationTObjectExtractor) + { + for (const auto& ent : m_entries) + { + if (ent.name == objectName) + { + return TObjectExtractor::extract(Span(m_data).slice(ent.instructions)); + } + } + + return def; + } + + void removeEntriesAndViewsSince(size_t startIndex); + private: const Type *m_type {nullptr}; // type std::vector m_data; // instructions diff --git a/BMEdit/GameLib/Include/GameLib/ZBioHelpers.h b/BMEdit/GameLib/Include/GameLib/ZBioHelpers.h new file mode 100644 index 0000000..6263387 --- /dev/null +++ b/BMEdit/GameLib/Include/GameLib/ZBioHelpers.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +// Just a few helpers for BinaryIO library +namespace gamelib +{ + struct ZBioHelpers + { + /** + * @brief Move position by offset from current position (pos += offset) + * @param reader + * @param offset + */ + static void seekBy(ZBio::ZBinaryReader::BinaryReader* reader, int64_t offset) + { + if (!reader) + { + assert(reader != nullptr); + return; + } + + const int64_t finalPos = offset + reader->tell(); + if (finalPos < 0 || finalPos > reader->size()) + { + assert(false && "Out of bounds!"); + return; + } + + reader->seek(finalPos); + } + }; + + template + concept TSeekable = requires (T t) + { + t.seek((int64_t)1337); + t.tell(); + }; + + template requires (TSeekable) + struct ZBioSeekGuard + { + ZBioSeekGuard() = delete; + ZBioSeekGuard(const ZBioSeekGuard&) = delete; + ZBioSeekGuard(ZBioSeekGuard&&) = delete; + ZBioSeekGuard& operator=(const ZBioSeekGuard&) = delete; + ZBioSeekGuard& operator=(ZBioSeekGuard&&) = delete; + + explicit ZBioSeekGuard(T* seekable) + { + m_seekable = seekable; + + if (seekable) + { + m_seekTo = seekable->tell(); + } + } + + ~ZBioSeekGuard() + { + if (m_seekable) + { + m_seekable->seek(m_seekTo); + m_seekable = nullptr; + } + } + + private: + T* m_seekable { nullptr }; + int64_t m_seekTo { 0 }; + }; +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/BoundingBox.cpp b/BMEdit/GameLib/Source/GameLib/BoundingBox.cpp index aa9709e..9937025 100644 --- a/BMEdit/GameLib/Source/GameLib/BoundingBox.cpp +++ b/BMEdit/GameLib/Source/GameLib/BoundingBox.cpp @@ -1,14 +1,97 @@ #include +#include using namespace gamelib; -BoundingBox::BoundingBox(const gamelib::Vector3 &vMin, const gamelib::Vector3 &vMax) +BoundingBox::BoundingBox(const glm::vec3 &vMin, const glm::vec3 &vMax) : min(vMin), max(vMax) { } -Vector3 BoundingBox::getCenter() const +glm::vec3 BoundingBox::getCenter() const { return (min + max) / 2.f; +} + +void BoundingBox::expand(const BoundingBox& another) +{ + expand(another.min); + expand(another.max); +} + +void BoundingBox::expand(const glm::vec3 &vPoint) +{ + min.x = std::min(min.x, vPoint.x); + min.y = std::min(min.y, vPoint.y); + min.z = std::min(min.z, vPoint.z); + max.x = std::max(max.x, vPoint.x); + max.y = std::max(max.y, vPoint.y); + max.z = std::max(max.z, vPoint.z); +} + +bool BoundingBox::contains(const glm::vec3& vPoint) const +{ + return vPoint.x >= min.x && vPoint.x <= max.x && + vPoint.y >= min.y && vPoint.y <= max.y && + vPoint.z >= min.z && vPoint.z <= max.z; +} + +bool BoundingBox::intersect(const gamelib::BoundingBox& another) const +{ + if (min.x > another.max.x) return false; + if (max.x < another.min.x) return false; + if (min.y > another.max.y) return false; + if (max.y < another.min.y) return false; + if (min.z > another.max.z) return false; + if (max.z < another.min.z) return false; + + return true; +} + +double BoundingBox::getVolume() const +{ + static auto w = static_cast(max.x - min.x); + static auto h = static_cast(max.y - min.y); + static auto d = static_cast(max.z - min.z); + + return w * h * d; +} + +std::tuple BoundingBox::getDimensions() const +{ + return std::make_tuple( + max.x - min.x, + max.y - min.y, + max.z - min.z + ); +} + +BoundingBox BoundingBox::toWorld(const BoundingBox& source, const glm::mat4& mTransform) +{ + glm::vec3 vMin = source.min; + glm::vec3 vMax = source.max; + + glm::vec3 avVertices[8]; + avVertices[0] = vMin; + avVertices[1] = glm::vec3(vMax.x, vMin.y, vMin.z); + avVertices[2] = glm::vec3(vMin.x, vMax.y, vMin.z); + avVertices[3] = glm::vec3(vMax.x, vMax.y, vMin.z); + avVertices[4] = glm::vec3(vMin.x, vMin.y, vMax.z); + avVertices[5] = glm::vec3(vMax.x, vMin.y, vMax.z); + avVertices[6] = glm::vec3(vMin.x, vMax.y, vMax.z); + avVertices[7] = vMax; + + BoundingBox result {}; + result.min = glm::vec3(mTransform * glm::vec4(avVertices[0], 1.f)); + result.max = result.min; + + for (int i = 1; i < 8; i++) + { + glm::vec3 vTransformed = glm::vec3(mTransform * glm::vec4(avVertices[i], 1.f)); + result.min = glm::min(result.min, vTransformed); + result.max = glm::max(result.max, vTransformed); + } + + return result; } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/GMS/GMSGeomEntity.cpp b/BMEdit/GameLib/Source/GameLib/GMS/GMSGeomEntity.cpp index 69c845d..dbfe9bf 100644 --- a/BMEdit/GameLib/Source/GameLib/GMS/GMSGeomEntity.cpp +++ b/BMEdit/GameLib/Source/GameLib/GMS/GMSGeomEntity.cpp @@ -38,7 +38,7 @@ namespace gamelib::gms bool GMSGeomEntity::isRootOfGroup() const { - return (m_geomFlags & 0x1000000) != 0u; + return (m_geomFlags & 0x1000000) != 0u; // bit #24 } uint32_t GMSGeomEntity::getRelativeDepthLevel() const @@ -46,6 +46,15 @@ namespace gamelib::gms return (m_geomFlags >> 25u); } + uint32_t GMSGeomEntity::getGeomFlags() const + { + // D1 = 50776 = 0000 0000 0000 0000 1100 0110 0101 1000 + // D2 = 50808 = 0000 0000 0000 0000 1100 0110 0111 1000 + // #5 + + return m_geomFlags; + } + void GMSGeomEntity::deserialize(GMSGeomEntity &entity, ZBio::ZBinaryReader::BinaryReader *gmsBinaryReader, ZBio::ZBinaryReader::BinaryReader *bufBinaryReader) { // Read name diff --git a/BMEdit/GameLib/Source/GameLib/GMS/GMSHeader.cpp b/BMEdit/GameLib/Source/GameLib/GMS/GMSHeader.cpp index fbef468..6dfc614 100644 --- a/BMEdit/GameLib/Source/GameLib/GMS/GMSHeader.cpp +++ b/BMEdit/GameLib/Source/GameLib/GMS/GMSHeader.cpp @@ -41,7 +41,7 @@ namespace gamelib::gms { // Validate format BinaryReaderSeekScope rootScope { gmsFileReader }; - gmsFileReader->seek(4); + gmsFileReader->seek(4); // skip first block (Entities) static constexpr std::array kExpectedSignature = { 0, 0, 4 }; std::array sections = { 0, 0, 0 }; @@ -215,53 +215,4 @@ namespace gamelib::gms } } } - - CachedRuntimeTypes::CachedRuntimeTypes() - { - auto &rttiRegistry = TypeRegistry::getInstance(); - -#define IS_VALID_TYPE_INSTANCE(x) ((x) && (x->getKind() == TypeKind::COMPLEX) && reinterpret_cast((x))->hasGeomInfo()) - - ZSHAPE = rttiRegistry.findTypeByName("ZSHAPE"); - if (IS_VALID_TYPE_INSTANCE(ZSHAPE)) { - ZSHAPE_Runtime = &reinterpret_cast(ZSHAPE)->getGeomInfo(); - } - - ZSTDOBJ = rttiRegistry.findTypeByName("ZSTDOBJ"); - if (IS_VALID_TYPE_INSTANCE(ZSTDOBJ)) { - ZSTDOBJ_Runtime = &reinterpret_cast(ZSTDOBJ)->getGeomInfo(); - } - - ZBOUND = rttiRegistry.findTypeByName("ZBOUND"); - if (IS_VALID_TYPE_INSTANCE(ZBOUND)) { - ZBOUND_Runtime = &reinterpret_cast(ZBOUND)->getGeomInfo(); - } - - ZSNDOBJ = rttiRegistry.findTypeByName("ZSNDOBJ"); - if (IS_VALID_TYPE_INSTANCE(ZSNDOBJ)) { - ZSNDOBJ_Runtime = &reinterpret_cast(ZSNDOBJ)->getGeomInfo(); - } - - ZGROUP = rttiRegistry.findTypeByName("ZGROUP"); - if (IS_VALID_TYPE_INSTANCE(ZGROUP)) { - ZGROUP_Runtime = &reinterpret_cast(ZGROUP)->getGeomInfo(); - } - - ZLIGHT = rttiRegistry.findTypeByName("ZLIGHT"); - if (IS_VALID_TYPE_INSTANCE(ZLIGHT)) { - ZLIGHT_Runtime = &reinterpret_cast(ZLIGHT)->getGeomInfo(); - } - -#undef IS_VALID_TYPE_INSTANCE - } - - CachedRuntimeTypes::operator bool() const noexcept - { - return ZSHAPE && ZSHAPE_Runtime - && ZSTDOBJ && ZSTDOBJ_Runtime - && ZBOUND && ZBOUND_Runtime - && ZSNDOBJ && ZSNDOBJ_Runtime - && ZGROUP && ZGROUP_Runtime - && ZLIGHT && ZLIGHT_Runtime; - } } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/GMS/Room/ZRoomDefs.cpp b/BMEdit/GameLib/Source/GameLib/GMS/Room/ZRoomDefs.cpp new file mode 100644 index 0000000..7447664 --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/GMS/Room/ZRoomDefs.cpp @@ -0,0 +1,45 @@ +#include +#include +#include + + +namespace gamelib::gms::room +{ + void ZRoomExit::deserialize(ZRoomExit& eXit, ZBio::ZBinaryReader::BinaryReader* bufBinaryReader) + { + bufBinaryReader->read(glm::value_ptr(eXit.v0), 3); + bufBinaryReader->read(glm::value_ptr(eXit.v1), 3); + bufBinaryReader->read(glm::value_ptr(eXit.v2), 3); + bufBinaryReader->read(glm::value_ptr(eXit.v3), 3); + eXit.iRoomREF = bufBinaryReader->read(); + eXit.unk1C = bufBinaryReader->read(); + eXit.unk1D = bufBinaryReader->read(); + eXit.unkFlags = bufBinaryReader->read(); + eXit.unk1F = bufBinaryReader->read(); + } + + void ZRoomExit::deserialize(ZRoomExit& eXit, const Span& byteBufferSpan) + { + if (byteBufferSpan.size() < sizeof(ZRoomExit)) + { + assert(byteBufferSpan.size() >= sizeof(ZRoomExit) && "Too small span buffer"); + return; + } + + ZBio::ZBinaryReader::BinaryReader binaryReader(reinterpret_cast(byteBufferSpan.data()), byteBufferSpan.size()); + ZRoomExit::deserialize(eXit, &binaryReader); + } + + void ZRoomNeighbor::deserialize(ZRoomNeighbor& neighbor, ZBio::ZBinaryReader::BinaryReader *bufBinaryReader) + { + neighbor.rRoomREF = bufBinaryReader->read(); + neighbor.unk4 = bufBinaryReader->read(); + neighbor.unk8 = bufBinaryReader->read(); + } + + void ZRoomNeighbor::deserialize(gamelib::gms::room::ZRoomNeighbor& neighbor, const Span& byteBufferSpan) + { + ZBio::ZBinaryReader::BinaryReader binaryReader(reinterpret_cast(byteBufferSpan.data()), byteBufferSpan.size()); + ZRoomNeighbor::deserialize(neighbor, &binaryReader); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/LOC/LOC.cpp b/BMEdit/GameLib/Source/GameLib/LOC/LOC.cpp new file mode 100644 index 0000000..228cb42 --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/LOC/LOC.cpp @@ -0,0 +1 @@ +#include \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/LOC/LOCReader.cpp b/BMEdit/GameLib/Source/GameLib/LOC/LOCReader.cpp new file mode 100644 index 0000000..6af546f --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/LOC/LOCReader.cpp @@ -0,0 +1,55 @@ +#include +#include +#include +#include + + +namespace gamelib::loc +{ + LOCReader::LOCReader() = default; + + bool LOCReader::parse(const uint8_t *locFileBuffer, int64_t locFileSize) + { + // First node is always ROOT and starts from 1 byte 0x1 + ZBio::ZBinaryReader::BinaryReader reader { (const char*)locFileBuffer, (int64_t)locFileSize }; + + // Check first byte + auto rootChildNr = reader.read(); + if (rootChildNr < 1) return false; // no root nodes found + + // Create pseudo ROOT + m_pRoot = std::make_shared(); + m_pRoot->type = LOCTreeNodeType::CHILDREN; + m_pRoot->name = "ROOT"; + + std::vector offsets {}; + offsets.resize(rootChildNr); + offsets[0] = 0x0; // zero offset + + for (int i = 0; i < rootChildNr - 1; i++) + { + offsets[i + 1] = reader.read(); + } + + // Create children + m_pRoot->children.resize(rootChildNr); + + for (int i = 0; i < rootChildNr; i++) + { + ZBioSeekGuard guard { &reader }; + ZBioHelpers::seekBy(&reader, offsets[i]); + + m_pRoot->children[i] = std::make_shared(); + m_pRoot->children[i]->parent = m_pRoot; // store parent + + LOCTreeNode::deserialize(m_pRoot->children[i], &reader); + } + + return true; + } + + const LOCTreeNode::Ptr& LOCReader::getRoot() const + { + return m_pRoot; + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/LOC/LOCTreeNode.cpp b/BMEdit/GameLib/Source/GameLib/LOC/LOCTreeNode.cpp new file mode 100644 index 0000000..687e46a --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/LOC/LOCTreeNode.cpp @@ -0,0 +1,248 @@ +#include +#include +#include +#include +#include +#include + + +namespace gamelib::loc +{ + bool LOCTreeNode::canHaveValue() const + { + return + type == LOCTreeNodeType::LOCALIZED_STRING || + type == LOCTreeNodeType::SUBTITLES || + type == LOCTreeNodeType::SUBTITLES_HINT || + type == LOCTreeNodeType::SUBTITLES_FIN || + type == LOCTreeNodeType::SUBTITLES_HINT_WITH_COMMENT; + } + + bool LOCTreeNode::canHaveChildren() const + { + return type == LOCTreeNodeType::CHILDREN; + } + + void LOCTreeNode::deserialize(const gamelib::loc::LOCTreeNode::Ptr &node, ZBio::ZBinaryReader::BinaryReader *binaryReader) + { + // Read self name + node->name = binaryReader->readCString(); + + const auto type = binaryReader->read(); + if (type != LOCTreeNodeType::CHILDREN && type != LOCTreeNodeType::LOCALIZED_STRING && + type != LOCTreeNodeType::SUBTITLES && type != LOCTreeNodeType::EMPTY_BLOCK && + type != LOCTreeNodeType::SUBTITLES_FIN && type != LOCTreeNodeType::SUBTITLES_HINT && + type != LOCTreeNodeType::SUBTITLES_HINT_WITH_COMMENT) + { + auto errorMessage = fmt::format("Invalid LOC format: unexpected entity code 0x{:02X} at offset {} (0x{:X})", type, binaryReader->tell(), binaryReader->tell()); + throw std::runtime_error(errorMessage); + } + + // Store type + node->type = static_cast(type); + + if (node->type == LOCTreeNodeType::CHILDREN) + { + // Read amount + auto amount = binaryReader->read(); + if (amount) + { + node->children.reserve(amount); + + std::vector offsets {}; + offsets.resize(amount); + offsets[0] = 0x0; // zero offset since next nodes + + for (int i = 1; i < amount; i++) + { + offsets[i] = binaryReader->read(); + } + + // Now we've ready to iterate over offsets and read child nodes + for (const auto& offset : offsets) + { + ZBioSeekGuard guard { binaryReader }; + ZBioHelpers::seekBy(binaryReader, offset); + + auto childNode = std::make_shared(); + childNode->parent = node; + + // Read node + LOCTreeNode::deserialize(childNode, binaryReader); + + // Save node + node->children.emplace_back(childNode); + } + } + } + else if (node->type == LOCTreeNodeType::LOCALIZED_STRING) + { + // Read string value + node->value = binaryReader->readCString(); + ZBioHelpers::seekBy(binaryReader, 4); // Need seek by 4 because value strings are "aligned". + // Formula: len + 1 + 4 (+1 - zero terminator, 4 - "alignment") + } + else if (node->type == LOCTreeNodeType::SUBTITLES) + { + // Subtitles text + node->value = binaryReader->readCString(); + + // Read subtitle data. In most cases there are 2xu32, but when first u32 zeroed next u32 not presented + // I love IOI because they don't give me an opportunity to relax... + uint32_t first = binaryReader->read(); + uint32_t second = 0; + if (first != 0) + { + // Ok, read second + second = binaryReader->read(); + } + + *reinterpret_cast(&node->subtitle.unkData[0]) = first; + *reinterpret_cast(&node->subtitle.unkData[4]) = second; + } + else if (node->type == LOCTreeNodeType::SUBTITLES_HINT) + { + // Subtitles text + node->value = binaryReader->readCString(); + + // Read extra hint string + node->subtitle.extraHint = binaryReader->readCString(); + + // Read subtitle data. In most cases there are 2xu32, but when first u32 zeroed next u32 not presented + // I love IOI because they don't give me an opportunity to relax... + uint32_t first = binaryReader->read(); + uint32_t second = 0; + if (first != 0) + { + // Ok, read second + second = binaryReader->read(); + } + + *reinterpret_cast(&node->subtitle.unkData[0]) = first; + *reinterpret_cast(&node->subtitle.unkData[4]) = second; + } + else if (node->type == LOCTreeNodeType::SUBTITLES_FIN) + { + // Always only 1 u32 + uint32_t first = binaryReader->read(); + *reinterpret_cast(&node->subtitle.unkData[0]) = first; + } + else if (node->type == LOCTreeNodeType::SUBTITLES_HINT_WITH_COMMENT) + { + // Read value + node->value = binaryReader->readCString(); + + // Read hint (?) + node->subtitle.extraHint = binaryReader->readCString(); + + // DronCode: I'm not sure that next few bytes always zeroed. + // As we remember in LOCTreeNodeType::SUBTITLES & LOCTreeNodeType::SUBTITLES_HINT we have extra 8 bytes (2xu32 but sometimes only 1xu32 when it's zeroed). + // This could be our case. Idk, let's use code from SUBTITLES_FIN (idk why) + uint32_t first = binaryReader->read(); + *reinterpret_cast(&node->subtitle.unkData[0]) = first; + } + else if (node->type == LOCTreeNodeType::EMPTY_BLOCK) + { + // Do nothing here, it's just empty + } + else + { + throw std::runtime_error { "Unsupported block" }; + } + } + + void LOCTreeNode::serialize(const LOCTreeNode::Ptr &node, ZBio::ZBinaryWriter::BinaryWriter *binaryWriter, std::vector>& replacement) // NOLINT(*-no-recursion) + { + // Write name (not aligned) + binaryWriter->writeCString(node->name); + + // Write node type + binaryWriter->write(static_cast(node->type)); + + if (node->type == LOCTreeNodeType::CHILDREN) + { + // Sort children + auto children = node->children; // Need copy + //std::sort(children.begin(), children.end(), [](const LOCTreeNode::Ptr& a, const LOCTreeNode::Ptr& b) { return a->name < b->name; }); + + // Write count + const auto childrenCount = children.size() > 0xFF ? 0xFF : static_cast(children.size()); + binaryWriter->write(childrenCount); + + // Write offsets + std::vector offsets { 0u }; + + for (int i = 1; i < childrenCount; i++) + { + offsets.push_back(binaryWriter->tell()); + binaryWriter->write(0xDDDDDDDDu); + } + + // Write node by node & restore offsets + const uint32_t baseOffset = binaryWriter->tell(); + + for (int i = 0; i < childrenCount; i++) + { + if (i > 0) + { + // Save replacement instruction + uint32_t newOffset = binaryWriter->tell(); + replacement.emplace_back(offsets[i], newOffset - baseOffset); + } + + // Write node itself + LOCTreeNode::serialize(children[i], binaryWriter, replacement); + } + } + else if (node->type == LOCTreeNodeType::LOCALIZED_STRING) + { + // Write string + binaryWriter->writeCString(node->value); + + // Write 4 byte alignment + binaryWriter->write(0); + } + else if (node->type == LOCTreeNodeType::SUBTITLES || node->type == LOCTreeNodeType::SUBTITLES_HINT) + { + // Write unaligned string + binaryWriter->writeCString(node->value); + + if (node->type == LOCTreeNodeType::SUBTITLES_HINT) + { + // For hinted string need to write extra hint + binaryWriter->writeCString(node->subtitle.extraHint); + } + + uint32_t first = *reinterpret_cast(&node->subtitle.unkData[0]); + uint32_t second = *reinterpret_cast(&node->subtitle.unkData[4]); + + // Write subtitles data + binaryWriter->write(first); + if (first) + binaryWriter->write(second); + } + else if (node->type == LOCTreeNodeType::SUBTITLES_HINT_WITH_COMMENT) + { + // Write value + binaryWriter->writeCString(node->value); + + // Write hint + binaryWriter->writeCString(node->subtitle.extraHint); + + // Write u32 (see deserializer code for details) + uint32_t first = *reinterpret_cast(&node->subtitle.unkData[0]); + binaryWriter->write(first); + } + else if (node->type == LOCTreeNodeType::SUBTITLES_FIN) + { + // Store tutorial data here + uint32_t first = *reinterpret_cast(&node->subtitle.unkData[0]); + + binaryWriter->write(first); + } + else if (node->type == LOCTreeNodeType::EMPTY_BLOCK) + { + // Do nothing here + } + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/LOC/LOCWriter.cpp b/BMEdit/GameLib/Source/GameLib/LOC/LOCWriter.cpp new file mode 100644 index 0000000..459157e --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/LOC/LOCWriter.cpp @@ -0,0 +1,62 @@ +#include +#include +#include + + +namespace gamelib::loc +{ + void LOCWriter::write(const LOCTreeNode::Ptr &root, std::vector &outBuffer) + { + if (!root || root->children.empty()) return; + + auto writerSink = std::make_unique(); + auto binaryWriter = ZBio::ZBinaryWriter::BinaryWriter(std::move(writerSink)); + + std::vector> replacement {}; + + // Sort children by name. It's required because game wants to interact with sorted tree + auto children = root->children; + //std::sort(children.begin(), children.end(), [](const LOCTreeNode::Ptr& a, const LOCTreeNode::Ptr& b) { return a->name < b->name; }); + + // Write base + binaryWriter.write(static_cast(children.size())); + + std::vector offsets { 0u }; // zero stored by default + + // Fill offsets if root child more than 1 + for (int i = 1; i < children.size(); i++) + { + // Here we need to know an offset of subject after first created node. Instead of that we will save current offset and then jump back + offsets.push_back(binaryWriter.tell()); + binaryWriter.write(0xDDDDDDDDu); // Temp value, will be replaced later + } + + // And now we've ready to write node by node + const uint32_t baseOffset = binaryWriter.tell(); // Save current offset (need to calculate relative offset) + + for (int i = 0; i < children.size(); i++) + { + if (i > 0) + { + // Save replacement + uint32_t newOffset = binaryWriter.tell(); + replacement.emplace_back(offsets[i], newOffset - baseOffset); + } + + // Write node itself + LOCTreeNode::serialize(children[i], &binaryWriter, replacement); + } + + // Take value + auto raw = binaryWriter.release().value(); + + // Apply replacements + for (const auto& [offset, value] : replacement) + { + *reinterpret_cast(raw.data() + offset) = value; + } + + // Done + std::copy(raw.begin(), raw.end(), std::back_inserter(outBuffer)); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/Level.cpp b/BMEdit/GameLib/Source/GameLib/Level.cpp index 1199940..049c616 100644 --- a/BMEdit/GameLib/Source/GameLib/Level.cpp +++ b/BMEdit/GameLib/Source/GameLib/Level.cpp @@ -6,13 +6,43 @@ #include #include -#include #include -#include +#include +#include + +#include + +#include +#include +#include namespace gamelib { + glm::i16vec3 LevelRooms::RoomGroup::worldToRoom(const glm::vec3& vWorld) const + { + glm::i16vec3 vR { 0 }; + const float* pfvSrc = glm::value_ptr(vWorld); + const float* pfvOrigin = glm::value_ptr(header.vWorldOrigin); + int16_t* pvDst = glm::value_ptr(vR); + + for (int i = 0; i < 3; i++) + { + pvDst[i] = static_cast((pfvSrc[i] - pfvOrigin[i]) * header.fWorldScale + 32768.f); + } + + return vR; + } + + glm::vec3 LevelRooms::RoomGroup::roomToWorld(const glm::i16vec3& vRoom) const + { + return { + static_cast(vRoom.x - 32768) / header.fWorldScale + header.vWorldOrigin.x, + static_cast(vRoom.y - 32768) / header.fWorldScale + header.vWorldOrigin.y, + static_cast(vRoom.z - 32768) / header.fWorldScale + header.vWorldOrigin.z + }; + } + Level::Level(std::unique_ptr &&levelAssetsProvider) : m_assetProvider(std::move(levelAssetsProvider)) { @@ -40,6 +70,26 @@ namespace gamelib return false; } + if (!loadLevelTextures()) + { + return false; + } + + if (!loadLevelMaterials()) + { + return false; + } + + if (!loadLevelRooms()) + { + return false; + } + + if (!loadLevelLocalization()) + { + return false; + } + // TODO: Load things (it's time to combine GMS, PRP & BUF files) m_isLevelLoaded = true; return true; @@ -76,21 +126,118 @@ namespace gamelib return nullptr; } - const LevelGeometry *Level::getLevelGeometry() const + const LevelTextures* Level::getSceneTextures() const + { + return &m_levelTextures; + } + + LevelTextures* Level::getSceneTextures() + { + return &m_levelTextures; + } + + + const LevelGeometry* Level::getLevelGeometry() const { return &m_levelGeometry; } - LevelGeometry *Level::getLevelGeometry() + LevelGeometry* Level::getLevelGeometry() { return &m_levelGeometry; } + const LevelMaterials* Level::getLevelMaterials() const + { + return &m_levelMaterials; + } + + LevelMaterials* Level::getLevelMaterials() + { + return &m_levelMaterials; + } + + const LevelRooms* Level::getLevelRooms() const + { + return &m_levelRooms; + } + + LevelRooms* Level::getLevelRooms() + { + return &m_levelRooms; + } + + const LevelLocalization* Level::getLevelLocalization() const + { + return &m_levelLocalization; + } + + LevelLocalization* Level::getLevelLocalization() + { + return &m_levelLocalization; + } + const std::vector &Level::getSceneObjects() const { return m_sceneObjects; } + [[nodiscard]] scene::SceneObject::Ptr Level::getSceneObjectByGEOMREF(const std::string& path) const + { + std::stringstream pathStream { path }; + std::string segment; + std::vector dividedPath {}; + + while(std::getline(pathStream, segment, '\\')) + { + dividedPath.push_back(segment); + } + + scene::SceneObject::Ptr object = m_sceneObjects[0]; + + for (const auto& pathBlock : dividedPath) + { + if (pathBlock == "ROOT") + continue; + + bool bFound = false; + + for (const auto& childrenRef : object->getChildren()) + { + if (auto child = childrenRef.lock(); child && child->getName() == pathBlock) + { + bFound = true; + object = child; + break; + } + } + + if (!bFound) + { + return nullptr; + } + } + + return object; + } + + scene::SceneObject::Ptr Level::getSceneObjectByInstanceID(std::uint32_t instanceID) const + { + // It's lazy method. Just walk over all entities and check InstanceID + for (const auto& entity : getSceneObjects()) + { + if (entity->getGeomInfo().getInstanceId() == instanceID) + return entity; + } + + return nullptr; + } + + Span Level::getStaticBuffer() const + { + return m_buf.data ? Span(m_buf.data.get(), m_buf.size) : nullptr; + } + void Level::dumpAsset(io::AssetKind assetKind, std::vector &outBuffer) const { if (assetKind == io::AssetKind::PROPERTIES) @@ -98,6 +245,44 @@ namespace gamelib scene::SceneObjectPropertiesDumper dumper; dumper.dump(this, &outBuffer); } + else if (assetKind == io::AssetKind::LOCALIZATION) + { + loc::LOCWriter::write(m_levelLocalization.localizationRoot, outBuffer); + } + else assert(false && "Unsupported"); + } + + void Level::forEachObjectOfType(const std::string& objectTypeName, const std::function& pred) const + { + for (const auto& object: m_sceneObjects) + { + if (object->getType()->getName() == objectTypeName) + { + if (pred(object)) + continue; + + return; + } + } + } + + bool isComplexTypeInheritedOf(const std::string& baseTypeName, const TypeComplex* pType) + { + return pType != nullptr && (pType->getName() == baseTypeName || isComplexTypeInheritedOf(baseTypeName, reinterpret_cast(pType->getParent()))); + } + + void Level::forEachObjectOfTypeWithInheritance(const std::string& objectBaseType, const std::function& pred) const + { + for (const auto& object: m_sceneObjects) + { + if (object->getType()->getKind() == TypeKind::COMPLEX && isComplexTypeInheritedOf(objectBaseType, reinterpret_cast(object->getType()))) + { + if (pred(object)) + continue; + + return; + } + } } bool Level::loadLevelProperties() @@ -125,7 +310,6 @@ namespace gamelib bool Level::loadLevelScene() { int64_t gmsFileSize = 0; - int64_t bufFileSize = 0; // Load raw data auto gmsFileBuffer = m_assetProvider->getAsset(io::AssetKind::SCENE, gmsFileSize); @@ -134,14 +318,14 @@ namespace gamelib return false; } - auto bufFileBuffer = m_assetProvider->getAsset(io::AssetKind::BUFFER, bufFileSize); - if (!bufFileBuffer || !bufFileSize) + m_buf.data = m_assetProvider->getAsset(io::AssetKind::BUFFER, m_buf.size); + if (!m_buf.data || !m_buf.size) { return false; } gms::GMSReader reader; - if (!reader.parse(&m_sceneProperties.header, gmsFileBuffer.get(), gmsFileSize, bufFileBuffer.get(), bufFileSize)) + if (!reader.parse(&m_sceneProperties.header, gmsFileBuffer.get(), gmsFileSize, m_buf.data.get(), m_buf.size)) { return false; } @@ -222,12 +406,145 @@ namespace gamelib return false; } - prm::PRMReader reader { m_levelGeometry.header, m_levelGeometry.chunkDescriptors, m_levelGeometry.chunks }; - if (!reader.read(Span(prmFileBuffer.get(), prmFileSize))) + PRMReader reader; + if (!reader.parse(prmFileBuffer.get(), prmFileSize)) + { + return false; + } + + m_levelGeometry.primitives = std::move(reader.takePrimitives()); + + return true; + } + + bool Level::loadLevelTextures() + { + // Read TEX file + int64_t texFileSize = 0; + auto texFileBuffer = m_assetProvider->getAsset(gamelib::io::AssetKind::TEXTURES, texFileSize); + + if (!texFileSize || !texFileBuffer) + { + return false; + } + + tex::TEXReader reader; + const bool parseResult = reader.parse(texFileBuffer.get(), texFileSize); + if (!parseResult) + { + return false; + } + + m_levelTextures.header = reader.m_header; + m_levelTextures.entries = std::move(reader.m_entries); + m_levelTextures.table1Offsets = reader.m_texturesPool; + m_levelTextures.table2Offsets = reader.m_cubeMapsPool; + m_levelTextures.countOfEmptyOffsets = reader.m_countOfEmptyOffsets; + + return true; + } + + bool Level::loadLevelMaterials() + { + // Read MAT file + int64_t matFileSize = 0; + auto matFileBuffer = m_assetProvider->getAsset(gamelib::io::AssetKind::MATERIALS, matFileSize); + + if (!matFileSize || !matFileBuffer) + { + return false; + } + + mat::MATReader reader; + const bool parseResult = reader.parse(matFileBuffer.get(), matFileSize); + if (!parseResult) { return false; } + m_levelMaterials.header = reader.getHeader(); + m_levelMaterials.materialClasses = std::move(reader.takeClasses()); + m_levelMaterials.materialInstances = std::move(reader.takeInstances()); + + return true; + } + + bool Level::loadLevelRooms() + { + // Read outside rooms + { + int64_t rmcFileSize = 0; + auto rmcFileBuffer = m_assetProvider->getAsset(gamelib::io::AssetKind::ROOM_TREE_OUTSIDE, rmcFileSize); + + if (!rmcFileBuffer || !rmcFileSize) + { + return false; + } + + oct::OCTReader rmcReader {}; + const bool bParseResult = rmcReader.parse(rmcFileBuffer.get(), rmcFileSize); + + if (!bParseResult) + { + return false; + } + + m_levelRooms.outside.header = rmcReader.getHeader(); + m_levelRooms.outside.nodes = std::move(rmcReader.takeNodes()); + m_levelRooms.outside.objects = std::move(rmcReader.takeObjects()); + m_levelRooms.outside.ubs = std::move(rmcReader.takeUBS()); + } + + // Read inside rooms + { + int64_t rmiFileSize = 0; + auto rmiFileBuffer = m_assetProvider->getAsset(gamelib::io::AssetKind::ROOM_TREE_INSIDE, rmiFileSize); + + if (!rmiFileBuffer || !rmiFileSize) + { + return false; + } + + oct::OCTReader rmiReader {}; + const bool bParseResult = rmiReader.parse(rmiFileBuffer.get(), rmiFileSize); + + if (!bParseResult) + { + return false; + } + + m_levelRooms.inside.header = rmiReader.getHeader(); + m_levelRooms.inside.nodes = std::move(rmiReader.takeNodes()); + m_levelRooms.inside.objects = std::move(rmiReader.takeObjects()); + m_levelRooms.inside.ubs = std::move(rmiReader.takeUBS()); + } + + // Read collisions + { + } + return true; } + + bool Level::loadLevelLocalization() + { + int64_t locFileSize = 0; + auto locFileBuffer = m_assetProvider->getAsset(gamelib::io::AssetKind::LOCALIZATION, locFileSize); + + if (!locFileBuffer || !locFileSize) + { + return false; + } + + loc::LOCReader locReader {}; + const bool bParseResult = locReader.parse(locFileBuffer.get(), locFileSize); + + if (!bParseResult) + { + return false; + } + + m_levelLocalization.localizationRoot = locReader.getRoot(); + return m_levelLocalization.localizationRoot != nullptr && !m_levelLocalization.localizationRoot->children.empty(); + } } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATBind.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATBind.cpp new file mode 100644 index 0000000..9a36fcd --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATBind.cpp @@ -0,0 +1,84 @@ +#include +#include +#include +#include + + +namespace gamelib::mat +{ + MATBind::MATBind() = default; + + MATBind MATBind::makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount) + { + MATBind b {}; + + for (int i = 0; i < propertiesCount; i++) + { + MATPropertyEntry entry; + MATPropertyEntry::deserialize(entry, binaryReader); + + if (entry.kind == MATPropertyKind::PK_NAME) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + b.name = binaryReader->readCString(); + } + else if (entry.kind == MATPropertyKind::PK_RENDER_STATE) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + b.renderStates.emplace_back(MATRenderState::makeFromStream(binaryReader, entry.containerCapacity)); + } + else if (entry.kind == MATPropertyKind::PK_TEXTURE) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + b.textures.emplace_back(MATTexture::makeFromStream(binaryReader, entry.containerCapacity)); + } + else if (entry.kind == MATPropertyKind::PK_COLOR) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + b.colorChannels.emplace_back(MATColorChannel::makeFromStream(binaryReader, entry.containerCapacity)); + } + else if (entry.kind == MATPropertyKind::PK_SPRITE) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + b.sprites.emplace_back(MATSprite::makeFromStream(binaryReader, entry.containerCapacity)); + } + else if (entry.kind == MATPropertyKind::PK_BOOLEAN) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + b.options.emplace_back(MATOption::makeFromStream(binaryReader, entry.containerCapacity)); + } + else if (entry.kind == MATPropertyKind::PK_SCROLL) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + b.scrolls.emplace_back(MATScroll::makeFromStream(binaryReader, entry.containerCapacity)); + } + else if (entry.kind == MATPropertyKind::PK_FLOAT_VALUE) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + b.floats.emplace_back(MATFloat::makeFromStream(binaryReader, entry.containerCapacity)); + } + else + { + assert(false && "Unprocessed entry!"); + } + } + + return b; + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATClass.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATClass.cpp new file mode 100644 index 0000000..456cccf --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATClass.cpp @@ -0,0 +1,26 @@ +#include +#include + + +namespace gamelib::mat +{ + MATClass::MATClass(std::string className, std::string parentClass, std::vector&& properties, std::vector&& subClasses) + : m_name(std::move(className)), m_parentClass(std::move(parentClass)), m_properties(std::move(properties)), m_subClasses(std::move(subClasses)) + { + } + + const std::string& MATClass::getName() const + { + return m_name; + } + + const std::string& MATClass::getParentClassName() const + { + return m_parentClass; + } + + const std::vector& MATClass::getSubClasses() const + { + return m_subClasses; + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATColorChannel.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATColorChannel.cpp new file mode 100644 index 0000000..83e72ab --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATColorChannel.cpp @@ -0,0 +1,65 @@ +#include +#include +#include +#include +#include + + +namespace gamelib::mat +{ + MATColorChannel::MATColorChannel(std::string name, bool bEnabled, MATValU&& color) + : m_name(std::move(name)), m_bEnabled(bEnabled), m_color(color) + { + } + + MATColorChannel MATColorChannel::makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount) + { + std::string name {}; + bool bEnabled { false }; + MATValU color; + + for (int i = 0; i < propertiesCount; i++) + { + MATPropertyEntry entry; + MATPropertyEntry::deserialize(entry, binaryReader); + + if (auto kind = entry.kind; kind == MATPropertyKind::PK_NAME) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + name = binaryReader->readCString(); + } + else if (kind == MATPropertyKind::PK_ENABLE) + { + bEnabled = static_cast(entry.reference); + } + else if (kind == MATPropertyKind::PK_VAL_U) + { + // Read ValU + color = MATValU::makeFromStream(binaryReader, entry); + } + else + { + assert(false && "Unprocessed entry!"); + } + } + + return MATColorChannel(std::move(name), bEnabled, std::move(color)); + } + + const std::string& MATColorChannel::getName() const + { + return m_name; + } + + bool MATColorChannel::isEnabled() const + { + return m_bEnabled; + } + + const MATValU& MATColorChannel::getColor() const + { + return m_color; + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATEntries.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATEntries.cpp new file mode 100644 index 0000000..3bc99bd --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATEntries.cpp @@ -0,0 +1,84 @@ +#include +#include + + +namespace gamelib::mat +{ + void MATHeader::deserialize(MATHeader& header, ZBio::ZBinaryReader::BinaryReader* binaryReader) + { + header.classListOffset = binaryReader->read(); + header.instancesListOffset = binaryReader->read(); + header.zeroed = binaryReader->read(); + header.unknownTableOffset = binaryReader->read(); + } + + void MATPropertyEntry::deserialize(MATPropertyEntry& entry, ZBio::ZBinaryReader::BinaryReader* binaryReader) + { + entry.kind = static_cast(binaryReader->read()); + entry.reference = binaryReader->read(); + entry.containerCapacity = binaryReader->read(); + entry.valueType = static_cast(binaryReader->read()); + } + + void MATClassDescription::deserialize(MATClassDescription& classDescription, ZBio::ZBinaryReader::BinaryReader* binaryReader) + { + { + const auto offset = binaryReader->read(); + + if (offset >= 0x10) + { + const auto oldPos = binaryReader->tell(); + + // Read class name from valid offset + binaryReader->seek(offset); + classDescription.parentClass = binaryReader->readCString(); + + // Move back + binaryReader->seek(oldPos); + } + } + + classDescription.unk4 = binaryReader->read(); + classDescription.unk8 = binaryReader->read(); + classDescription.unkC = binaryReader->read(); + classDescription.unk10 = binaryReader->read(); + classDescription.unk14 = binaryReader->read(); + classDescription.unk18 = binaryReader->read(); + classDescription.classDeclarationOffset = binaryReader->read(); + classDescription.unk20 = binaryReader->read(); + classDescription.unk24 = binaryReader->read(); + classDescription.unk28 = binaryReader->read(); + classDescription.unk2C = binaryReader->read(); + } + + void MATInstanceDescription::deserialize(MATInstanceDescription& instanceDescription, ZBio::ZBinaryReader::BinaryReader* binaryReader) + { + { + const auto offset = binaryReader->read(); + + if (offset >= 0x10) + { + const auto oldPos = binaryReader->tell(); + + // Read class name from valid offset + binaryReader->seek(offset); + instanceDescription.instanceParentClassName = binaryReader->readCString(); + + // Move back + binaryReader->seek(oldPos); + } + } + + instanceDescription.unk4 = binaryReader->read(); + instanceDescription.unk8 = binaryReader->read(); + instanceDescription.unkC = binaryReader->read(); + instanceDescription.unk10 = binaryReader->read(); + instanceDescription.unk14 = binaryReader->read(); + instanceDescription.unk18 = binaryReader->read(); + instanceDescription.instanceDeclarationOffset = binaryReader->read(); + instanceDescription.unk20 = binaryReader->read(); + instanceDescription.unk24 = binaryReader->read(); + instanceDescription.unk28 = binaryReader->read(); + instanceDescription.unk2C = binaryReader->read(); + } +} diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATFloat.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATFloat.cpp new file mode 100644 index 0000000..3fda106 --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATFloat.cpp @@ -0,0 +1,51 @@ +#include +#include +#include +#include +#include + + +namespace gamelib::mat +{ + MATFloat::MATFloat(std::string name, bool bEnabled, MATValU&& valU) + : m_name(std::move(name)), m_bEnabled(bEnabled), m_valU(std::move(valU)) + { + } + + MATFloat MATFloat::makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount) + { + std::string name {}; + bool bEnabled { false }; + MATValU valU {}; + + + for (int i = 0; i < propertiesCount; i++) + { + MATPropertyEntry entry; + MATPropertyEntry::deserialize(entry, binaryReader); + + if (entry.kind == MATPropertyKind::PK_NAME) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + name = binaryReader->readCString(); + } + else if (entry.kind == MATPropertyKind::PK_ENABLE) + { + bEnabled = static_cast(entry.reference); + } + else if (entry.kind == MATPropertyKind::PK_VAL_U) + { + // Make ValU by right way + valU = MATValU::makeFromStream(binaryReader, entry); + } + else + { + assert(false && "Unprocessed case"); + } + } + + return MATFloat(std::move(name), bEnabled, std::move(valU)); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATInstance.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATInstance.cpp new file mode 100644 index 0000000..056a4f2 --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATInstance.cpp @@ -0,0 +1,25 @@ +#include + + +namespace gamelib::mat +{ + MATInstance::MATInstance(std::string instanceName, std::string parentClassName, std::vector&& properties, std::vector&& binders) + : m_name(std::move(instanceName)), m_parentName(std::move(parentClassName)), m_properties(std::move(properties)), m_binders(std::move(binders)) + { + } + + const std::string& MATInstance::getName() const + { + return m_name; + } + + const std::string& MATInstance::getParentName() const + { + return m_parentName; + } + + const std::vector& MATInstance::getBinders() const + { + return m_binders; + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATLayer.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATLayer.cpp new file mode 100644 index 0000000..d9f0779 --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATLayer.cpp @@ -0,0 +1,79 @@ +#include +#include +#include +#include + + +namespace gamelib::mat +{ + MATLayer::MATLayer(std::string name, std::string type, std::string shaderPath, std::string identity, std::string val_i) + : m_name(std::move(name)), m_type(std::move(type)), m_shaderPath(std::move(shaderPath)), m_identity(std::move(identity)), m_valI(std::move(val_i)) + { + } + + const std::string& MATLayer::getName() const + { + return m_name; + } + + const std::string& MATLayer::getType() const + { + return m_type; + } + + const std::string& MATLayer::getShaderProgramName() const + { + return m_shaderPath; + } + + const std::string& MATLayer::getIdentity() const + { + return m_identity; + } + + MATLayer MATLayer::makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount) + { + assert(propertiesCount > 0 && "Bad props count"); + assert(binaryReader != nullptr && "Bad reader"); + + std::string name {}, type {}, shaderPath {}, identity {}, valI {}; + + for (int i = 0; i < propertiesCount; i++) + { + MATPropertyEntry entry; + MATPropertyEntry::deserialize(entry, binaryReader); + + // Save pos & seek to value + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + // Read value by type + if (auto kind = entry.kind; kind == MATPropertyKind::PK_NAME) + { + name = binaryReader->readCString(); + } + else if (kind == MATPropertyKind::PK_TYPE) + { + type = binaryReader->readCString(); + } + else if (kind == MATPropertyKind::PK_PATH) + { + shaderPath = binaryReader->readCString(); + } + else if (kind == MATPropertyKind::PK_IDEN) + { + identity = binaryReader->readCString(); + } + else if (kind == MATPropertyKind::PK_VAL_I) + { + valI = binaryReader->readCString(); + } + else + { + assert(false && "Unprocessed entry!"); + } + } + + return MATLayer(std::move(name), std::move(type), std::move(shaderPath), std::move(identity), std::move(valI)); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATOption.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATOption.cpp new file mode 100644 index 0000000..0d0729c --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATOption.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include + + +namespace gamelib::mat +{ + MATOption::MATOption(std::string name, bool bEnabled, MATValU&& valU) + : m_name(std::move(name)), m_bEnabled(bEnabled), m_valU(std::move(valU)) + { + } + + const std::string& MATOption::getName() const + { + return m_name; + } + + bool MATOption::isEnabled() const + { + return m_bEnabled; + } + + const MATValU& MATOption::getValU() const + { + return m_valU; + } + + MATOption MATOption::makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount) + { + std::string name {}; + bool bEnabled = false; + MATValU valU {}; + + for (int i = 0; i < propertiesCount; i++) + { + MATPropertyEntry entry; + MATPropertyEntry::deserialize(entry, binaryReader); + + if (auto kind = entry.kind; kind == MATPropertyKind::PK_NAME) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + name = binaryReader->readCString(); + } + else if (kind == MATPropertyKind::PK_ENABLE) + { + bEnabled = static_cast(entry.reference); + } + else if (kind == MATPropertyKind::PK_VAL_U) + { + valU = MATValU::makeFromStream(binaryReader, entry); + } + else + { + assert(false && "Unprocessed entry!"); + } + } + + return MATOption(std::move(name), bEnabled, std::move(valU)); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATReader.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATReader.cpp new file mode 100644 index 0000000..e03cb1b --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATReader.cpp @@ -0,0 +1,238 @@ +#include +#include +#include +#include + + +namespace gamelib::mat +{ + MATReader::MATReader() = default; + + bool MATReader::parse(const uint8_t *pMatBuffer, size_t iMatBufferSize) + { + // https://github.com/glacier-modding/io_scene_blood_money/blob/libraries/BMExport/src/mati.cpp#L148 + // https://github.com/glacier-modding/io_scene_blood_money/blob/libraries/BMExport/src/mati.h + // see sub_4930C0 + // see sub_497820 + // see sub_471D70 + ZBio::ZBinaryReader::BinaryReader matReader { reinterpret_cast(pMatBuffer), static_cast(iMatBufferSize) }; + + // Read header + MATHeader::deserialize(m_header, &matReader); + + // Collect classes + matReader.seek(m_header.classListOffset); + if (!collectMaterialClasses(&matReader)) + { + return false; + } + + // Collect instances + matReader.seek(m_header.instancesListOffset); + if (!collectMaterialInstances(&matReader)) + { + return false; + } + + // Save constant table + // TODO: Copy const table + + return true; + } + + bool MATReader::collectMaterialClasses(ZBio::ZBinaryReader::BinaryReader* matReader) + { + static constexpr size_t kMaxClassCount = 0x20; + + // Clear class list + m_classes.clear(); + + for (size_t classIndex = 0; classIndex < kMaxClassCount; ++classIndex) + { + const auto classDeclOffset = matReader->read(); + if (classDeclOffset == 0) + continue; // null reference (skipped) + + // Save position to seek back + ZBioSeekGuard guard(matReader); + + // Continue work (we will seek back on end of iteration) + matReader->seek(classDeclOffset); + + // Read class description + MATClassDescription classDescription; + MATClassDescription::deserialize(classDescription, matReader); + + if (classDescription.classDeclarationOffset < 0x10) + { + assert(false && "Bad class declaration offset"); + return false; + } + + // Jump to class decl + matReader->seek(classDescription.classDeclarationOffset); + + // Read header decl + MATPropertyEntry classDeclEntry; + MATPropertyEntry::deserialize(classDeclEntry, matReader); + + // Check that entry is valid + if (classDeclEntry.kind != MATPropertyKind::PK_CLASS) + { + assert(false && "Expected to have class here"); + return false; + } + + if (classDeclEntry.valueType != MATValueType::PT_LIST) + { + assert(false && "Expected to have list here"); + return false; + } + + // Jump to first entry and iterate over entries + matReader->seek(classDeclEntry.reference); + + std::vector properties; + std::vector subclasses; + subclasses.reserve(classDeclEntry.containerCapacity); + properties.resize(classDeclEntry.containerCapacity); + + std::string className; + + for (int i = 0; i < classDeclEntry.containerCapacity; i++) + { + MATPropertyEntry::deserialize(properties[i], matReader); + + if (properties[i].kind == MATPropertyKind::PK_NAME) + { + assert(className.empty() && "Possible bug: class name override detected!"); + + // Maybe class name override? + ZBioSeekGuard readNameGuard { matReader }; + + matReader->seek(properties[i].reference); + className = matReader->readCString(); + } + else if (properties[i].kind == MATPropertyKind::PK_SUB_CLASS) + { + // Build subclass from entry + ZBioSeekGuard buildSubClassGuard { matReader }; + matReader->seek(properties[i].reference); + + subclasses.emplace_back(MATSubClass::makeFromStream(matReader, properties[i].containerCapacity)); + } + } + + if (className.empty()) + { + assert(false && "Expected at least 1 PK_NAME property, but no one presented."); + return false; + } + + m_classes.emplace_back( + std::move(className), + std::move(classDescription.parentClass), + std::move(properties), + std::move(subclasses) + ); + } + + return true; + } + + bool MATReader::collectMaterialInstances(ZBio::ZBinaryReader::BinaryReader* matReader) + { + static constexpr size_t kMaxMaterialInstancesCount = 0x800; + + for (size_t instanceIndex = 0; instanceIndex < kMaxMaterialInstancesCount; ++instanceIndex) + { + const auto instanceOffset = matReader->read(); + if (instanceOffset == 0) + continue; // skip null reference + + // Save position in guard + ZBioSeekGuard guard { matReader }; + + // Jump to offset + matReader->seek(instanceOffset); + + MATInstanceDescription instanceDescription; + MATInstanceDescription::deserialize(instanceDescription, matReader); + + if (instanceDescription.instanceDeclarationOffset < 0x10) + { + assert(false && "Bad instance declaration offset"); + return false; + } + + // Seek to decl position + matReader->seek(instanceDescription.instanceDeclarationOffset); + + // Read instance definition property + MATPropertyEntry instanceDeclarationProperty; + MATPropertyEntry::deserialize(instanceDeclarationProperty, matReader); + + if (instanceDeclarationProperty.kind != MATPropertyKind::PK_INSTANCE) + { + assert(false && "Instance expected!"); + return false; + } + + if (instanceDeclarationProperty.valueType != MATValueType::PT_LIST) + { + assert(false && "List in instance expected!"); + return false; + } + + // Jump to instance + matReader->seek(instanceDeclarationProperty.reference); + + // Read properties + std::vector properties; + std::vector binders; + binders.reserve(instanceDeclarationProperty.containerCapacity); + properties.resize(instanceDeclarationProperty.containerCapacity); + + std::string instanceName; + + for (int i = 0; i < instanceDeclarationProperty.containerCapacity; i++) + { + MATPropertyEntry::deserialize(properties[i], matReader); + + if (properties[i].kind == MATPropertyKind::PK_NAME) + { + assert(instanceName.empty() && "Possible bug: instance name override detected!"); + + ZBioSeekGuard seekNameGuard { matReader }; + + matReader->seek(properties[i].reference); + instanceName = matReader->readCString(); + } + else if (properties[i].kind == MATPropertyKind::PK_BIND) + { + ZBioSeekGuard seekBinderGuard { matReader }; + + matReader->seek(properties[i].reference); + + binders.emplace_back(MATBind::makeFromStream(matReader, static_cast(properties[i].containerCapacity))); + } + } + + if (instanceName.empty()) + { + assert(false && "Expected to have instance name, but it's not presented!"); + return false; + } + + // Save instance + m_instances.emplace_back( + std::move(instanceName), + std::move(instanceDescription.instanceParentClassName), + std::move(properties), + std::move(binders) + ); + } + + return true; + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATRenderState.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATRenderState.cpp new file mode 100644 index 0000000..e3a72b5 --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATRenderState.cpp @@ -0,0 +1,135 @@ +#include +#include +#include +#include + + +namespace gamelib::mat +{ + MATRenderState MATRenderState::makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount) + { + std::string name {}; + bool bEnabled { false }, bBlendEnabled { false }, bAlphaTest { false }, bFogEnabled { false }, bZBias { false }; + float fOpacity { 1.0f }, fZOffset { .0f }; + uint32_t iAlphaReference { 0u }; + MATCullMode cullMode { MATCullMode::CM_DontCare }; + MATBlendMode blendMode { MATBlendMode::BM_ADD }; + MATValU valU {}; + + for (int i = 0; i < propertiesCount; i++) + { + MATPropertyEntry entry; + MATPropertyEntry::deserialize(entry, binaryReader); + + switch (entry.kind) + { + case MATPropertyKind::PK_NAME: + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + name = binaryReader->readCString(); + } + break; + case MATPropertyKind::PK_ENABLE: + { + bEnabled = static_cast(entry.reference); + } + break; + case MATPropertyKind::PK_BLEND_ENABLE: + { + bBlendEnabled = static_cast(entry.reference); + } + break; + case MATPropertyKind::PK_ALPHA_TEST: + { + bAlphaTest = static_cast(entry.reference); + } + break; + case MATPropertyKind::PK_FOG_ENABLED: + { + bFogEnabled = static_cast(entry.reference); + } + break; + case MATPropertyKind::PK_Z_BIAS: + { + bZBias = static_cast(entry.reference); + } + break; + case MATPropertyKind::PK_OPACITY: + { + fOpacity = static_cast(entry.reference); + } + break; + case MATPropertyKind::PK_Z_OFFSET: + { + fZOffset = static_cast(entry.reference); + } + break; + case MATPropertyKind::PK_ALPHA_REFERENCE: + { + iAlphaReference = static_cast(entry.reference); + } + break; + case MATPropertyKind::PK_CULL_MODE: + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + std::string temp = binaryReader->readCString(); + + if (temp == "DontCare") + { + cullMode = MATCullMode::CM_DontCare; + } + else if (temp == "OneSided") + { + cullMode = MATCullMode::CM_OneSided; + } + else if (temp == "TwoSided") + { + cullMode = MATCullMode::CM_TwoSided; + } + else + { + assert(false && "Unsupported mode!"); + } + } + break; + case MATPropertyKind::PK_BLEND_MODE: + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + std::string temp = binaryReader->readCString(); + + if (temp == "TRANS") blendMode = MATBlendMode::BM_TRANS; + else if (temp == "TRANS_ON_OPAQUE") blendMode = MATBlendMode::BM_TRANS_ON_OPAQUE; + else if (temp == "TRANSADD_ON_OPAQUE") blendMode = MATBlendMode::BM_TRANSADD_ON_OPAQUE; + else if (temp == "ADD_BEFORE_TRANS") blendMode = MATBlendMode::BM_ADD_BEFORE_TRANS; + else if (temp == "ADD_ON_OPAQUE") blendMode = MATBlendMode::BM_ADD_ON_OPAQUE; + else if (temp == "ADD") blendMode = MATBlendMode::BM_ADD; + else if (temp == "SHADOW") blendMode = MATBlendMode::BM_SHADOW; + else if (temp == "STATICSHADOW") blendMode = MATBlendMode::BM_STATICSHADOW; + else + { + assert(false && "Unsupported mode!"); + } + } + break; + case MATPropertyKind::PK_VAL_U: + { + assert(valU.getValues().empty() && "Must be empty here!"); + + valU = MATValU::makeFromStream(binaryReader, entry); + } + break; + default: + assert(false && "Unprocessed entry!"); + break; + } + } + + return MATRenderState(std::move(name), bEnabled, bBlendEnabled, bAlphaTest, bFogEnabled, bZBias, fOpacity, fZOffset, iAlphaReference, cullMode, blendMode, std::move(valU)); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATScroll.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATScroll.cpp new file mode 100644 index 0000000..9898b14 --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATScroll.cpp @@ -0,0 +1,55 @@ +#include +#include +#include +#include +#include + + +namespace gamelib::mat +{ + MATScroll::MATScroll(std::string name, bool bEnabled, std::vector&& speedVector) + : m_name(std::move(name)), m_bEnabled(bEnabled), m_vfSpeed(std::move(speedVector)) + { + } + + MATScroll MATScroll::makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount) + { + std::string name {}; + bool bEnabled { false }; + std::vector speedVector {}; + + for (int i = 0; i < propertiesCount; i++) + { + MATPropertyEntry entry; + MATPropertyEntry::deserialize(entry, binaryReader); + + if (entry.kind == MATPropertyKind::PK_NAME) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + name = binaryReader->readCString(); + } + else if (entry.kind == MATPropertyKind::PK_ENABLE) + { + bEnabled = static_cast(entry.reference); + } + else if (entry.kind == MATPropertyKind::PK_SCROLL_SPEED) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + assert(speedVector.empty() && "It should be empty here!"); + + speedVector.resize(entry.containerCapacity); + binaryReader->read(speedVector.data(), entry.containerCapacity); + } + else + { + assert(false && "Unprocessed case"); + } + } + + return MATScroll(std::move(name), bEnabled, std::move(speedVector)); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATSprite.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATSprite.cpp new file mode 100644 index 0000000..644e956 --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATSprite.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include + + +namespace gamelib::mat +{ + MATSprite::MATSprite(std::string name, bool bUnk0, bool bUnk1) + : m_name(std::move(name)), m_bUnk0(bUnk0), m_bUnk1(bUnk1) + { + } + + MATSprite MATSprite::makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount) + { + std::string name {}; + bool bUnk0 { false }; + bool bUnk1 { false }; + int lastUpdatedUnk { 0 }; + + for (int i = 0; i < propertiesCount; i++) + { + MATPropertyEntry entry; + MATPropertyEntry::deserialize(entry, binaryReader); + + if (auto kind = entry.kind; kind == MATPropertyKind::PK_NAME) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + name = binaryReader->readCString(); + } + else if (kind == MATPropertyKind::PK_ENABLE) + { + if (lastUpdatedUnk == 0) bUnk0 = static_cast(entry.reference); + else if (lastUpdatedUnk == 1) bUnk1 = static_cast(entry.reference); + + ++lastUpdatedUnk; + } + else + { + assert(false && "Unprocessed entry!"); + } + } + + return MATSprite(std::move(name), bUnk0, bUnk1); + } + + const std::string& MATSprite::getName() const + { + return m_name; + } + + bool MATSprite::getUnk0() const + { + return m_bUnk0; + } + + bool MATSprite::getUnk1() const + { + return m_bUnk1; + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATSubClass.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATSubClass.cpp new file mode 100644 index 0000000..091905e --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATSubClass.cpp @@ -0,0 +1,65 @@ +#include +#include +#include +#include +#include + + +namespace gamelib::mat +{ + MATSubClass MATSubClass::makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount) + { + std::string name {}, oTyp {}, sTyp {}; + std::vector layers {}; + std::vector valI {}; + + for (int i = 0; i < propertiesCount; i++) + { + MATPropertyEntry entry; + MATPropertyEntry::deserialize(entry, binaryReader); + + if (entry.kind == MATPropertyKind::PK_NAME) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + name = binaryReader->readCString(); + } + else if (entry.kind == MATPropertyKind::PK_OTYP) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + oTyp = binaryReader->readCString(); + } + else if (entry.kind == MATPropertyKind::PK_STYP) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + sTyp = binaryReader->readCString(); + } + else if (entry.kind == MATPropertyKind::PK_LAYER) + { + // Read layer + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + layers.emplace_back(MATLayer::makeFromStream(binaryReader, entry.containerCapacity)); + } + else if (entry.kind == MATPropertyKind::PK_VAL_I) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + valI.emplace_back(binaryReader->readCString()); + } + else + { + assert(false && "Unprocessed entry!"); + } + } + + return MATSubClass(std::move(name), std::move(oTyp), std::move(sTyp), std::move(layers)); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATTexture.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATTexture.cpp new file mode 100644 index 0000000..bc4befb --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATTexture.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include + + +namespace gamelib::mat +{ + MATTexture::MATTexture(std::string name, bool bEnabled, uint32_t textureId, std::string path, MATTilingMode tilingU, MATTilingMode tilingV, MATTilingMode tilingW) + : m_name(std::move(name)), m_texturePath(std::move(path)), m_bEnabled(bEnabled), m_iTextureId(textureId), m_tileU(tilingU), m_tileV(tilingV), m_tileW(tilingW) + { + } + + MATTexture MATTexture::makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, int propertiesCount) + { + std::string name{}; + std::string path{}; + bool bEnabled{false}; + uint32_t textureId {0}; + MATTilingMode tileU { MATTilingMode::TM_NONE }, tileV { MATTilingMode::TM_NONE }, tileW { MATTilingMode::TM_NONE }; + + for (int i = 0; i < propertiesCount; i++) + { + MATPropertyEntry entry; + MATPropertyEntry::deserialize(entry, binaryReader); + + if (auto kind = entry.kind; kind == MATPropertyKind::PK_NAME) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + name = binaryReader->readCString(); + } + else if (kind == MATPropertyKind::PK_ENABLE) + { + bEnabled = static_cast(entry.reference); + } + else if (kind == MATPropertyKind::PK_TEXTURE_ID) + { + textureId = entry.reference; + } + else if (kind == MATPropertyKind::PK_TILINIG_U || kind == MATPropertyKind::PK_TILINIG_V || kind == MATPropertyKind::PK_TILINIG_W) + { + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + std::string temp = binaryReader->readCString(); + MATTilingMode tempMode = MATTilingMode::TM_NONE; + + if (temp == "NONE" || temp.empty()) + { + tempMode = MATTilingMode::TM_NONE; + } + else if (temp == "TILED") + { + tempMode = MATTilingMode::TM_TILED; + } + else if (temp == "MIRRORED") + { + tempMode = MATTilingMode::TM_MIRRORED; + } + else + { + assert(false && "Unsupported mode!"); + continue; + } + + if (kind == MATPropertyKind::PK_TILINIG_U) tileU = tempMode; + if (kind == MATPropertyKind::PK_TILINIG_V) tileV = tempMode; + if (kind == MATPropertyKind::PK_TILINIG_W) tileW = tempMode; + } + else if (kind == MATPropertyKind::PK_PATH) + { + // Path to the texture + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(entry.reference); + + path = binaryReader->readCString(); + } + else + { + assert(false && "Unsupported entry! Need to support!"); + } + } + + return MATTexture(std::move(name), bEnabled, textureId, path, tileU, tileV, tileW); + } + + const std::string& MATTexture::getName() const + { + return m_name; + } + + bool MATTexture::isEnabled() const + { + return m_bEnabled; + } + + uint32_t MATTexture::getTextureId() const + { + return m_iTextureId; + } + + const std::string& MATTexture::getTexturePath() const + { + return m_texturePath; + } + + MATTilingMode MATTexture::getTilingU() const + { + return m_tileU; + } + + MATTilingMode MATTexture::getTilingV() const + { + return m_tileV; + } + + MATTilingMode MATTexture::getTilingW() const + { + return m_tileW; + } + + PresentedTextureSource MATTexture::getPresentedTextureSources() const + { + if (m_texturePath.empty() && m_iTextureId == 0) + return PresentedTextureSource::PTS_NOTHING; + + if (m_texturePath.empty() && m_iTextureId > 0) + return PresentedTextureSource::PTS_TEXTURE_ID; + + if (!m_texturePath.empty() && m_iTextureId == 0) + return PresentedTextureSource::PTS_TEXTURE_PATH; + + return PresentedTextureSource::PTS_TEXTURE_ID_AND_PATH; + } +} diff --git a/BMEdit/GameLib/Source/GameLib/MAT/MATValU.cpp b/BMEdit/GameLib/Source/GameLib/MAT/MATValU.cpp new file mode 100644 index 0000000..987c55e --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/MAT/MATValU.cpp @@ -0,0 +1,71 @@ +#include +#include +#include + + + +namespace gamelib::mat +{ + MATValU::MATValU() = default; + + MATValU::MATValU(std::vector&& values) : m_values(std::move(values)) + { + } + + MATValU &MATValU::operator=(std::vector &&values) noexcept + { + m_values = std::move(values); + return *this; + } + + MATValU MATValU::makeFromStream(ZBio::ZBinaryReader::BinaryReader* binaryReader, const MATPropertyEntry& selfDecl) + { + assert(selfDecl.kind == MATPropertyKind::PK_VAL_U); + + std::vector values {}; + values.resize(selfDecl.containerCapacity); + + if (selfDecl.containerCapacity == 1) + { + // Parse single value + if (selfDecl.valueType == MATValueType::PT_UINT32) + { + // Single uint32 + values[0] = static_cast(selfDecl.reference); + } + else if (selfDecl.valueType == MATValueType::PT_FLOAT) + { + // Single float + values[0] = static_cast(selfDecl.reference); + } + else + { + assert(false && "Unsupported & impossible case!"); + } + } + else + { + // Parse group + ZBioSeekGuard guard { binaryReader }; + binaryReader->seek(selfDecl.reference); + + for (int i = 0; i < selfDecl.containerCapacity; i++) + { + if (selfDecl.valueType == MATValueType::PT_UINT32) + { + values[i] = binaryReader->read(); + } + else if (selfDecl.valueType == MATValueType::PT_FLOAT) + { + values[i] = binaryReader->read(); + } + else + { + assert(false && "Unsupported & impossible case!"); + } + } + } + + return MATValU(std::move(values)); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/OCT/OCTEntries.cpp b/BMEdit/GameLib/Source/GameLib/OCT/OCTEntries.cpp new file mode 100644 index 0000000..156d6ee --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/OCT/OCTEntries.cpp @@ -0,0 +1,42 @@ +#include +#include +#include + + + +namespace gamelib::oct +{ + void OCTHeader::deserialize(OCTHeader& header, ZBio::ZBinaryReader::BinaryReader* binaryReader) + { + header.objectsOffset = binaryReader->read(); + binaryReader->read(glm::value_ptr(header.vWorldOrigin), 3); + header.fWorldScale = binaryReader->read(); + } + + void OCTNode::deserialize(OCTNode& node, ZBio::ZBinaryReader::BinaryReader* binaryReader) + { + node.childCountData.iVal = binaryReader->read(); + node.childIndex = binaryReader->read(); + node.objectIndex = binaryReader->read(); + } + + void OCTObject::deserialize(OCTObject& object, ZBio::ZBinaryReader::BinaryReader* binaryReader) + { + object.gameObjectREF = binaryReader->read(); + binaryReader->read(glm::value_ptr(object.vMin), 3); + binaryReader->read(glm::value_ptr(object.vMax), 3); + } + + void OCTUnknownBlock::deserialize(gamelib::oct::OCTUnknownBlock& block, ZBio::ZBinaryReader::BinaryReader* binaryReader) + { + block.unk0 = binaryReader->read(); + + binaryReader->read(glm::value_ptr(block.vUnk4), 9); + binaryReader->read(glm::value_ptr(block.vUnk28), 3); + binaryReader->read(glm::value_ptr(block.vUnk34), 3); + binaryReader->read(glm::value_ptr(block.vUnk40), 3); + + block.unk4C = binaryReader->read(); + block.unk50 = binaryReader->read(); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/OCT/OCTReader.cpp b/BMEdit/GameLib/Source/GameLib/OCT/OCTReader.cpp new file mode 100644 index 0000000..1d4dcec --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/OCT/OCTReader.cpp @@ -0,0 +1,72 @@ +#include +#include +#include + + +namespace gamelib::oct +{ + OCTReader::OCTReader() = default; + + bool OCTReader::parse(const uint8_t* pOCTBuffer, size_t iBufferSize) + { + if (!pOCTBuffer || !iBufferSize) + return false; + + ZBio::ZBinaryReader::BinaryReader octReader { reinterpret_cast(pOCTBuffer), static_cast(iBufferSize) }; + + // Read header + OCTHeader::deserialize(m_header, &octReader); + + // Seek by 0x14 (after header) + octReader.seek(0x14); + + // And read objects + const uint32_t nodesCount = (m_header.objectsOffset - 0x14) / 0x6; // 0x14 - always start point of nodes list, 0x6 - size of each node entry + int totalObjects = 0; + + m_nodes.reserve(nodesCount); + for (int i = 0; i < nodesCount; i++) + { + OCTNode node; + OCTNode::deserialize(node, &octReader); + + if (node.childCountData.iVal == 0xCDCDu && node.childIndex == 0xCDCDu && node.objectIndex == 0xCDCDu) + { + // Alignment node. Skip and break + break; + } + + // Store max index of requested object to obtain count of objects at all + if (node.objectIndex > totalObjects) + totalObjects = node.objectIndex; + + m_nodes.emplace_back(node); + } + + // Because previously totalObjects was index, not a count + if (totalObjects > 0) + { + ++totalObjects; + + // Read objects + // Seek to begin + octReader.seek(m_header.objectsOffset); + m_objects.resize(totalObjects); + + for (int i = 0; i < totalObjects; i++) + { + OCTObject::deserialize(m_objects[i], &octReader); + } + + // And last block... + m_unknownBlocks.resize(totalObjects); + + for (int i = 0; i < totalObjects; i++) + { + OCTUnknownBlock::deserialize(m_unknownBlocks[i], &octReader); + } + } + + return true; + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/PRM/PRMBadChunkException.cpp b/BMEdit/GameLib/Source/GameLib/PRM/PRMBadChunkException.cpp deleted file mode 100644 index 4ca8145..0000000 --- a/BMEdit/GameLib/Source/GameLib/PRM/PRMBadChunkException.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include -#include - - -namespace gamelib::prm -{ - PRMBadChunkException::PRMBadChunkException(std::uint32_t chunkIndex) : PRMException() - { - m_errorMessage = "Bad chunk #" + std::to_string(chunkIndex); - } - - const char *PRMBadChunkException::what() const - { - return m_errorMessage.data(); - } -} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/PRM/PRMBadFile.cpp b/BMEdit/GameLib/Source/GameLib/PRM/PRMBadFile.cpp deleted file mode 100644 index 842069c..0000000 --- a/BMEdit/GameLib/Source/GameLib/PRM/PRMBadFile.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include - - -namespace gamelib::prm -{ - PRMBadFile::PRMBadFile(const std::string& reason) : PRMException() - { - m_message = "Bad file: " + reason; - } - - const char *PRMBadFile::what() const - { - return m_message.data(); - } -} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/PRM/PRMChunk.cpp b/BMEdit/GameLib/Source/GameLib/PRM/PRMChunk.cpp deleted file mode 100644 index 054105b..0000000 --- a/BMEdit/GameLib/Source/GameLib/PRM/PRMChunk.cpp +++ /dev/null @@ -1,165 +0,0 @@ -#include -#include - - -namespace gamelib::prm -{ - PRMChunk::PRMChunk() = default; - - PRMChunk::PRMChunk(std::uint32_t chunkIndex, int totalChunksNr, std::unique_ptr &&buffer, std::size_t size) - : m_chunkIndex(chunkIndex) - , m_buffer(std::move(buffer)) - , m_bufferSize(size) - { - // Recognize type & save data - if (chunkIndex == 0u) - { - m_recognizedKind = PRMChunkRecognizedKind::CRK_ZERO_CHUNK; - } - else - { - recognizeChunkKindAndSaveData(getBuffer(), totalChunksNr); - } - } - - std::uint32_t PRMChunk::getIndex() const - { - return m_chunkIndex; - } - - Span PRMChunk::getBuffer() - { - return { m_buffer.get(), static_cast(m_bufferSize) }; - } - - PRMChunkRecognizedKind PRMChunk::getKind() const - { - return m_recognizedKind; - } - - const PRMDescriptionChunkBaseHeader* PRMChunk::getDescriptionBufferHeader() const - { - return std::get_if(&m_data); - } - - PRMDescriptionChunkBaseHeader* PRMChunk::getDescriptionBufferHeader() - { - return std::get_if(&m_data); - } - - const PRMIndexChunkHeader* PRMChunk::getIndexBufferHeader() const - { - return std::get_if(&m_data); - } - - PRMIndexChunkHeader* PRMChunk::getIndexBufferHeader() - { - return std::get_if(&m_data); - } - - const PRMVertexBufferHeader* PRMChunk::getVertexBufferHeader() const - { - return std::get_if(&m_data); - } - - PRMVertexBufferHeader* PRMChunk::getVertexBufferHeader() - { - return std::get_if(&m_data); - } - - void PRMChunk::recognizeChunkKindAndSaveData(Span chunk, int totalChunksNr) - { - // Description buffer - if (chunk.size() >= sizeof(PRMDescriptionChunkBaseHeader)) - { - auto binaryReader = ZBio::ZBinaryReader::BinaryReader(reinterpret_cast(&chunk[0]), chunk.size()); - - PRMDescriptionChunkBaseHeader chunkHdr; - PRMDescriptionChunkBaseHeader::deserialize(chunkHdr, &binaryReader); - - // Helpers - // PRM_IS_VALID_KIND - check for all known values - // PRM_IS_VALID_PACK_TYPE - check for all known pack types -#define PRM_IS_VALID_KIND(k) (k) == 0 || (k) == 1 || (k) == 4 || (k) == 6 || (k) == 7 || (k) == 8 || (k) == 10 || (k) == 11 || (k) == 12 -#define PRM_IS_VALID_PACK_TYPE(p) (p) == 0 - - if (chunkHdr.ptrObjects <= totalChunksNr && PRM_IS_VALID_KIND(chunkHdr.kind) && PRM_IS_VALID_PACK_TYPE(chunkHdr.primPackType) && chunkHdr.ptrParts < totalChunksNr && chunkHdr.ptrObjects < totalChunksNr) - { - m_recognizedKind = PRMChunkRecognizedKind::CRK_DESCRIPTION_BUFFER; - m_data.emplace(chunkHdr); // Copy data - return; - } - } - - // Index buffer - if (chunk.size() > 4 && (chunk.size() % 0x10) == 0) - { - // So, we need to check second two bytes - auto binaryReader = ZBio::ZBinaryReader::BinaryReader(reinterpret_cast(&chunk[0]), chunk.size()); - - PRMIndexChunkHeader chunkHdr {}; - PRMIndexChunkHeader::deserialize(chunkHdr, &binaryReader); - - if (chunkHdr.indicesCount <= ((chunk.size() - 4) / 2)) - { - m_recognizedKind = PRMChunkRecognizedKind::CRK_INDEX_BUFFER; - m_data.emplace(chunkHdr); - return; - } - } - - // Vertex buffer - if (auto chunkSize = chunk.size(); (chunkSize % 0x10) == 0 || (chunkSize % 0x24) == 0 || (chunkSize % 0x28) == 0 || (chunkSize % 0x34) == 0) - { - PRMVertexBufferHeader vertexBufferHeader; - vertexBufferHeader.vertexFormat = PRMVertexBufferFormat::VBF_UNKNOWN_VERTEX; - - if ((chunkSize % 0x28) == 0) - { - auto binaryReader = ZBio::ZBinaryReader::BinaryReader(reinterpret_cast(&chunk[0x24]), chunk.size()); - const auto b28 = binaryReader.read(); - const bool is28k = (b28 == 0xCDCDCDCDu); - if (!is28k) - { - m_recognizedKind = PRMChunkRecognizedKind::CRK_UNKNOWN_BUFFER; - } - else - { - vertexBufferHeader.vertexFormat = PRMVertexBufferFormat::VBF_VERTEX_28; - m_recognizedKind = PRMChunkRecognizedKind::CRK_VERTEX_BUFFER; - } - } - - if (m_recognizedKind == PRMChunkRecognizedKind::CRK_UNKNOWN_BUFFER) - { - if ((chunkSize % 0x24) == 0) - { - vertexBufferHeader.vertexFormat = PRMVertexBufferFormat::VBF_VERTEX_24; - } - else if ((chunkSize % 0x34) == 0) - { - vertexBufferHeader.vertexFormat = PRMVertexBufferFormat::VBF_VERTEX_34; - } - else if ((chunkSize % 0x10) == 0) - { - vertexBufferHeader.vertexFormat = PRMVertexBufferFormat::VBF_SIMPLE_VERTEX; - } - } - - m_recognizedKind = PRMChunkRecognizedKind::CRK_VERTEX_BUFFER; - m_data.emplace(vertexBufferHeader); - return; - } - - // Bone description - if (auto chunkSize = chunk.size(); (chunkSize % 0x40) == 0) - { - assert(false && "Unsupported thing"); - m_recognizedKind = PRMChunkRecognizedKind::CRK_UNKNOWN_BUFFER; - m_data.emplace(); - return; - } -#undef PRM_IS_VALID_KIND -#undef PRM_IS_VALID_PACK_TYPE - } -} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/PRM/PRMChunkDescriptor.cpp b/BMEdit/GameLib/Source/GameLib/PRM/PRMChunkDescriptor.cpp deleted file mode 100644 index e6b09fb..0000000 --- a/BMEdit/GameLib/Source/GameLib/PRM/PRMChunkDescriptor.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include -#include - - -namespace gamelib::prm -{ - void PRMChunkDescriptor::deserialize(PRMChunkDescriptor &descriptor, ZBio::ZBinaryReader::BinaryReader *binaryReader) - { - auto &[offset, size, kind, unkC] = descriptor; - - offset = binaryReader->read(); - size = binaryReader->read(); - kind = binaryReader->read(); - unkC = binaryReader->read(); - } -} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/PRM/PRMDescriptionChunkBaseHeader.cpp b/BMEdit/GameLib/Source/GameLib/PRM/PRMDescriptionChunkBaseHeader.cpp deleted file mode 100644 index cbe2fd5..0000000 --- a/BMEdit/GameLib/Source/GameLib/PRM/PRMDescriptionChunkBaseHeader.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include -#include - - -using namespace gamelib::prm; - -void PRMDescriptionChunkBaseHeader::deserialize(gamelib::prm::PRMDescriptionChunkBaseHeader &header, ZBio::ZBinaryReader::BinaryReader *binaryReader) -{ - header.boneDeclOffset = binaryReader->read(); - header.primPackType = binaryReader->read(); - header.kind = binaryReader->read(); - header.textureId = binaryReader->read(); - header.unk6 = binaryReader->read(); - header.nextVariation = binaryReader->read(); - header.unkC = binaryReader->read(); - header.unkD = binaryReader->read(); - header.unkE = binaryReader->read(); - header.currentVariation = binaryReader->read(); - header.ptrParts = binaryReader->read(); - header.materialIdx = binaryReader->read(); - header.totalVariations = binaryReader->read(); - header.ptrObjects = binaryReader->read(); - header.ptrObjects_HI = binaryReader->read(); - header.unk3 = binaryReader->read(); - header.boundingBox.min.x = binaryReader->read(); - header.boundingBox.min.y = binaryReader->read(); - header.boundingBox.min.z = binaryReader->read(); - header.boundingBox.max.x = binaryReader->read(); - header.boundingBox.max.y = binaryReader->read(); - header.boundingBox.max.z = binaryReader->read(); -} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/PRM/PRMEntries.cpp b/BMEdit/GameLib/Source/GameLib/PRM/PRMEntries.cpp new file mode 100644 index 0000000..591b262 --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/PRM/PRMEntries.cpp @@ -0,0 +1,243 @@ +#include +#include +#include +#include + + +namespace gamelib::prm +{ + void Index::deserialize(Index& index, ZBio::ZBinaryReader::BinaryReader* binaryReader) + { + index.a = binaryReader->read(); + index.b = binaryReader->read(); + index.c = binaryReader->read(); + } + + void BoundingBox::deserialize(BoundingBox& boundingBox, ZBio::ZBinaryReader::BinaryReader* binaryReader) + { + boundingBox.vMin.x = binaryReader->read(); + boundingBox.vMin.y = binaryReader->read(); + boundingBox.vMin.z = binaryReader->read(); + boundingBox.vMax.x = binaryReader->read(); + boundingBox.vMax.y = binaryReader->read(); + boundingBox.vMax.z = binaryReader->read(); + } + + void Mesh::deserialize(Mesh& mesh, ZBio::ZBinaryReader::BinaryReader* binaryReader, const PrmFile& prmFile) + { + mesh.boneDecl = binaryReader->read(); + mesh.packType = binaryReader->read(); + mesh.kind = binaryReader->read(); + mesh.textureId = binaryReader->read(); + mesh.unk6 = binaryReader->read(); + mesh.nextVariation = binaryReader->read(); + mesh.unkC = binaryReader->read(); + mesh.unkD = binaryReader->read(); + + assert(binaryReader->tell() == 0xE && "Bad offset"); + if (binaryReader->tell() != 0xE) + return; + + mesh.lod = binaryReader->read(); + + if (mesh.lod & (uint8_t)1 == (uint8_t)1) + { + // Read mesh variation index + mesh.variationId = binaryReader->read(); + + // Seed another 2 bytes? + ZBioHelpers::seekBy(binaryReader, 0x2); + + // Read material id + mesh.material_id = binaryReader->read(); + + // Jump next + ZBioHelpers::seekBy(binaryReader, 0x14); + + uint32_t meshDescriptionChunk = 0; + + { + uint32_t meshDescriptionPointerChunk = 0; + + // Read description chunk index + meshDescriptionPointerChunk = binaryReader->read(); + + if (meshDescriptionPointerChunk >= prmFile.chunks.size()) + { + // Invalid chunk? Or not? + // TODO: Weird case, need investigate it later + return; + } + + // Read description + ZBio::ZBinaryReader::BinaryReader modelDescriptionReader { + reinterpret_cast(prmFile.chunks[meshDescriptionPointerChunk].data.get()), + static_cast(prmFile.entries[meshDescriptionPointerChunk].size) + }; + + // Instead of meshDescriptionPointerChunk this value pointed by meshDescriptionPointerChunk (another IOI shit code, who cares?) + meshDescriptionChunk = modelDescriptionReader.read(); + + if (meshDescriptionChunk >= prmFile.chunks.size()) + { + // Invalid chunk? Invalid wtf? IOI!!111111 + return; + } + } + + // Now we almost ready to read model description (rly?) + ZBio::ZBinaryReader::BinaryReader meshDescriptionReader { + reinterpret_cast(prmFile.chunks[meshDescriptionChunk].data.get()), + static_cast(prmFile.entries[meshDescriptionChunk].size) + }; + + uint32_t vertexCount = 0, vertexChunk = 0, trianglesChunk = 0; + + vertexCount = meshDescriptionReader.read(); + vertexChunk = meshDescriptionReader.read(); + + // Another seek (rly?) + ZBioHelpers::seekBy(&meshDescriptionReader, 0x4); + + trianglesChunk = meshDescriptionReader.read(); + + // Check that we have something valid here + if (vertexCount != 0 && vertexChunk != 0 && trianglesChunk != 0) + { + // And another one reader + ZBio::ZBinaryReader::BinaryReader trianglesReader { + reinterpret_cast(prmFile.chunks[trianglesChunk].data.get()), + static_cast(prmFile.entries[trianglesChunk].size) + }; + + // Skip first 2 bytes + ZBioHelpers::seekBy(&trianglesReader, 0x2); + + uint16_t trianglesCount = 0; + trianglesCount = trianglesReader.read(); + mesh.trianglesCount = trianglesCount; + + // Magic (rly?) + uint32_t vertexSize = 0; + vertexSize = static_cast(prmFile.entries[vertexChunk].size / vertexCount); + vertexSize -= vertexSize % 4; + + if (vertexSize != 0x28 && vertexSize % 0x28 == 0) + { + vertexCount *= vertexSize / 0x28; + vertexSize = 0x28; + } + + // And vertex reader + ZBio::ZBinaryReader::BinaryReader vertexReader { + reinterpret_cast(prmFile.chunks[vertexChunk].data.get()), + static_cast(prmFile.entries[vertexChunk].size) + }; + + assert(vertexSize == 0x10 || vertexSize == 0x24 || vertexSize == 0x28 || vertexSize == 0x34); + if (vertexSize == 0x10 || vertexSize == 0x24 || vertexSize == 0x28 || vertexSize == 0x34) + { + mesh.vertexFormat = static_cast(vertexSize); + + switch (mesh.vertexFormat) + { + case VertexFormat::VF_10: + { + for (uint32_t j = 0; j < vertexCount; j++) + { + glm::vec3& vertex = mesh.vertices.emplace_back(); + vertexReader.read(glm::value_ptr(vertex), 3); + + uint8_t l[4] { 0, 0, 0, 0 }; + vertexReader.read(&l[0], 4); + } + } + break; + case VertexFormat::VF_24: + { + for (uint32_t j = 0; j < vertexCount; j++) + { + glm::vec3& vertex = mesh.vertices.emplace_back(); + vertexReader.read(glm::value_ptr(vertex), 3); + + // Skip another 0x10 useful info + // TODO: Fix this! + ZBioHelpers::seekBy(&vertexReader, 0x10); + + // Read UVs + glm::vec2& uv = mesh.uvs.emplace_back(); + vertexReader.read(glm::value_ptr(uv), 2); + } + } + break; + case VertexFormat::VF_28: + { + for (uint32_t j = 0; j < vertexCount; j++) + { + glm::vec3& vertex = mesh.vertices.emplace_back(); + vertexReader.read(glm::value_ptr(vertex), 3); + + // Skip another 0x10 useful info + // TODO: Fix this! + ZBioHelpers::seekBy(&vertexReader, 0x8); + + // Read UVs + glm::vec2& uv = mesh.uvs.emplace_back(); + vertexReader.read(glm::value_ptr(uv), 2); + + // Another seek + // TODO: Fix this! + ZBioHelpers::seekBy(&vertexReader, 0xC); + } + } + break; + case VertexFormat::VF_34: + { + for (uint32_t j = 0; j < vertexCount; j++) + { + glm::vec3& vertex = mesh.vertices.emplace_back(); + vertexReader.read(glm::value_ptr(vertex), 3); + + // TODO: Fix this! + ZBioHelpers::seekBy(&vertexReader, 0x18); + + glm::vec2& uv = mesh.uvs.emplace_back(); + vertexReader.read(glm::value_ptr(uv), 2); + + // TODO: Fix this + ZBioHelpers::seekBy(&vertexReader, 0x8); + } + } + break; + default: + assert(false && "Unsupported format"); + break; + } + + // Store triangle indices + for (uint32_t j = 0; j < mesh.trianglesCount / 3; j++) + { + prm::Index& index = mesh.indices.emplace_back(); + prm::Index::deserialize(index, &trianglesReader); + } + } + } + } + } + + void Entry::deserialize(Entry& entry, ZBio::ZBinaryReader::BinaryReader* binaryReader) + { + entry.offset = binaryReader->read(); + entry.size = binaryReader->read(); + entry.type = binaryReader->read(); + entry.pad = binaryReader->read(); + } + + void Header::deserialize(Header& header, ZBio::ZBinaryReader::BinaryReader* binaryReader) + { + header.table_offset = binaryReader->read(); + header.table_count = binaryReader->read(); + header.table_offset2 = binaryReader->read(); + header.zeroed = binaryReader->read(); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/PRM/PRMHeader.cpp b/BMEdit/GameLib/Source/GameLib/PRM/PRMHeader.cpp deleted file mode 100644 index ebc5ea7..0000000 --- a/BMEdit/GameLib/Source/GameLib/PRM/PRMHeader.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include -#include - - -namespace gamelib::prm -{ - void PRMHeader::deserialize(gamelib::prm::PRMHeader &header, ZBio::ZBinaryReader::BinaryReader *binaryReader) - { - header.chunkOffset = binaryReader->read(); - header.countOfPrimitives = binaryReader->read(); - header.chunkOffset2 = binaryReader->read(); - header.zeroed = binaryReader->read(); - } -} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/PRM/PRMIndexChunkHeader.cpp b/BMEdit/GameLib/Source/GameLib/PRM/PRMIndexChunkHeader.cpp deleted file mode 100644 index 08ce83b..0000000 --- a/BMEdit/GameLib/Source/GameLib/PRM/PRMIndexChunkHeader.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include -#include - - -namespace gamelib::prm -{ - void PRMIndexChunkHeader::deserialize(gamelib::prm::PRMIndexChunkHeader &header, ZBio::ZBinaryReader::BinaryReader *binaryReader) - { - header.unk0 = binaryReader->read(); - header.indicesCount = binaryReader->read(); - } -} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/PRM/PRMReader.cpp b/BMEdit/GameLib/Source/GameLib/PRM/PRMReader.cpp index 0269e5e..467c021 100644 --- a/BMEdit/GameLib/Source/GameLib/PRM/PRMReader.cpp +++ b/BMEdit/GameLib/Source/GameLib/PRM/PRMReader.cpp @@ -1,109 +1,119 @@ -#include -#include -#include -#include #include -#include -#include - +#include #include -namespace gamelib::prm +namespace gamelib { - constexpr std::size_t kMaxChunksPerFile = 40960; // There are 40960 geoms max - - PRMReader::PRMReader(gamelib::prm::PRMHeader &header, std::vector &chunkDescriptors, std::vector &chunks) - : m_header(header) - , m_chunkDescriptors(chunkDescriptors) - , m_chunks(chunks) - { - } + PRMReader::PRMReader() = default; - bool PRMReader::read(Span buffer) + bool PRMReader::parse(const std::uint8_t *pBuffer, std::size_t iBufferSize) { - if (!buffer) - { + if (!pBuffer || !iBufferSize) return false; - } - ZBio::ZBinaryReader::BinaryReader binaryReader(reinterpret_cast(buffer.data()), buffer.size()); + // Initialize global reader + ZBio::ZBinaryReader::BinaryReader reader { reinterpret_cast(pBuffer), static_cast(iBufferSize) }; // Read header - PRMHeader::deserialize(m_header, &binaryReader); - if (m_header.zeroed != 0x0) - { - throw PRMBadFile("Zeroed field must be zeroed!"); - } + prm::Header::deserialize(m_file.header, &reader); - if (m_header.countOfPrimitives >= kMaxChunksPerFile) + // Seek & read entries { - throw PRMBadFile("Possibly invalid PRM file. Game supports max 40959 unique primitives per level"); - } + ZBio::ZBinaryReader::BinaryReader entriesReader { reinterpret_cast(pBuffer), static_cast(iBufferSize) }; + entriesReader.seek(m_file.header.table_offset); + m_file.entries.reserve(m_file.header.table_count); // actually, it's not a table count, it's amount of chunks. - m_chunks.reserve(m_header.countOfPrimitives); - m_chunkDescriptors.reserve(m_header.countOfPrimitives); + for (int i = 0; i < m_file.header.table_count; i++) + { + prm::Entry& entry = m_file.entries.emplace_back(); + prm::Entry::deserialize(entry, &entriesReader); + } - for (std::uint32_t chunkIndex = 0u; chunkIndex < m_header.countOfPrimitives; ++chunkIndex) + m_file.chunks.reserve(m_file.header.table_count); + } + + // Prepare chunks { - if (m_header.chunkOffset + (chunkIndex * PRMChunkDescriptor::kDescriptorSize) >= buffer.size()) + for (int i = 0; i < m_file.header.table_count; i++) { - throw PRMBadChunkException(chunkIndex); - } + ZBio::ZBinaryReader::BinaryReader chunksReader { reinterpret_cast(pBuffer), static_cast(iBufferSize) }; + chunksReader.seek(m_file.entries[i].offset); - // Read chunk descriptor - binaryReader.seek(m_header.chunkOffset + (chunkIndex * PRMChunkDescriptor::kDescriptorSize)); - auto &descriptor = m_chunkDescriptors.emplace_back(); - PRMChunkDescriptor::deserialize(descriptor, &binaryReader); + prm::Chunk& chunk = m_file.chunks.emplace_back(); + chunk.data = std::make_unique(m_file.entries[i].size); - // Read chunk - auto chunkBufferSize = descriptor.declarationSize; - auto chunkBuffer = std::make_unique(chunkBufferSize); + // TODO: Need to check that data is valid (malloc is ok) + chunksReader.read(chunk.data.get(), m_file.entries[i].size); - { - BinaryReaderSeekScope scope { &binaryReader }; + // TODO: Need to refactor and use former header for chunk buffer instead of cropping few bytes (will fix later) + // TODO: Need to use proper way to read bytes (endianness) + if (m_file.entries[i].size == 0x40 && *reinterpret_cast(chunk.data.get()) == 0x70100) + { + chunk.is_model = true; + chunk.model = static_cast(m_file.models.size()); - binaryReader.seek(descriptor.declarationOffset); - binaryReader.read(&chunkBuffer[0], static_cast(chunkBufferSize)); + prm::Model& newMdl = m_file.models.emplace_back(); + newMdl.chunk = i; + } } - - m_chunks.emplace_back(chunkIndex, m_header.countOfPrimitives, std::move(chunkBuffer), chunkBufferSize); } - int unrecognizedChunks = 0; - for (const auto& chk: m_chunks) + // Read models { - if (chk.getKind() == PRMChunkRecognizedKind::CRK_UNKNOWN_BUFFER) + for (prm::Model& model : m_file.models) { - unrecognizedChunks++; + ZBio::ZBinaryReader::BinaryReader modelReader { + reinterpret_cast(m_file.chunks[model.chunk].data.get()), + static_cast(m_file.entries[model.chunk].size) + }; + + modelReader.seek(0x14); + uint32_t meshCount = 0, meshTable = 0; + + meshCount = modelReader.read(); // 0x14 -> 0x18 + meshTable = modelReader.read(); // 0x18 -> 0x1C + + // Read bbox + ZBioHelpers::seekBy(&modelReader, 0x4); + + prm::BoundingBox::deserialize(model.boundingBox, &modelReader); + + // Read mesh table + ZBio::ZBinaryReader::BinaryReader meshTableReader { + reinterpret_cast(m_file.chunks[meshTable].data.get()), + static_cast(m_file.entries[meshTable].size) + }; + + for (uint32_t i = 0; i < meshCount; i++) + { + // Read mesh chunk index + uint32_t currentMeshChunkIdx = meshTableReader.read(); // NOLINT(*-use-auto) + + // And read mesh itself + ZBio::ZBinaryReader::BinaryReader meshEntryReader { + reinterpret_cast(m_file.chunks[currentMeshChunkIdx].data.get()), + static_cast(m_file.entries[currentMeshChunkIdx].size) + }; + + // Read mesh + prm::Mesh currentMesh {}; + prm::Mesh::deserialize(currentMesh, &meshEntryReader, m_file); + + if (currentMesh.lod & (uint8_t)1 == (uint8_t)1) + { + // Save mesh + model.meshes.emplace_back(std::move(currentMesh)); + } + } } } - if (unrecognizedChunks > 0) - { - throw PRMBadFile("Found at least 1 unrecognized chunk. Need to check level by devs"); - } - return true; } - const PRMHeader &PRMReader::getHeader() const - { - return m_header; - } - - const std::vector &PRMReader::getChunkDescriptors() const - { - return m_chunkDescriptors; - } - - PRMChunk* PRMReader::getChunkAt(size_t chunkIndex) - { - return chunkIndex >= m_chunks.size() ? nullptr : &m_chunks[chunkIndex]; - } - - const PRMChunk* PRMReader::getChunkAt(size_t chunkIndex) const + prm::PrmFile&& PRMReader::takePrimitives() { - return chunkIndex >= m_chunks.size() ? nullptr : &m_chunks[chunkIndex]; + return std::move(m_file); } } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/PRP/PRPInstruction.cpp b/BMEdit/GameLib/Source/GameLib/PRP/PRPInstruction.cpp index 2120a9f..17d0fee 100644 --- a/BMEdit/GameLib/Source/GameLib/PRP/PRPInstruction.cpp +++ b/BMEdit/GameLib/Source/GameLib/PRP/PRPInstruction.cpp @@ -248,4 +248,6 @@ namespace gamelib::prp { return !operator==(other); } + + const PRPOperandVal PRPOperandVal::kInitedOperandValue { false }; } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/PRP/PRPOpCode.cpp b/BMEdit/GameLib/Source/GameLib/PRP/PRPOpCode.cpp index be72342..c15174a 100644 --- a/BMEdit/GameLib/Source/GameLib/PRP/PRPOpCode.cpp +++ b/BMEdit/GameLib/Source/GameLib/PRP/PRPOpCode.cpp @@ -74,7 +74,10 @@ namespace gamelib PRPOpCode fromString(const std::string &asString) { -#define OPSW(x) if (asString.starts_with("PRPOpCode.") && asString.find(#x) != std::string::npos) return PRPOpCode::x; + if (!asString.starts_with("PRPOpCode.")) return PRPOpCode::ERR_UNKNOWN; + const std::string argName = asString.substr(10); + +#define OPSW(x) if (argName == #x) return PRPOpCode::x; OPSW(Array) OPSW(BeginObject) OPSW(Reference) diff --git a/BMEdit/GameLib/Source/GameLib/Plane.cpp b/BMEdit/GameLib/Source/GameLib/Plane.cpp new file mode 100644 index 0000000..66eedd5 --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/Plane.cpp @@ -0,0 +1,84 @@ +#include + + +namespace gamelib +{ + Plane::Plane() = default; + + Plane::Plane(const glm::vec3 &v0, const glm::vec3 &v1, const glm::vec3 &v2, const glm::vec3 &v3) + { + m_aVertices[0] = v0; + m_aVertices[1] = v1; + m_aVertices[2] = v2; + m_aVertices[3] = v3; + } + + BoundingBox Plane::makeBoundingBox() const + { + glm::vec3 vMin = m_aVertices[0]; + glm::vec3 vMax = m_aVertices[0]; + + for (int i = 1; i < 4; i++) + { + vMin = glm::min(vMin, m_aVertices[i]); + vMax = glm::min(vMax, m_aVertices[i]); + } + + return { vMin, vMax }; + } + + glm::vec3 Plane::getNormal() const + { + glm::vec3 u = m_aVertices[1] - m_aVertices[0]; + glm::vec3 v = m_aVertices[2] - m_aVertices[0]; + glm::vec3 vNormal = glm::cross(u, v); + return glm::normalize(vNormal); + } + + glm::vec3 Plane::getBiNormal() const + { + return -getNormal(); + } + + glm::vec3 Plane::getCenter() const + { + glm::vec3 center { .0f }; + + for (const auto& vPoint : m_aVertices) { + center += vPoint; + } + + center /= 4.0f; + return center; + } + + const glm::vec3& Plane::getPoint(size_t idx) const + { + if (idx >= 0 && idx <= 3) + { + return m_aVertices[idx]; + } + + static const glm::vec3 kNull { 0.f }; + return kNull; + } + + float Plane::getSize() const + { + float fMaxDistance = .0f; + + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + if (i == j) + continue; + + float fDistance = glm::distance(m_aVertices[i], m_aVertices[j]); + fMaxDistance = std::max(fMaxDistance, fDistance); + } + } + + return fMaxDistance; + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/Scene/SceneObject.cpp b/BMEdit/GameLib/Source/GameLib/Scene/SceneObject.cpp index e7bd299..cef68ac 100644 --- a/BMEdit/GameLib/Source/GameLib/Scene/SceneObject.cpp +++ b/BMEdit/GameLib/Source/GameLib/Scene/SceneObject.cpp @@ -1,4 +1,11 @@ #include +#include +#include +#include +#include +#include +#include +#include #include @@ -17,7 +24,7 @@ namespace gamelib::scene bool SceneObject::Controller::operator==(const SceneObject::Controller &other) const { - return name == other.name && properties == other.properties; + return name == other.name && properties == other.properties && type == other.type; } bool SceneObject::Controller::operator!=(const SceneObject::Controller &other) const @@ -81,9 +88,9 @@ namespace gamelib::scene return m_properties; } - Value &SceneObject::getProperties() + void SceneObject::setProperties(const gamelib::Value &v) { - return m_properties; + m_properties = v; } const gms::GMSGeomEntity &SceneObject::getGeomInfo() const @@ -110,4 +117,121 @@ namespace gamelib::scene { return m_children; } + + bool SceneObject::isInheritedOf(const std::string& baseType) const + { + const auto* type = getType(); + + if (!type) return false; + + while (type) + { + if (type->getName() == baseType) + return true; + + if (type->getKind() != TypeKind::COMPLEX) + return false; + + type = static_cast(type)->getParent(); // NOLINT(*-pro-type-static-cast-downcast) + } + + return false; + } + + bool SceneObject::is(const std::string& type) const + { + const auto* pType = getType(); + if (!pType) return false; + + return pType->getName() == type; + } + + glm::mat4 SceneObject::getLocalTransform() const + { + const auto vPosition = getPosition(); + const auto mMatrix = getOriginalTransform(); + + glm::mat4 mResult = glm::mat4(1.f); + glm::mat3 mMatrixT = glm::transpose(mMatrix); + + const float* pSrcMatrix = glm::value_ptr(mMatrix); + float* pDstMatrix = glm::value_ptr(mResult); + + // Result of reverse engineering of MatPosToMatrix (or Transform3x3To4x4Matrix from PC version (sub_489740)) + pDstMatrix[0] = pSrcMatrix[6]; + pDstMatrix[1] = pSrcMatrix[7]; + pDstMatrix[2] = pSrcMatrix[8]; + pDstMatrix[3] = 0.0f; + pDstMatrix[4] = pSrcMatrix[3]; + pDstMatrix[5] = pSrcMatrix[4]; + pDstMatrix[6] = pSrcMatrix[5]; + pDstMatrix[7] = 0.0f; + pDstMatrix[8] = pSrcMatrix[0]; + pDstMatrix[9] = pSrcMatrix[1]; + pDstMatrix[10] = pSrcMatrix[2]; + pDstMatrix[11] = 0.0f; + pDstMatrix[12] = vPosition.x; + pDstMatrix[13] = vPosition.y; + pDstMatrix[14] = vPosition.z; + pDstMatrix[15] = 1.0f; + + return mResult; + } + + glm::vec3 SceneObject::getPosition() const + { + return getProperties().getObject("Position", glm::vec3(0.f)); + } + + glm::mat3 SceneObject::getOriginalTransform() const + { + return getProperties().getObject("Matrix", glm::mat3(1.f)); + } + + glm::mat4 SceneObject::getWorldTransform() const // NOLINT(*-no-recursion) + { + glm::mat4 mMatrix = getLocalTransform(); + + if (!getParent().expired()) + { + mMatrix = getParent().lock()->getWorldTransform() * mMatrix; + } + + return mMatrix; + } + + void SceneObject::visitChildren(const std::function& pred) const + { + if (!pred) + return; + + internalVisitChildObjects(pred); + } + + SceneObject::EVisitResult SceneObject::internalVisitChildObjects(const std::function& pred) const + { + for (const auto rChild : getChildren()) + { + if (auto pChild = rChild.lock()) + { + const auto predRes = pred(pChild); + + switch (predRes) + { + case EVisitResult::VR_CONTINUE: + { + auto predResInner = pChild->internalVisitChildObjects(pred); + if (predResInner == EVisitResult::VR_STOP_ALL) + return predResInner; + } + break; + + case EVisitResult::VR_STOP_ALL: return EVisitResult::VR_STOP_ALL; + case EVisitResult::VR_NEXT: continue; + } + } + } + + return EVisitResult::VR_CONTINUE; + } } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/Scene/SceneObjectPropertiesDumper.cpp b/BMEdit/GameLib/Source/GameLib/Scene/SceneObjectPropertiesDumper.cpp index 5be43bf..ecf6755 100644 --- a/BMEdit/GameLib/Source/GameLib/Scene/SceneObjectPropertiesDumper.cpp +++ b/BMEdit/GameLib/Source/GameLib/Scene/SceneObjectPropertiesDumper.cpp @@ -88,7 +88,7 @@ void SceneObjectPropertiesDumper::visitSceneObject(const SceneObject *sceneObjec // Controllers out.emplace_back(PRPInstruction(PRPOpCode::Container, PRPOperandVal(static_cast(sceneObject->getControllers().size())))); - for (const auto& [name, properties] : sceneObject->getControllers()) + for (const auto& [name, properties, type] : sceneObject->getControllers()) { out.reserve(3 + properties.getInstructions().size()); diff --git a/BMEdit/GameLib/Source/GameLib/Scene/SceneObjectPropertiesLoader.cpp b/BMEdit/GameLib/Source/GameLib/Scene/SceneObjectPropertiesLoader.cpp index 536c506..d4f1334 100644 --- a/BMEdit/GameLib/Source/GameLib/Scene/SceneObjectPropertiesLoader.cpp +++ b/BMEdit/GameLib/Source/GameLib/Scene/SceneObjectPropertiesLoader.cpp @@ -111,7 +111,7 @@ namespace gamelib::scene throw SceneObjectVisitorException(objectIdx, "Invalid instructions set (verification failed) [2]"); } - currentObject->getProperties() = *value; + currentObject->setProperties(*value); ip = newIP; // Assign new ip } @@ -192,6 +192,7 @@ namespace gamelib::scene auto& controller = currentObject->getControllers().emplace_back(); controller.name = controllerName; controller.properties = controllerMapResult.value(); + controller.type = controllerType; if (ip[0].getOpCode() != PRPOpCode::EndObject && reinterpret_cast(controllerType)->areUnexposedInstructionsAllowed()) { diff --git a/BMEdit/GameLib/Source/GameLib/TEX/TEXEntry.cpp b/BMEdit/GameLib/Source/GameLib/TEX/TEXEntry.cpp new file mode 100644 index 0000000..52d1a6f --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/TEX/TEXEntry.cpp @@ -0,0 +1,221 @@ +#include +#include +#include +#include + + +namespace gamelib::tex +{ + template + static void alignStream(ZBio::ZBinaryWriter::BinaryWriter *writerStream) + { + const size_t remainder = writerStream->tell() % Align; + const size_t paddingSize = remainder > 0 ? Align - remainder : 0; + + for (int i = 0; i < paddingSize; i++) + { + writerStream->write(Pad); + } + } + + void PALPalette::deserialize(PALPalette &palette, ZBio::ZBinaryReader::BinaryReader *binaryReader) + { + palette.m_size = binaryReader->read(); + palette.m_data = std::make_unique(palette.m_size * 4); + binaryReader->read(palette.m_data.get(), static_cast(palette.m_size) * 4); + } + + void PALPalette::serialize(const PALPalette &palette, ZBio::ZBinaryWriter::BinaryWriter *writerStream) + { + writerStream->write(palette.m_size); + writerStream->write(palette.m_data.get(), static_cast(palette.m_size) * 4); + } + + void TEXMipLevel::deserialize(TEXMipLevel &mipLevel, ZBio::ZBinaryReader::BinaryReader *binaryReader) + { + mipLevel.m_mipLevelSize = binaryReader->read(); + mipLevel.m_buffer = std::make_unique(mipLevel.m_mipLevelSize); + binaryReader->read(mipLevel.m_buffer.get(), static_cast(mipLevel.m_mipLevelSize)); + } + + void TEXMipLevel::serialize(const TEXMipLevel &mipLevel, ZBio::ZBinaryWriter::BinaryWriter *writerStream) + { + writerStream->write(mipLevel.m_mipLevelSize); + writerStream->write(mipLevel.m_buffer.get(), static_cast(mipLevel.m_mipLevelSize)); + } + + void TEXCubeMaps::deserialize(TEXCubeMaps &cubeMaps, ZBio::ZBinaryReader::BinaryReader *binaryReader) + { + cubeMaps.m_count = binaryReader->read(); + cubeMaps.m_textureIndices.resize(cubeMaps.m_count); + binaryReader->read(cubeMaps.m_textureIndices.data(), cubeMaps.m_count); + } + + void TEXCubeMaps::serialize(const TEXCubeMaps &cubeMaps, ZBio::ZBinaryWriter::BinaryWriter *writerStream) + { + writerStream->write(static_cast(cubeMaps.m_textureIndices.size())); + writerStream->write(cubeMaps.m_textureIndices.data(), static_cast(cubeMaps.m_textureIndices.size())); + } + + void TEXEntry::deserialize(TEXEntry &entry, ZBio::ZBinaryReader::BinaryReader *binaryReader) + { + entry.m_fileSize = binaryReader->read(); + auto checkFormat = [](uint32_t format) -> bool { + if (auto type = static_cast(format); + type != TEXEntryType::ET_BITMAP_I8 && type != TEXEntryType::ET_BITMAP_EMBM && + type != TEXEntryType::ET_BITMAP_DOT3 && type != TEXEntryType::ET_BITMAP_CUBE && type != TEXEntryType::ET_BITMAP_DMAP && + type != TEXEntryType::ET_BITMAP_PAL && type != TEXEntryType::ET_BITMAP_PAL_OPAC && + type != TEXEntryType::ET_BITMAP_32 && type != TEXEntryType::ET_BITMAP_U8V8 && + type != TEXEntryType::ET_BITMAP_DXT1 && type != TEXEntryType::ET_BITMAP_DXT3 + ) { + return false; + } + + return true; + }; + + { + if (auto type = binaryReader->read(); !checkFormat(type)) + { + assert(false && "Bad format!"); + return; + } + else + { + entry.m_type1 = static_cast(type); + } + } + + { + if (auto type = binaryReader->read(); !checkFormat(type)) + { + assert(false && "Bad format [2]!"); + return; + } + else + { + entry.m_type2 = static_cast(type); + } + } + + entry.m_index = binaryReader->read(); + entry.m_height = binaryReader->read(); + entry.m_width = binaryReader->read(); + entry.m_numOfMipMaps = binaryReader->read(); + + // https://github.com/pavledev/GlacierTEXEditor/blob/master/GlacierTEXEditor/Form1.cs#L296 + + entry.m_flags = binaryReader->read(); + entry.m_unk2 = binaryReader->read(); + entry.m_unk3 = binaryReader->read(); + + // Allocate mip levels + entry.m_mipLevels.resize(entry.m_numOfMipMaps); + + // Read filename + if (auto fileName = binaryReader->readCString(); !fileName.empty()) + { + entry.m_fileName = std::move(fileName); + } + else + { + entry.m_fileName = std::nullopt; + } + + // Read mip levels + for (auto& mipEntry : entry.m_mipLevels) + { + TEXMipLevel::deserialize(mipEntry, binaryReader); + } + + // If PALN - need to save palette. TODO: Maybe for PALO need save this too? + if (entry.m_type1 == TEXEntryType::ET_BITMAP_PAL) + { + PALPalette& palette = entry.m_palPalette.emplace(); + PALPalette::deserialize(palette, binaryReader); + } + } + + void TEXEntry::serialize(const TEXEntry &entry, ZBio::ZBinaryWriter::BinaryWriter *writerStream, uint32_t& entryOffset, uint32_t& cubeMapsDescOffset) + { + // Save stream pos + entryOffset = writerStream->tell(); + + // Write data + writerStream->write(entry.m_fileSize); + writerStream->write(static_cast(entry.m_type1)); + writerStream->write(static_cast(entry.m_type2)); + writerStream->write(entry.m_index); + writerStream->write(entry.m_height); + writerStream->write(entry.m_width); + writerStream->write(static_cast(entry.m_mipLevels.size())); + writerStream->write(entry.m_flags); + writerStream->write(entry.m_unk2); + writerStream->write(entry.m_unk3); + if (entry.m_fileName.has_value() && !entry.m_fileName.value().empty()) + { + writerStream->writeCString(entry.m_fileName.value()); + } + else + { + writerStream->write(0u); + } + + // Write MIP levels + for (const auto& mipEntry : entry.m_mipLevels) + { + TEXMipLevel::serialize(mipEntry, writerStream); + } + + if (entry.m_type1 == TEXEntryType::ET_BITMAP_PAL) //NOTE: Check for PALO (Opac) here, or remove support of that texture type + { + // Write palette + assert(entry.m_palPalette.has_value()); + PALPalette::serialize(entry.m_palPalette.value(), writerStream); + } + + alignStream<0x10, 0x00>(writerStream); + + if (!entry.m_cubeMaps.m_textureIndices.empty()) + { + cubeMapsDescOffset = writerStream->tell(); + + TEXCubeMaps::serialize(entry.m_cubeMaps, writerStream); + alignStream<0x10, 0x00>(writerStream); + } + else + { + cubeMapsDescOffset = 0; + } + } + + uint32_t TEXEntry::calculateSize() const + { + /** + * @source https://github.com/pavledev/GlacierTEXEditor/blob/master/GlacierTEXEditor/Form1.cs#L852 + */ + const std::string kEmptyStr {}; + uint32_t fileSize = sizeof(m_type1) + sizeof(m_type1) + + sizeof(uint32_t) + + sizeof(uint16_t) + sizeof(uint16_t) + + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + + (m_fileName.value_or(kEmptyStr).length()) + 1; + + for (const auto& mipLevel : m_mipLevels) + { + fileSize += mipLevel.m_mipLevelSize; + } + + if (m_type1 == TEXEntryType::ET_BITMAP_PAL) + { + fileSize += m_palPalette.value().m_size * 4; + } + + if (!m_cubeMaps.m_textureIndices.empty()) + { + fileSize += sizeof(uint32_t) + m_cubeMaps.m_textureIndices.size(); + } + + return fileSize; + } +} diff --git a/BMEdit/GameLib/Source/GameLib/TEX/TEXHeader.cpp b/BMEdit/GameLib/Source/GameLib/TEX/TEXHeader.cpp new file mode 100644 index 0000000..c2d72bb --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/TEX/TEXHeader.cpp @@ -0,0 +1,31 @@ +#include +#include +#include + + +namespace gamelib::tex +{ + TEXHeader::TEXHeader(uint32_t table1Offset, uint32_t table2Offset, uint32_t unk1, uint32_t unk2) + : m_texturesPoolOffset(table1Offset) + , m_cubeMapsPoolOffset(table2Offset) + , m_unk1(unk1) + , m_unk2(unk2) + { + } + + void TEXHeader::deserialize(TEXHeader &header, ZBio::ZBinaryReader::BinaryReader *binaryReader) + { + header.m_texturesPoolOffset = binaryReader->read(); + header.m_cubeMapsPoolOffset = binaryReader->read(); + header.m_unk1 = binaryReader->read(); + header.m_unk2 = binaryReader->read(); + } + + void TEXHeader::serialize(const TEXHeader &header, ZBio::ZBinaryWriter::BinaryWriter *writerStream) + { + writerStream->write(header.m_texturesPoolOffset); + writerStream->write(header.m_cubeMapsPoolOffset); + writerStream->write(header.m_unk1); + writerStream->write(header.m_unk2); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/TEX/TEXReader.cpp b/BMEdit/GameLib/Source/GameLib/TEX/TEXReader.cpp new file mode 100644 index 0000000..304b5e2 --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/TEX/TEXReader.cpp @@ -0,0 +1,91 @@ +#include +#include + + +namespace gamelib::tex +{ + bool TEXReader::parse(const uint8_t *texFileBuffer, int64_t texFileSize) + { + if (!texFileBuffer || !texFileSize) + return false; + + { + ZBio::ZBinaryReader::BinaryReader reader{ + reinterpret_cast(texFileBuffer), + static_cast(texFileSize)}; + + // Read header + TEXHeader::deserialize(m_header, &reader); + } + + if (m_header.m_texturesPoolOffset >= texFileSize || m_header.m_cubeMapsPoolOffset >= texFileSize) + return false; // Invalid file + + // Read textures pool + { + ZBio::ZBinaryReader::BinaryReader reader{ + reinterpret_cast(&texFileBuffer[m_header.m_texturesPoolOffset]), + static_cast(texFileSize) + }; + + reader.read(m_texturesPool.data(), static_cast(m_texturesPool.size())); + } + + ///NOTE: For PS4 support see https://github.com/pavledev/GlacierTEXEditor/blob/master/GlacierTEXEditor/Form1.cs#L261 + // Now read entries + m_entries.clear(); + + for (const uint32_t& entryOffset : m_texturesPool) + { + if (entryOffset != 0) + { + // Do read + ZBio::ZBinaryReader::BinaryReader reader{ + reinterpret_cast(&texFileBuffer[entryOffset]), + static_cast(texFileSize - entryOffset) // Technically we must set some constants, but who cares? + }; + + TEXEntry& entry = m_entries.emplace_back(); + entry.m_offset = entryOffset; + TEXEntry::deserialize(entry, &reader); + } + else + { + if (m_entries.empty()) + { + ++m_countOfEmptyOffsets; + } + } + } + + // Read cube maps pool #2 + { + ZBio::ZBinaryReader::BinaryReader reader{ + reinterpret_cast(&texFileBuffer[m_header.m_cubeMapsPoolOffset]), + static_cast(texFileSize) + }; + + reader.read(m_cubeMapsPool.data(), static_cast(m_cubeMapsPool.size())); + } + + // Read cubemaps + { + for (auto& entry : m_entries) + { + // A right way to get cube maps working in game + if (!(entry.m_flags & TEXEntryFlags::TEX_EF_IsCubeMap)) + continue; + + const uint32_t offset = m_cubeMapsPool[entry.m_index]; + + ZBio::ZBinaryReader::BinaryReader reader{ + reinterpret_cast(&texFileBuffer[offset]), static_cast(texFileSize) + }; + + TEXCubeMaps::deserialize(entry.m_cubeMaps, &reader); + } + } + + return true; + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/TEX/TEXWriter.cpp b/BMEdit/GameLib/Source/GameLib/TEX/TEXWriter.cpp new file mode 100644 index 0000000..cddeeac --- /dev/null +++ b/BMEdit/GameLib/Source/GameLib/TEX/TEXWriter.cpp @@ -0,0 +1,42 @@ +#include +#include + + +namespace gamelib::tex +{ + void TEXWriter::write(const TEXHeader &header, const std::vector &entries, std::vector &outBuffer) + { + auto writerSink = std::make_unique(); + auto binaryWriter = ZBio::ZBinaryWriter::BinaryWriter(std::move(writerSink)); + + TEXHeader localHeader = header; + OffsetsPool textureOffsetsPool { 0u }; + OffsetsPool cubeMapsOffsetsPool { 0u }; + + // Allocate space for header (will be serialized later) + binaryWriter.seek(sizeof(TEXHeader)); + + // Write entry by entry + for (size_t entryIndex = 0; entryIndex < entries.size(); ++entryIndex) // NOLINT(modernize-loop-convert) + { + const auto& entry = entries[entryIndex]; + TEXEntry::serialize(entry, &binaryWriter, textureOffsetsPool[entry.m_index], cubeMapsOffsetsPool[entry.m_index]); + } + + // Next is going offsets table + localHeader.m_texturesPoolOffset = binaryWriter.tell(); + binaryWriter.write(textureOffsetsPool.data(), static_cast(textureOffsetsPool.size())); + + // And write cube map indices pool + localHeader.m_cubeMapsPoolOffset = binaryWriter.tell(); + binaryWriter.write(cubeMapsOffsetsPool.data(), static_cast(cubeMapsOffsetsPool.size())); + + // Serialize header + binaryWriter.seek(0); + TEXHeader::serialize(localHeader, &binaryWriter); + + // Finally, copy data to final buffer + auto raw = binaryWriter.release().value(); + std::copy(raw.begin(), raw.end(), std::back_inserter(outBuffer)); + } +} \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/Type.cpp b/BMEdit/GameLib/Source/GameLib/Type.cpp index 503a080..963d07c 100644 --- a/BMEdit/GameLib/Source/GameLib/Type.cpp +++ b/BMEdit/GameLib/Source/GameLib/Type.cpp @@ -32,4 +32,9 @@ namespace gamelib { throw NotImplemented("You must implement this method in your own class!"); } + + Value Type::makeDefaultPropertiesPack() const + { + throw NotImplemented("You must implement this method in your own class!"); + } } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/TypeAlias.cpp b/BMEdit/GameLib/Source/GameLib/TypeAlias.cpp index 548e89b..7bd221d 100644 --- a/BMEdit/GameLib/Source/GameLib/TypeAlias.cpp +++ b/BMEdit/GameLib/Source/GameLib/TypeAlias.cpp @@ -64,6 +64,23 @@ namespace gamelib throw std::runtime_error("TypeAlias::map() failed. Alias not inited yet!"); } + Value TypeAlias::makeDefaultPropertiesPack() const + { + if (auto typePtr = std::get_if(&m_resultTypeInfo); typePtr != nullptr) + { + // An alias to another type - jmp + return (*typePtr)->makeDefaultPropertiesPack(); + } + + if (auto typeOpCodeValue = std::get_if(&m_resultTypeInfo); typeOpCodeValue != nullptr) + { + // A holder of single instruction (like ZGEOMREF) + return { this, { prp::PRPInstruction(*typeOpCodeValue, prp::PRPOperandVal::kInitedOperandValue) } }; + } + + throw std::runtime_error("TypeAlias::makeDefaultPropertiesPack() failed. Alias not inited yet!"); + } + const Type* TypeAlias::getFinalType() const { if (auto typePtrPtr = std::get_if(&m_resultTypeInfo); typePtrPtr != nullptr) @@ -83,4 +100,19 @@ namespace gamelib return prp::PRPOpCode::ERR_UNKNOWN; } + + bool TypeAlias::hasToolHint() const + { + return !m_toolHint.empty(); + } + + const std::string &TypeAlias::getToolHint() const + { + return m_toolHint; + } + + void TypeAlias::setToolHint(const std::string &toolHint) + { + m_toolHint = toolHint; + } } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/TypeArray.cpp b/BMEdit/GameLib/Source/GameLib/TypeArray.cpp index 48d60e3..380c3b6 100644 --- a/BMEdit/GameLib/Source/GameLib/TypeArray.cpp +++ b/BMEdit/GameLib/Source/GameLib/TypeArray.cpp @@ -124,4 +124,22 @@ namespace gamelib Value(this, std::move(data), views), instructions.slice(sliceSize, instructions.size() - sliceSize)); } + + Value TypeArray::makeDefaultPropertiesPack() const + { + // create an array. It presented via at least 3 instructions: BeginArray, [m_entryType opcode], EndArray + std::vector instructions{}; + instructions.resize(2 + m_requiredCapacity); + + instructions.front() = PRPInstruction(PRPOpCode::Array, PRPOperandVal(static_cast(m_requiredCapacity))); + instructions.back() = PRPInstruction(PRPOpCode::EndArray); + + // Fill by empty instructions. For float - 0.f, for int - 0, for bool - false, for string - "" + for (int i = 1; i < m_requiredCapacity + 1; i++) + { + instructions[i] = PRPInstruction(m_entryType, prp::PRPOperandVal::kInitedOperandValue); + } + + return Value(this, instructions); + } } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/TypeBitfield.cpp b/BMEdit/GameLib/Source/GameLib/TypeBitfield.cpp index 055e6e0..c97c158 100644 --- a/BMEdit/GameLib/Source/GameLib/TypeBitfield.cpp +++ b/BMEdit/GameLib/Source/GameLib/TypeBitfield.cpp @@ -51,4 +51,11 @@ namespace gamelib std::vector data { instructions[0] }; return Type::DataMappingResult(Value(this, std::move(data)), span); } + + Value TypeBitfield::makeDefaultPropertiesPack() const + { + // By default, we will produce empty StringArray opcode + constexpr int32_t kNoArgs = 0; + return Value(this, { PRPInstruction(PRPOpCode::StringArray, PRPOperandVal(kNoArgs)) }); + } } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/TypeComplex.cpp b/BMEdit/GameLib/Source/GameLib/TypeComplex.cpp index 48783de..2a98b24 100644 --- a/BMEdit/GameLib/Source/GameLib/TypeComplex.cpp +++ b/BMEdit/GameLib/Source/GameLib/TypeComplex.cpp @@ -1,4 +1,5 @@ #include +#include #include @@ -222,4 +223,44 @@ namespace gamelib // Done return Type::DataMappingResult(resultValue, ourSlice); } + + Value TypeComplex::makeDefaultPropertiesPack() const + { + // NOTE: Complex type with unexposed properties will produce potentially wrong object. We will produce only exposed properties, nothing more. + Value result {this, {}, {}}; + + // 1: Get pack of parent properties before + if (auto parent = getParent(); parent != nullptr) + { + // Really shitty code copypaste from ::map() method. What the hell was in my mind at that moment??? + auto parentPack = parent->makeDefaultPropertiesPack(); + for (const auto& [name, ip, views]: parentPack.getEntries()) + { + result += std::make_pair(name, Value(parentPack.getType(), Span(parentPack.getInstructions()).slice(ip).as>(), views)); + } + } + + // 2: Add our properties + for (const auto &view: m_instructionViews) + { + if (view.isTrivialType()) + { + // Trivial subject - just append + result += std::pair(view.getName(), Value(this, { PRPInstruction(view.getTrivialType(), PRPOperandVal::kInitedOperandValue) }, { view })); + } + else + { + // Ok, need to ask inner type properties pack + if (auto innerType = view.getType()) + { + // Just add inner type properties pack + auto pack = innerType->makeDefaultPropertiesPack(); + result += std::pair(view.getName(), Value(this, pack.getInstructions(), { view })); + } + else throw std::runtime_error("TypeComplex::makeDefaultPropertiesPack: unable to make default properties pack for " + view.getName()); + } + } + + return result; + } } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/TypeContainer.cpp b/BMEdit/GameLib/Source/GameLib/TypeContainer.cpp index 5924c82..ecb111e 100644 --- a/BMEdit/GameLib/Source/GameLib/TypeContainer.cpp +++ b/BMEdit/GameLib/Source/GameLib/TypeContainer.cpp @@ -53,4 +53,11 @@ namespace gamelib return Type::DataMappingResult(Value(this, std::move(data)), instructions.slice(sliceSize, instructions.size() - sliceSize)); } + + Value TypeContainer::makeDefaultPropertiesPack() const + { + // Just produce an empty container + constexpr int32_t kEmptyContainerSize = 0; + return Value(this, { PRPInstruction(PRPOpCode::Container, PRPOperandVal(kEmptyContainerSize)) }); + } } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/TypeEnum.cpp b/BMEdit/GameLib/Source/GameLib/TypeEnum.cpp index 9020a2e..96c103d 100644 --- a/BMEdit/GameLib/Source/GameLib/TypeEnum.cpp +++ b/BMEdit/GameLib/Source/GameLib/TypeEnum.cpp @@ -1,5 +1,6 @@ #include #include +#include namespace gamelib @@ -57,6 +58,22 @@ namespace gamelib return Type::DataMappingResult(Value(this, std::move(valueData), { ValueView("Value", instructions[0].getOpCode(), this) }), newSlice); } + Value TypeEnum::makeDefaultPropertiesPack() const + { + // Here we need to select lowest possible value of presented and return it. When we have an empty array we should raise an exception because our behaviour is undefined in this case + if (m_possibleValues.empty()) + throw std::runtime_error("Invalid enum declaration! Unable to produce 'default' enum without any variations!"); + + if (m_possibleValues.size() == 1) + { + // just return this entry anyway + return Value(this, { PRPInstruction(PRPOpCode::StringOrArray_E, PRPOperandVal(m_possibleValues[0].name)) }); + } + + auto lowest = std::min_element(m_possibleValues.begin(), m_possibleValues.end(), [](const Entry& a, const Entry& b) { return a.value < b.value; }); + return Value(this, { PRPInstruction(PRPOpCode::StringOrArray_E, PRPOperandVal(lowest->name)) }); + } + const TypeEnum::Entries &TypeEnum::getPossibleValues() const { return m_possibleValues; diff --git a/BMEdit/GameLib/Source/GameLib/TypeFactory.cpp b/BMEdit/GameLib/Source/GameLib/TypeFactory.cpp index 825e3b9..2b5f87e 100644 --- a/BMEdit/GameLib/Source/GameLib/TypeFactory.cpp +++ b/BMEdit/GameLib/Source/GameLib/TypeFactory.cpp @@ -33,12 +33,28 @@ namespace gamelib auto aliasTypeName = alias.get(); auto aliasTypeAsOpCode = prp::fromString(aliasTypeName); + std::unique_ptr finalType = nullptr; + if (OPCODE_VALID(aliasTypeAsOpCode)) { - return std::make_unique(typeName, aliasTypeAsOpCode); + finalType = std::make_unique(typeName, aliasTypeAsOpCode); + } + else + { + finalType = std::make_unique(typeName, aliasTypeName); + } + + // read tool hint + if (json.contains("editor")) + { + const auto& toolHint = json["editor"].get(); + if (!toolHint.empty()) + { + finalType->setToolHint(toolHint); + } } - return std::make_unique(typeName, aliasTypeName); + return finalType; } case TypeKind::COMPLEX: { std::vector properties = {}; diff --git a/BMEdit/GameLib/Source/GameLib/TypeRawData.cpp b/BMEdit/GameLib/Source/GameLib/TypeRawData.cpp index aebfa1a..42c7577 100644 --- a/BMEdit/GameLib/Source/GameLib/TypeRawData.cpp +++ b/BMEdit/GameLib/Source/GameLib/TypeRawData.cpp @@ -61,4 +61,11 @@ namespace gamelib std::vector data { instructions[0], instructions[1] }; return Type::DataMappingResult (Value(this, std::move(data)), instructions.slice(2, instructions.size() - 2)); } + + Value TypeRawData::makeDefaultPropertiesPack() const + { + // Produce an empty Container opcode + constexpr int32_t kEmptyContainerSize = 0; + return Value(this, { PRPInstruction(PRPOpCode::Container, PRPOperandVal(kEmptyContainerSize)) }); + } } \ No newline at end of file diff --git a/BMEdit/GameLib/Source/GameLib/TypeRegistry.cpp b/BMEdit/GameLib/Source/GameLib/TypeRegistry.cpp index e8727e1..3f9b90c 100644 --- a/BMEdit/GameLib/Source/GameLib/TypeRegistry.cpp +++ b/BMEdit/GameLib/Source/GameLib/TypeRegistry.cpp @@ -60,6 +60,159 @@ namespace gamelib linkTypes(); } + void produceOpCode(ScriptInfo& si, prp::PRPOpCode opCode) + { + const gamelib::prp::PRPOperandVal kNullOperand(0); // initialized with zero but it's not typed holder + prp::PRPInstruction instruction { opCode, kNullOperand }; + si.initialInstructions.emplace_back(instruction); + } + + bool collectParameters(const std::unordered_map& base, const nlohmann::json& current, ScriptInfo& info, int& instructionOffset) // NOLINT(*-no-recursion) + { + // Push current parameters on top + if (current.contains("parent")) + { + const std::string parentName = current["parent"].get(); + if (auto it = base.find(parentName); it != base.end()) + { + if (!collectParameters(base, it->second, info, instructionOffset)) + return false; + } + else + { + return false; + } + } + + // Copy parameters to front + if (auto parametersIt = current.find("parameters"); parametersIt != current.end()) + { + for (const auto& item : (*parametersIt)) + { + const std::string kind = item["kind"].get(); + + if (kind == "UNKNOWN") + { + // It's skip block. Just skip N instructions + if (!item.contains("opcodes")) + return false; // bad format + + const auto& opcodes = item["opcodes"]; + for (const auto& opcodeAsStr : opcodes) + { + if (auto opCode = prp::fromString(opcodeAsStr.get()); OPCODE_VALID(opCode)) + { + // store opcode as instruction into info + produceOpCode(info, opCode); + } + else + { + // WTF? + assert(false && "Bad opcode"); + return false; + } + } + + instructionOffset += static_cast(item["opcodes"].size()); + } + else if (kind == "VARIABLE") + { + if (!item.contains("typename") || !item.contains("name")) + return false; + + const std::string typeName = item["typename"].get(); + const std::string varName = item["name"].get(); + + if (typeName.empty() || varName.empty()) + return false; // invalid entry + + // build entry + ValueEntry entry {}; + entry.name = varName; + entry.instructions.iOffset = instructionOffset; + entry.instructions.iSize = 0; // Will calculate later + + // std::make_unique(typeName, aliasTypeAsOpCode); + if (auto opCode = prp::fromString(typeName); OPCODE_VALID(opCode)) + { + // presented as opcode + entry.views.emplace_back(varName, opCode, nullptr); // name, opcode, no owner type + entry.instructions.iSize = 1; // presented as single opcode + + // store opcode as instruction into info + produceOpCode(info, opCode); + } + else + { + // ok, let's try to find a type + if (const auto* pType = TypeRegistry::getInstance().findTypeByName(typeName)) + { + if (pType->getKind() == TypeKind::COMPLEX && reinterpret_cast(pType)->areUnexposedInstructionsAllowed()) + { + // Whoops, it's not allowed to be here! ZScriptC can not refs to ZScriptC! + return false; + } + + entry.views.emplace_back(varName, pType, nullptr); // name, known type, no owner type + entry.instructions.iSize = static_cast(pType->makeDefaultPropertiesPack().getInstructions().size()); // weird way but why not? + + // produce & copy + auto defInstru = pType->makeDefaultPropertiesPack().getInstructions(); + std::copy(defInstru.begin(), defInstru.end(), std::back_inserter(info.initialInstructions)); + } + else + { + // type not found + return false; + } + } + + // And continue work + if (entry.instructions.iSize > 0) + { + instructionOffset += static_cast(entry.instructions.iSize); + info.entries.insert(info.entries.end(), entry); + } // otherwise no reason to save this entry + } + } + } + + return true; + } + + void TypeRegistry::registerScripts(std::unordered_map &&scriptInfoMap) + { + for (const auto& [scriptId, scriptDef] : scriptInfoMap) + { + // Ok, here we need to check if we have parent scripts - we need to collect all parameters from that scripts + ScriptInfo info {}; + info.name = scriptId; + int ip = 0; // base is 0, for the future usage user must add base ip to another ip + if (!collectParameters(scriptInfoMap, scriptDef, info, ip)) + { + // maybe throw something? + continue; + } + + m_scriptsByName[scriptId] = info; + } + } + + std::optional TypeRegistry::getScriptInfo(const std::string &scriptName) + { + if (auto it = m_scriptsByName.find(scriptName); it != m_scriptsByName.end()) + { + return it->second; + } + + return std::nullopt; + } + + bool TypeRegistry::hasScriptInfo(const std::string &scriptName) + { + return m_scriptsByName.contains(scriptName); + } + const Type *TypeRegistry::findTypeByName(const std::string &typeName) const { auto it = m_typesByName.find(typeName); @@ -141,6 +294,19 @@ namespace gamelib } } + void TypeRegistry::forEachScript(const std::function &predicate) + { + if (!predicate) + { + return; + } + + for (const auto& [scriptName, scriptInfo] : m_scriptsByName) + { + predicate(scriptName, scriptInfo); + } + } + void TypeRegistry::linkTypes() { // Resolve links diff --git a/BMEdit/GameLib/Source/GameLib/Value.cpp b/BMEdit/GameLib/Source/GameLib/Value.cpp index cfda6b7..bff5490 100644 --- a/BMEdit/GameLib/Source/GameLib/Value.cpp +++ b/BMEdit/GameLib/Source/GameLib/Value.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -55,7 +56,15 @@ namespace gamelib return *this; } - Span Value::operator[](const char* token) + Value &Value::operator+=(const gamelib::ValueEntry &ent) + { + m_entries.emplace_back(ent); + std::copy(ent.views.begin(), ent.views.end(), std::back_inserter(m_views)); // TODO: Remove? + + return *this; + } + + Span Value::operator[](const char* token) const { if (m_entries.empty()) { @@ -155,4 +164,13 @@ namespace gamelib return false; } + + void Value::removeEntriesAndViewsSince(size_t startIndex) + { + if (startIndex < m_entries.size()) + m_entries.erase(m_entries.begin() + static_cast(startIndex), m_entries.end()); + + if (startIndex < m_views.size()) + m_views.erase(m_views.begin() + static_cast(startIndex), m_views.end()); + } } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 498a559..3a33180 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,16 +3,17 @@ project(BMEdit) set(CMAKE_CXX_STANDARD 20) -# --- Conan deps -if(EXISTS ${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) - include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) - conan_basic_setup() -else() - message(FATAL_ERROR "The file conanbuildinfo.cmake doesn't exist (lookup ${CMAKE_BINARY_DIR}), please, follow README.md \"Build\" for more details!") -endif() +# --- Deps +find_package(ZLIB REQUIRED HINTS ${CMAKE_BINARY_DIR}) +find_package(BZip2 REQUIRED HINTS ${CMAKE_BINARY_DIR}) +find_package(LibLZMA REQUIRED HINTS ${CMAKE_BINARY_DIR}) +find_package(zstd REQUIRED HINTS ${CMAKE_BINARY_DIR}) +find_package(libzip REQUIRED HINTS ${CMAKE_BINARY_DIR}) +find_package(libsquish REQUIRED HINTS ${CMAKE_BINARY_DIR}) # --- Global dependencies add_subdirectory(ThirdParty/fmt) +add_subdirectory(ThirdParty/glm) # --- Project modules add_subdirectory(BMEdit/Editor) @@ -41,12 +42,7 @@ if (WIN32) add_custom_command(TARGET BMEdit POST_BUILD COMMAND "${CMAKE_COMMAND}" -E env PATH="${_qt_bin_dir}" "${WINDEPLOYQT_EXECUTABLE}" - "$" -3dcore -3drenderer -3dinput -3danimation -3dextras -network + "$" --no-translations -network COMMENT "Running windeployqt...") - - # Copy binary dependencies - add_custom_command(TARGET BMEdit POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy "${CONAN_BIN_DIRS_LIBZIP}/zip.dll" "$" - COMMENT "Copy conan dependencies to bin") endif() # TODO: Support other OS later \ No newline at end of file diff --git a/README.md b/README.md index 7b2731a..e07dcba 100644 --- a/README.md +++ b/README.md @@ -18,22 +18,24 @@ Dependencies * [Nlohmann JSON](https://github.com/nlohmann/json) * [zlib](https://github.com/madler/zlib) * [conan](https://conan.io) (see "Build" for details) + * [Kenney Prototype Textures](https://www.kenney.nl/assets/prototype-textures) Build ----- First of all you need to install [conan](https://conan.io) dependencies manager on your system. -Then download (or git clone) this repository and do -``` -conan install . -s build_type=Debug --install-folder=cmake-build-debug -``` -or +**Note** Currently supported only Conan 2 (2.0.9 in my env) + +Download (or git clone) this repository and do ``` -conan install . -s build_type=Release --install-folder=cmake-build-release +conan profile detect --force +conan install . --output-folder=cmake-build-debug --build=missing -s build_type=Debug ``` -Reload cmake project in your IDE or +(replace `cmake-build-debug` to your ; replace Debug to Release for release build) + +Then reload cmake project in your IDE or ``` cd cmake --build . diff --git a/ThirdParty/glm b/ThirdParty/glm new file mode 160000 index 0000000..bf71a83 --- /dev/null +++ b/ThirdParty/glm @@ -0,0 +1 @@ +Subproject commit bf71a834948186f4097caa076cd2663c69a10e1e diff --git a/conanfile.txt b/conanfile.txt index 64362d5..7e2382c 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,8 +1,7 @@ [requires] +zlib/1.2.13 libzip/1.8.0 +libsquish/1.15 [generators] -cmake - -[options] -libzip:shared=True \ No newline at end of file +CMakeDeps \ No newline at end of file