diff --git a/.gitignore b/.gitignore index c841c68..48002ef 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ *.lib *.pdb *.log -*.txt *.idb *.pch *.tlog @@ -49,4 +48,5 @@ ModelBaker-test-* *.exe *.o *.obj -*.lst \ No newline at end of file +*.lst +*.code-workspace \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 8c595d7..65f4b41 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,9 @@ [submodule "Deps/json"] path = Deps/json url = https://github.com/nlohmann/json.git +[submodule "Deps/EASTL"] + path = Deps/EASTL + url = https://github.com/electronicarts/EASTL.git +[submodule "Deps/EABase"] + path = Deps/EABase + url = https://github.com/electronicarts/EABase.git diff --git a/BuildScripts/build_eastl.bat b/BuildScripts/build_eastl.bat new file mode 100644 index 0000000..15837b4 --- /dev/null +++ b/BuildScripts/build_eastl.bat @@ -0,0 +1,6 @@ +@echo off +cd %2 +set build_folder=%1 +mkdir %build_folder% +pushd %build_folder% +call cmake ../ %3 -DEASTL_BUILD_TESTS:BOOL=OFF -DEASTL_BUILD_BENCHMARK:BOOL=OFF \ No newline at end of file diff --git a/BuildScripts/project_eastl.lua b/BuildScripts/project_eastl.lua new file mode 100644 index 0000000..dbcbf2a --- /dev/null +++ b/BuildScripts/project_eastl.lua @@ -0,0 +1,61 @@ +local outputDir = "%{cfg.buildcfg}-%{cfg.system}-%{cfg.architecture}" +local loc = "/Deps/EASTL" +local locBase = "/Deps/EABase" + +local function readFile(strPath) + local f = io.open(strPath, "r") + assert( f ) + + local t = {} + for line in f:lines() do + t[#t+1] = line + end + + local src = table.concat(t, "\n") + f:close() + return src +end + +local function writeFile(strPath, strData) + local f = io.open(strPath, "w") + assert(f, "Failed to open file ".. strPath.. "!\n") + f:write(strData) + f:close() +end + +return function( root, platform ) + local args = "-G" + + if platform == "vs2019" then + args = args.. "Visual Studio 16 2019" + else + args = args.. "Visual Studio 15 2017 Win64" + end + + -- CMake blows + -- No way (that I can see) to force the static runtime on the command line + -- Rewrite the cmakelists file to do this for the targets we care about + local flags = [[set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd")]] + -- Lua also blows + local flagsEscaped = [[set%(CMAKE_CXX_FLAGS_RELEASE "%${CMAKE_CXX_FLAGS_RELEASE} /MT"%) +set%(CMAKE_CXX_FLAGS_DEBUG "%${CMAKE_CXX_FLAGS_DEBUG} /MTd"%)]] + + local data = readFile(root.. loc.. "/CMakeLists.txt") + if not data:find(flagsEscaped) then + data = data.. "\n".. flags + writeFile(root.. loc.. "/CMakeLists.txt", data) + end + + os.execute(("call \"%s\" \"%s\" \"%s\" \"%s\""):format( + "BuildScripts/build_eastl.bat", + root.. loc .. "/build", + root.. loc, + args + )) + externalproject "EASTL" + location(root.. loc .. "/build") + uuid "5BC9CEB8-8B4A-11D0-8D21-01A0C91FF942" + kind "StaticLib" + language "C++" +end \ No newline at end of file diff --git a/BuildScripts/project_polyhook.lua b/BuildScripts/project_polyhook.lua index 0c3e6d0..356031b 100644 --- a/BuildScripts/project_polyhook.lua +++ b/BuildScripts/project_polyhook.lua @@ -66,9 +66,8 @@ project "PolyHook2" functionlevellinking "Off" runtime "Release" omitframepointer "On" - flags { "MultiProcessorCompile" } flags { "LinkTimeOptimization", "NoBufferSecurityCheck", - "NoMinimalRebuild", "NoRuntimeChecks", + "NoMinimalRebuild", "NoRuntimeChecks", "MultiProcessorCompile" } \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index b968e7e..fdd62c5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,52 @@ +# Beta 1.4 +* Bumped module and MCM version number to 13 + +**Fixes:** +* Merge pull request #48 from ersh1/master (Compatibility fix for True Directional Movement). +* Github issue #31, Fixed FOV issues with mods like Undeath and Triumvirate (Wildshape) +* Github issue #35, directory enumeration no longer throws on unicode paths. +* Fixed distance clamping causing the camera to jump when strafing while using True Directional Movement. +* Fixed FOV offsets being applied in the map menu. +* Fixed an issue where a setting string might already be in the string cache from another mod, but in the wrong case. +* Fixed an issue that caused the normal game crosshair to disappear. +* Fixed new game not hooking D3D11 until after saving and loading. +* Fixed Improved Camera patch causing some issues - Now requires the offical reddit release version (beta4) for the compatibility fix to work - sorry, but I have to read a memory address inside Improved Camera to achieve a functional compatibility mode. Fixes distance clamping causing the camera to jump around when the player moves off-screen. +* Invalidate crosshair enable status when exiting a dialog menu. +* Camera is more aggressive about moving to the desired goal position on both camera state transitions and after loading a new cell. +* Now dynamically reading arrow tilt angle during runtime - Mods that change these values should work correctly now. + +**New Stuff:** +* Added an FOMOD installer and consolidated both versions in 1 download. +* Github issue request #30, Added a new key binding to cycle through saved presets (Keep in mind each saved preset needs this key bound, otherwise when you switch the loaded preset won't have your keybind saved). +* Github issue request #43, Added a new option and key binding to turn the camera on and off. +* Github issue request #44, Added a new setting and key binding to offset the camera height by a set amount via hotkey. +* Now factoring arrow draw time in the arrow trajectory computation. (Should help with inaccuracy reported when using the eagle eye perk) +* Added a new setting under the "General" page which allows you to force the camera state back to third-person, meant to fix rare instances where the camera gets stuck in a different state. +* Added a new setting under the "General" page which resets the crosshair back to sane settings, in an attempt to recover a missing crosshair reported by some users. +* Added new settings to offset the stealth meter/sneak crosshair by a configurable amount - either when the world-space crosshair is active, or at all times while sneaking. +* Crosshair now supports conjuration casting mode - crosshair changes color to a red tint when the player is unable to summon at the desired location. +* Now smoothing POV transition from third to first person. +* Added a custom crash handler which can be enabled via the MCM and will detect when a crash happens inside SmoothCam, creating a mini-dump file. If you have this happen, follow the instructions in the message box to report this issue and share the dump file so the crash may be investigated. +* Added EASTL as a source code/compile dependency. + +**Changes:** +* Renamed the "Info" page in the MCM to the "General" page. +* Removed the compatibility page from the MCM, moved compat options to the "General" page. +* Slightly altered the raycasting logic for camera collisions (Fixes camera collision jitter). +* Adjustments to arrow prediction math. +* Increased the allowed size range of the crosshair in the MCM. +* Explicitly looking for the arrow node for the raycast origin now - Accounts for some mods that attach other effects to the arrow before being fired (maybe). +* New external debug console, for printing useful information (in debug builds). +* Update gen_addrmap to look at member_fn_0..N macros. +* Added offset caching, we can now unload the offset DB and free up some more ram. +* Refactored camera into the low-level camera, and Thirdperson+Firstperson cameras owned by the low-level camera. +* Refactored camera states. +* Rewrote the papyrus preprocessor in D, using a more proper tokenizer system. +* Some reverse engineering of Skyrim's renderer, updates to d3d_context. +* Added a shader cache. +* New abstracted detour utilities. +* Updated build scripts. + ## Beta 1.3 * Bumped module and MCM version number to 12 diff --git a/CodeGen/MCM/SmoothCamMCM.pex b/CodeGen/MCM/SmoothCamMCM.pex deleted file mode 100644 index 4e5ce69..0000000 Binary files a/CodeGen/MCM/SmoothCamMCM.pex and /dev/null differ diff --git a/CodeGen/MCM/mcm.psc b/CodeGen/MCM/mcm.psc deleted file mode 100644 index 4a26cf3..0000000 --- a/CodeGen/MCM/mcm.psc +++ /dev/null @@ -1,1198 +0,0 @@ -ScriptName SmoothCamMCM extends SKI_ConfigBase -Import SKSE -; I hate everything about this -; This used to be way worse until I wrote the code generation tools - -string[] interpMethods -string[] presets -string[] crosshairTypes -string activePage - -Function SmoothCam_SetIntConfig(string member, int value) global native -Function SmoothCam_SetStringConfig(string member, string value) global native -Function SmoothCam_SetBoolConfig(string member, bool value) global native -Function SmoothCam_SetFloatConfig(string member, float value) global native -Function SmoothCam_ResetConfig() global native - -int Function SmoothCam_GetIntConfig(string member) global native -string Function SmoothCam_GetStringConfig(string member) global native -bool Function SmoothCam_GetBoolConfig(string member) global native -float Function SmoothCam_GetFloatConfig(string member) global native - -string Function SmoothCam_SaveAsPreset(int index, string name) global native -bool Function SmoothCam_LoadPreset(int index) global native -string Function SmoothCam_GetPresetNameAtIndex(int index) global native - -int Function GetCurrentArrayIndex(string setting, string[] array) - string value = SmoothCam_GetStringConfig(setting) - - Int i = array.Length - While i - i -= 1 - if (array[i] == value) - return i - endIf - endWhile - - return 0 -endFunction - -#constexpr_struct ScriptMeta { - int version = 0 -} - -#constexpr_struct SliderSetting { - real_int ref = 0 - float min = -100.0 - float max = 100.0 - float interval = 1.0 - float defaultValue = 0.0 - string settingName = "" - string displayName = "" - string desc = "" - string displayFormat = "{1}" - string page = "" - - MACRO implControl = { - this->ref = AddSliderOption(this->displayName, SmoothCam_GetFloatConfig(this->settingName), this->displayFormat) - } - - MACRO implOpenHandler = { - SetSliderDialogStartValue(SmoothCam_GetFloatConfig(this->settingName)) - SetSliderDialogDefaultValue(this->defaultValue) - SetSliderDialogRange(this->min, this->max) - SetSliderDialogInterval(this->interval) - } - - MACRO implAcceptHandler = { - SetSliderOptionValue(a_option, a_value, this->displayFormat) - SmoothCam_SetFloatConfig(this->settingName, a_value) - } - - MACRO implDesc = { - SetInfoText(this->desc) - } -} - -#constexpr_struct ToggleSetting { - real_int ref = 0 - string settingName = "" - string displayName = "" - string desc = "" - string page = "" - - MACRO implControl = { - this->ref = AddToggleOption(this->displayName, SmoothCam_GetBoolConfig(this->settingName)) - } - - MACRO implSelectHandler = { - SmoothCam_SetBoolConfig(this->settingName, !SmoothCam_GetBoolConfig(this->settingName)) - SetToggleOptionValue(a_option, SmoothCam_GetBoolConfig(this->settingName)) - } - - MACRO implDesc = { - SetInfoText(this->desc) - } -} - -#constexpr_struct ResetSetting { - real_int ref = 0 - string displayName = "" - string desc = "" - string page = "" - - MACRO implControl = { - this->ref = AddToggleOption(this->displayName, false) - } - - MACRO implSelectHandler = { - if (ShowMessage("Are you sure? This will reset all settings to their default values.")) - SmoothCam_ResetConfig() - ShowMessage("Settings reset.") - endIf - } - - MACRO implDesc = { - SetInfoText(this->desc) - } -} - -#constexpr_struct ListSetting { - real_int ref = 0 - string settingName = "" - string displayName = "" - string desc = "" - LITERAL arrayType = 0 - string page = "" - - MACRO implControl = { - this->ref = AddMenuOption(this->displayName, this->arrayType[GetCurrentArrayIndex(this->settingName, this->arrayType)]) - } - - MACRO implOpenHandler = { - SetMenuDialogStartIndex(GetCurrentArrayIndex(this->settingName, this->arrayType)) - SetMenuDialogDefaultIndex(0) - SetMenuDialogOptions(this->arrayType) - } - - MACRO implAcceptHandler = { - SetMenuOptionValue(a_option, this->arrayType[a_index]) - SmoothCam_SetStringConfig(this->settingName, this->arrayType[a_index]) - } - - MACRO implDesc = { - SetInfoText(this->desc) - } -} - -#constexpr_struct SavePresetSetting { - real_int ref = 0 - int presetIndex = 0 - string displayName = "" - string desc = "Save your current settings to this preset slot" - string page = "" - - MACRO implControl = { - this->ref = AddInputOption(this->displayName + " " + SmoothCam_GetPresetNameAtIndex(this->presetIndex), SmoothCam_GetPresetNameAtIndex(this->presetIndex)) - } - - MACRO implOpenHandler = { - string value = SmoothCam_GetPresetNameAtIndex(this->presetIndex) - if (value == "Empty") - SetInputDialogStartText("Enter a preset name") - else - SetInputDialogStartText(value) - endIf - } - - MACRO implAcceptHandler = { - if (SmoothCam_SaveAsPreset(this->presetIndex, a_input) == "") - ShowMessage("Preset saved.", false) - else - ShowMessage("Failed to save preset!", false) - endIf - ForcePageReset() - } - - MACRO implDesc = { - SetInfoText(this->desc) - } -} - -#constexpr_struct LoadPresetSetting { - real_int ref = 0 - int presetIndex = 0 - string displayName = "" - string desc = "Load this preset" - string page = "" - - MACRO implControl = { - this->ref = AddToggleOption(this->displayName + " " + SmoothCam_GetPresetNameAtIndex(this->presetIndex), false) - } - - MACRO implSelectHandler = { - if (!SmoothCam_LoadPreset(this->presetIndex)) - ShowMessage("Failed to load preset! Have you saved this slot yet?", false) - else - ShowMessage("Preset loaded.", false) - ForcePageReset() - endIf - } - - MACRO implDesc = { - SetInfoText(this->desc) - } -} - -#constexpr_struct KeyBindSetting { - real_int ref = 0 - string settingName = "" - string displayName = "" - string desc = "" - string page = "" - - MACRO implControl = { - this->ref = AddKeyMapOption(this->displayName, SmoothCam_GetIntConfig(this->settingName)) - } - - MACRO implSelectHandler = { - if (StringUtil.GetLength(a_conflictControl) == 0) - SmoothCam_SetIntConfig(this->settingName, a_keyCode) - SetKeyMapOptionValue(this->ref, a_keyCode) - else - if (ShowMessage(a_conflictControl + " conflicts with another control assigned to " + a_conflictName + ".\nAre you sure you want to assign this control?")) - SmoothCam_SetIntConfig(this->settingName, a_keyCode) - SetKeyMapOptionValue(this->ref, a_keyCode) - endIf - endIf - } - - MACRO implDesc = { - SetInfoText(this->desc) - } -} - -ScriptMeta scriptMetaInfo -> { - version: 12 -} - -; Presets -SavePresetSetting savePresetSlot1 -> { - presetIndex: 0 - displayName: "Slot 1:" - page: " Presets" -} -SavePresetSetting savePresetSlot2 -> { - presetIndex: 1 - displayName: "Slot 2:" - page: " Presets" -} -SavePresetSetting savePresetSlot3 -> { - presetIndex: 2 - displayName: "Slot 3:" - page: " Presets" -} -SavePresetSetting savePresetSlot4 -> { - presetIndex: 3 - displayName: "Slot 4:" - page: " Presets" -} -SavePresetSetting savePresetSlot5 -> { - presetIndex: 4 - displayName: "Slot 5:" - page: " Presets" -} -SavePresetSetting savePresetSlot6 -> { - presetIndex: 5 - displayName: "Slot 6:" - page: " Presets" -} - -LoadPresetSetting loadPresetSlot1 -> { - presetIndex: 0 - displayName: "Slot 1:" - page: " Presets" -} -LoadPresetSetting loadPresetSlot2 -> { - presetIndex: 1 - displayName: "Slot 2:" - page: " Presets" -} -LoadPresetSetting loadPresetSlot3 -> { - presetIndex: 2 - displayName: "Slot 3:" - page: " Presets" -} -LoadPresetSetting loadPresetSlot4 -> { - presetIndex: 3 - displayName: "Slot 4:" - page: " Presets" -} -LoadPresetSetting loadPresetSlot5 -> { - presetIndex: 4 - displayName: "Slot 5:" - page: " Presets" -} -LoadPresetSetting loadPresetSlot6 -> { - presetIndex: 5 - displayName: "Slot 6:" - page: " Presets" -} - -; Compat -ToggleSetting icCompat -> { - settingName: "ICCompat" - displayName: "Improved Camera" - desc: "Enable compat fixes for Improved Camera." - page: " Compatibility" -} -ToggleSetting accCompat -> { - settingName: "ACCCompat" - displayName: "Alternate Conversation Camera" - desc: "Enable compat fixes for Alternate Conversation Camera." - page: " Compatibility" -} -ToggleSetting ifpvCompat -> { - settingName: "IFPVCompat" - displayName: "Immersive First Person View" - desc: "Enable compat fixes for Improved First Person View." - page: " Compatibility" -} -ToggleSetting agoCompat -> { - settingName: "AGOCompat" - displayName: "Archery Gameplay Overhaul" - desc: "Enable compat fixes for AGO when using the 3D crosshair/prediction arc." - page: " Compatibility" -} - -; Reset -ResetSetting reset -> { - displayName: "Reset All Settings" - desc: "Set all settings back to their defual values." - page: " Following" -} - -; Following -ToggleSetting interpEnabled -> { - settingName: "InterpolationEnabled" - displayName: "Interpolation Enabled" - desc: "Enable camera smoothing." - page: " Following" -} -ToggleSetting sepZInterpEnabled -> { - settingName: "SeparateZInterpEnabled" - displayName: "Separate Z Interpolation Enabled" - desc: "Enable the separate Z smoothing settings for smoothing camera height differently." - page: " Following" -} -ToggleSetting sepLocalInterpEnabled -> { - settingName: "SeparateLocalInterpolation" - displayName: "Local-Space Interpolation Enabled" - desc: "Enable separate local-space camera smoothing (Camera rotation)." - page: " Following" -} -ToggleSetting offsetInterpEnabled -> { - settingName: "OffsetTransitionEnabled" - displayName: "Offset Interpolation Enabled" - desc: "Enable smoothing of camera offset state transitions." - page: " Following" -} -ToggleSetting zoomInterpEnabled -> { - settingName: "ZoomTransitionEnabled" - displayName: "Zoom Interpolation Enabled" - desc: "Enable smoothing of camera zoom state transitions." - page: " Following" -} -ToggleSetting fovInterpEnabled -> { - settingName: "FOVTransitionEnabled" - displayName: "FOV Interpolation Enabled" - desc: "Enable smoothing of camera FOV state transitions." - page: " Following" -} -ToggleSetting disableDeltaTime -> { - settingName: "DisableDeltaTime" - displayName: "Disable Delta Time Factoring" - desc: "Remove time from interpolation math. May result in less jitter but can cause speed to vary with frame rate." - page: " Following" -} -ToggleSetting cameraDistanceClampXEnable -> { - settingName: "CameraDistanceClampXEnable" - displayName: "Enable X Distance Clamp" - desc: "Clamp the maximum distance the camera may move away from the target position along the X (side) axis." - page: " Following" -} -ToggleSetting cameraDistanceClampYEnable -> { - settingName: "CameraDistanceClampYEnable" - displayName: "Enable Y Distance Clamp" - desc: "Clamp the maximum distance the camera may move away from the target position along the Y (forward) axis." - page: " Following" -} -ToggleSetting cameraDistanceClampZEnable -> { - settingName: "CameraDistanceClampZEnable" - displayName: "Enable Z Distance Clamp" - desc: "Clamp the maximum distance the camera may move away from the target position along the Z (up) axis." - page: " Following" -} -ToggleSetting swapDistanceClampXAxis -> { - settingName: "ShoulderSwapXClamping" - displayName: "Also Swap X Axis Clamping" - desc: "When shoulder swapping, will also swap the distance clamping X axis range." - page: " Following" -} - -ListSetting interpMethod -> { - settingName: "InterpolationMethod" - displayName: "Interpolation Method" - desc: "The scalar method to use for camera smoothing." - arrayType: interpMethods - page: " Following" -} -ListSetting sepZInterpMethod -> { - settingName: "SeparateZInterpMethod" - displayName: "Sep. Z Interpolation Method" - desc: "The scalar method to use for smoothing the camera height (If enabled)." - arrayType: interpMethods - page: " Following" -} -ListSetting sepLocalInterpMethod -> { - settingName: "SepLocalInterpMethod" - displayName: "Local-Space Interpolation Method" - desc: "The scalar method to use for local-space smoothing (If enabled)." - arrayType: interpMethods - page: " Following" -} -ListSetting offsetInterpMethod -> { - settingName: "OffsetTransitionMethod" - displayName: "Offset Interpolation Method" - desc: "The scalar method to use for offset transition smoothing (If enabled)." - arrayType: interpMethods - page: " Following" -} -ListSetting zoomInterpMethod -> { - settingName: "ZoomTransitionMethod" - displayName: "Zoom Interpolation Method" - desc: "The scalar method to use for zoom transition smoothing (If enabled)." - arrayType: interpMethods - page: " Following" -} -ListSetting fovInterpMethod -> { - settingName: "FOVTransitionMethod" - displayName: "FOV Interpolation Method" - desc: "The scalar method to use for FOV transition smoothing (If enabled)." - arrayType: interpMethods - page: " Following" -} - -SliderSetting minCameraFollowRate -> { - settingName: "MinCameraFollowRate" - displayName: "Min Follow Rate" - desc: "The smoothing rate to use when the camera is close to the player." - defaultValue: 0.5 - interval: 0.01 - min: 0.01 - max: 1.0 - displayFormat: "{2}" - page: " Following" -} -SliderSetting maxCameraFollowRate -> { - settingName: "MaxCameraFollowRate" - displayName: "Max Follow Rate" - desc: "The smoothing rate to use when the camera is far away from the player." - defaultValue: 0.8 - interval: 0.01 - min: 0.01 - max: 1.0 - displayFormat: "{2}" - page: " Following" -} -SliderSetting maxSmoothingInterpDistance -> { - settingName: "MaxSmoothingInterpDistance" - displayName: "Max Interpolation Distance" - desc: "The distance at which the max follow rate value is used for smoothing. Below this value a mix of min and max is used." - defaultValue: 650.0 - interval: 1.0 - min: 1.0 - max: 650.0 - displayFormat: "{0}" - page: " Following" -} -SliderSetting minCameraFollowDistance -> { - settingName: "MinFollowDistance" - displayName: "Min Follow Distance" - desc: "The closest the camera may get to the player when at the lowest zoom level." - defaultValue: 64.0 - interval: 0.1 - min: 0.0 - max: 256.0 - page: " Following" -} -SliderSetting zoomMul -> { - settingName: "ZoomMul" - displayName: "Zoom Multiplier" - desc: "The amount of distance to add to the camera for each zoom level." - defaultValue: 500.0 - interval: 1.0 - min: 1.0 - max: 500.0 - page: " Following" -} -SliderSetting minSepZFollowRate -> { - settingName: "SepZMinFollowRate" - displayName: "Separate Z Min Follow Rate" - desc: "The smoothing rate to use when the camera is close to the player." - defaultValue: 0.5 - interval: 0.01 - min: 0.01 - max: 1.0 - displayFormat: "{2}" - page: " Following" -} -SliderSetting maxSepZFollowRate -> { - settingName: "SepZMaxFollowRate" - displayName: "Separate Z Max Follow Rate" - desc: "The smoothing rate to use when the camera is far away from the player." - defaultValue: 0.5 - interval: 0.01 - min: 0.01 - max: 1.0 - displayFormat: "{2}" - page: " Following" -} -SliderSetting maxSepZSmoothingDistance -> { - settingName: "SepZMaxInterpDistance" - displayName: "Sep. Z Max Interpolation Distance" - desc: "The distance at which the max follow rate value is used for smoothing. Below this value a mix of min and max is used." - defaultValue: 100.0 - interval: 1.0 - min: 1.0 - max: 300.0 - displayFormat: "{0}" - page: " Following" -} -SliderSetting offsetTransitionDuration -> { - settingName: "OffsetTransitionDuration" - displayName: "Offset Interpolation Duration" - desc: "The smoothing duration to use when the camera changes offsets (In seconds)." - defaultValue: 1.0 - interval: 0.01 - min: 0.01 - max: 5.0 - displayFormat: "{2}" - page: " Following" -} -SliderSetting zoomTransitionDuration -> { - settingName: "ZoomTransitionDuration" - displayName: "Zoom Interpolation Duration" - desc: "The smoothing duration to use when the camera changes zoom distance (In seconds)." - defaultValue: 0.2 - interval: 0.01 - min: 0.01 - max: 5.0 - displayFormat: "{2}" - page: " Following" -} -SliderSetting fovTransitionDuration -> { - settingName: "FOVTransitionDuration" - displayName: "FOV Interpolation Duration" - desc: "The smoothing duration to use when the camera changes FOV (In seconds)." - defaultValue: 0.2 - interval: 0.01 - min: 0.01 - max: 5.0 - displayFormat: "{2}" - page: " Following" -} -SliderSetting cameraDistanceClampXMin -> { - settingName: "CameraDistanceClampXMin" - displayName: "Distance Clamp X Min" - desc: "The minimal distance the camera may get from the target position along the X axis before being clamped." - defaultValue: -75.0 - interval: 1.0 - min: -300.0 - max: 0.0 - displayFormat: "{0}" - page: " Following" -} -SliderSetting cameraDistanceClampXMax -> { - settingName: "CameraDistanceClampXMax" - displayName: "Distance Clamp X Max" - desc: "The maximal distance the camera may get from the target position along the X axis before being clamped." - defaultValue: 75.0 - interval: 1.0 - min: 0.0 - max: 300.0 - displayFormat: "{0}" - page: " Following" -} -SliderSetting cameraDistanceClampYMin -> { - settingName: "CameraDistanceClampYMin" - displayName: "Distance Clamp Y Min" - desc: "The minimal distance the camera may get from the target position along the Y axis before being clamped." - defaultValue: -100.0 - interval: 1.0 - min: -500.0 - max: 0.0 - displayFormat: "{0}" - page: " Following" -} -SliderSetting cameraDistanceClampYMax -> { - settingName: "CameraDistanceClampYMax" - displayName: "Distance Clamp Y Max" - desc: "The maximal distance the camera may get from the target position along the Y axis before being clamped." - defaultValue: 100.0 - interval: 1.0 - min: 0.0 - max: 500.0 - displayFormat: "{0}" - page: " Following" -} -SliderSetting cameraDistanceClampZMin -> { - settingName: "CameraDistanceClampZMin" - displayName: "Distance Clamp Z Min" - desc: "The minimal distance the camera may get from the target position along the Z axis before being clamped." - defaultValue: -50.0 - interval: 1.0 - min: -300.0 - max: 0.0 - displayFormat: "{0}" - page: " Following" -} -SliderSetting cameraDistanceClampZMax -> { - settingName: "CameraDistanceClampZMax" - displayName: "Distance Clamp Z Max" - desc: "The maximal distance the camera may get from the target position along the Z axis before being clamped." - defaultValue: 50.0 - interval: 1.0 - min: 0.0 - max: 300.0 - displayFormat: "{0}" - page: " Following" -} -SliderSetting sepLocalSpaceInterpRate -> { - settingName: "SepLocalInterpRate" - displayName: "Local-Space Follow Rate" - desc: "The smoothing rate to use for local-space smoothing." - defaultValue: 0.5 - interval: 0.01 - min: 0.01 - max: 1.0 - displayFormat: "{2}" - page: " Following" -} - -KeyBindSetting shoulderSwapKey -> { - settingName: "ShoulderSwapKeyCode" - displayName: "Shoulder Swap Key" - desc: "Inverts the current X offset of the camera" - page: " Following" -} - -; Crosshair -ToggleSetting crosshair3DBowEnabled -> { - settingName: "Enable3DBowCrosshair" - displayName: "3D Bow Crosshair Enabled" - desc: "Enable the raycasted 3D crosshair when aiming with the bow." - page: " Crosshair" -} -ToggleSetting crosshair3DMagicEnabled -> { - settingName: "Enable3DMagicCrosshair" - displayName: "3D Magic Crosshair Enabled" - desc: "Enable the raycasted 3D crosshair when using magic." - page: " Crosshair" -} -ToggleSetting crosshair3DWorldEnabled -> { - settingName: "UseWorldCrosshair" - displayName: "Use World-Space Crosshair" - desc: "When your crosshair ray has hit something, use a crosshair mesh rendered in-world, not on the HUD." - page: " Crosshair" -} -ListSetting worldCrosshairType -> { - settingName: "WorldCrosshairType" - displayName: "Crosshair Type" - desc: "Select the style of world-space crosshair to use, if world-space crosshair is enabled." - arrayType: crosshairTypes - page: " Crosshair" -} -ToggleSetting worldCrosshairDepthTest -> { - settingName: "WorldCrosshairDepthTest" - displayName: "Crosshair Occlusion" - desc: "When using the world-space crosshair, disable this option to make it draw on top of everything rather than allow other geometry to cover it." - page: " Crosshair" -} -ToggleSetting hideCrosshairOutOfCombat -> { - settingName: "HideCrosshairOutOfCombat" - displayName: "Hide Non-Combat Crosshair" - desc: "Hide the crosshair when not in combat." - page: " Crosshair" -} -ToggleSetting hideCrosshairMeleeCombat -> { - settingName: "HideCrosshairMeleeCombat" - displayName: "Hide Melee Combat Crosshair" - desc: "Hide the crosshair when in melee combat." - page: " Crosshair" -} -ToggleSetting enableCrosshairSizeManip -> { - settingName: "EnableCrosshairSizeManip" - displayName: "Enable Size Manipulation" - desc: "Changes the size of the crosshair, based on 'Min/Max Size' and 'NPC Hit Size'. Disable if this causes conflicts with other mods. May require a game restart." - page: " Crosshair" -} -SliderSetting crosshairNPCGrowSize -> { - settingName: "CrosshairNPCGrowSize" - displayName: "NPC Hit Size" - desc: "When the 3D crosshair is over an NPC, grow the size of the crosshair by this amount." - defaultValue: 16 - interval: 1 - min: 0 - max: 32 - page: " Crosshair" -} -SliderSetting crosshairMinDistSize -> { - settingName: "CrosshairMinDistSize" - displayName: "Min Crosshair Size" - desc: "Sets the size of the 3D crosshair when the player's aim ray is at the maximum distance." - defaultValue: 16 - interval: 1 - min: 8 - max: 32 - page: " Crosshair" -} -SliderSetting crosshairMaxDistSize -> { - settingName: "CrosshairMaxDistSize" - displayName: "Max Crosshair Size" - desc: "Sets the size of the 3D crosshair when the player's aim ray is at the minimum distance." - defaultValue: 24 - interval: 1 - min: 8 - max: 32 - page: " Crosshair" -} -ToggleSetting enableArrowPrediction -> { - settingName: "EnableArrowPrediction" - displayName: "Enable Arrow Prediction" - desc: "When the 3D crosshair is enabled for ranged combat, the crosshair will account for gravity when aiming with a bow." - page: " Crosshair" -} -ToggleSetting drawArrowArc -> { - settingName: "DrawArrowArc" - displayName: "Draw Arrow Prediction Arc" - desc: "When the 3D crosshair is enabled for ranged combat and 'Enable Arrow Prediction' is selected, an arc will be drawn while aiming with bows which indicates the flight path your arrow will take." - page: " Crosshair" -} -SliderSetting maxArrowPredictionRange -> { - settingName: "MaxArrowPredictionRange" - displayName: "Max Arrow Prediction Distance" - desc: "The furthest distance to allow arrow prediction, if enabled, to function." - defaultValue: 10000 - interval: 1 - min: 500 - max: 12000 - displayFormat: "{0}" - page: " Crosshair" -} -SliderSetting arrowArcColorR -> { - settingName: "ArrowArcColorR" - displayName: "Arrow Arc Color: Red" - desc: "The amount of red coloration to add to the arrow arc." - defaultValue: 255.0 - interval: 1 - min: 0.0 - max: 255.0 - displayFormat: "{0}" - page: " Crosshair" -} -SliderSetting arrowArcColorG -> { - settingName: "ArrowArcColorG" - displayName: "Arrow Arc Color: Green" - desc: "The amount of green coloration to add to the arrow arc." - defaultValue: 255.0 - interval: 1 - min: 0.0 - max: 255.0 - displayFormat: "{0}" - page: " Crosshair" -} -SliderSetting arrowArcColorB -> { - settingName: "ArrowArcColorB" - displayName: "Arrow Arc Color: Blue" - desc: "The amount of blue coloration to add to the arrow arc." - defaultValue: 255.0 - interval: 1 - min: 0.0 - max: 255.0 - displayFormat: "{0}" - page: " Crosshair" -} -SliderSetting arrowArcColorA -> { - settingName: "ArrowArcColorA" - displayName: "Arrow Arc Color: Transparency" - desc: "The amount of transparency coloration to add to the arrow arc." - defaultValue: 127.0 - interval: 1 - min: 0.0 - max: 255.0 - displayFormat: "{0}" - page: " Crosshair" -} - - -; Standing -DECLARE_OFFSET_GROUP_CONTROLS(Standing, " Standing") -; Walking -DECLARE_OFFSET_GROUP_CONTROLS(Walking, " Walking") -; Running -DECLARE_OFFSET_GROUP_CONTROLS(Running, " Running") -; Sprinting -DECLARE_OFFSET_GROUP_CONTROLS(Sprinting, " Sprinting") -; Sneaking -DECLARE_OFFSET_GROUP_CONTROLS(Sneaking, " Sneaking") -; Swimming -DECLARE_OFFSET_GROUP_CONTROLS(Swimming, " Swimming", NoMelee, NoRanged, NoMagic) -; Sitting -DECLARE_OFFSET_GROUP_CONTROLS(Sitting, " Sitting", NoMelee, NoRanged, NoMagic) -; Horseback -DECLARE_OFFSET_GROUP_CONTROLS(Horseback, " Horseback") -; Dragon -DECLARE_OFFSET_GROUP_CONTROLS(Dragon, " Dragon", NoMelee, NoRanged, NoMagic) -; Vampire Lord -DECLARE_OFFSET_GROUP_CONTROLS(VampireLord, " Vampire Lord", NoRanged) -; Werewolf -DECLARE_OFFSET_GROUP_CONTROLS(Werewolf, " Werewolf", NoRanged, NoMagic) -; Group edit -DECLARE_OFFSET_GROUP_CONTROLS(Group, " Group Edit", NoInterpToggles) - -; Bow aiming -SliderSetting bowaim_sideOffset -> { - settingName: "Bowaim:SideOffset" - displayName: "Side Offset" - desc: "The amount to move the camera to the right." - defaultValue: 25.0 - page: " Bow Aiming" -} -SliderSetting bowaim_upOffset -> { - settingName: "Bowaim:UpOffset" - displayName: "Up Offset" - desc: "The amount to move the camera up." - page: " Bow Aiming" -} -SliderSetting bowaim_zoomOffset -> { - settingName: "Bowaim:ZoomOffset" - displayName: "Zoom Offset" - desc: "The amount to offset the camera zoom by." - min: -200.0 - max: 200.0 - page: " Bow Aiming" -} -SliderSetting bowaim_fovOffset -> { - settingName: "Bowaim:FOVOffset" - displayName: "FOV Offset" - desc: "The amount to offset the camera FOV by. Note this will be clamped to a lower bound of 10 and an upper bound of 170." - min: -120.0 - max: 120.0 - page: " Bow Aiming" -} -SliderSetting bowaim_sideOffsetHorseback -> { - settingName: "BowaimHorse:SideOffset" - displayName: "Horseback Side Offset" - desc: "The amount to move the camera to the right when on horseback." - defaultValue: 25.0 - page: " Bow Aiming" -} -SliderSetting bowaim_upOffsetHorseback -> { - settingName: "BowaimHorse:UpOffset" - displayName: "Horseback Up Offset" - desc: "The amount to move the camera up when on horseback." - page: " Bow Aiming" -} -SliderSetting bowaim_zoomOffsetHorseback -> { - settingName: "BowaimHorse:ZoomOffset" - displayName: "Horseback Zoom Offset" - desc: "The amount to offset the camera zoom by when on horseback." - min: -200.0 - max: 200.0 - page: " Bow Aiming" -} -SliderSetting bowaim_fovOffsetHorseback -> { - settingName: "BowaimHorse:FOVOffset" - displayName: "Horseback FOV Offset" - desc: "The amount to offset the camera FOV by when on horseback. Note this will be clamped to a lower bound of 10 and an upper bound of 170." - min: -120.0 - max: 120.0 - page: " Bow Aiming" -} -SliderSetting bowaim_sideOffsetSneaking -> { - settingName: "BowaimSneak:SideOffset" - displayName: "Sneaking Side Offset" - desc: "The amount to move the camera to the right when sneaking." - defaultValue: 25.0 - page: " Bow Aiming" -} -SliderSetting bowaim_upOffsetSneaking -> { - settingName: "BowaimSneak:UpOffset" - displayName: "Sneaking Up Offset" - desc: "The amount to move the camera up when sneaking." - page: " Bow Aiming" -} -SliderSetting bowaim_zoomOffsetSneaking -> { - settingName: "BowaimSneak:ZoomOffset" - displayName: "Sneaking Zoom Offset" - desc: "The amount to offset the camera zoom by when sneaking." - min: -200.0 - max: 200.0 - page: " Bow Aiming" -} -SliderSetting bowaim_fovOffsetSneaking -> { - settingName: "BowaimSneak:FOVOffset" - displayName: "Sneaking FOV Offset" - desc: "The amount to offset the camera FOV by when sneaking. Note this will be clamped to a lower bound of 10 and an upper bound of 170." - min: -120.0 - max: 120.0 - page: " Bow Aiming" -} -ToggleSetting bowaim_interp -> { - settingName: "InterpBowAim" - displayName: "Enable Interpolation" - desc: "Enables interpolation in this state." - page: " Bow Aiming" -} -ToggleSetting bowaim_interpHorseback -> { - settingName: "InterpBowAimHorseback" - displayName: "Enable Horseback Interpolation" - desc: "Enables interpolation in this state." - page: " Bow Aiming" -} -ToggleSetting bowaim_interpSneaking -> { - settingName: "InterpBowAimSneaking" - displayName: "Enable Sneaking Interpolation" - desc: "Enables interpolation in this state." - page: " Bow Aiming" -} - -int Function GetVersion() - return scriptMetaInfo.version -endFunction - -event OnConfigInit() - Pages = new string[] -> { - " Info", " Compatibility", " Following", " Crosshair", " Standing", - " Walking", " Running", " Sprinting", " Sneaking", - " Swimming", " Bow Aiming", " Sitting", " Horseback", - " Dragon", " Vampire Lord", " Werewolf", " Group Edit", " Presets" - } - interpMethods = new string[] -> { - "linear", "quadraticEaseIn", "quadraticEaseOut", - "quadraticEaseInOut", "cubicEaseIn", "cubicEaseOut", - "cubicEaseInOut", "quarticEaseIn", "quarticEaseOut", - "quarticEaseInOut", "quinticEaseIn", "quinticEaseOut", - "quinticEaseInOut", "sineEaseIn", "sineEaseOut", - "sineEaseInOut", "circularEaseIn", "circularEaseOut", - "circularEaseInOut", "exponentialEaseIn", "exponentialEaseOut", - "exponentialEaseInOut" - } - crosshairTypes = new string[] -> { - "Skyrim", "Dot" - } -endEvent - -event OnVersionUpdate(int version) - OnConfigInit() -endEvent - -event OnPageReset(string a_page) - SetCursorFillMode(TOP_TO_BOTTOM) - activePage = a_page - - if (a_page == " Info") - int version_T = AddTextOption("DLL Version", GetPluginVersion("SmoothCam"), OPTION_FLAG_DISABLED) - int s_version_T = AddTextOption("MCM Script Version", scriptMetaInfo.version, OPTION_FLAG_DISABLED) - int hasDXContext = AddTextOption("D3D11 Hooked", SmoothCam_GetBoolConfig("D3DHooked"), OPTION_FLAG_DISABLED) - - elseIf (a_page == " Compatibility") - AddHeaderOption("General") - accCompat->!implControl - icCompat->!implControl - ifpvCompat->!implControl - agoCompat->!implControl - - elseIf (a_page == " Following") - AddHeaderOption("Interpolation") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - interpMethod, interpEnabled, minCameraFollowDistance, minCameraFollowRate, maxCameraFollowRate, maxSmoothingInterpDistance - }) - - AddHeaderOption("Separate Z Interpolation") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - sepZInterpMethod, sepZInterpEnabled, minSepZFollowRate, maxSepZFollowRate, maxSepZSmoothingDistance - }) - - AddHeaderOption("Local-Space Interpolation") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - sepLocalInterpEnabled, sepLocalInterpMethod, sepLocalSpaceInterpRate - }) - - AddHeaderOption("Offset Interpolation") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - offsetInterpEnabled, offsetInterpMethod, offsetTransitionDuration - }) - - AddHeaderOption("Zoom Interpolation") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - zoomInterpEnabled, zoomInterpMethod, zoomTransitionDuration - }) - - AddHeaderOption("FOV Interpolation") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - fovInterpEnabled, fovInterpMethod, fovTransitionDuration - }) - - SetCursorPosition(1) - AddHeaderOption("Distance Clamping") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - cameraDistanceClampXEnable, cameraDistanceClampXMin, cameraDistanceClampXMax, - cameraDistanceClampYEnable, cameraDistanceClampYMin, cameraDistanceClampYMax, - cameraDistanceClampZEnable, cameraDistanceClampZMin, cameraDistanceClampZMax - }) - - AddHeaderOption("Misc") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - shoulderSwapKey, swapDistanceClampXAxis, zoomMul, disableDeltaTime, reset - }) - - elseIf (a_page == " Crosshair") - AddHeaderOption("3D Crosshair Settings") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - crosshair3DBowEnabled, crosshair3DMagicEnabled, - crosshair3DWorldEnabled, worldCrosshairType, worldCrosshairDepthTest, - enableCrosshairSizeManip, crosshairMinDistSize, - crosshairMaxDistSize, crosshairNPCGrowSize - }) - - AddHeaderOption("Crosshair Hiding") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - hideCrosshairOutOfCombat, hideCrosshairMeleeCombat, - }) - - SetCursorPosition(1) - AddHeaderOption("Archery Features") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - enableArrowPrediction, drawArrowArc, maxArrowPredictionRange, - arrowArcColorR, arrowArcColorG, arrowArcColorB, - arrowArcColorA, - }) - - elseIf (a_page == " Standing") - IMPL_OFFSET_GROUP_PAGE(Standing) - - elseIf (a_page == " Walking") - IMPL_OFFSET_GROUP_PAGE(Walking) - - elseIf (a_page == " Running") - IMPL_OFFSET_GROUP_PAGE(Running) - - elseIf (a_page == " Sprinting") - IMPL_OFFSET_GROUP_PAGE(Sprinting) - - elseIf (a_page == " Sneaking") - IMPL_OFFSET_GROUP_PAGE(Sneaking) - - elseIf (a_page == " Swimming") - IMPL_OFFSET_GROUP_PAGE(Swimming) - - elseIf (a_page == " Bow Aiming") - AddHeaderOption("Bow Aiming Offsets") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - bowaim_sideOffset, - bowaim_upOffset, - bowaim_zoomOffset, - bowaim_fovOffset, - bowaim_sideOffsetHorseback, - bowaim_upOffsetHorseback, - bowaim_zoomOffsetHorseback, - bowaim_fovOffsetHorseback, - bowaim_sideOffsetSneaking, - bowaim_upOffsetSneaking, - bowaim_zoomOffsetSneaking, - bowaim_fovOffsetSneaking - }) - - SetCursorPosition(1) - AddHeaderOption("Interpolation") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - bowaim_interp, - bowaim_interpHorseback, - bowaim_interpSneaking - }) - - elseIf (a_page == " Sitting") - IMPL_OFFSET_GROUP_PAGE(Sitting) - - elseIf (a_page == " Horseback") - IMPL_OFFSET_GROUP_PAGE(Horseback) - - elseIf (a_page == " Dragon") - IMPL_OFFSET_GROUP_PAGE(Dragon) - - elseIf (a_page == " Vampire Lord") - IMPL_OFFSET_GROUP_PAGE(VampireLord) - - elseIf (a_page == " Werewolf") - IMPL_OFFSET_GROUP_PAGE(Werewolf) - - elseIf (a_page == " Group Edit") - AddHeaderOption("Edit All Offset Groups") - IMPL_OFFSET_GROUP_PAGE(Group, NoSliderHeader, NoInterpToggles) - - elseIf (a_page == " Presets") - AddHeaderOption("Save Preset") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - savePresetSlot1, - savePresetSlot2, - savePresetSlot3, - savePresetSlot4, - savePresetSlot5, - savePresetSlot6 - }) - - SetCursorPosition(1) - AddHeaderOption("Load Preset") - IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, { - loadPresetSlot1, - loadPresetSlot2, - loadPresetSlot3, - loadPresetSlot4, - loadPresetSlot5, - loadPresetSlot6 - }) - - endIf -endEvent - -event OnOptionSelect(int a_option) - IMPL_IFCHAIN_MACRO_INVOKE(a_option, ref, implSelectHandler, { - IMPL_ALL_IMPLS_OF_STRUCT(ToggleSetting), - IMPL_ALL_IMPLS_OF_STRUCT(ResetSetting), - IMPL_ALL_IMPLS_OF_STRUCT(LoadPresetSetting) - }, activePage) -endEvent - -event OnOptionSliderOpen(int a_option) - IMPL_IFCHAIN_MACRO_INVOKE( - a_option, ref, implOpenHandler, - {IMPL_ALL_IMPLS_OF_STRUCT(SliderSetting)}, - activePage - ) -endEvent - -event OnOptionSliderAccept(int a_option, float a_value) - IMPL_IFCHAIN_MACRO_INVOKE( - a_option, ref, implAcceptHandler, - {IMPL_ALL_IMPLS_OF_STRUCT(SliderSetting)}, activePage - ) -endEvent - -event OnOptionMenuOpen(int a_option) - IMPL_IFCHAIN_MACRO_INVOKE( - a_option, ref, implOpenHandler, - {IMPL_ALL_IMPLS_OF_STRUCT(ListSetting)}, activePage - ) -endEvent - -event OnOptionMenuAccept(int a_option, int a_index) - IMPL_IFCHAIN_MACRO_INVOKE( - a_option, ref, implAcceptHandler, - {IMPL_ALL_IMPLS_OF_STRUCT(ListSetting)}, activePage - ) -endEvent - -event OnOptionInputOpen(int a_option) - IMPL_IFCHAIN_MACRO_INVOKE( - a_option, ref, implOpenHandler, - {IMPL_ALL_IMPLS_OF_STRUCT(SavePresetSetting)}, activePage - ) -endEvent - -event OnOptionInputAccept(int a_option, string a_input) - IMPL_IFCHAIN_MACRO_INVOKE( - a_option, ref, implAcceptHandler, - {IMPL_ALL_IMPLS_OF_STRUCT(SavePresetSetting)}, activePage - ) -endEvent - -event OnOptionKeyMapChange(int a_option, int a_keyCode, string a_conflictControl, string a_conflictName) - IMPL_IFCHAIN_MACRO_INVOKE( - a_option, ref, implSelectHandler, - {IMPL_ALL_IMPLS_OF_STRUCT(KeyBindSetting)}, activePage - ) -endEvent - -event OnOptionHighlight(int a_option) - IMPL_IFCHAIN_MACRO_INVOKE(a_option, ref, implDesc, { - IMPL_ALL_IMPLS_OF_STRUCT(SliderSetting), - IMPL_ALL_IMPLS_OF_STRUCT(ToggleSetting), - IMPL_ALL_IMPLS_OF_STRUCT(ResetSetting), - IMPL_ALL_IMPLS_OF_STRUCT(ListSetting), - IMPL_ALL_IMPLS_OF_STRUCT(SavePresetSetting), - IMPL_ALL_IMPLS_OF_STRUCT(LoadPresetSetting), - IMPL_ALL_IMPLS_OF_STRUCT(KeyBindSetting) - }, activePage) -endEvent \ No newline at end of file diff --git a/CodeGen/MCM/mcm/include/compat.psc b/CodeGen/MCM/mcm/include/compat.psc new file mode 100644 index 0000000..e69de29 diff --git a/CodeGen/MCM/mcm/include/controls.psc b/CodeGen/MCM/mcm/include/controls.psc new file mode 100644 index 0000000..43cfa19 --- /dev/null +++ b/CodeGen/MCM/mcm/include/controls.psc @@ -0,0 +1,323 @@ +#constexpr_struct SliderSetting [ + real int ref = 0 + float min = -100.0 + float max = 100.0 + float interval = 1.0 + float defaultValue = 0.0 + mangle string settingName = "" + string displayName = "" + string desc = "" + string displayFormat = "{1}" + string page = "" + + MACRO implControl = [ + this->ref = AddSliderOption(this->displayName, SmoothCam_GetFloatConfig(this->settingName), this->displayFormat) + ] + + MACRO implOpenHandler = [ + SetSliderDialogStartValue(SmoothCam_GetFloatConfig(this->settingName)) + SetSliderDialogDefaultValue(this->defaultValue) + SetSliderDialogRange(this->min, this->max) + SetSliderDialogInterval(this->interval) + ] + + MACRO implAcceptHandler = [ + SetSliderOptionValue(a_option, a_value, this->displayFormat) + SmoothCam_SetFloatConfig(this->settingName, a_value) + ] + + MACRO implDesc = [ + SetInfoText(this->desc) + ] +] +#constexpr_struct ToggleSetting [ + real int ref = 0 + mangle string settingName = "" + string displayName = "" + string desc = "" + string page = "" + + MACRO implControl = [ + this->ref = AddToggleOption(this->displayName, SmoothCam_GetBoolConfig(this->settingName)) + ] + + MACRO implSelectHandler = [ + SmoothCam_SetBoolConfig(this->settingName, !SmoothCam_GetBoolConfig(this->settingName)) + SetToggleOptionValue(a_option, SmoothCam_GetBoolConfig(this->settingName)) + ] + + MACRO implDesc = [ + SetInfoText(this->desc) + ] +] +#constexpr_struct ResetSetting [ + real int ref = 0 + string displayName = "" + string desc = "" + string page = "" + + MACRO implControl = [ + this->ref = AddToggleOption(this->displayName, false) + ] + + MACRO implSelectHandler = [ + if (ShowMessage("Are you sure? This will reset all settings to their default values.")) + SmoothCam_ResetConfig() + ShowMessage("Settings reset.") + ForcePageReset() + endIf + ] + + MACRO implDesc = [ + SetInfoText(this->desc) + ] +] +#constexpr_struct ResetCrosshairSetting [ + real int ref = 0 + string displayName = "" + string desc = "" + string page = "" + + MACRO implControl = [ + this->ref = AddToggleOption(this->displayName, false) + ] + + MACRO implSelectHandler = [ + SmoothCam_ResetCrosshair() + ShowMessage("Crosshair reset.") + ForcePageReset() + ] + + MACRO implDesc = [ + SetInfoText(this->desc) + ] +] +#constexpr_struct FixStateSetting [ + real int ref = 0 + string displayName = "" + string desc = "" + string page = "" + + MACRO implControl = [ + this->ref = AddToggleOption(this->displayName, false) + ] + + MACRO implSelectHandler = [ + if (ShowMessage("Are you sure? You should only do this if the camera is stuck when you should be in normal thirdperson (Not on horseback or a dragon). Doing otherwise can cause issues. This is intended to fix the bug where camera rotation becomes locked.")) + SmoothCam_FixCameraState() + ShowMessage("Camera state forced back to thirdperson.") + endIf + ] + + MACRO implDesc = [ + SetInfoText(this->desc) + ] +] +#constexpr_struct ListSetting [ + real int ref = 0 + mangle string settingName = "" + string displayName = "" + string desc = "" + literal arrayType = PLACEHOLDER + string page = "" + + MACRO implControl = [ + this->ref = AddMenuOption(this->displayName, this->arrayType[GetCurrentArrayIndex(this->settingName, this->arrayType)]) + ] + + MACRO implOpenHandler = [ + SetMenuDialogStartIndex(GetCurrentArrayIndex(this->settingName, this->arrayType)) + SetMenuDialogDefaultIndex(0) + SetMenuDialogOptions(this->arrayType) + ] + + MACRO implAcceptHandler = [ + SetMenuOptionValue(a_option, this->arrayType[a_index]) + SmoothCam_SetStringConfig(this->settingName, this->arrayType[a_index]) + ] + + MACRO implDesc = [ + SetInfoText(this->desc) + ] +] +#constexpr_struct SavePresetSetting [ + real int ref = 0 + int presetIndex = 0 + string displayName = "" + string desc = "Save your current settings to this preset slot" + string page = "" + + MACRO implControl = [ + this->ref = AddInputOption(this->displayName + " " + SmoothCam_GetPresetNameAtIndex(this->presetIndex), SmoothCam_GetPresetNameAtIndex(this->presetIndex)) + ] + + MACRO implOpenHandler = [ + string value = SmoothCam_GetPresetNameAtIndex(this->presetIndex) + if (value == "Empty") + SetInputDialogStartText("Enter a preset name") + else + SetInputDialogStartText(value) + endIf + ] + + MACRO implAcceptHandler = [ + if (SmoothCam_SaveAsPreset(this->presetIndex, a_input) == "") + ShowMessage("Preset saved.", false) + else + ShowMessage("Failed to save preset!", false) + endIf + ForcePageReset() + ] + + MACRO implDesc = [ + SetInfoText(this->desc) + ] +] +#constexpr_struct LoadPresetSetting [ + real int ref = 0 + int presetIndex = 0 + string displayName = "" + string desc = "Load this preset" + string page = "" + + MACRO implControl = [ + this->ref = AddToggleOption(this->displayName + " " + SmoothCam_GetPresetNameAtIndex(this->presetIndex), false) + ] + + MACRO implSelectHandler = [ + if (!SmoothCam_LoadPreset(this->presetIndex)) + ShowMessage("Failed to load preset! Have you saved this slot yet?", false) + else + ShowMessage("Preset loaded.", false) + ForcePageReset() + endIf + ] + + MACRO implDesc = [ + SetInfoText(this->desc) + ] +] +#constexpr_struct KeyBindSetting [ + real int ref = 0 + mangle string settingName = "" + string displayName = "" + string desc = "" + string page = "" + + MACRO implControl = [ + this->ref = AddKeyMapOption(this->displayName, SmoothCam_GetIntConfig(this->settingName)) + ] + + MACRO implSelectHandler = [ + if (StringUtil.GetLength(a_conflictControl) == 0) + SmoothCam_SetIntConfig(this->settingName, a_keyCode) + SetKeyMapOptionValue(this->ref, a_keyCode) + else + if (ShowMessage(a_conflictControl + " conflicts with another control assigned to " + a_conflictName + ".\nAre you sure you want to assign this control?")) + SmoothCam_SetIntConfig(this->settingName, a_keyCode) + SetKeyMapOptionValue(this->ref, a_keyCode) + endIf + endIf + ] + + MACRO implDesc = [ + SetInfoText(this->desc) + ] +] + +event OnOptionSelect(int a_option) + #StructInvokeSwitchIfEquals( + a_option, ref, + activePage, page, + implSelectHandler, + [ + ToggleSetting # ImplsOf, + ResetSetting # ImplsOf, + ResetCrosshairSetting # ImplsOf, + FixStateSetting # ImplsOf, + LoadPresetSetting # ImplsOf + ] + ) +endEvent + +event OnOptionSliderOpen(int a_option) + #StructInvokeSwitchIfEquals( + a_option, ref, + activePage, page, + implOpenHandler, + [SliderSetting # ImplsOf] + ) +endEvent + +event OnOptionSliderAccept(int a_option, float a_value) + #StructInvokeSwitchIfEquals( + a_option, ref, + activePage, page, + implAcceptHandler, + [SliderSetting # ImplsOf] + ) +endEvent + +event OnOptionMenuOpen(int a_option) + #StructInvokeSwitchIfEquals( + a_option, ref, + activePage, page, + implOpenHandler, + [ListSetting # ImplsOf] + ) +endEvent + +event OnOptionMenuAccept(int a_option, int a_index) + #StructInvokeSwitchIfEquals( + a_option, ref, + activePage, page, + implAcceptHandler, + [ListSetting # ImplsOf] + ) +endEvent + +event OnOptionInputOpen(int a_option) + #StructInvokeSwitchIfEquals( + a_option, ref, + activePage, page, + implOpenHandler, + [SavePresetSetting # ImplsOf] + ) +endEvent + +event OnOptionInputAccept(int a_option, string a_input) + #StructInvokeSwitchIfEquals( + a_option, ref, + activePage, page, + implAcceptHandler, + [SavePresetSetting # ImplsOf] + ) +endEvent + +event OnOptionKeyMapChange(int a_option, int a_keyCode, string a_conflictControl, string a_conflictName) + #StructInvokeSwitchIfEquals( + a_option, ref, + activePage, page, + implSelectHandler, + [KeyBindSetting # ImplsOf] + ) +endEvent + +event OnOptionHighlight(int a_option) + #StructInvokeSwitchIfEquals( + a_option, ref, + activePage, page, + implDesc, + [ + SliderSetting # ImplsOf, + ToggleSetting # ImplsOf, + ResetSetting # ImplsOf, + ResetCrosshairSetting # ImplsOf, + FixStateSetting # ImplsOf, + ListSetting # ImplsOf, + SavePresetSetting # ImplsOf, + LoadPresetSetting # ImplsOf, + KeyBindSetting # ImplsOf + ] + ) +endEvent \ No newline at end of file diff --git a/CodeGen/MCM/mcm/include/crosshair.psc b/CodeGen/MCM/mcm/include/crosshair.psc new file mode 100644 index 0000000..e6b7ff0 --- /dev/null +++ b/CodeGen/MCM/mcm/include/crosshair.psc @@ -0,0 +1,180 @@ +ToggleSetting crosshair3DBowEnabled -> [ + settingName: "Enable3DBowCrosshair" + displayName: "3D Bow Crosshair Enabled" + desc: "Enable the raycasted 3D crosshair when aiming with the bow." + page: " Crosshair" +] +ToggleSetting crosshair3DMagicEnabled -> [ + settingName: "Enable3DMagicCrosshair" + displayName: "3D Magic Crosshair Enabled" + desc: "Enable the raycasted 3D crosshair when using magic." + page: " Crosshair" +] +ToggleSetting crosshair3DWorldEnabled -> [ + settingName: "UseWorldCrosshair" + displayName: "Use World-Space Crosshair" + desc: "When your crosshair ray has hit something, use a crosshair mesh rendered in-world, not on the HUD." + page: " Crosshair" +] +ListSetting worldCrosshairType -> [ + settingName: "WorldCrosshairType" + displayName: "Crosshair Type" + desc: "Select the style of world-space crosshair to use, if world-space crosshair is enabled." + arrayType: crosshairTypes + page: " Crosshair" +] +ToggleSetting worldCrosshairDepthTest -> [ + settingName: "WorldCrosshairDepthTest" + displayName: "Crosshair Occlusion" + desc: "When using the world-space crosshair, disable this option to make it draw on top of everything rather than allow other geometry to cover it." + page: " Crosshair" +] +ToggleSetting hideCrosshairOutOfCombat -> [ + settingName: "HideCrosshairOutOfCombat" + displayName: "Hide Non-Combat Crosshair" + desc: "Hide the crosshair when not in combat." + page: " Crosshair" +] +ToggleSetting hideCrosshairMeleeCombat -> [ + settingName: "HideCrosshairMeleeCombat" + displayName: "Hide Melee Combat Crosshair" + desc: "Hide the crosshair when in melee combat." + page: " Crosshair" +] +ToggleSetting enableCrosshairSizeManip -> [ + settingName: "EnableCrosshairSizeManip" + displayName: "Enable Size Manipulation" + desc: "Changes the size of the crosshair, based on 'Min/Max Size' and 'NPC Hit Size'. Disable if this causes conflicts with other mods. May require a game restart." + page: " Crosshair" +] +SliderSetting crosshairNPCGrowSize -> [ + settingName: "CrosshairNPCGrowSize" + displayName: "NPC Hit Size" + desc: "When the 3D crosshair is over an NPC, grow the size of the crosshair by this amount." + defaultValue: 16 + interval: 1 + min: 0 + max: 64 + page: " Crosshair" +] +SliderSetting crosshairMinDistSize -> [ + settingName: "CrosshairMinDistSize" + displayName: "Min Crosshair Size" + desc: "Sets the size of the 3D crosshair when the player's aim ray is at the maximum distance." + defaultValue: 16 + interval: 1 + min: 8 + max: 64 + page: " Crosshair" +] +SliderSetting crosshairMaxDistSize -> [ + settingName: "CrosshairMaxDistSize" + displayName: "Max Crosshair Size" + desc: "Sets the size of the 3D crosshair when the player's aim ray is at the minimum distance." + defaultValue: 24 + interval: 1 + min: 8 + max: 128 + page: " Crosshair" +] +ToggleSetting offsetStealthMeter -> [ + settingName: "OffsetStealthMeter" + displayName: "Enable Stealth Meter Offset" + desc: "When using the world-space crosshair and sneaking, offset the stealth meter by a set amount when the crosshair is active." + page: " Crosshair" +] +ToggleSetting alwaysOffsetStealthMeter -> [ + settingName: "AlwaysOffsetStealthMeter" + displayName: "Always Offset Stealth Meter" + desc: "When selected, offset settings for the stealth meter will always be applied, even if the world-space crosshair is not active." + page: " Crosshair" +] +SliderSetting stealthMeterOffsetX -> [ + settingName: "StealthMeterOffsetX" + displayName: "Stealth Meter X Offset" + desc: "Offset the position of the stealth meter by this amount when the world-space crosshair is active while sneaking." + defaultValue: 0 + interval: 0.1 + min: -640 + max: 640 + displayFormat: "{1}" + page: " Crosshair" +] +SliderSetting stealthMeterOffsetY -> [ + settingName: "StealthMeterOffsetY" + displayName: "Stealth Meter Y Offset" + desc: "Offset the position of the stealth meter by this amount when the world-space crosshair is active while sneaking." + defaultValue: 0 + interval: 0.1 + min: -360 + max: 360 + displayFormat: "{1}" + page: " Crosshair" +] +ToggleSetting enableArrowPrediction -> [ + settingName: "EnableArrowPrediction" + displayName: "Enable Arrow Prediction" + desc: "When the 3D crosshair is enabled for ranged combat, the crosshair will account for gravity when aiming with a bow." + page: " Crosshair" +] +ToggleSetting drawArrowArc -> [ + settingName: "DrawArrowArc" + displayName: "Draw Arrow Prediction Arc" + desc: "When the 3D crosshair is enabled for ranged combat and 'Enable Arrow Prediction' is selected, an arc will be drawn while aiming with bows which indicates the flight path your arrow will take." + page: " Crosshair" +] +SliderSetting maxArrowPredictionRange -> [ + settingName: "MaxArrowPredictionRange" + displayName: "Max Arrow Prediction Distance" + desc: "The furthest distance to allow arrow prediction, if enabled, to function." + defaultValue: 10000 + interval: 1 + min: 500 + max: 12000 + displayFormat: "{0}" + page: " Crosshair" +] +SliderSetting arrowArcColorR -> [ + settingName: "ArrowArcColorR" + displayName: "Arrow Arc Color: Red" + desc: "The amount of red coloration to add to the arrow arc." + defaultValue: 255.0 + interval: 1 + min: 0.0 + max: 255.0 + displayFormat: "{0}" + page: " Crosshair" +] +SliderSetting arrowArcColorG -> [ + settingName: "ArrowArcColorG" + displayName: "Arrow Arc Color: Green" + desc: "The amount of green coloration to add to the arrow arc." + defaultValue: 255.0 + interval: 1 + min: 0.0 + max: 255.0 + displayFormat: "{0}" + page: " Crosshair" +] +SliderSetting arrowArcColorB -> [ + settingName: "ArrowArcColorB" + displayName: "Arrow Arc Color: Blue" + desc: "The amount of blue coloration to add to the arrow arc." + defaultValue: 255.0 + interval: 1 + min: 0.0 + max: 255.0 + displayFormat: "{0}" + page: " Crosshair" +] +SliderSetting arrowArcColorA -> [ + settingName: "ArrowArcColorA" + displayName: "Arrow Arc Color: Transparency" + desc: "The amount of transparency coloration to add to the arrow arc." + defaultValue: 127.0 + interval: 1 + min: 0.0 + max: 255.0 + displayFormat: "{0}" + page: " Crosshair" +] \ No newline at end of file diff --git a/CodeGen/MCM/mcm/include/following.psc b/CodeGen/MCM/mcm/include/following.psc new file mode 100644 index 0000000..eab574a --- /dev/null +++ b/CodeGen/MCM/mcm/include/following.psc @@ -0,0 +1,319 @@ +ToggleSetting interpEnabled -> [ + settingName: "InterpolationEnabled" + displayName: "Interpolation Enabled" + desc: "Enable camera smoothing." + page: " Following" +] +ToggleSetting sepZInterpEnabled -> [ + settingName: "SeparateZInterpEnabled" + displayName: "Interpolation Enabled" + desc: "Enable the separate Z smoothing settings for smoothing camera height differently." + page: " Following" +] +ToggleSetting sepLocalInterpEnabled -> [ + settingName: "SeparateLocalInterpolation" + displayName: "Interpolation Enabled" + desc: "Enable separate local-space camera smoothing (Camera rotation)." + page: " Following" +] +ToggleSetting offsetInterpEnabled -> [ + settingName: "OffsetTransitionEnabled" + displayName: "Interpolation Enabled" + desc: "Enable smoothing of camera offset state transitions." + page: " Following" +] +ToggleSetting zoomInterpEnabled -> [ + settingName: "ZoomTransitionEnabled" + displayName: "Interpolation Enabled" + desc: "Enable smoothing of camera zoom state transitions." + page: " Following" +] +ToggleSetting fovInterpEnabled -> [ + settingName: "FOVTransitionEnabled" + displayName: "Interpolation Enabled" + desc: "Enable smoothing of camera FOV state transitions." + page: " Following" +] +ToggleSetting disableDeltaTime -> [ + settingName: "DisableDeltaTime" + displayName: "Disable Delta Time Factoring" + desc: "Remove time from interpolation math. May result in less jitter but can cause speed to vary with frame rate." + page: " Following" +] +ToggleSetting cameraDistanceClampXEnable -> [ + settingName: "CameraDistanceClampXEnable" + displayName: "Enable X Distance Clamp" + desc: "Clamp the maximum distance the camera may move away from the target position along the X (side) axis." + page: " Following" +] +ToggleSetting cameraDistanceClampYEnable -> [ + settingName: "CameraDistanceClampYEnable" + displayName: "Enable Y Distance Clamp" + desc: "Clamp the maximum distance the camera may move away from the target position along the Y (forward) axis." + page: " Following" +] +ToggleSetting cameraDistanceClampZEnable -> [ + settingName: "CameraDistanceClampZEnable" + displayName: "Enable Z Distance Clamp" + desc: "Clamp the maximum distance the camera may move away from the target position along the Z (up) axis." + page: " Following" +] +ToggleSetting swapDistanceClampXAxis -> [ + settingName: "ShoulderSwapXClamping" + displayName: "Also Swap X Axis Clamping" + desc: "When shoulder swapping, will also swap the distance clamping X axis range." + page: " Following" +] + +ListSetting interpMethod -> [ + settingName: "InterpolationMethod" + displayName: "Method" + desc: "The scalar method to use for camera smoothing." + arrayType: interpMethods + page: " Following" +] +ListSetting sepZInterpMethod -> [ + settingName: "SeparateZInterpMethod" + displayName: "Method" + desc: "The scalar method to use for smoothing the camera height (If enabled)." + arrayType: interpMethods + page: " Following" +] +ListSetting sepLocalInterpMethod -> [ + settingName: "SepLocalInterpMethod" + displayName: "Method" + desc: "The scalar method to use for local-space smoothing (If enabled)." + arrayType: interpMethods + page: " Following" +] +ListSetting offsetInterpMethod -> [ + settingName: "OffsetTransitionMethod" + displayName: "Method" + desc: "The scalar method to use for offset transition smoothing (If enabled)." + arrayType: interpMethods + page: " Following" +] +ListSetting zoomInterpMethod -> [ + settingName: "ZoomTransitionMethod" + displayName: "Method" + desc: "The scalar method to use for zoom transition smoothing (If enabled)." + arrayType: interpMethods + page: " Following" +] +ListSetting fovInterpMethod -> [ + settingName: "FOVTransitionMethod" + displayName: "Method" + desc: "The scalar method to use for FOV transition smoothing (If enabled)." + arrayType: interpMethods + page: " Following" +] + +SliderSetting minCameraFollowRate -> [ + settingName: "MinCameraFollowRate" + displayName: "Min Follow Rate" + desc: "The smoothing rate to use when the camera is close to the player." + defaultValue: 0.5 + interval: 0.01 + min: 0.01 + max: 1.0 + displayFormat: "{2}" + page: " Following" +] +SliderSetting maxCameraFollowRate -> [ + settingName: "MaxCameraFollowRate" + displayName: "Max Follow Rate" + desc: "The smoothing rate to use when the camera is far away from the player." + defaultValue: 0.8 + interval: 0.01 + min: 0.01 + max: 1.0 + displayFormat: "{2}" + page: " Following" +] +SliderSetting maxSmoothingInterpDistance -> [ + settingName: "MaxSmoothingInterpDistance" + displayName: "Max Interpolation Distance" + desc: "The distance at which the max follow rate value is used for smoothing. Below this value a mix of min and max is used." + defaultValue: 650.0 + interval: 1.0 + min: 1.0 + max: 650.0 + displayFormat: "{0}" + page: " Following" +] +SliderSetting minCameraFollowDistance -> [ + settingName: "MinFollowDistance" + displayName: "Min Follow Distance" + desc: "The closest the camera may get to the player when at the lowest zoom level." + defaultValue: 64.0 + interval: 0.1 + min: 0.0 + max: 256.0 + page: " Following" +] +SliderSetting zoomMul -> [ + settingName: "ZoomMul" + displayName: "Zoom Multiplier" + desc: "The amount of distance to add to the camera for each zoom level." + defaultValue: 500.0 + interval: 1.0 + min: 1.0 + max: 500.0 + page: " Following" +] +SliderSetting minSepZFollowRate -> [ + settingName: "SepZMinFollowRate" + displayName: "Min Follow Rate" + desc: "The smoothing rate to use when the camera is close to the player." + defaultValue: 0.5 + interval: 0.01 + min: 0.01 + max: 1.0 + displayFormat: "{2}" + page: " Following" +] +SliderSetting maxSepZFollowRate -> [ + settingName: "SepZMaxFollowRate" + displayName: "Max Follow Rate" + desc: "The smoothing rate to use when the camera is far away from the player." + defaultValue: 0.5 + interval: 0.01 + min: 0.01 + max: 1.0 + displayFormat: "{2}" + page: " Following" +] +SliderSetting maxSepZSmoothingDistance -> [ + settingName: "SepZMaxInterpDistance" + displayName: "Max Interpolation Distance" + desc: "The distance at which the max follow rate value is used for smoothing. Below this value a mix of min and max is used." + defaultValue: 100.0 + interval: 1.0 + min: 1.0 + max: 300.0 + displayFormat: "{0}" + page: " Following" +] +SliderSetting offsetTransitionDuration -> [ + settingName: "OffsetTransitionDuration" + displayName: "Interpolation Duration" + desc: "The smoothing duration to use when the camera changes offsets (In seconds)." + defaultValue: 1.0 + interval: 0.01 + min: 0.01 + max: 5.0 + displayFormat: "{2}" + page: " Following" +] +SliderSetting zoomTransitionDuration -> [ + settingName: "ZoomTransitionDuration" + displayName: "Interpolation Duration" + desc: "The smoothing duration to use when the camera changes zoom distance (In seconds)." + defaultValue: 0.2 + interval: 0.01 + min: 0.01 + max: 5.0 + displayFormat: "{2}" + page: " Following" +] +SliderSetting fovTransitionDuration -> [ + settingName: "FOVTransitionDuration" + displayName: "Interpolation Duration" + desc: "The smoothing duration to use when the camera changes FOV (In seconds)." + defaultValue: 0.2 + interval: 0.01 + min: 0.01 + max: 5.0 + displayFormat: "{2}" + page: " Following" +] +SliderSetting cameraDistanceClampXMin -> [ + settingName: "CameraDistanceClampXMin" + displayName: "Distance Clamp X Min" + desc: "The minimal distance the camera may get from the target position along the X axis before being clamped." + defaultValue: -75.0 + interval: 1.0 + min: -300.0 + max: 0.0 + displayFormat: "{0}" + page: " Following" +] +SliderSetting cameraDistanceClampXMax -> [ + settingName: "CameraDistanceClampXMax" + displayName: "Distance Clamp X Max" + desc: "The maximal distance the camera may get from the target position along the X axis before being clamped." + defaultValue: 75.0 + interval: 1.0 + min: 0.0 + max: 300.0 + displayFormat: "{0}" + page: " Following" +] +SliderSetting cameraDistanceClampYMin -> [ + settingName: "CameraDistanceClampYMin" + displayName: "Distance Clamp Y Min" + desc: "The minimal distance the camera may get from the target position along the Y axis before being clamped." + defaultValue: -100.0 + interval: 1.0 + min: -500.0 + max: 0.0 + displayFormat: "{0}" + page: " Following" +] +SliderSetting cameraDistanceClampYMax -> [ + settingName: "CameraDistanceClampYMax" + displayName: "Distance Clamp Y Max" + desc: "The maximal distance the camera may get from the target position along the Y axis before being clamped." + defaultValue: 100.0 + interval: 1.0 + min: 0.0 + max: 500.0 + displayFormat: "{0}" + page: " Following" +] +SliderSetting cameraDistanceClampZMin -> [ + settingName: "CameraDistanceClampZMin" + displayName: "Distance Clamp Z Min" + desc: "The minimal distance the camera may get from the target position along the Z axis before being clamped." + defaultValue: -50.0 + interval: 1.0 + min: -300.0 + max: 0.0 + displayFormat: "{0}" + page: " Following" +] +SliderSetting cameraDistanceClampZMax -> [ + settingName: "CameraDistanceClampZMax" + displayName: "Distance Clamp Z Max" + desc: "The maximal distance the camera may get from the target position along the Z axis before being clamped." + defaultValue: 50.0 + interval: 1.0 + min: 0.0 + max: 300.0 + displayFormat: "{0}" + page: " Following" +] +SliderSetting sepLocalSpaceInterpRate -> [ + settingName: "SepLocalInterpRate" + displayName: "Follow Rate" + desc: "The smoothing rate to use for local-space smoothing." + defaultValue: 0.5 + interval: 0.01 + min: 0.01 + max: 1.0 + displayFormat: "{2}" + page: " Following" +] + +KeyBindSetting shoulderSwapKey -> [ + settingName: "ShoulderSwapKeyCode" + displayName: "Shoulder Swap Key" + desc: "Inverts the current X offset of the camera." + page: " Following" +] +KeyBindSetting nextPresetKey -> [ + settingName: "NextPresetKeyCode" + displayName: "Load Next Preset Key" + desc: "Loads the next found valid preset. Note that SmoothCam doesn't save what preset you are currently using between games - When starting skyrim preset 1 will be assumed to be the active preset." + page: " Following" +] \ No newline at end of file diff --git a/CodeGen/MCM/mcm/include/general.psc b/CodeGen/MCM/mcm/include/general.psc new file mode 100644 index 0000000..3bf1bdf --- /dev/null +++ b/CodeGen/MCM/mcm/include/general.psc @@ -0,0 +1,75 @@ +ToggleSetting modEnabled -> [ + settingName: "ModEnabled" + displayName: "Disable SmoothCam" + desc: "When selected, disables SmoothCam." + page: " General" +] +ToggleSetting enableCrashDumps -> [ + settingName: "EnableCrashDumps" + displayName: "Enable Crash Dump Handler" + desc: "When enabled, SmoothCam will install a crash dump handler at game startup and will attempt to generate a mini-dump if the game crashes during SmoothCam code execution. You must restart the game for this setting to apply. If a crash does happen, follow the instructions in the message box to report the issue." + page: " General" +] +ResetCrosshairSetting resetCrosshair -> [ + displayName: "Reset Crosshair" + desc: "If your crosshair disappears and you can't get it back, try this option." + page: " General" +] +ResetSetting reset -> [ + displayName: "Reset All Settings" + desc: "Set all settings back to their default values." + page: " General" +] +FixStateSetting forceCameraState -> [ + displayName: "Force camera to thirdperson" + desc: "Force the camera state back to thirdperson, should the camera be stuck." + page: " General" +] +KeyBindSetting modEnabledKey -> [ + settingName: "ModEnabledKeyCode" + displayName: "Toggle SmoothCam Key" + desc: "Toggles SmoothCam on and off, see 'Disable SmoothCam' setting." + page: " General" +] +KeyBindSetting toggleCustomZOffset -> [ + settingName: "ToggleCustomZKeyCode" + displayName: "Toggle Z Offset Key" + desc: "When pressed, applies/removes an offset to the camera height, the amount being set by the 'Z Offset Amount' slider." + page: " General" +] +SliderSetting customZOffsetAmount -> [ + settingName: "CustomZOffsetAmount" + displayName: "Z Offset Amount" + desc: "Amount to offset the Z axis (camera height) by when using pressing the key bound to 'Toggle Z Offset'." + defaultValue: 0 + interval: 1 + min: -256 + max: 256 + displayFormat: "{0}" + page: " General" +] + +ToggleSetting icCompat -> [ + settingName: "ICCompat" + displayName: "Improved Camera" + desc: "Enable compat fixes for Improved Camera. You must be using the offical release of Improved Camera (beta4), no other build will work and will show VERSION MISMATCH." + page: " General" +] +ToggleSetting accCompat -> [ + settingName: "ACCCompat" + displayName: "Alternate Conversation Camera" + desc: "Enable compat fixes for Alternate Conversation Camera." + page: " General" +] +ToggleSetting ifpvCompat -> [ + settingName: "IFPVCompat" + displayName: "Immersive First Person View" + desc: "Enable compat fixes for Improved First Person View." + page: " General" +] +ToggleSetting agoCompat -> [ + settingName: "AGOCompat" + displayName: "Archery Gameplay Overhaul" + desc: "Enable compat fixes for AGO when using the 3D crosshair/prediction arc." + page: " General" +] \ No newline at end of file diff --git a/CodeGen/MCM/mcm/include/native_functions.psc b/CodeGen/MCM/mcm/include/native_functions.psc new file mode 100644 index 0000000..f870c90 --- /dev/null +++ b/CodeGen/MCM/mcm/include/native_functions.psc @@ -0,0 +1,15 @@ +Function SmoothCam_SetIntConfig(string member, int value) global native +Function SmoothCam_SetStringConfig(string member, string value) global native +Function SmoothCam_SetBoolConfig(string member, bool value) global native +Function SmoothCam_SetFloatConfig(string member, float value) global native +Function SmoothCam_ResetConfig() global native +Function SmoothCam_ResetCrosshair() global native +Function SmoothCam_FixCameraState() global native +int Function SmoothCam_GetIntConfig(string member) global native +string Function SmoothCam_GetStringConfig(string member) global native +bool Function SmoothCam_GetBoolConfig(string member) global native +float Function SmoothCam_GetFloatConfig(string member) global native +string Function SmoothCam_SaveAsPreset(int index, string name) global native +bool Function SmoothCam_LoadPreset(int index) global native +string Function SmoothCam_GetPresetNameAtIndex(int index) global native +string Function SmoothCam_IsImprovedCameraDetected() global native \ No newline at end of file diff --git a/CodeGen/MCM/mcm/include/offset_groups.psc b/CodeGen/MCM/mcm/include/offset_groups.psc new file mode 100644 index 0000000..a1a766c --- /dev/null +++ b/CodeGen/MCM/mcm/include/offset_groups.psc @@ -0,0 +1,130 @@ +; Standing +#CreateOffsetGroup(Standing, " Standing") +; Walking +#CreateOffsetGroup(Walking, " Walking") +; Running +#CreateOffsetGroup(Running, " Running") +; Sprinting +#CreateOffsetGroup(Sprinting, " Sprinting") +; Sneaking +#CreateOffsetGroup(Sneaking, " Sneaking") +; Swimming +#CreateOffsetGroup(Swimming, " Swimming", NoMelee, NoRanged, NoMagic) +; Sitting +#CreateOffsetGroup(Sitting, " Sitting", NoMelee, NoRanged, NoMagic) +; Horseback +#CreateOffsetGroup(Horseback, " Horseback") +; Dragon +#CreateOffsetGroup(Dragon, " Dragon", NoMelee, NoRanged, NoMagic) +; Vampire Lord +#CreateOffsetGroup(VampireLord, " Vampire Lord", NoRanged) +; Werewolf +#CreateOffsetGroup(Werewolf, " Werewolf", NoRanged, NoMagic) +; Group edit +#CreateOffsetGroup(Group, " Group Edit", NoInterpToggles) +; Bow aiming +SliderSetting bowaim_sideOffset -> [ + settingName: "Bowaim:SideOffset" + displayName: "Side Offset" + desc: "The amount to move the camera to the right." + defaultValue: 25.0 + page: " Bow Aiming" +] +SliderSetting bowaim_upOffset -> [ + settingName: "Bowaim:UpOffset" + displayName: "Up Offset" + desc: "The amount to move the camera up." + page: " Bow Aiming" +] +SliderSetting bowaim_zoomOffset -> [ + settingName: "Bowaim:ZoomOffset" + displayName: "Zoom Offset" + desc: "The amount to offset the camera zoom by." + min: -200.0 + max: 200.0 + page: " Bow Aiming" +] +SliderSetting bowaim_fovOffset -> [ + settingName: "Bowaim:FOVOffset" + displayName: "FOV Offset" + desc: "The amount to offset the camera FOV by. Note this will be clamped to a lower bound of 10 and an upper bound of 170." + min: -120.0 + max: 120.0 + page: " Bow Aiming" +] +SliderSetting bowaim_sideOffsetHorseback -> [ + settingName: "BowaimHorse:SideOffset" + displayName: "Horseback Side Offset" + desc: "The amount to move the camera to the right when on horseback." + defaultValue: 25.0 + page: " Bow Aiming" +] +SliderSetting bowaim_upOffsetHorseback -> [ + settingName: "BowaimHorse:UpOffset" + displayName: "Horseback Up Offset" + desc: "The amount to move the camera up when on horseback." + page: " Bow Aiming" +] +SliderSetting bowaim_zoomOffsetHorseback -> [ + settingName: "BowaimHorse:ZoomOffset" + displayName: "Horseback Zoom Offset" + desc: "The amount to offset the camera zoom by when on horseback." + min: -200.0 + max: 200.0 + page: " Bow Aiming" +] +SliderSetting bowaim_fovOffsetHorseback -> [ + settingName: "BowaimHorse:FOVOffset" + displayName: "Horseback FOV Offset" + desc: "The amount to offset the camera FOV by when on horseback. Note this will be clamped to a lower bound of 10 and an upper bound of 170." + min: -120.0 + max: 120.0 + page: " Bow Aiming" +] +SliderSetting bowaim_sideOffsetSneaking -> [ + settingName: "BowaimSneak:SideOffset" + displayName: "Sneaking Side Offset" + desc: "The amount to move the camera to the right when sneaking." + defaultValue: 25.0 + page: " Bow Aiming" +] +SliderSetting bowaim_upOffsetSneaking -> [ + settingName: "BowaimSneak:UpOffset" + displayName: "Sneaking Up Offset" + desc: "The amount to move the camera up when sneaking." + page: " Bow Aiming" +] +SliderSetting bowaim_zoomOffsetSneaking -> [ + settingName: "BowaimSneak:ZoomOffset" + displayName: "Sneaking Zoom Offset" + desc: "The amount to offset the camera zoom by when sneaking." + min: -200.0 + max: 200.0 + page: " Bow Aiming" +] +SliderSetting bowaim_fovOffsetSneaking -> [ + settingName: "BowaimSneak:FOVOffset" + displayName: "Sneaking FOV Offset" + desc: "The amount to offset the camera FOV by when sneaking. Note this will be clamped to a lower bound of 10 and an upper bound of 170." + min: -120.0 + max: 120.0 + page: " Bow Aiming" +] +ToggleSetting bowaim_interp -> [ + settingName: "InterpBowAim" + displayName: "Enable Interpolation" + desc: "Enables interpolation in this state." + page: " Bow Aiming" +] +ToggleSetting bowaim_interpHorseback -> [ + settingName: "InterpBowAimHorseback" + displayName: "Enable Horseback Interpolation" + desc: "Enables interpolation in this state." + page: " Bow Aiming" +] +ToggleSetting bowaim_interpSneaking -> [ + settingName: "InterpBowAimSneaking" + displayName: "Enable Sneaking Interpolation" + desc: "Enables interpolation in this state." + page: " Bow Aiming" +] \ No newline at end of file diff --git a/CodeGen/MCM/mcm/include/presets.psc b/CodeGen/MCM/mcm/include/presets.psc new file mode 100644 index 0000000..c6e94e8 --- /dev/null +++ b/CodeGen/MCM/mcm/include/presets.psc @@ -0,0 +1,61 @@ +SavePresetSetting savePresetSlot1 -> [ + presetIndex: 0 + displayName: "Slot 1:" + page: " Presets" +] +SavePresetSetting savePresetSlot2 -> [ + presetIndex: 1 + displayName: "Slot 2:" + page: " Presets" +] +SavePresetSetting savePresetSlot3 -> [ + presetIndex: 2 + displayName: "Slot 3:" + page: " Presets" +] +SavePresetSetting savePresetSlot4 -> [ + presetIndex: 3 + displayName: "Slot 4:" + page: " Presets" +] +SavePresetSetting savePresetSlot5 -> [ + presetIndex: 4 + displayName: "Slot 5:" + page: " Presets" +] +SavePresetSetting savePresetSlot6 -> [ + presetIndex: 5 + displayName: "Slot 6:" + page: " Presets" +] + +LoadPresetSetting loadPresetSlot1 -> [ + presetIndex: 0 + displayName: "Slot 1:" + page: " Presets" +] +LoadPresetSetting loadPresetSlot2 -> [ + presetIndex: 1 + displayName: "Slot 2:" + page: " Presets" +] +LoadPresetSetting loadPresetSlot3 -> [ + presetIndex: 2 + displayName: "Slot 3:" + page: " Presets" +] +LoadPresetSetting loadPresetSlot4 -> [ + presetIndex: 3 + displayName: "Slot 4:" + page: " Presets" +] +LoadPresetSetting loadPresetSlot5 -> [ + presetIndex: 4 + displayName: "Slot 5:" + page: " Presets" +] +LoadPresetSetting loadPresetSlot6 -> [ + presetIndex: 5 + displayName: "Slot 6:" + page: " Presets" +] \ No newline at end of file diff --git a/CodeGen/MCM/mcm/mcm.psc b/CodeGen/MCM/mcm/mcm.psc new file mode 100644 index 0000000..56eedbc --- /dev/null +++ b/CodeGen/MCM/mcm/mcm.psc @@ -0,0 +1,257 @@ +ScriptName SmoothCamMCM extends SKI_ConfigBase +Import SKSE +string[] interpMethods +string[] presets +string[] crosshairTypes +string activePage + +#include "include/native_functions.psc" + +int Function GetCurrentArrayIndex(string setting, string[] array) + string value = SmoothCam_GetStringConfig(setting) + + Int i = array.Length + While i + i -= 1 + if (array[i] == value) + return i + endIf + endWhile + + return 0 +endFunction + +#include "include/presets.psc" +#include "include/general.psc" +#include "include/following.psc" +#include "include/crosshair.psc" +#include "include/offset_groups.psc" +#include "include/controls.psc" + +#constexpr_struct ScriptMeta [ + int version = 0 + mangle string d3dHookString = "D3DHooked" +] + +ScriptMeta scriptMetaInfo -> [ + version: 13 +] + +int Function GetVersion() + return scriptMetaInfo.version +endFunction + +event OnConfigInit() + Pages = new string[] -> [ + " General", " Following", " Crosshair", " Standing", + " Walking", " Running", " Sprinting", " Sneaking", + " Swimming", " Bow Aiming", " Sitting", " Horseback", + " Dragon", " Vampire Lord", " Werewolf", " Group Edit", " Presets" + ] + interpMethods = new string[] -> [ + "linear", "quadraticEaseIn", "quadraticEaseOut", + "quadraticEaseInOut", "cubicEaseIn", "cubicEaseOut", + "cubicEaseInOut", "quarticEaseIn", "quarticEaseOut", + "quarticEaseInOut", "quinticEaseIn", "quinticEaseOut", + "quinticEaseInOut", "sineEaseIn", "sineEaseOut", + "sineEaseInOut", "circularEaseIn", "circularEaseOut", + "circularEaseInOut", "exponentialEaseIn", "exponentialEaseOut", + "exponentialEaseInOut" + ] + crosshairTypes = new string[] -> [ + "Skyrim", "Dot" + ] +endEvent + +event OnVersionUpdate(int version) + OnConfigInit() +endEvent + +event OnPageReset(string a_page) + SetCursorFillMode(TOP_TO_BOTTOM) + activePage = a_page + + if (a_page == " General") + AddHeaderOption("Plugin Info") + int version_T = AddTextOption("DLL Version", GetPluginVersion("SmoothCam"), OPTION_FLAG_DISABLED) + int s_version_T = AddTextOption("MCM Script Version", scriptMetaInfo.version, OPTION_FLAG_DISABLED) + int hasDXContext = AddTextOption("D3D11 Hooked", SmoothCam_GetBoolConfig(scriptMetaInfo.d3dHookString), OPTION_FLAG_DISABLED) + + AddHeaderOption("General") + #StructInvokeOn(implControl, [ + modEnabled, modEnabledKey, + toggleCustomZOffset, customZOffsetAmount, + ]) + + SetCursorPosition(1) + AddHeaderOption("Bug Fixes & Misc Options") + #StructInvokeOn(implControl, [ + forceCameraState, resetCrosshair, + reset, enableCrashDumps + ]) + + AddHeaderOption("Compatibility Options") + int icDetected = AddTextOption("Improved Camera Beta4", SmoothCam_IsImprovedCameraDetected(), OPTION_FLAG_DISABLED) + + #StructInvokeOn(implControl, [ + accCompat, icCompat, ifpvCompat, agoCompat + ]) + + elseIf (a_page == " Following") + AddHeaderOption("Interpolation") + #StructInvokeOn(implControl, [ + interpEnabled, interpMethod, minCameraFollowDistance, minCameraFollowRate, maxCameraFollowRate, maxSmoothingInterpDistance + ]) + + AddHeaderOption("Separate Z Interpolation") + #StructInvokeOn(implControl, [ + sepZInterpEnabled, sepZInterpMethod, minSepZFollowRate, maxSepZFollowRate, maxSepZSmoothingDistance + ]) + + AddHeaderOption("Local-Space Interpolation") + #StructInvokeOn(implControl, [ + sepLocalInterpEnabled, sepLocalInterpMethod, sepLocalSpaceInterpRate + ]) + + AddHeaderOption("Offset Interpolation") + #StructInvokeOn(implControl, [ + offsetInterpEnabled, offsetInterpMethod, offsetTransitionDuration + ]) + + AddHeaderOption("Zoom Interpolation") + #StructInvokeOn(implControl, [ + zoomInterpEnabled, zoomInterpMethod, zoomTransitionDuration + ]) + + AddHeaderOption("FOV Interpolation") + #StructInvokeOn(implControl, [ + fovInterpEnabled, fovInterpMethod, fovTransitionDuration + ]) + + SetCursorPosition(1) + AddHeaderOption("Distance Clamping") + #StructInvokeOn(implControl, [ + cameraDistanceClampXEnable, cameraDistanceClampXMin, cameraDistanceClampXMax, + cameraDistanceClampYEnable, cameraDistanceClampYMin, cameraDistanceClampYMax, + cameraDistanceClampZEnable, cameraDistanceClampZMin, cameraDistanceClampZMax + ]) + + AddHeaderOption("Misc") + #StructInvokeOn(implControl, [ + shoulderSwapKey, swapDistanceClampXAxis, nextPresetKey, + zoomMul, disableDeltaTime + ]) + + elseIf (a_page == " Crosshair") + AddHeaderOption("3D Crosshair Settings") + #StructInvokeOn(implControl, [ + crosshair3DBowEnabled, crosshair3DMagicEnabled, + crosshair3DWorldEnabled, worldCrosshairType, worldCrosshairDepthTest, + enableCrosshairSizeManip, crosshairMinDistSize, + crosshairMaxDistSize, crosshairNPCGrowSize + ]) + + AddHeaderOption("Crosshair Hiding") + #StructInvokeOn(implControl, [ + hideCrosshairOutOfCombat, hideCrosshairMeleeCombat, + ]) + + SetCursorPosition(1) + AddHeaderOption("Archery Features") + #StructInvokeOn(implControl, [ + enableArrowPrediction, drawArrowArc, maxArrowPredictionRange, + arrowArcColorR, arrowArcColorG, arrowArcColorB, + arrowArcColorA, + ]) + + AddHeaderOption("Sneak Settings") + #StructInvokeOn(implControl, [ + offsetStealthMeter, alwaysOffsetStealthMeter, stealthMeterOffsetX, stealthMeterOffsetY + ]) + + elseIf (a_page == " Standing") + #ImplOffsetGroupPage(Standing) + + elseIf (a_page == " Walking") + #ImplOffsetGroupPage(Walking) + + elseIf (a_page == " Running") + #ImplOffsetGroupPage(Running) + + elseIf (a_page == " Sprinting") + #ImplOffsetGroupPage(Sprinting) + + elseIf (a_page == " Sneaking") + #ImplOffsetGroupPage(Sneaking) + + elseIf (a_page == " Swimming") + #ImplOffsetGroupPage(Swimming) + + elseIf (a_page == " Bow Aiming") + AddHeaderOption("Bow Aiming Offsets") + #StructInvokeOn(implControl, [ + bowaim_sideOffset, + bowaim_upOffset, + bowaim_zoomOffset, + bowaim_fovOffset, + bowaim_sideOffsetHorseback, + bowaim_upOffsetHorseback, + bowaim_zoomOffsetHorseback, + bowaim_fovOffsetHorseback, + bowaim_sideOffsetSneaking, + bowaim_upOffsetSneaking, + bowaim_zoomOffsetSneaking, + bowaim_fovOffsetSneaking + ]) + + SetCursorPosition(1) + AddHeaderOption("Interpolation") + #StructInvokeOn(implControl, [ + bowaim_interp, + bowaim_interpHorseback, + bowaim_interpSneaking + ]) + + elseIf (a_page == " Sitting") + #ImplOffsetGroupPage(Sitting) + + elseIf (a_page == " Horseback") + #ImplOffsetGroupPage(Horseback) + + elseIf (a_page == " Dragon") + #ImplOffsetGroupPage(Dragon) + + elseIf (a_page == " Vampire Lord") + #ImplOffsetGroupPage(VampireLord) + + elseIf (a_page == " Werewolf") + #ImplOffsetGroupPage(Werewolf) + + elseIf (a_page == " Group Edit") + AddHeaderOption("Edit All Offset Groups") + #ImplOffsetGroupPage(Group, NoSliderHeader, NoInterpToggles) + + elseIf (a_page == " Presets") + AddHeaderOption("Save Preset") + #StructInvokeOn(implControl, [ + savePresetSlot1, + savePresetSlot2, + savePresetSlot3, + savePresetSlot4, + savePresetSlot5, + savePresetSlot6 + ]) + + SetCursorPosition(1) + AddHeaderOption("Load Preset") + #StructInvokeOn(implControl, [ + loadPresetSlot1, + loadPresetSlot2, + loadPresetSlot3, + loadPresetSlot4, + loadPresetSlot5, + loadPresetSlot6 + ]) + + endIf +endEvent \ No newline at end of file diff --git a/CodeGen/MCM/paper/.vscode/launch.json b/CodeGen/MCM/paper/.vscode/launch.json new file mode 100644 index 0000000..a540fd4 --- /dev/null +++ b/CodeGen/MCM/paper/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(Windows) Launch", + "type": "cppvsdbg", + "request": "launch", + "program": "${workspaceFolder}/paper.exe", + "args": ["..\\mcm.psc", "out.psc"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "console": "externalTerminal" + } + ] +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/.vscode/tasks.json b/CodeGen/MCM/paper/.vscode/tasks.json new file mode 100644 index 0000000..8e62ea5 --- /dev/null +++ b/CodeGen/MCM/paper/.vscode/tasks.json @@ -0,0 +1,33 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "type": "dub", + "run": true, + "compiler": "dmd", + "archType": "x86_64", + "buildType": "debug", + "configuration": "application-debug", + "_generated": true, + "problemMatcher": [ + "$dmd" + ], + "group": "build" + } + { + "type": "dub", + "run": true, + "compiler": "dmd", + "archType": "x86_64", + "buildType": "release", + "configuration": "application-release", + "_generated": true, + "problemMatcher": [ + "$dmd" + ], + "group": "build" + } + ] +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/dub.json b/CodeGen/MCM/paper/dub.json new file mode 100644 index 0000000..af48638 --- /dev/null +++ b/CodeGen/MCM/paper/dub.json @@ -0,0 +1,34 @@ +{ + "authors": [ + "mwilsnd" + ], + "copyright": "Copyright © 2020, mwilsnd", + "description": "Preprocessor for Skyrim papyrus", + "license": "", + "name": "paper", + "configurations": [ + { + "name": "application-debug", + "platforms": [ + "windows" + ], + "targetType": "executable", + "versions": [ + "DesktopApp" + ] + }, + { + "dflags": [ + "-boundscheck=off" + ], + "name": "application-release", + "platforms": [ + "windows" + ], + "targetType": "executable", + "versions": [ + "DesktopApp" + ] + } + ] +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/dub.selections.json b/CodeGen/MCM/paper/dub.selections.json new file mode 100644 index 0000000..322586b --- /dev/null +++ b/CodeGen/MCM/paper/dub.selections.json @@ -0,0 +1,5 @@ +{ + "fileVersion": 1, + "versions": { + } +} diff --git a/CodeGen/MCM/paper/source/app.d b/CodeGen/MCM/paper/source/app.d new file mode 100644 index 0000000..70c1b1d --- /dev/null +++ b/CodeGen/MCM/paper/source/app.d @@ -0,0 +1,157 @@ +import tokenizer; +import tokenizer.utils; + +import constructs.include; +import constructs.const_struct; +import constructs.all_of_struct; +import constructs.struct_invoke_on; +import constructs.struct_invoke_switchifeq; +import constructs.auto_array; +import constructs.decl_offset_group; +import constructs.impl_offset_group; + +import fs = std.file; +import std.stdio; +import std.container.array; +import std.path; + +void main(string[] args) { + writeln("paper - I probably over-engineered this"); + + if (args.length < 2) { + writeln("You must provide an input script path"); + return; + } + + if (args.length < 3) { + writeln("You must provide an output script path"); + return; + } + + string data; + try { + data = cast(string)fs.read(args[1]); + } catch(fs.FileException e) { + writeln("Failed to read input file: ", e.toString()); + return; + } + + // Change the working directory to that of the input file + auto p = absolutePath(args[1]).buildNormalizedPath.dirName; + fs.chdir(p); + + TokenStream tokens; + auto err = tokenize(data, tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + + { + scope auto inc = new Include(); + err = inc.apply(tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + } + + auto declOfs = new DeclareOffsetGroup(); + err = declOfs.apply(tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + + { + scope auto implOfs = new ImplOffsetGroup(); + implOfs.setOFSMgr(declOfs); + err = implOfs.apply(tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + } + + auto constStruct = new ConstStructParser(); + err = constStruct.parse(tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + + { + scope auto allOfStruct = new AllOfStruct(); + allOfStruct.setConstStructTool(constStruct); + err = allOfStruct.apply(tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + } + { + scope auto invokeOn = new StructInvokeOn(); + invokeOn.setConstStructTool(constStruct); + err = invokeOn.apply(tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + } + { + scope auto autoArray = new AutoArray(); + err = autoArray.apply(tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + } + { + scope auto invokeSwitch = new StructInvokeSwitchIfEq(); + invokeSwitch.setConstStructTool(constStruct); + err = invokeSwitch.apply(tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + } + + err = constStruct.apply(tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + + /*{ + scope auto concat = new Concat(); + err = concat.apply(tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + } + + { + scope auto en = new Enumeration(); + err = en.parse(tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + + err = en.apply(tokens); + if (!err.isOk()) { + writeln(err.msg); + return; + } + }*/ + + try { + if (fs.exists(args[2])) + fs.remove(args[2]); + + fs.append(args[2], streamToString(tokens)); + } catch (fs.FileException e) { + writeln("Failed to write to output file: ", e.toString()); + } +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/constructs/all_of_struct.d b/CodeGen/MCM/paper/source/constructs/all_of_struct.d new file mode 100644 index 0000000..24c0c65 --- /dev/null +++ b/CodeGen/MCM/paper/source/constructs/all_of_struct.d @@ -0,0 +1,155 @@ +module constructs.all_of_struct; +import constructs.iconstruct; + +import constructs.const_struct; + +import keywords; +import tokenizer : Token, TokenStream; +import tokenizer.tokens; +import tokenizer.utils; +import lex_machine; + +import std.string; +import std.container.array; + +private enum MacroName = "ImplsOf"; + +/** + * DeclType # ImplsOf + * + * Becomes: implNameA, implNameB, implNameC, ... + */ +final class AllOfStruct : IConstruct { + public: + @disable this(ref return scope AllOfStruct rhs); + + /// ctor + this() {} + + void setConstStructTool(ref ConstStructParser parser) { + this.parser = parser; + } + + /** + * Process the token stream and build constructs + * Params: + * stream: Input tokens to parse + * Returns: Parse result + */ + Result!bool parse(ref const(TokenStream) stream) @trusted { + // do nothing + return Result!(bool).make(true); + } + + /** + * Apply generated constructs to the token stream + * Params: + * stream: Mutable token stream to apply to + * Returns: Apply result + */ + Result!bool apply(ref TokenStream stream) @trusted { + struct Memory { + string declType; + ulong startIndex = 0; + } + enum State { + ExpectDeclType, + ExpectHash, + ExpectMacro, + } + enum Res { + Continue, + BreakInner, + BreakOuter, + } + + LexMachine!( + State, State.ExpectDeclType, + Res, Res.Continue, + Memory + ) state; + + state.onState(State.ExpectDeclType, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.mem.declType = tok.value; + state.mem.startIndex = position; + state.gotoState(State.ExpectHash); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectHash, Tok.Hash, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectMacro); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectDeclType); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectMacro, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + if (tok.value != MacroName) { + state.resetMemory(); + state.gotoState(State.ExpectDeclType); + return Result!(Res).make(Res.Continue); + } + + // Done, now look for the decl type + const auto ty = parser.getDeclType(state.mem.declType); + if (ty is null) + return Result!(Res).fail("Unknown struct '" ~ + state.mem.declType ~ "'!" + ); + + // Now get all impls of that type + const auto list = parser.getImplsOfDeclType(ty); + if (list.length() == 0) + return Result!(Res).fail("No implementations of struct type '" ~ + state.mem.declType ~ "' were found!" + ); + + // And slice in our comma separated list of impl names + auto pre = stream[0..state.mem.startIndex]; + auto post = stream[position+1..$]; + + TokenStream mutated; + mutated ~= pre; + + for (ulong j = 0; j < list.length(); j++) { + const ConstStructImpl impl = list[j]; + mutated ~= Token(Tok.OtherValue, impl.implName); + + if (j != list.length()-1) + mutated ~= Token(Tok.Comma, ","); + } + + mutated ~= post; + stream = mutated; + + return Result!(Res).make(Res.BreakInner); + } + ); + + state.onEOS((ref TokenStream stream) => Result!(Res).make(Res.BreakOuter)); + + while (true) { + const auto res = state.exec(stream); + if (!res.isOk()) Result!(bool).failFrom(res); + if (res.unwrap() == Res.BreakOuter) break; + state.resetMemory(); + state.gotoState(State.ExpectDeclType); + } + + return Result!(bool).make(true); + } + + private: + ConstStructParser parser = null; +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/constructs/auto_array.d b/CodeGen/MCM/paper/source/constructs/auto_array.d new file mode 100644 index 0000000..b2e71d3 --- /dev/null +++ b/CodeGen/MCM/paper/source/constructs/auto_array.d @@ -0,0 +1,235 @@ +module constructs.auto_array; +import constructs.iconstruct; + +import keywords; +import tokenizer : Token, TokenStream; +import tokenizer.tokens; +import tokenizer.utils; +import lex_machine; + +import std.string; +import std.container.array; + +/** + * var = new type[] -> [a, b, c, ...] + * + * Becomes: + * var = new type[3] + * var[0] = a + * var[1] = b + * var[2] = c + */ +final class AutoArray : IConstruct { + public: + @disable this(ref return scope AutoArray rhs); + + /// ctor + this() {} + + /** + * Process the token stream and build constructs + * Params: + * stream: Input tokens to parse + * Returns: Parse result + */ + Result!bool parse(ref const(TokenStream) stream) @trusted { + // do nothing + return Result!(bool).make(true); + } + + /** + * Apply generated constructs to the token stream + * Params: + * stream: Mutable token stream to apply to + * Returns: Apply result + */ + Result!bool apply(ref TokenStream stream) @trusted { + import std.conv : to; + + struct Memory { + Tok type = Tok.tNone; + string varName; + ulong startIndex = 0; + } + enum State { + ExpectVarName, + ExpectAssignment, + ExpectNew, + ExpectType, + ExpectOpenBrace, + ExpectCloseBrace, + ExpectArrow, + ExpectInitList, + } + enum Res { + Continue, + BreakInner, + BreakOuter, + } + + LexMachine!( + State, State.ExpectVarName, + Res, Res.Continue, + Memory + ) state; + + state.onState(State.ExpectVarName, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.mem.varName = tok.value; + state.mem.startIndex = position; + state.gotoState(State.ExpectAssignment); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectAssignment, Tok.Assignment, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectNew); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectVarName); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectNew, Tok.kNew, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectType); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectVarName); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectType, [Tok.tBool, Tok.tFloat, Tok.tInt, Tok.tString], + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.mem.type = tok.type; + state.gotoState(State.ExpectOpenBrace); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectVarName); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectOpenBrace, Tok.OpenBrace, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectCloseBrace); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectVarName); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectCloseBrace, Tok.CloseBrace, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectArrow); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectVarName); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectArrow, Tok.Arrow, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectInitList); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectInitList, Tok.OpenBrace, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + // Now, read the contents of the list [...] + auto subStream = TokenStream(stream[position+1..$]); + auto values = getBalancedContent(subStream, Tok.OpenBrace, Tok.CloseBrace); + if (!values.isOk()) + return Result!(Res).failFrom(values); + + // We should have a comma separated list of value literals + auto list = explode(values.unwrap(), Tok.Comma); + if (!list.isOk()) + return Result!(Res).failFrom(list); + + const auto entries = list.unwrap(); + const auto num = entries.length(); + + // Now emit the array + TokenStream output; + auto pre = stream[0..state.mem.startIndex]; + output ~= pre; + + auto ws = getIndentation(stream, state.mem.startIndex-1); + + output ~= Token(Tok.OtherValue, state.mem.varName); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.Assignment, "="); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.kNew, "new"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.OtherValue, typeToString(state.mem.type)); + output ~= Token(Tok.OpenBrace, "["); + output ~= Token(Tok.NumericValue, to!string(num)); + output ~= Token(Tok.CloseBrace, "]"); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + ulong idx = 0; + foreach (ref Token e; entries) { + // Add indentation to make it look a bit nicer + if (ws.isOk()) + output ~= ws.unwrap()[0..$]; + + output ~= Token(Tok.OtherValue, state.mem.varName); + output ~= Token(Tok.OpenBrace, "["); + output ~= Token(Tok.NumericValue, to!string(idx)); + output ~= Token(Tok.CloseBrace, "]"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.Assignment, "="); + output ~= Token(Tok.Space, " "); + output ~= e; + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + idx++; + } + + auto post = stream[position+values.unwrap().length()+2..$]; + // Slice off any trailing empty line left over + auto postE = isEmptyLine(TokenStream(post)); + if (postE.isOk()) + post = post[postE.unwrap()..$]; + + output ~= post; + stream = output; + return Result!(Res).make(Res.BreakInner); + } + ); + + state.onEOS((ref TokenStream stream) => Result!(Res).make(Res.BreakOuter)); + + while (true) { + const auto res = state.exec(stream); + if (!res.isOk()) Result!(bool).failFrom(res); + if (res.unwrap() == Res.BreakOuter) break; + state.resetMemory(); + state.gotoState(State.ExpectVarName); + } + + return Result!(bool).make(true); + } +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/constructs/const_struct.d b/CodeGen/MCM/paper/source/constructs/const_struct.d new file mode 100644 index 0000000..3526f63 --- /dev/null +++ b/CodeGen/MCM/paper/source/constructs/const_struct.d @@ -0,0 +1,1285 @@ +module constructs.const_struct; +import constructs.iconstruct; + +import keywords; +import tokenizer : Token, TokenStream; +import tokenizer.tokens; +import tokenizer.utils; +import lex_machine; +import mangler; + +import std.string; +import std.uni : toLower; +import std.container.array; + +/// Container for supported base values +union VarValue { + /// Boolean value + bool boolValue = void; + /// Int value + int intValue = void; + /// Float value + float floatValue = void; + /// String value, also used to hold a literal + string stringValue = void; +} + +/// A member variable in a const struct +struct MemberVar { + /// The underlying type + Tok type = Tok.tNone; + /// If this is a real value or an inlined one + bool isReal = false; + /// Should we mangle this string value? + bool mangle = false; + /// The value assigned + VarValue defaultValue; + + invariant { + assert( + type == Tok.tBool || type == Tok.tFloat || + type == Tok.tInt || type == Tok.tString || + type == Tok.tLiteral || type == Tok.tNone + ); + + if (type == Tok.tLiteral) + assert(!isReal); + + if (mangle) + assert(type == Tok.tString); + } + + /** + * Converts the default value to a string + * Returns: string + */ + string valueToString() const { + import std.conv : to; + + switch (type) { + case Tok.tBool: + return to!string(defaultValue.boolValue); + case Tok.tInt: + return to!string(defaultValue.intValue); + case Tok.tFloat: + return to!string(defaultValue.floatValue); + case Tok.tLiteral: goto case; + case Tok.tString: + return defaultValue.stringValue; + default: assert(0); + } + } +} + +/// A member macro in a const struct +struct MemberMacro { + /// Name of the macro + string name; + /// Contents of the macro + TokenStream code; +} + +/// A #constexpr_struct decl +struct ConstStructDecl { + /// The name of the type + string typeName; + /// Default member vars + MemberVar[string] memberVars; + /// Macros + MemberMacro[string] memberMacros; + + /** + * Generate processed macro code for the given impl + * Params: + * impl = Implementation of the struct to process and populate + * Returns: Result + */ + Result!bool generateMacroCodeForImpl(ref ConstStructImpl impl) const + in(impl.typeName == this.typeName, "Attempt to generate macro impl for mismatched struct types!") + { + struct Memory { + ulong startIndex = 0; + } + enum State { + ExpectThis, + ExpectArrow, + ExpectVar, + } + enum Res { + Continue, + BreakInner, + BreakOuter, + } + + LexMachine!( + State, State.ExpectThis, + Res, Res.Continue, + Memory + ) state; + + state.onState(State.ExpectThis, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.mem.startIndex = position; + state.gotoState(State.ExpectArrow); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectArrow, Tok.Arrow, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectVar); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectThis); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectVar, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + auto pre = stream[0..state.mem.startIndex]; + auto post = stream[position+1..$]; + + auto va = impl.memberToActual(tok.value, true); + if (!va.isOk()) return Result!(Res).failFrom(va); + + TokenStream mutated; + mutated ~= pre; + mutated ~= va.unwrap(); + mutated ~= post; + stream = mutated; + return Result!(Res).make(Res.BreakInner); + } + ); + + state.onEOS((ref TokenStream stream) => Result!(Res).make(Res.BreakOuter)); + + foreach (string macroName, const MemberMacro mac; memberMacros) { + // We need to find and replace this-> with impl.memberToActual + // Copy the code + auto code = TokenStream(mac.code[0..$]); + + // Lex + while (true) { + state.resetMemory(); + state.gotoState(State.ExpectThis); + + const auto res = state.exec(code); + if (!res.isOk()) return Result!(bool).failFrom(res); + if (res.unwrap() == Res.BreakOuter) break; + state.resetMemory(); + state.gotoState(State.ExpectThis); + } + + // Register with impl + impl.macroCode[macroName] = code; + } + + return Result!(bool).make(true); + } +} + +/// A #constexpr_struct impl +struct ConstStructImpl { + /// The name of the struct type + string typeName; + /// The variable name of the impl + string implName; + /// All vars as defined in the struct, with values optionally set in the impl + MemberVar[string] memberVars; + /// Processed macro code, with 'this->' replaced with the unfolded name or inlined value + TokenStream[string] macroCode; + + /** + * Return the processed value of the given member variable + * + * Produces either a value literal or unfolded variable name + * Params: + * memberName = Member to access + * isRef = If the type is real, passing true generates a reference to the decl, not the decl itself + * Returns: actual name/value + */ + Result!TokenStream memberToActual()(auto ref inout(string) memberName, bool isRef = false) inout return { + import std.conv : to; + + const MemberVar* it = memberName in memberVars; + if (it is null) + return Result!(TokenStream).fail(memberName ~ " is not a member of " ~ implName ~ "!"); + + TokenStream output; + if (it.isReal) { + if (!isRef) { + output ~= Token(it.type, typeToString(it.type)); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.OtherValue, implName ~ "_" ~ memberName); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.Assignment, "="); + output ~= Token(Tok.Space, " "); + } else { + // We just want the name of the variable, this is a reference and not a declaration + output ~= Token(Tok.OtherValue, implName ~ "_" ~ memberName); + return Result!(TokenStream).make(output); + } + } + + switch (it.type) { + case Tok.tInt: + output ~= Token(Tok.NumericValue, to!string(it.defaultValue.intValue)); + break; + + case Tok.tFloat: + output ~= Token(Tok.NumericValue, to!string(it.defaultValue.floatValue)); + break; + + case Tok.tBool: + output ~= Token(Tok.NumericValue, to!string(it.defaultValue.boolValue)); + break; + + case Tok.tLiteral: + output ~= Token(Tok.OtherValue, it.defaultValue.stringValue); + break; + + case Tok.tString: + if (it.mangle) + output ~= Token(Tok.StringValue, mangle(it.defaultValue.stringValue)); + else + output ~= Token(Tok.StringValue, it.defaultValue.stringValue); + + break; + + default: assert(0); + } + + return Result!(TokenStream).make(output); + } +} + +/// Parser generator for #constexpr_struct +final class ConstStructParser : IConstruct { + public: + @disable this(ref return scope ConstStructParser rhs); + + /// ctor + this() {} + + /** + * Process the token stream and build constructs + * Params: + * tokenStream: Input tokens to parse + * Returns: Parse result + */ + Result!bool parse(ref const(TokenStream) tokenStream) @trusted { + auto err = findDecls(tokenStream); + if (!err.isOk()) return err; + + err = findImpls(tokenStream); + if (!err.isOk()) return err; + + return Result!(bool).make(true); + } + + /** + * Apply generated constructs to the token stream + * Params: + * tokenStream: Mutable token stream to apply to + * Returns: Apply result + */ + Result!bool apply(ref TokenStream tokenStream) @trusted { + auto err = stripDecls(tokenStream); + if (!err.isOk()) return err; + + err = stripImpls(tokenStream); + if (!err.isOk()) return err; + + err = applyMacroInvocations(tokenStream); + if (!err.isOk()) return err; + + err = applyDotAccessors(tokenStream); + if (!err.isOk()) return err; + + return Result!(bool).make(true); + } + + /** + * Gets the struct decl using the given name + * Params: + * name = Type name to lookup + * Returns: Decl* or null + */ + ConstStructDecl* getDeclType()(auto ref const(string) name) @safe { + return name in decls; + } + + /** + * Gets the struct impl using the given name + * Params: + * name = Impl name to lookup + * Returns: Impl* or null + */ + ConstStructImpl* getImplType()(auto ref const(string) name) @safe { + return name in impls; + } + + /** + * Gets a list of all impls of the given decl + * Params: + * decl = The declared struct type to get implementations of + * Returns: Impl array + */ + Array!ConstStructImpl getImplsOfDeclType(const ConstStructDecl* decl) @trusted { + Array!ConstStructImpl list; + foreach (ref const(string) name, ref ConstStructImpl impl; impls) { + if (impl.typeName == decl.typeName) + list ~= impl; + } + return list; + } + + private: + ConstStructDecl[string] decls; + ConstStructImpl[string] impls; + + /** + * Find all #constexpr_struct decls in the token stream + * Params: + * tokenStream = Stream to search + * Returns: Parse result + */ + Result!bool findDecls(ref const(TokenStream) stream) { + struct Memory { + string name; + } + enum State { + ExpectHash, + ExpectKeyword, + ExpectName, + ExpectOpenBrace, + } + enum Res { + Continue, + } + LexMachine!( + State, State.ExpectHash, + Res, Res.Continue, + Memory, const(TokenStream) + ) state; + + state.onState(State.ExpectHash, Tok.Hash, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + state.gotoState(State.ExpectKeyword); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectKeyword, Tok.OtherValue, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + if (tok.value == "constexpr_struct") + state.gotoState(State.ExpectName); + else + state.gotoState(State.ExpectHash); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + state.gotoState(State.ExpectHash); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectName, Tok.OtherValue, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + state.gotoState(State.ExpectOpenBrace); + state.mem.name = tok.value; + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectOpenBrace, Tok.OpenBrace, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + auto blkStream = TokenStream(stream[position+1..$]); + auto block = getBalancedContent(blkStream, Tok.OpenBrace, Tok.CloseBrace); + if (!block.isOk()) return Result!(Res).failFrom(block); + + auto blk = block.unwrap(); + auto err = digestDeclBlock(state.mem.name, blk); + if (!err.isOk()) return Result!(Res).failFrom(err); + + state.jump(blk.length() + 3); + state.resetMemory(); + state.gotoState(State.ExpectHash); + return Result!(Res).make(Res.Continue); + } + ); + + const auto res = state.exec(stream); + if (!res.isOk()) return Result!(bool).failFrom(res); + + return Result!(bool).make(true); + } + + /** + * Process the contents of a struct decl block and build metadata for it + * Params: + * name = The name of the struct type + * stream = Input stream of all tokens in the block + * Returns: Parse result + */ + Result!bool digestDeclBlock()(ref const(string) name, auto ref const(TokenStream) stream) { + import std.conv : to; + + // Inside a block we can expect the following things: + // A member variable decl + // A macro decl + ConstStructDecl decl; + decl.typeName = name; + + struct Memory { + bool isMacro = false; + bool isReal = false; + bool mangle = false; + Token type; + Token name; + } + enum State { + ExpectType, + ExpectName, + ExpectAssignment, + ExpectMacroCode, + ExpectVarValue, + } + enum Res { + Continue, + Break, + } + + LexMachine!( + State, State.ExpectType, + Res, Res.Continue, + Memory, const(TokenStream) + ) state; + + state.onState( + State.ExpectType, + [Tok.OtherValue, Tok.tBool, Tok.tString, Tok.tInt, Tok.tFloat, Tok.tLiteral], + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + if (tok.value == "real") { + if (state.mem.isReal) + return Result!(Res).fail( + "Cannot apply 'real' twice to member '" ~ tok.value ~ "'!" + ); + state.mem.isReal = true; + + } else if (tok.value == "mangle") { + if (state.mem.mangle) + return Result!(Res).fail( + "Cannot apply 'mangle' twice to member '" ~ tok.value ~ "'!" + ); + state.mem.mangle = true; + + } else { + if (tok.value == "MACRO") { + if (state.mem.isReal || state.mem.mangle) + return Result!(Res).fail( + "Cannot apply 'real' or 'mangle' to macro '" ~ tok.value ~ "'!" + ); + + state.mem.isMacro = true; + state.gotoState(State.ExpectName); + + } else { + switch (tok.type) { + case Tok.tLiteral: + // Reals are actual variables and require a defined storage class + if (state.mem.isReal) + return Result!(Res).fail("Cannot apply 'real' attr to a literal type!"); + goto case; + + case Tok.tBool: goto case; + case Tok.tFloat: goto case; + case Tok.tInt: goto case; + case Tok.tString: + if (state.mem.mangle && tok.type != Tok.tString) + return Result!(Res).fail("'mangle' can only be applied to strings!"); + + state.mem.type = tok; + state.gotoState(State.ExpectName); + break; + default: + return Result!(Res).fail("Expected a type but got '" ~ tok.value ~ "'!"); + } + } + } + + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectName, Tok.OtherValue, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + state.mem.name = tok; + state.gotoState(State.ExpectAssignment); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectAssignment, Tok.Assignment, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + if (state.mem.isMacro) + state.gotoState(State.ExpectMacroCode); + else + state.gotoState(State.ExpectVarValue); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectVarValue, [Tok.OtherValue, Tok.NumericValue, Tok.StringValue], + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + // Enforce numeric literals for number types + if (state.mem.type.type == Tok.tFloat || state.mem.type.type == Tok.tInt) { + if (tok.type != Tok.NumericValue) + return Result!(Res).fail( + "Expected a numeric value, not '" ~ tok.value ~ "'!" + ); + + // And strings for strings + } else if (state.mem.type.type == Tok.tString) { + if (tok.type != Tok.StringValue) + return Result!(Res).fail( + "Expected a string value, not '" ~ tok.value ~ "'!" + ); + + // OtherValue for literals + } else if (state.mem.type.type == Tok.tLiteral) { + if (tok.type != Tok.OtherValue) + return Result!(Res).fail( + "Expected a literal value, not '" ~ tok.value ~ "'!" + ); + } else if (state.mem.type.type == Tok.tBool) { + if (tok.type != Tok.OtherValue || (tok.value.toLower != "true" && tok.value.toLower != "false")) + return Result!(Res).fail( + "Expected a boolean value, not '" ~ tok.value ~ "'!" + ); + } + + MemberVar var; + var.type = state.mem.type.type; + var.mangle = state.mem.mangle; + var.isReal = state.mem.isReal; + + switch (var.type) { + case Tok.tBool: + if (tok.value.toLower == "true") + var.defaultValue.boolValue = true; + else if (tok.value.toLower == "false") + var.defaultValue.boolValue = false; + else + return Result!(Res).fail("Expected a boolean but got '" ~ tok.value ~ "'!"); + break; + + case Tok.tInt: + var.defaultValue.intValue = to!int(tok.value); + break; + + case Tok.tFloat: + var.defaultValue.floatValue = to!float(tok.value); + break; + + case Tok.tLiteral: goto case; + case Tok.tString: + var.defaultValue.stringValue = tok.value; + break; + + default: assert(0); + } + + decl.memberVars[state.mem.name.value] = var; + state.resetMemory(); + state.gotoState(State.ExpectType); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectMacroCode, Tok.OpenBrace, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + const auto subStream = TokenStream(stream[position+1..$]); + auto err = getBalancedContent(subStream, Tok.OpenBrace, Tok.CloseBrace); + if (!err.isOk()) + return Result!(Res).failFrom(err); + + auto code = err.unwrap(); + MemberMacro mac; + mac.code = trimIndentation(trim!true(code)); + mac.name = state.mem.name.value; + decl.memberMacros[state.mem.name.value] = mac; + + state.jump(code.length()+2); + state.resetMemory(); + state.gotoState(State.ExpectType); + return Result!(Res).make(Res.Continue); + } + ); + + const auto res = state.exec(stream); + if (!res.isOk()) return Result!(bool).failFrom(res); + + decls[name] = decl; + return Result!(bool).make(true); + } + + /** + * Find implementations of #constexpr_struct decls who's type we've seen + * Params: + * tokenStream = Stream to search + * Returns: Parse result + */ + Result!bool findImpls(ref const(TokenStream) stream) { + struct Memory { + string typeName; + string implName; + } + enum State { + ExpectType, + ExpectName, + ExpectArrow, + ExpectBody, + } + enum Res { + Continue, + } + LexMachine!( + State, State.ExpectType, + Res, Res.Continue, + Memory, const(TokenStream) + ) state; + + state.onState(State.ExpectType, Tok.OtherValue, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + state.mem.typeName = tok.value; + state.gotoState(State.ExpectName); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectName, Tok.OtherValue, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + state.mem.implName = tok.value; + state.gotoState(State.ExpectArrow); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectType); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectArrow, Tok.Arrow, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + state.gotoState(State.ExpectBody); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectType); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectBody, Tok.OpenBrace, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + const auto decl = state.mem.typeName in decls; + if (decl is null) + return Result!(Res).fail( + "Cannot create instance of unknown struct type '" ~ state.mem.typeName ~ "'!" + ); + + auto subStream = TokenStream(stream[position+1..$]); + auto err = getBalancedContent(subStream, Tok.OpenBrace, Tok.CloseBrace); + if (!err.isOk()) + return Result!(Res).failFrom(err); + + auto blk = err.unwrap(); + auto implErr = digestImplBlock(decl, state.mem.implName, blk); + if (!implErr.isOk()) + return Result!(Res).failFrom(implErr); + + state.jump(blk.length() + 3); + state.resetMemory(); + state.gotoState(State.ExpectType); + return Result!(Res).make(Res.Continue); + } + ); + + const auto res = state.exec(stream); + if (!res.isOk()) Result!(bool).failFrom(res); + + return Result!(bool).make(true); + } + + /** + * Process the contents of a struct impl block and build metadata for it + * Params: + * decl = The decl type + * name = The name of the impl + * stream = Input stream of all tokens in the block + * Returns: Parse result + */ + Result!bool digestImplBlock()(const(ConstStructDecl*) decl, auto ref const(string) name, + auto ref const(TokenStream) stream) + { + import std.conv : to; + + struct Memory { + string memberName; + } + enum State { + ExpectName, + ExpectColon, + ExpectValue, + } + enum Res { + Continue, + } + LexMachine!( + State, State.ExpectName, + Res, Res.Continue, + Memory, const(TokenStream) + ) state; + + ConstStructImpl impl; + impl.typeName = decl.typeName; + impl.implName = name; + + // First copy over all defaults from the decl + foreach (ref const(string) memName, ref const(MemberVar) var; decl.memberVars) { + impl.memberVars[memName] = var; + } + + state.onState(State.ExpectName, Tok.OtherValue, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + state.mem.memberName = tok.value; + state.gotoState(State.ExpectColon); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectColon, Tok.Colon, + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + // Make sure this is a declared member in the decl + if (state.mem.memberName !in impl.memberVars) + return Result!(Res).fail( + "Attempt to assign to undeclared member '" ~ state.mem.memberName ~ "'!" + ); + + state.gotoState(State.ExpectValue); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState( + State.ExpectValue, + [Tok.BoolValue, Tok.NumericValue, Tok.StringValue, Tok.OtherValue], + (ref const(Token) tok, ref const(TokenStream) stream, ulong position) { + // Here we need to check the declared type of the var + // Then expect that exact type - for tLiteral, we want Tok.OtherValue + MemberVar* expectType = state.mem.memberName in impl.memberVars; + assert(expectType !is null); + + switch (expectType.type) { + case Tok.tInt: + if (tok.type != Tok.NumericValue) + return Result!(Res).fail("Expected a numeric value but got '" ~ tok.value ~ "'!"); + + expectType.defaultValue.intValue = to!int(tok.value); + break; + + case Tok.tFloat: + if (tok.type != Tok.NumericValue) + return Result!(Res).fail("Expected a numeric value but got '" ~ tok.value ~ "'!"); + + expectType.defaultValue.floatValue = to!float(tok.value); + break; + + case Tok.tBool: + if (tok.type != Tok.BoolValue) + return Result!(Res).fail("Expected a boolean value but got '" ~ tok.value ~ "'!"); + + if (tok.value.toLower != "true" && tok.value.toLower != "false") + return Result!(Res).fail("Malformed boolean value '" ~ tok.value ~ "'!"); + + expectType.defaultValue.boolValue = tok.value.toLower == "true"; + break; + + case Tok.tString: + if (tok.type != Tok.StringValue) + return Result!(Res).fail("Expected a string value but got '" ~ tok.value ~ "'!"); + + expectType.defaultValue.stringValue = tok.value; + break; + + case Tok.tLiteral: + if (tok.type != Tok.OtherValue) + return Result!(Res).fail("Expected a literal value but got '" ~ tok.value ~ "'!"); + + expectType.defaultValue.stringValue = tok.value; + break; + + default: assert(0); + } + + state.resetMemory(); + state.gotoState(State.ExpectName); + return Result!(Res).make(Res.Continue); + } + ); + + // Now for each var we see in the block, assert it is in our list above, + // then overwrite the value in the impl with what we read + const auto res = state.exec(stream); + if (!res.isOk()) Result!(bool).failFrom(res); + + // Register the impl and generate macro code + auto macGen = decl.generateMacroCodeForImpl(impl); + if (!macGen.isOk()) return macGen; + impls[impl.implName] = impl; + + return Result!(bool).make(true); + } + + /** + * Strip #constexpr_struct decls from the token stream + * Params: + * tokenStream = Stream to mutate + * Returns: Apply result + */ + Result!bool stripDecls(ref TokenStream stream) { + struct Memory { + ulong startIndex = 0; + } + enum State { + ExpectHash, + ExpectKeyword, + ExpectName, + ExpectOpenBrace, + } + enum Res { + Continue, + BreakInner, + BreakOuter, + } + LexMachine!( + State, State.ExpectHash, + Res, Res.Continue, + Memory, TokenStream + ) state; + + state.onState(State.ExpectHash, Tok.Hash, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectKeyword); + state.mem.startIndex = position; + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectKeyword, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + if (tok.value == "constexpr_struct") + state.gotoState(State.ExpectName); + else { + state.resetMemory(); + state.gotoState(State.ExpectHash); + } + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectHash); + state.resetMemory(); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectName, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectOpenBrace); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectOpenBrace, Tok.OpenBrace, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + auto blkStream = TokenStream(stream[position+1..$]); + auto block = getBalancedContent(blkStream, Tok.OpenBrace, Tok.CloseBrace); + if (!block.isOk()) return Result!(Res).failFrom(block); + + // Now we know the bounds of this decl, slice it out + auto pre = stream[0..state.mem.startIndex]; + auto post = stream[position+block.unwrap().length()+2..$]; + + // Slice off any trailing empty line left over + auto postE = isEmptyLine(TokenStream(post)); + if (postE.isOk()) + post = post[postE.unwrap()..$]; + + TokenStream sliced; + sliced ~= pre; + sliced ~= post; + stream = sliced; + + return Result!(Res).make(Res.BreakInner); + } + ); + + state.onEOS((ref TokenStream stream) => Result!(Res).make(Res.BreakOuter)); + + while (true) { + const auto res = state.exec(stream); + if (!res.isOk()) Result!(bool).failFrom(res); + if (res.unwrap() == Res.BreakOuter) break; + state.resetMemory(); + state.gotoState(State.ExpectHash); + } + + return Result!(bool).make(true); + } + + /** + * Strip #constexpr_struct impls from the token stream + * + * If any real members are defined, these are also placed in the now empty location in the stream + * Params: + * tokenStream = Stream to mutate + * Returns: Apply result + */ + Result!bool stripImpls(ref TokenStream stream) { + struct Memory { + ulong startIndex = 0; + string implName; + } + enum State { + ExpectType, + ExpectName, + ExpectArrow, + ExpectBody, + } + enum Res { + Continue, + BreakInner, + BreakOuter, + } + LexMachine!( + State, State.ExpectType, + Res, Res.Continue, + Memory, TokenStream + ) state; + + state.onState(State.ExpectType, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + if (tok.value in decls) { + state.gotoState(State.ExpectName); + state.mem.startIndex = position; + } + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectName, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.mem.implName = tok.value; + state.gotoState(State.ExpectArrow); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectType); + state.resetMemory(); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectArrow, Tok.Arrow, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectBody); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectType); + state.resetMemory(); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectBody, Tok.OpenBrace, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + auto blkStream = TokenStream(stream[position+1..$]); + auto block = getBalancedContent(blkStream, Tok.OpenBrace, Tok.CloseBrace); + if (!block.isOk()) return Result!(Res).failFrom(block); + + // Now we know the bounds of this decl, slice it out + auto pre = stream[0..state.mem.startIndex]; + auto post = stream[position+block.unwrap().length()+2..$]; + + // Slice off any trailing empty line left over + auto postE = isEmptyLine(TokenStream(post)); + if (postE.isOk()) + post = post[postE.unwrap()..$]; + + TokenStream sliced; + sliced ~= pre; + + // Generate reals for any members that specify so + const ConstStructImpl* impl = state.mem.implName in impls; + if (impl is null) + return Result!(Res).fail(state.mem.implName ~ " was not found!"); + + foreach (ref const(string) name, ref const(MemberVar) var; impl.memberVars) { + if (!var.isReal) continue; + + auto memReal = impl.memberToActual(name); + if (!memReal.isOk()) return Result!(Res).failFrom(memReal); + sliced ~= memReal.unwrap(); + sliced ~= Token(Tok.CharReturn, "\r"); + sliced ~= Token(Tok.NewLine, "\n"); + } + + sliced ~= post; + stream = sliced; + + return Result!(Res).make(Res.BreakInner); + } + ); + + state.onEOS((ref TokenStream stream) => Result!(Res).make(Res.BreakOuter)); + + while (true) { + const auto res = state.exec(stream); + if (!res.isOk()) Result!(bool).failFrom(res); + if (res.unwrap() == Res.BreakOuter) break; + state.resetMemory(); + state.gotoState(State.ExpectType); + } + + return Result!(bool).make(true); + } + + /** + * Matches all macro invocation syntax (->!) and replaces with generated code + * Params: + * stream = Stream to mutate + * Returns: Apply result + */ + Result!bool applyMacroInvocations(ref TokenStream stream) { + struct Memory { + ulong startIndex = 0; + string implName; + } + enum State { + ExpectImplName, + ExpectArrow, + ExpectExclamation, + ExpectMacroName, + } + enum Res { + Continue, + BreakInner, + BreakOuter, + } + LexMachine!( + State, State.ExpectImplName, + Res, Res.Continue, + Memory, TokenStream + ) state; + + state.onState(State.ExpectImplName, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.mem.startIndex = position; + state.mem.implName = tok.value; + state.gotoState(State.ExpectArrow); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectArrow, Tok.Arrow, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectExclamation); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectImplName); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectExclamation, Tok.Exclamation, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectMacroName); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectImplName); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectMacroName, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + // Find the impl + ConstStructImpl* impl = state.mem.implName in impls; + if (impl is null) + return Result!(Res).fail("Unknown struct type '" ~ state.mem.implName ~ "'!"); + + // Find the macro + auto mac = tok.value in impl.macroCode; + if (mac is null) + return Result!(Res).fail("Unknown macro '" ~ tok.value ~ "'!"); + + auto pre = stream[0..state.mem.startIndex]; + auto preE = getIndentation(stream, state.mem.startIndex-1); + if (preE.isOk() && preE.unwrap().length > 0) { + const auto pl = preE.unwrap().length; + const auto idx = state.mem.startIndex - pl; + pre = stream[0..idx]; + } + + TokenStream mutated; + mutated ~= pre; + + if (preE.isOk() && preE.unwrap().length > 0) { + mutated ~= indent(*mac, preE.unwrap()).unwrap(); + } else { + mutated ~= *mac; + } + + mutated ~= stream[position+1..$]; + stream = mutated; + return Result!(Res).make(Res.BreakInner); + } + ); + + state.onEOS((ref TokenStream stream) => Result!(Res).make(Res.BreakOuter)); + + while (true) { + const auto res = state.exec(stream); + if (!res.isOk()) Result!(bool).failFrom(res); + if (res.unwrap() == Res.BreakOuter) break; + state.resetMemory(); + state.gotoState(State.ExpectImplName); + } + + return Result!(bool).make(true); + } + + /** + * Convert impl.member syntax to actual l/rvalue + * Params: + * stream = Stream to mutate + * Returns: Apply result + */ + Result!bool applyDotAccessors(ref TokenStream stream) { + struct Memory { + ulong startIndex = 0; + string implName; + } + enum State { + ExpectImpl, + ExpectDot, + ExpectMember, + } + enum Res { + Continue, + BreakInner, + BreakOuter, + } + LexMachine!( + State, State.ExpectImpl, + Res, Res.Continue, + Memory, TokenStream + ) state; + + state.onState(State.ExpectImpl, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.mem.startIndex = position; + state.mem.implName = tok.value; + state.gotoState(State.ExpectDot); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectDot, Tok.Dot, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + ConstStructImpl* impl = state.mem.implName in impls; + if (impl is null) { + state.gotoState(State.ExpectImpl); + state.resetMemory(); + } else + state.gotoState(State.ExpectMember); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectImpl); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectMember, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + ConstStructImpl* impl = state.mem.implName in impls; + if (impl is null) + return Result!(Res).fail("Unknown struct type '" ~ state.mem.implName ~ "'!"); + + auto var = impl.memberToActual(tok.value, true); + if (!var.isOk()) + return Result!(Res).fail("Unknown member '" ~ tok.value ~ "'!"); + + TokenStream mutated; + mutated ~= stream[0..state.mem.startIndex]; + mutated ~= var.unwrap(); + mutated ~= stream[position+1..$]; + stream = mutated; + + return Result!(Res).make(Res.BreakInner); + } + ); + + state.onEOS((ref TokenStream stream) => Result!(Res).make(Res.BreakOuter)); + + while (true) { + const auto res = state.exec(stream); + if (!res.isOk()) Result!(bool).failFrom(res); + if (res.unwrap() == Res.BreakOuter) break; + state.resetMemory(); + state.gotoState(State.ExpectImpl); + } + + return Result!(bool).make(true); + } +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/constructs/decl_offset_group.d b/CodeGen/MCM/paper/source/constructs/decl_offset_group.d new file mode 100644 index 0000000..675c181 --- /dev/null +++ b/CodeGen/MCM/paper/source/constructs/decl_offset_group.d @@ -0,0 +1,515 @@ +module constructs.decl_offset_group; +import constructs.iconstruct; + +import keywords; +import tokenizer : Token, TokenStream; +import tokenizer.tokens; +import tokenizer.utils; +import lex_machine; + +import std.conv : to; +import std.uni : toLower; +import std.array : replace; +import std.string; +import std.container.array; + +private enum MacroName = "CreateOffsetGroup"; +public { + /** + * A declared offset group, holds variable names that were generated + */ + struct OffsetDecl { + /// Name of all offset sliders + string[] offsets; + /// Name of all interp toggles + string[] toggles; + } + + alias VarIDMap = OffsetDecl[string]; +} + +private struct OffsetGroupDecl { + Token groupName; + Token pageName; + VarIDMap* varMap = null; + bool noMelee = false; + bool noRanged = false; + bool noMagic = false; + bool noInterpToggles = false; + + /** + * Create a SliderSetting #constexpr_struct implementation + * Params: + * settingName = Name of the setting being referenced + * groupName = Standing, Walking, Running... + * subGroup = Combat:Ranged, Combat:Melee... + * niceName = Name of the control shown to the user + * desc = Description of the setting + * def = Default value + * min = Minimum value + * max = Maximum value + * Returns: Generated token stream containing the slider impl + */ + TokenStream makeSlider(string settingName, string groupName, const(string)* subGroup, + string niceName, string desc, float def, float min, float max) + { + const auto var = subGroup !is null ? replace(*subGroup, ":", "") : ""; + const auto varName = groupName.toLower ~ "_" ~ settingName ~ var; + const auto settingKey = subGroup !is null ? + (groupName ~ *subGroup ~ ":" ~ settingName) : + (groupName ~ ":" ~ settingName); + + TokenStream output; + // SliderSetting -> [ + output ~= Token(Tok.OtherValue, "SliderSetting"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.OtherValue, varName); + output ~= Token(Tok.Arrow, "->"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.OpenBrace, "["); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + if (groupName !in (*varMap)) (*varMap)[groupName] = OffsetDecl(); + (*varMap)[groupName].offsets ~= varName; + + // var: value + output ~= Token(Tok.OtherValue, "settingName"); + output ~= Token(Tok.Colon, ":"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.StringValue, settingKey); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + output ~= Token(Tok.OtherValue, "displayName"); + output ~= Token(Tok.Colon, ":"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.StringValue, niceName); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + output ~= Token(Tok.OtherValue, "desc"); + output ~= Token(Tok.Colon, ":"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.StringValue, desc); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + output ~= Token(Tok.OtherValue, "defaultValue"); + output ~= Token(Tok.Colon, ":"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.NumericValue, to!string(def)); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + output ~= Token(Tok.OtherValue, "min"); + output ~= Token(Tok.Colon, ":"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.NumericValue, to!string(min)); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + output ~= Token(Tok.OtherValue, "max"); + output ~= Token(Tok.Colon, ":"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.NumericValue, to!string(max)); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + output ~= Token(Tok.OtherValue, "page"); + output ~= Token(Tok.Colon, ":"); + output ~= Token(Tok.Space, " "); + output ~= pageName; + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + // ] + output ~= Token(Tok.CloseBrace, "]"); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + return output; + } + + /** + * Create a ToggleSetting #constexpr_struct implementation + * Params: + * settingName = Name of the setting being referenced + * groupName = Standing, Walking, Running... + * subGroup = RangedCombat, ... + * niceName = Name of the control shown to the user + * desc = Description of the setting + * Returns: Generated token stream containing the toggle impl + */ + TokenStream makeToggle(string settingName, string groupName, const(string)* subGroup, + string niceName, string desc) + { + const auto var = subGroup !is null ? replace(*subGroup, ":", "") : settingName; + const auto varName = groupName.toLower ~ "_" ~ var; + const auto settingKey = settingName ~ groupName ~ (subGroup !is null ? *subGroup : ""); + + TokenStream output; + + // ToggleSetting -> [ + output ~= Token(Tok.OtherValue, "ToggleSetting"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.OtherValue, varName); + output ~= Token(Tok.Arrow, "->"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.OpenBrace, "["); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + (*varMap)[groupName].toggles ~= varName; + + // var: value + output ~= Token(Tok.OtherValue, "settingName"); + output ~= Token(Tok.Colon, ":"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.StringValue, settingKey); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + output ~= Token(Tok.OtherValue, "displayName"); + output ~= Token(Tok.Colon, ":"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.StringValue, niceName); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + output ~= Token(Tok.OtherValue, "desc"); + output ~= Token(Tok.Colon, ":"); + output ~= Token(Tok.Space, " "); + output ~= Token(Tok.StringValue, desc); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + output ~= Token(Tok.OtherValue, "page"); + output ~= Token(Tok.Colon, ":"); + output ~= Token(Tok.Space, " "); + output ~= pageName; + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + + // ] + output ~= Token(Tok.CloseBrace, "]"); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + return output; + } + + /** + * Create the complete collection of controls for the defined offset group + * Returns: Token stream containing all control impls + */ + TokenStream generate() { + TokenStream output; + + output ~= makeSlider( + "SideOffset", groupName.value, null, + "Side Offset", "The amount to move the camera to the right.", + 25, -100, 100 + ); + output ~= makeSlider( + "UpOffset", groupName.value, null, + "Up Offset", "The amount to move the camera up.", + 0, -100, 100 + ); + output ~= makeSlider( + "ZoomOffset", groupName.value, null, + "Zoom Offset", "The amount to offset the camera zoom by.", + 0, -200, 200 + ); + output ~= makeSlider( + "FOVOffset", groupName.value, null, + "FOV Offset", "The amount to offset the camera FOV by." ~ + " Note this will be clamped to a lower bound of 10 and an upper bound of 170.", + 0, -120, 120 + ); + + if (!noRanged) { + const auto ranged = "Combat:Ranged"; + output ~= makeSlider( + "SideOffset", groupName.value, &ranged, + "Ranged Side Offset", "The amount to move the camera to the right when in ranged combat.", + 25, -100, 100 + ); + output ~= makeSlider( + "UpOffset", groupName.value, &ranged, + "Ranged Up Offset", "The amount to move the camera up when in ranged combat.", + 0, -100, 100 + ); + output ~= makeSlider( + "ZoomOffset", groupName.value, &ranged, + "Ranged Zoom Offset", "The amount to offset the camera zoom by when in ranged combat.", + 0, -200, 200 + ); + output ~= makeSlider( + "FOVOffset", groupName.value, &ranged, + "Ranged FOV Offset", "The amount to offset the camera FOV by when in ranged combat." ~ + " Note this will be clamped to a lower bound of 10 and an upper bound of 170.", + 0, -120, 120 + ); + } + + if (!noMagic) { + const auto magic = "Combat:Magic"; + output ~= makeSlider( + "SideOffset", groupName.value, &magic, + "Magic Side Offset", "The amount to move the camera to the right when in magic combat.", + 25, -100, 100 + ); + output ~= makeSlider( + "UpOffset", groupName.value, &magic, + "Magic Up Offset", "The amount to move the camera up when in magic combat.", + 0, -100, 100 + ); + output ~= makeSlider( + "ZoomOffset", groupName.value, &magic, + "Magic Zoom Offset", "The amount to offset the camera zoom by when in magic combat.", + 0, -200, 200 + ); + output ~= makeSlider( + "FOVOffset", groupName.value, &magic, + "Magic FOV Offset", "The amount to offset the camera FOV by when in magic combat." ~ + " Note this will be clamped to a lower bound of 10 and an upper bound of 170.", + 0, -120, 120 + ); + } + + if (!noMelee) { + const auto melee = "Combat:Melee"; + output ~= makeSlider( + "SideOffset", groupName.value, &melee, + "Melee Side Offset", "The amount to move the camera to the right when in melee combat.", + 25, -100, 100 + ); + output ~= makeSlider( + "UpOffset", groupName.value, &melee, + "Melee Up Offset", "The amount to move the camera up when in melee combat.", + 0, -100, 100 + ); + output ~= makeSlider( + "ZoomOffset", groupName.value, &melee, + "Melee Zoom Offset", "The amount to offset the camera zoom by when in melee combat.", + 0, -200, 200 + ); + output ~= makeSlider( + "FOVOffset", groupName.value, &melee, + "Melee FOV Offset", "The amount to offset the camera FOV by when in melee combat." ~ + " Note this will be clamped to a lower bound of 10 and an upper bound of 170.", + 0, -120, 120 + ); + } + + if (!noInterpToggles) { + output ~= makeToggle( + "Interp", groupName.value, null, + "Enable Interpolation", "Enables interpolation in this state." + ); + if (!noRanged) { + const rc = "RangedCombat"; + output ~= makeToggle( + "Interp", groupName.value, &rc, + "Enable Ranged Interpolation", "Enables interpolation in this state." + ); + } + if (!noMagic) { + const magc = "MagicCombat"; + output ~= makeToggle( + "Interp", groupName.value, &magc, + "Enable Magic Interpolation", "Enables interpolation in this state." + ); + } + if (!noMelee) { + const melc = "MeleeCombat"; + output ~= makeToggle( + "Interp", groupName.value, &melc, + "Enable Melee Interpolation", "Enables interpolation in this state." + ); + } + } + + return output; + } +} + +/** + * Ex: #CreateOffsetGroup(Sitting, " Sitting", NoMelee, NoRanged, NoMagic) + * Generates all page elements for configuring the group + * + * Args: + * required: literal: group name + * required: string: page name + * opt... : flag literals + */ +final class DeclareOffsetGroup : IConstruct { + public: + @disable this(ref return scope DeclareOffsetGroup rhs); + + /// ctor + this() {} + + /** + * Process the token stream and build constructs + * Params: + * stream: Input tokens to parse + * Returns: Parse result + */ + Result!bool parse(ref const(TokenStream) stream) @trusted { + // do nothing + return Result!(bool).make(true); + } + + /** + * Apply generated constructs to the token stream + * Params: + * stream: Mutable token stream to apply to + * Returns: Apply result + */ + Result!bool apply(ref TokenStream stream) @trusted { + struct Memory { + ulong startIndex = 0; + OffsetGroupDecl decl; + } + enum State { + ExpectHash, + ExpectMacro, + ExpectOpenParen, + ExpectName, + ExpectPage, + ExpectComma, + ExpectCommaOrCloseParen, + ExpectOptFlag, + } + enum Res { + Continue, + BreakInner, + BreakOuter, + } + + LexMachine!( + State, State.ExpectHash, + Res, Res.Continue, + Memory + ) state; + + state.onState(State.ExpectHash, Tok.Hash, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectMacro); + state.mem.startIndex = position; + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectMacro, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + if (tok.value == MacroName) + state.gotoState(State.ExpectOpenParen); + else + state.gotoState(State.ExpectHash); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectHash); + state.resetMemory(); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectOpenParen, Tok.OpenParen, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectName); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectName, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectComma); + state.mem.decl.groupName = tok; + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectComma, Tok.Comma, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectPage); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectPage, Tok.StringValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectCommaOrCloseParen); + state.mem.decl.pageName = tok; + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectCommaOrCloseParen, [Tok.Comma, Tok.CloseParen], + (ref const(Token) tok, ref TokenStream stream, ulong position) { + // Here we can either end the invocation, or pass a list of flag literals + if (tok.type == Tok.Comma) + state.gotoState(State.ExpectOptFlag); + else { + auto pre = stream[0..state.mem.startIndex]; + auto post = stream[position+1..$]; + + state.mem.decl.varMap = &this.varIDs; + + TokenStream mutated; + mutated ~= pre; + mutated ~= state.mem.decl.generate()[0..$]; + mutated ~= post; + stream = mutated; + return Result!(Res).make(Res.BreakInner); + } + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectOptFlag, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + if (tok.value == "NoMelee") + state.mem.decl.noMelee = true; + else if (tok.value == "NoMagic") + state.mem.decl.noMagic = true; + else if (tok.value == "NoRanged") + state.mem.decl.noRanged = true; + else if (tok.value == "NoInterpToggles") + state.mem.decl.noInterpToggles = true; + else + return Result!(Res).fail("Unknown flag '" ~ tok.value ~ "'!"); + + state.gotoState(State.ExpectCommaOrCloseParen); + return Result!(Res).make(Res.Continue); + } + ); + + state.onEOS((ref TokenStream stream) => Result!(Res).make(Res.BreakOuter)); + + while (true) { + const auto res = state.exec(stream); + if (!res.isOk()) return Result!(bool).failFrom(res); + if (res.unwrap() == Res.BreakOuter) break; + state.resetMemory(); + state.gotoState(State.ExpectHash); + } + + return Result!(bool).make(true); + } + + /** + * Gets all generated offset group variable names + * Returns: VarID map + */ + ref const(VarIDMap) getVarIDs() const return { + return varIDs; + } + + private: + // When generating a new group, var names of all controls are stored here + VarIDMap varIDs; +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/constructs/iconstruct.d b/CodeGen/MCM/paper/source/constructs/iconstruct.d new file mode 100644 index 0000000..a8460bb --- /dev/null +++ b/CodeGen/MCM/paper/source/constructs/iconstruct.d @@ -0,0 +1,22 @@ +module constructs.iconstruct; +public import result; +import tokenizer : Token, TokenStream; + +/// A generic language addon construct +interface IConstruct { + /** + * Process the token stream and build constructs + * Params: + * tokenStream: Input tokens to parse + * Returns: Parse result + */ + Result!bool parse(ref const(TokenStream) tokenStream) @trusted; + + /** + * Apply generated constructs to the token stream + * Params: + * tokenStream: Mutable token stream to apply to + * Returns: Apply result + */ + Result!bool apply(ref TokenStream tokenStream) @trusted; +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/constructs/impl_offset_group.d b/CodeGen/MCM/paper/source/constructs/impl_offset_group.d new file mode 100644 index 0000000..cb5a5c7 --- /dev/null +++ b/CodeGen/MCM/paper/source/constructs/impl_offset_group.d @@ -0,0 +1,226 @@ +module constructs.impl_offset_group; +import constructs.iconstruct; +import constructs.decl_offset_group; + +import keywords; +import tokenizer : Token, TokenStream; +import tokenizer.tokens; +import tokenizer.utils; +import lex_machine; + +import std.container.array; + +private enum MacroName = "ImplOffsetGroupPage"; + +/** + * #ImplOffsetGroupPage(GroupName) + * Unrolls to ->!implControl on each control generated from #CreateOffsetGroup + */ +final class ImplOffsetGroup : IConstruct { + public: + @disable this(ref return scope DeclareOffsetGroup rhs); + + /// ctor + this() {} + + /** + * Sets the generator used to create offset groups + * Params: + * mgr = DeclareOffsetGroup + */ + void setOFSMgr(ref DeclareOffsetGroup mgr) { + ofsGroups = mgr; + } + + /** + * Process the token stream and build constructs + * Params: + * stream: Input tokens to parse + * Returns: Parse result + */ + Result!bool parse(ref const(TokenStream) stream) @trusted { + // do nothing + return Result!(bool).make(true); + } + + /** + * Apply generated constructs to the token stream + * Params: + * stream: Mutable token stream to apply to + * Returns: Apply result + */ + Result!bool apply(ref TokenStream stream) @trusted { + struct Memory { + ulong startIndex = 0; + Token value; + bool noSliderHeader = false; + bool noInterpToggles = false; + } + enum State { + ExpectHash, + ExpectMacro, + ExpectOpenParen, + ExpectName, + ExpectCommaOrCloseParen, + ExpectOptFlag, + } + enum Res { + Continue, + BreakInner, + BreakOuter, + } + + LexMachine!( + State, State.ExpectHash, + Res, Res.Continue, + Memory + ) state; + + state.onState(State.ExpectHash, Tok.Hash, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectMacro); + state.mem.startIndex = position; + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectMacro, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + if (tok.value == MacroName) + state.gotoState(State.ExpectOpenParen); + else + state.gotoState(State.ExpectHash); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectHash); + state.resetMemory(); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectOpenParen, Tok.OpenParen, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectName); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectName, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + auto vars = tok.value in ofsGroups.getVarIDs(); + if (vars is null) + Result!(Res).fail("Unknown offset group '" ~ tok.value ~ "'!"); + + state.mem.value = tok; + state.gotoState(State.ExpectCommaOrCloseParen); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectCommaOrCloseParen, [Tok.Comma, Tok.CloseParen], + (ref const(Token) tok, ref TokenStream stream, ulong position) { + if (tok.type == Tok.Comma) + state.gotoState(State.ExpectOptFlag); + else { + auto pre = stream[0..state.mem.startIndex]; + auto post = stream[position+1..$]; + auto vars = state.mem.value.value in ofsGroups.getVarIDs(); + + TokenStream generated; + if (!state.mem.noSliderHeader) { + generated ~= Token(Tok.OtherValue, "AddHeaderOption"); + generated ~= Token(Tok.OpenParen, "("); + generated ~= Token(Tok.StringValue, state.mem.value.value ~ " Offsets"); + generated ~= Token(Tok.CloseParen, ")"); + generated ~= Token(Tok.CharReturn, "\r"); + generated ~= Token(Tok.NewLine, "\n"); + } + + foreach (ref const(string) varName; (*vars).offsets) { + generated ~= Token(Tok.OtherValue, varName); + generated ~= Token(Tok.Arrow, "->"); + generated ~= Token(Tok.Exclamation, "!"); + generated ~= Token(Tok.OtherValue, "implControl"); + generated ~= Token(Tok.CharReturn, "\r"); + generated ~= Token(Tok.NewLine, "\n"); + } + + if (!state.mem.noInterpToggles) { + generated ~= Token(Tok.OtherValue, "SetCursorPosition"); + generated ~= Token(Tok.OpenParen, "("); + generated ~= Token(Tok.NumericValue, "1"); + generated ~= Token(Tok.CloseParen, ")"); + generated ~= Token(Tok.CharReturn, "\r"); + generated ~= Token(Tok.NewLine, "\n"); + + generated ~= Token(Tok.OtherValue, "AddHeaderOption"); + generated ~= Token(Tok.OpenParen, "("); + generated ~= Token(Tok.StringValue, "Interpolation"); + generated ~= Token(Tok.CloseParen, ")"); + generated ~= Token(Tok.CharReturn, "\r"); + generated ~= Token(Tok.NewLine, "\n"); + + foreach (ref const(string) varName; (*vars).toggles) { + generated ~= Token(Tok.OtherValue, varName); + generated ~= Token(Tok.Arrow, "->"); + generated ~= Token(Tok.Exclamation, "!"); + generated ~= Token(Tok.OtherValue, "implControl"); + generated ~= Token(Tok.CharReturn, "\r"); + generated ~= Token(Tok.NewLine, "\n"); + } + } + + TokenStream mutated; + + auto ind = getIndentation(stream, state.mem.startIndex-1); + if (ind.isOk()) { + mutated ~= pre[0..state.mem.startIndex-ind.unwrap().length()]; + mutated ~= indent(generated, ind.unwrap()).unwrap(); + } else { + mutated ~= pre; + } + + mutated ~= post; + stream = mutated; + + return Result!(Res).make(Res.BreakInner); + } + + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectOptFlag, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + if (tok.value == "NoSliderHeader") + state.mem.noSliderHeader = true; + else if (tok.value == "NoInterpToggles") + state.mem.noInterpToggles = true; + else + return Result!(Res).fail("Unknown flag '" ~ tok.value ~ "'!"); + + state.gotoState(State.ExpectCommaOrCloseParen); + return Result!(Res).make(Res.Continue); + } + ); + + state.onEOS((ref TokenStream stream) => Result!(Res).make(Res.BreakOuter)); + + while (true) { + const auto res = state.exec(stream); + if (!res.isOk()) return Result!(bool).failFrom(res); + if (res.unwrap() == Res.BreakOuter) break; + state.resetMemory(); + state.gotoState(State.ExpectHash); + } + + return Result!(bool).make(true); + } + + private: + DeclareOffsetGroup ofsGroups = null; +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/constructs/include.d b/CodeGen/MCM/paper/source/constructs/include.d new file mode 100644 index 0000000..d5de434 --- /dev/null +++ b/CodeGen/MCM/paper/source/constructs/include.d @@ -0,0 +1,130 @@ +module constructs.include; + +import constructs.iconstruct; + +import keywords; +import tokenizer : Token, TokenStream; +import tokenizer.tokens; +import tokenizer.utils; +import tokenizer; +import lex_machine; + +import std.string; +import std.container.array; +import fs = std.file; + +private enum MacroName = "include"; + +/** + #include +*/ +final class Include : IConstruct { + public: + @disable this(ref return scope Include rhs); + + /// ctor + this() {} + + /** + * Process the token stream and build constructs + * Params: + * stream: Input tokens to parse + * Returns: Parse result + */ + Result!bool parse(ref const(TokenStream) stream) @trusted { + // do nothing + return Result!(bool).make(true); + } + + /** + * Apply generated constructs to the token stream + * Params: + * stream: Mutable token stream to apply to + * Returns: Apply result + */ + Result!bool apply(ref TokenStream stream) @trusted { + struct Memory { + ulong startIndex = 0; + } + enum State { + ExpectHash, + ExpectMacroName, + ExpectPath + } + enum Res { + Continue, + BreakInner, + BreakOuter, + } + + LexMachine!( + State, State.ExpectHash, + Res, Res.Continue, + Memory + ) state; + + state.onState(State.ExpectHash, Tok.Hash, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.mem.startIndex = position; + state.gotoState(State.ExpectMacroName); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectMacroName, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + if (tok.value == MacroName) + state.gotoState(State.ExpectPath); + else { + state.resetMemory(); + state.gotoState(State.ExpectHash); + } + + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectHash); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectPath, Tok.StringValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + string fileContents; + try { + fileContents = cast(string)fs.read("./" ~ tok.value); + } catch(fs.FileException e) { + return Result!(Res).fail(e.toString()); + } + + TokenStream includeStream; + auto err = tokenize(fileContents, includeStream); + if (!err.isOk()) + return Result!(Res).failFrom(err); + + TokenStream mutated; + mutated ~= TokenStream(stream[0..state.mem.startIndex]); + mutated ~= includeStream; + mutated ~= TokenStream(stream[position+1..$]); + stream = mutated; + return Result!(Res).make(Res.BreakInner); + } + ); + + state.onEOS((ref TokenStream stream) => Result!(Res).make(Res.BreakOuter)); + + while (true) { + const auto res = state.exec(stream); + if (!res.isOk()) Result!(bool).failFrom(res); + if (res.unwrap() == Res.BreakOuter) break; + state.resetMemory(); + state.gotoState(State.ExpectHash); + } + + return Result!(bool).make(true); + } +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/constructs/struct_invoke_on.d b/CodeGen/MCM/paper/source/constructs/struct_invoke_on.d new file mode 100644 index 0000000..ff0512f --- /dev/null +++ b/CodeGen/MCM/paper/source/constructs/struct_invoke_on.d @@ -0,0 +1,233 @@ +module constructs.struct_invoke_on; +import constructs.iconstruct; + +import constructs.const_struct; + +import keywords; +import tokenizer : Token, TokenStream; +import tokenizer.tokens; +import tokenizer.utils; +import lex_machine; + +import std.string; +import std.container.array; + +private enum MacroName = "StructInvokeOn"; + +/** + * #StructInvokeOn(macroName, [implA, implB, ...]) + * + * Becomes: + * implA->!macroName + * implB->!macroName + * ... + */ +final class StructInvokeOn : IConstruct { + public: + @disable this(ref return scope StructInvokeOn rhs); + + /// ctor + this() {} + + void setConstStructTool(ref ConstStructParser parser) { + this.parser = parser; + } + + /** + * Process the token stream and build constructs + * Params: + * stream: Input tokens to parse + * Returns: Parse result + */ + Result!bool parse(ref const(TokenStream) stream) @trusted { + // do nothing + return Result!(bool).make(true); + } + + /** + * Apply generated constructs to the token stream + * Params: + * stream: Mutable token stream to apply to + * Returns: Apply result + */ + Result!bool apply(ref TokenStream stream) @trusted { + struct Memory { + ulong startIndex = 0; + string invocation; + TokenStream generated; + } + enum State { + ExpectHash, + ExpectMacroName, + ExpectOpenParen, + ExpectInvocation, + ExpectComma, + ExpectOpenBrace, + ExpectCloseParen, + } + enum Res { + Continue, + BreakInner, + BreakOuter, + } + + LexMachine!( + State, State.ExpectHash, + Res, Res.Continue, + Memory + ) state; + + state.onState(State.ExpectHash, Tok.Hash, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.mem.startIndex = position; + state.gotoState(State.ExpectMacroName); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectMacroName, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + if (tok.value == MacroName) + state.gotoState(State.ExpectOpenParen); + else { + state.resetMemory(); + state.gotoState(State.ExpectHash); + } + + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.resetMemory(); + state.gotoState(State.ExpectHash); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectOpenParen, Tok.OpenParen, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectInvocation); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectInvocation, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.mem.invocation = tok.value; + state.gotoState(State.ExpectComma); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectComma, Tok.Comma, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectOpenBrace); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectOpenBrace, Tok.OpenBrace, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + auto subStream = TokenStream(stream[position+1..$]); + auto impls = getBalancedContent(subStream, Tok.OpenBrace, Tok.CloseBrace); + if (!impls.isOk()) + return Result!(Res).failFrom(impls); + + auto ws = getIndentation(stream, state.mem.startIndex-1); + + auto implList = impls.unwrap(); + auto generated = createInvocations(state.mem.invocation, implList, ws); + if (!generated.isOk()) + return Result!(Res).failFrom(generated); + + generated.unwrapEmplace(state.mem.generated); + state.gotoState(State.ExpectCloseParen); + state.jump(implList.length() + 2); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectCloseParen, Tok.CloseParen, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + auto pre = stream[0..state.mem.startIndex]; + auto post = stream[position+1..$]; + // Slice off any trailing empty line left over + auto postE = isEmptyLine(TokenStream(post)); + if (postE.isOk()) + post = post[postE.unwrap()..$]; + + TokenStream mutated; + mutated ~= pre; + mutated ~= state.mem.generated; + mutated ~= post; + stream = mutated; + return Result!(Res).make(Res.BreakInner); + } + ); + + state.onEOS((ref TokenStream stream) => Result!(Res).make(Res.BreakOuter)); + + while (true) { + const auto res = state.exec(stream); + if (!res.isOk()) Result!(bool).failFrom(res); + if (res.unwrap() == Res.BreakOuter) break; + state.resetMemory(); + state.gotoState(State.ExpectHash); + } + + return Result!(bool).make(true); + } + + private: + ConstStructParser parser = null; + + /** + * Generate a list of macro invocations from the input stream + * Params: + * name = Name of the macro to invoke + * stream = Comma separated list of impls to invoke name on + * indent = getIndentation() result for correct line indent matching + * Returns: Output stream of macro invocations + */ + Result!TokenStream createInvocations(ref const(string) name, ref const(TokenStream) stream, + ref Result!TokenStream indent) + { + TokenStream output; + + bool firstLine = true; + bool expectName = true; + bool expectComma = false; + for (ulong i = 0; i < stream.length(); i++) { + const Token tok = stream[i]; + if (isWhitespace(tok)) continue; + + if (expectName) { + if (tok.type != Tok.OtherValue) + return Result!(TokenStream).fail("Unexpected token '" ~ tok.value ~ "', wanted a name!"); + + if (!firstLine && indent.isOk()) + output ~= indent.unwrap()[0..$]; + + output ~= tok; + output ~= Token(Tok.Arrow, "->"); + output ~= Token(Tok.Exclamation, "!"); + output ~= Token(Tok.OtherValue, name); + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + expectName = false; + expectComma = true; + firstLine = false; + + } else if (expectComma) { + if (tok.type != Tok.Comma) + return Result!(TokenStream).fail("Unexpected token '" ~ tok.value ~ "', wanted ','!"); + expectComma = false; + expectName = true; + } + } + + return Result!(TokenStream).make(output); + } +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/constructs/struct_invoke_switchifeq.d b/CodeGen/MCM/paper/source/constructs/struct_invoke_switchifeq.d new file mode 100644 index 0000000..02e4f3a --- /dev/null +++ b/CodeGen/MCM/paper/source/constructs/struct_invoke_switchifeq.d @@ -0,0 +1,465 @@ +module constructs.struct_invoke_switchifeq; +import constructs.iconstruct; + +import constructs.const_struct; + +import keywords; +import tokenizer : Token, TokenStream; +import tokenizer.tokens; +import tokenizer.utils; +import lex_machine; + +import std.string; +import std.container.array; + +private enum MacroName = "StructInvokeSwitchIfEquals"; +private { + struct SwitchBranch { + /// Impls inside this branch + Array!ConstStructImpl impls; + + /** + * Generate the inner if case branches + * Params: + * lhs = Left hand side of the if case + * rhs = Right hand side of the if case (must be a member variable of each struct impl) + * macroName = Name of the macro to invoke + * Returns: Token stream containing complete if branch block + */ + Result!TokenStream generate()(auto ref const(string) lhs, auto ref const(string) rhs, + auto ref const(string) macroName) + { + if (impls.length() == 0) + return Result!(TokenStream).make(TokenStream()); + + TokenStream stream; + bool first = true; + for (ulong i = 0; i < impls.length(); i++) { + const ConstStructImpl impl = impls[i]; + + const MemberVar* var = rhs in impl.memberVars; + if (var is null) + return Result!(TokenStream).fail("Undefined variable '" ~ rhs ~ "'!"); + + // if/elseIf + if (first) { + stream ~= Token(Tok.Tab, "\t"); + stream ~= Token(Tok.kIf, "if"); + first = false; + } else { + stream ~= Token(Tok.CharReturn, "\r"); + stream ~= Token(Tok.NewLine, "\n"); + stream ~= Token(Tok.Tab, "\t"); + stream ~= Token(Tok.kElseIf, "elseIf"); + } + + // (lhs == resolve(rhs)) + // > + stream ~= Token(Tok.Space, " "); + stream ~= Token(Tok.OpenParen, "("); + stream ~= Token(Tok.OtherValue, lhs); + stream ~= Token(Tok.Space, " "); + stream ~= Token(Tok.Equals, "=="); + stream ~= Token(Tok.Space, " "); + + auto toks = impl.memberToActual(rhs, true); + if (!toks.isOk()) return Result!(TokenStream).failFrom(toks); + stream ~= toks.unwrap(); + + stream ~= Token(Tok.CloseParen, ")"); + stream ~= Token(Tok.CharReturn, "\r"); + stream ~= Token(Tok.NewLine, "\n"); + stream ~= Token(Tok.Tab, "\t"); + stream ~= Token(Tok.Tab, "\t"); + + // \tinvoke->!macroName + stream ~= Token(Tok.OtherValue, impl.implName); + stream ~= Token(Tok.Arrow, "->"); + stream ~= Token(Tok.Exclamation, "!"); + stream ~= Token(Tok.OtherValue, macroName); + } + + stream ~= Token(Tok.CharReturn, "\r"); + stream ~= Token(Tok.NewLine, "\n"); + stream ~= Token(Tok.Tab, "\t"); + stream ~= Token(Tok.kEndIf, "endIf"); + stream ~= Token(Tok.CharReturn, "\r"); + stream ~= Token(Tok.NewLine, "\n"); + return Result!(TokenStream).make(stream); + } + } + + struct Switch { + /// Member var name being switched on, must not be `real`, value must be known at 'compile' time + string memberVarBucket; + /// Set by the type of the first struct member seen, all types that follow must match + Tok resolvedType = Tok.tNone; + /// Bucket map, grouped by value literal of memberVarBucket + SwitchBranch[string] branches; + + /** + * Register a new impl with the switch thing + * Params: + * impl = Struct impl to add + */ + Result!bool put(ConstStructImpl* impl) { + import std.conv : to; + + const(MemberVar)* var = memberVarBucket in impl.memberVars; + if (var is null) + return Result!(bool).fail("Member var '" ~ memberVarBucket ~ "' not found on struct '" ~ + impl.implName ~ "'!" + ); + + if (var.isReal) + return Result!(bool).fail("Cannot #StructInvokeSwitchIfEquals on a member tagged 'real' (struct '" ~ + impl.implName ~ "')!" + ); + + string key = var.valueToString(); + + // First var, resolve type and insert branch + if (resolvedType == Tok.tNone) { + resolvedType = var.type; + + SwitchBranch branch; + branch.impls ~= *impl; + branches[key] = branch; + return Result!(bool).make(true); + } + + // Assert type match + if (resolvedType != var.type) + return Result!(bool).fail("Type mismatch in #StructInvokeSwitchIfEquals - expected type '" ~ + to!string(resolvedType) ~ "', not '" ~ to!string(var.type) ~ "'!" + ); + + // Check if we already have a branch for this value + SwitchBranch* branch = key in branches; + if (branch is null) { + SwitchBranch newBranch; + newBranch.impls ~= *impl; + branches[key] = newBranch; + return Result!(bool).make(true); + } + + // Already have a branch, add to it + branch.impls ~= *impl; + return Result!(bool).make(true); + } + } +} + +/** + * Invoke a macro among a list of struct impls if 2 nested if conditions are true + * Outer condition is generated from a unique list of inline values from all input structs + * (Meaning the outer switch can only be performed on values which are not 'real', must be known at 'compile' time) + * ``` + * #StructInvokeSwitchIfEquals( + * a_option, ref, ; Inner condition, ref can be 'real' + * activePage, page, ; Outer 'switch' condition, page cannot 'real' + * implSelectHandler, ; Macro to invoke + * [KeyBindSetting # ImplsOf] ; List of structs + * ) + * ``` + * Assuming unique `page` members on all structs condenses to [page1, page2], + * Becomes: + * ``` + * if (activePage == "page1") + * structs with page == "page1" + * if (inImpl1.ref == a_option) + * imImpl1->!implSelectHandler + * elseIf (inImpl2.ref == a_option) + * inImpl2->!implSelectHandler + * endIf + * elseIf (activePage == "page2") + * structs with page == "page2" + * if (inImpl3.ref == a_option) + * inImpl3->!implSelectHandler + * elseIf + * ... + * endIf + * ``` + * Effectively, this is a switch on the outer condition, then if case on the inner condition + */ +final class StructInvokeSwitchIfEq : IConstruct { + public: + @disable this(ref return scope StructInvokeSwitchIfEq rhs); + + /// ctor + this() {} + + void setConstStructTool(ref ConstStructParser parser) { + this.parser = parser; + } + + /** + * Process the token stream and build constructs + * Params: + * stream: Input tokens to parse + * Returns: Parse result + */ + Result!bool parse(ref const(TokenStream) stream) @trusted { + // do nothing + return Result!(bool).make(true); + } + + /** + * Apply generated constructs to the token stream + * Params: + * stream: Mutable token stream to apply to + * Returns: Apply result + */ + Result!bool apply(ref TokenStream stream) @trusted { + enum State { + ExpectHash, + ExpectMacro, + ExpectOpenParen, + ExpectIfLHS, + ExpectComma, + ExpectIfRHS, + ExpectSwitchCond, + ExpectSwitchValue, + ExpectInvocation, + ExpectOpenBrace, + ExpectCloseParen, + } + struct Memory { + ulong startIndex = 0; + Token ifLhs; + Token ifRhs; + Token switchCond; + Token switchValue; + Token invocation; + State jmp; + Switch branchGenerator; + } + enum Res { + Continue, + BreakInner, + BreakOuter, + } + + LexMachine!( + State, State.ExpectHash, + Res, Res.Continue, + Memory + ) state; + + state.onState(State.ExpectHash, Tok.Hash, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectMacro); + state.mem.startIndex = position; + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectMacro, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + if (tok.value == MacroName) + state.gotoState(State.ExpectOpenParen); + else + state.gotoState(State.ExpectHash); + return Result!(Res).make(Res.Continue); + }, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectHash); + state.resetMemory(); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectOpenParen, Tok.OpenParen, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectIfLHS); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectIfLHS, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectComma); + state.mem.jmp = State.ExpectIfRHS; + state.mem.ifLhs = tok; + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectComma, Tok.Comma, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(state.mem.jmp); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectIfRHS, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectComma); + state.mem.jmp = State.ExpectSwitchCond; + state.mem.ifRhs = tok; + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectSwitchCond, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectComma); + state.mem.jmp = State.ExpectSwitchValue; + state.mem.switchCond = tok; + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectSwitchValue, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectComma); + state.mem.jmp = State.ExpectInvocation; + state.mem.switchValue = tok; + state.mem.branchGenerator.memberVarBucket = tok.value; + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectInvocation, Tok.OtherValue, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + state.gotoState(State.ExpectComma); + state.mem.jmp = State.ExpectOpenBrace; + state.mem.invocation = tok; + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectOpenBrace, Tok.OpenBrace, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + // Now we can read the list of impls + auto subStream = TokenStream(stream[position+1..$]); + auto values = getBalancedContent(subStream, Tok.OpenBrace, Tok.CloseBrace); + if (!values.isOk()) + return Result!(Res).failFrom(values); + + // We should have a comma separated list of value literals + auto list = explode(values.unwrap(), Tok.Comma); + if (!list.isOk()) + return Result!(Res).failFrom(list); + + // Build a unique list of values for each, sort into buckets + auto impls = list.unwrap(); + foreach (ref Token t; impls) { + if (t.type != Tok.OtherValue) + return Result!(Res).fail("Unexpected token '" ~ + t.value ~ "' when parsing StructInvokeSwitchIfEquals!" + ); + + ConstStructImpl* impl = parser.getImplType(t.value); + if (impl is null) + return Result!(Res).fail("Unknown variable '" ~ + t.value ~ "' when parsing StructInvokeSwitchIfEquals!" + ); + + auto err = state.mem.branchGenerator.put(impl); + if (!err.isOk()) + return Result!(Res).failFrom(err); + } + + state.gotoState(State.ExpectCloseParen); + state.jump(values.unwrap().length()+2); + return Result!(Res).make(Res.Continue); + } + ); + + state.onState(State.ExpectCloseParen, Tok.CloseParen, + (ref const(Token) tok, ref TokenStream stream, ulong position) { + auto pre = stream[0..state.mem.startIndex]; + auto post = stream[position+1..$]; + + TokenStream mutated; + mutated ~= pre; + + auto level = getIndentation( + TokenStream(pre), + pre.length > 0 ? cast(long)pre.length -1 : cast(long)pre.length + ); + + // Generate branches, slice in stream + bool first = true; + foreach (ref const(string) key, ref SwitchBranch branch; state.mem.branchGenerator.branches) { + auto branchStream = branch.generate( + state.mem.ifLhs.value, state.mem.ifRhs.value, state.mem.invocation.value + ); + + if (!branchStream.isOk()) + return Result!(Res).failFrom(branchStream); + + if (first) + mutated ~= Token(Tok.kIf, "if"); + else + mutated ~= Token(Tok.kElseIf, "elseIf"); + + first = false; + mutated ~= Token(Tok.Space, " "); + mutated ~= Token(Tok.OpenParen, "("); + mutated ~= state.mem.switchCond; + mutated ~= Token(Tok.Space, " "); + mutated ~= Token(Tok.Equals, "=="); + mutated ~= Token(Tok.Space, " "); + + switch (state.mem.branchGenerator.resolvedType) { + case Tok.tBool: + mutated ~= Token(Tok.BoolValue, key); + break; + + case Tok.tInt: goto case; + case Tok.tFloat: + mutated ~= Token(Tok.NumericValue, key); + break; + + case Tok.tLiteral: + mutated ~= Token(Tok.OtherValue, key); + break; + + case Tok.tString: + mutated ~= Token(Tok.StringValue, key); + break; + + default: assert(0); + } + + mutated ~= Token(Tok.CloseParen, ")"); + mutated ~= Token(Tok.CharReturn, "\r"); + mutated ~= Token(Tok.NewLine, "\n"); + + if (level.isOk()) + mutated ~= indent(branchStream.unwrap(), level.unwrap()).unwrap()[0..$]; + else + mutated ~= branchStream.unwrap()[0..$]; + } + + mutated ~= Token(Tok.kEndIf, "endIf"); + mutated ~= Token(Tok.CharReturn, "\r"); + mutated ~= Token(Tok.NewLine, "\n"); + mutated ~= post; + stream = mutated; + + return Result!(Res).make(Res.BreakInner); + } + ); + + state.onEOS((ref TokenStream stream) => Result!(Res).make(Res.BreakOuter)); + + while (true) { + const auto res = state.exec(stream); + if (!res.isOk()) return Result!(bool).failFrom(res); + if (res.unwrap() == Res.BreakOuter) break; + state.resetMemory(); + state.gotoState(State.ExpectHash); + } + + return Result!(bool).make(true); + } + + private: + ConstStructParser parser = null; +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/keywords/package.d b/CodeGen/MCM/paper/source/keywords/package.d new file mode 100644 index 0000000..49c844b --- /dev/null +++ b/CodeGen/MCM/paper/source/keywords/package.d @@ -0,0 +1,102 @@ +module keywords; +import tokenizer.tokens : Tok; +import result; +import std.string; + +alias KeywordMap = Tok[string]; +alias TypeMap = Tok[string]; + +private __gshared KeywordMap keywords; +private __gshared bool kinit = false; + +private __gshared TypeMap types; +private __gshared bool tinit = false; + +private void loadKeywords() @trusted nothrow { + keywords["scriptname"] = Tok.kScriptName; + keywords["extends"] = Tok.kExtends; + keywords["import"] = Tok.kImport; + keywords["function"] = Tok.kFunction; + keywords["endfunction"] = Tok.kEndFunction; + keywords["event"] = Tok.kEvent; + keywords["endevent"] = Tok.kEndEvent; + keywords["if"] = Tok.kIf; + keywords["elseif"] = Tok.kElseIf; + keywords["endif"] = Tok.kEndIf; + keywords["property"] = Tok.kProperty; + keywords["endproperty"] = Tok.kEndProperty; + keywords["state"] = Tok.kState; + keywords["endstate"] = Tok.kEndState; + keywords["as"] = Tok.kAs; + keywords["global"] = Tok.kGlobal; + keywords["native"] = Tok.kNative; + keywords["auto"] = Tok.kAuto; + keywords["conditional"] = Tok.kConditional; + keywords["hidden"] = Tok.kHidden; + keywords["autoreadonly"] = Tok.kAutoReadOnly; + keywords["self"] = Tok.kSelf; + keywords["while"] = Tok.kWhile; + keywords["endwhile"] = Tok.kEndWhile; + keywords["length"] = Tok.kLength; + keywords["new"] = Tok.kNew; + keywords["parent"] = Tok.kParent; + keywords["return"] = Tok.kReturn; + kinit = true; +} + +private void loadTypes() @trusted nothrow { + types["bool"] = Tok.tBool; + types["float"] = Tok.tFloat; + types["int"] = Tok.tInt; + types["string"] = Tok.tString; + types["literal"] = Tok.tLiteral; + tinit = true; +} + +/** + * Gets the map of all language keywords + * Returns: KeywordMap + */ +ref const(KeywordMap) getKeywords() @trusted nothrow { + if (!kinit) loadKeywords(); + return keywords; +} + +/** + * Checks if the given string matches a language keyword + * Params: + * str = Input string + * Returns: Is keyword + */ +Result!Tok isKeyword(ref const(string) str) @trusted { + if (!kinit) loadKeywords(); + const auto lower = str.toLower; + auto it = lower in keywords; + if (it is null) + return Result!(Tok).fail(str ~ " is not a keyword!"); + return Result!(Tok).make(*it); +} + +/** + * Gets the map of all language types + * Returns: TypeMap + */ +ref const(TypeMap) getTypes() @trusted nothrow { + if (!tinit) loadTypes(); + return types; +} + +/** + * Checks if the given string matches a language type + * Params: + * str = Input string + * Returns: Is type + */ +Result!Tok isType(ref const(string) str) @trusted { + if (!tinit) loadTypes(); + const auto lower = str.toLower; + auto it = lower in types; + if (it is null) + return Result!(Tok).fail(str ~ " is not a type!"); + return Result!(Tok).make(*it); +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/lex_machine.d b/CodeGen/MCM/paper/source/lex_machine.d new file mode 100644 index 0000000..843c069 --- /dev/null +++ b/CodeGen/MCM/paper/source/lex_machine.d @@ -0,0 +1,266 @@ +module lex_machine; +import tokenizer : Token, TokenStream; +import tokenizer.tokens; +import tokenizer.utils; +import result; +import std.conv : to; + +/** + * A state machine for processing a token stream + * + * T is any enum collection. + * U is any type used as the return result. + * GoodResult is the U value used to indicate a good result. Any other value breaks out of the state machine. + * Memory is any struct type used as memory for the state machine. + * Stream is the type of token stream to operate on + */ +struct LexMachine( + T, T InitialState, + U, U GoodResult, + Memory, Stream = TokenStream, + bool IgnoreWhitespace = true +) + if (is(Memory == struct) && is(T == enum)) +{ + /** + * A callback function run for the given state it is registered with + * Params: + * tok = Current token being evaluated + * stream = The stream being operated on + * position = The current position in the stream + * Returns: Result + */ + alias StateFunctor = Result!U delegate( + ref const(Token) tok, ref Stream stream, ulong position + ); + + /** + * A callback function to run when we reach the end of the stream + * Params: + * stream = The stream being operated on + * Returns: Result + */ + alias EOSFunctor = Result!U delegate( + ref Stream stream + ); + + /** + * Execute the state machine on the input stream + * Params: + * stream = Input token stream + * Returns: Result + */ + Result!U exec(ref Stream stream) @trusted { + done = false; + shouldJump = false; + + for (curPos = 0; curPos < stream.length(); curPos++) { + if (shouldJump) { + if (jumpTo != curPos) continue; + shouldJump = false; + } + + const Token tok = stream[curPos]; + static if (IgnoreWhitespace) + if (isWhitespace(tok)) continue; + + auto execFCB(V)(T curState, Token tok, V expected, ref Stream stream, ulong pos) { + auto fcb = curState in failureFunctors; + if (fcb !is null) { + const auto err = (*fcb)(tok, stream, pos); + if (!err.isOk() || err.value != GoodResult) + return err; + + } else { + // No handler, this is an error + return Result!(U).fail( + "Expected '" ~ to!string(expected) ~ "' but got '" ~ to!string(tok.type) ~ "'!" + ); + } + + return Result!(U).make(GoodResult); + } + + // Prefer an exact match + if (currentState in stateExpect) { + const auto expected = stateExpect[currentState]; + if (expected != tok.type) { + // If we have a failure method, run that + const auto err = execFCB(currentState, tok, expected, stream, curPos); + if (!err.isOk() || err.value != GoodResult) + return err; + + continue; + } + + } else if (currentState in stateMultiExpect) { + // Check in the array + auto arr = stateMultiExpect[currentState]; + bool matched = false; + + foreach (ref Tok t; arr) { + if (t == tok.type) { + matched = true; + break; + } + } + + if (!matched) { + // If we have a failure method, run that + const auto err = execFCB(currentState, tok, arr, stream, curPos); + if (!err.isOk() || err.value != GoodResult) + return err; + } + // Otherwise, we matched a token in the list and can run the state functor + + } else { + // Unhandled state + assert(0, "Unhandled state!"); + } + + const StateFunctor* cb = currentState in stateFunctors; + if (cb is null) + assert(0); + + const auto err = (*cb)(tok, stream, curPos); + if (!err.isOk() || err.value != GoodResult) + return err; + + if (done) break; + } + + if (eosFunctor !is null) { + const auto err = eosFunctor(stream); + if (!err.isOk() || err.value != GoodResult) + return err; + } + + return Result!(U).make(GoodResult); + } + + /** + * Register a callback to run for the given state, when the expected token `expect` is seen. + * Params: + * state = State to run on + * expect = Expected token to trigger on + * functor = Callback to run + * matchFailure = Callback to optionally run if the token doesn't match + */ + void onState(T state, Tok expect, StateFunctor functor, StateFunctor matchFailure = null) @safe nothrow { + stateExpect[state] = expect; + stateFunctors[state] = functor; + + if (matchFailure !is null) + failureFunctors[state] = matchFailure; + } + + /** + * Register a callback to run for the given state, when the expected token `expect` is seen. + * + * This variant accepts a list of tokens and will invoke on any match + * Params: + * state = State to run on + * expect = Expected token(s) to trigger on + * functor = Callback to run + * matchFailure = Callback to optionally run if the token doesn't match + */ + void onState(T state, Tok[] expect, StateFunctor functor, StateFunctor matchFailure = null) @safe nothrow { + stateMultiExpect[state] = expect; + stateFunctors[state] = functor; + + if (matchFailure !is null) + failureFunctors[state] = matchFailure; + } + + /** + * Run the given callback when the end of the stream has been reached + * Params: + * functor = Callback to run + */ + void onEOS(EOSFunctor functor) @safe nothrow { + eosFunctor = functor; + } + + /** + * Move to a new state + * Params: + * state = New state + */ + pragma(inline, true) + void gotoState(T state) @safe @nogc nothrow { + lastState = currentState; + currentState = state; + } + + /** + * Call from a state functor to end the state machine execution + */ + pragma(inline, true) + void finish() @safe @nogc nothrow { + done = true; + } + + /** + * Gets the state machine's working memory + * Returns: Memory + */ + pragma(inline, true) + @property + ref inout(Memory) mem() inout return @trusted @nogc nothrow { + return memory; + } + + /** + * Reset memory to the initial state + */ + pragma(inline, true) + void resetMemory() @trusted @nogc nothrow { + memory = Memory.init; + } + + /** + * Jump ahead n tokens in the stream + * Params: + * ahead = Number of tokens to jump over + */ + pragma(inline, true) + void jump(ulong ahead) @safe @nogc nothrow { + shouldJump = true; + jumpTo = ahead + curPos; + } + + /** + * Gets the current state + * Returns: T + */ + T getCurrentState() @safe @nogc nothrow const { + return currentState; + } + + /** + * Look at the next token in the stream ahead of our current position + * Params: + * stream: Stream to peek + * Returns: Token of valid type or tNone if at end of stream + */ + Token peekNext(ref Stream stream) @safe @nogc nothrow const { + if (curPos == stream.length()) return Token(Tok.tNone, ""); + return stream[curPos+1]; + } + + private: + T currentState = InitialState; + T lastState = InitialState; + + Tok[T] stateExpect; + Tok[][T] stateMultiExpect; + StateFunctor[T] stateFunctors; + StateFunctor[T] failureFunctors; + EOSFunctor eosFunctor = null; + + Memory memory; + bool done = false; + bool shouldJump = false; + ulong jumpTo = 0; + ulong curPos = 0; +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/mangler.d b/CodeGen/MCM/paper/source/mangler.d new file mode 100644 index 0000000..db96932 --- /dev/null +++ b/CodeGen/MCM/paper/source/mangler.d @@ -0,0 +1,14 @@ +module mangler; + +/** + * Mangles the input string + * + * This should generate the exact same string as defined in the C++ module! + * This is used to cache-bust the game's string cache. + * Params: + * value = String to mangle + * Returns: Mangled output + */ +string mangle()(auto ref const(string) value) @safe nothrow { + return "_^@" ~ value ~ "_SmoothCamSetting"; +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/result.d b/CodeGen/MCM/paper/source/result.d new file mode 100644 index 0000000..837d8be --- /dev/null +++ b/CodeGen/MCM/paper/source/result.d @@ -0,0 +1,87 @@ +module result; +import std.traits : isCopyable, Unqual; +import std.algorithm : moveEmplace; + +/// Return value based error/result +struct Result(T) { + /// False if there is an error + bool ok = false; + /// Error message if ok = false + string msg = void; + /// Return value + Unqual!T value; + + /// explicit ctor + pragma(inline, true) + this()(bool ok, auto ref string message, auto ref inout(T) v) @trusted { + this.ok = ok; + msg = message; + + static if (isCopyable!T || __traits(isPOD, T)) + value = v; + else + moveEmplace(v, value); + } + + @disable this(this); + + /// copy + static if (isCopyable!T || __traits(isPOD, T)) + pragma(inline, true) + this(ref return scope inout(Result!T) rhs) inout return @trusted { + ok = rhs.ok; + msg = rhs.msg; + value = rhs.value; + } + + /// Returns if the result is not an error + pragma(inline, true) + bool isOk() const { + return ok; + } + + /// Return the value + static if (isCopyable!T || __traits(isPOD, T)) + pragma(inline, true) + Unqual!T unwrap() const @trusted { + assert(ok); + /// ehhhh + return cast(Unqual!T)value; + } + + /// Place the value in `loc` + pragma(inline, true) + void unwrapEmplace(ref T loc) @trusted { + assert(ok); + moveEmplace(cast(T)value, loc); + } + + /// Return a failed result with the given message + static Result!T fail()(auto ref string message) @trusted { + Result!T res; + res.ok = false; + res.msg = message; + + return res; + } + + /// Return a failed result using the message of other + static Result!T failFrom(U)(auto ref inout(Result!U) other) @trusted { + Result!T res; + res.ok = false; + res.msg = other.msg; + return res; + } + + /// Return an OK result using the given value + /// Moves the value if not copy-constructable + static Result!T make()(auto ref inout(T) v) @trusted { + Result!T res; + res.ok = true; + static if (isCopyable!T || __traits(isPOD, T)) + res.value = cast(Unqual!T)v; + else + moveEmplace(cast(Unqual!T)v, res.value); + return res; + } +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/tokenizer/package.d b/CodeGen/MCM/paper/source/tokenizer/package.d new file mode 100644 index 0000000..f91496f --- /dev/null +++ b/CodeGen/MCM/paper/source/tokenizer/package.d @@ -0,0 +1,472 @@ +module tokenizer; +public import tokenizer.tokens; +import keywords; +import result; + +import std.container.array; + +/// A generic token +struct Token { + /// Type of token + Tok type = void; + /// Value of the token + string value = void; +} + +alias TokenStream = Array!Token; + +private { + /// Characters which are valid to use in a var or type name + enum validVarChars = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ~ + "0123456789" ~ + "_"; + + /// Characters which are valid to use in a numeric literal + enum numberChars = "01234567890.-f"; + + /// Characters that count as valid whitespace + enum spaceChars = " \r\n\t"; + + enum CharacterClass : ushort { + Decl, // VarChars + Numeric // NumberChars + } + + // Generic parser state + struct TokenState { + // Expect to read a new token + bool expect = true; + // Currently reading a token + bool reading = false; + // the start position reading began at + ulong startPos = 0; + } + + nothrow: + + /** + * Look ahead 1 character + * Params: + * source = Input string + * curPos = Current position, look ahead curPos+1 + * Returns: Character or 0x0 + */ + char lookAhead()(auto ref const(string) source, ulong curPos) @safe @nogc nothrow { + if (source.length <= curPos) return 0x0; + return source[curPos + 1]; + } + + /** + * Look behind 1 character + * Params: + * source = Input string + * curPos = Current position, look behind curPos-1 + * Returns: Character or 0x0 + */ + char lookBehind()(auto ref const(string) source, ulong curPos) @safe @nogc nothrow { + if (curPos == 0) return 0x0; + return source[curPos - 1]; + } + + /** + * Checks if the given character is valid whitespace + * Params: + * c = Input character + * Returns: Is whitespace + */ + bool isWhitespace(char c) @safe @nogc nothrow { + foreach (char space; spaceChars) + if (space == c) return true; + return false; + } + + /** + * Checks if the given character is a valid number literal character + * Params: + * c = Input character + * Returns: Is numeric + */ + bool isNumeric(char c) @safe @nogc nothrow { + foreach (char n; numberChars) + if (c == n) return true; + return false; + } + + /** + * Checks if the given character is a valid var/decl character + * Params: + * c = Input character + * Returns: Is var char + */ + bool isVarChar(char c) @safe @nogc nothrow { + foreach (char v; validVarChars) + if (c == v) return true; + return false; + } + + /** + * Return the position of the following new line + * Params: + * source = Input string + * Returns: Position to continue reading at, after the current line + */ + Result!ulong nextLine()(auto ref const(string) source) @safe @nogc nothrow { + foreach (ulong i, char c; source) { + if (c == '\r' && lookAhead(source, i) == '\n') return Result!(ulong).make(i+2); + if (c == '\n') return Result!(ulong).make(i+1); + } + return Result!(ulong).fail("Expected new line but got end of file!"); + } + + /** + * Read until `delim` is found, return the position right after `delim` + * Params: + * source = Inout string + * delim = Substring to read to + * Returns: Position to continue reading at, following `delim` + */ + Result!ulong readUntil()(auto ref const(string) source, auto ref const(string) delim) @safe nothrow { + foreach (ulong i, char c; source) { + bool match = true; + + foreach (ulong j, char d; delim) { + if (j == 0) { + if (d != c) { + match = false; + break; + } + } else { + if (lookAhead(source, i+j-1) != delim[j]) { + match = false; + break; + } + } + } + + if (match) + return Result!(ulong).make(i+delim.length); + } + + return Result!(ulong).fail("Expected '" ~ delim ~ "' but got end of file!"); + } + + /** + * Read a string literal + * Params: + * source = Input string + * contents = The contents of the string literal + * Returns: Position after the ending quote to continue reading + */ + Result!ulong readStringLiteral()(auto ref const(string) source, out string contents) @safe @nogc nothrow { + foreach (ulong i, char c; source) { + if (c == '"') { + if (lookBehind(source, i) == '\\') continue; + + contents = source[0..i]; + return Result!(ulong).make(i); + } + } + return Result!(ulong).fail("Expected '\"' character but got end of file!"); + } + + /** + * Read contents of a balanced character pair + * Params: + * source = Input string + * open = Open character + * close = Close character + * contents = Contents of the matched pair + * Returns: Position after closing pair to continue reading + */ + Result!ulong readBalanced()(auto ref const(string) source, char open, char close, out string contents) + @safe nothrow + { + ulong nest = 1; + foreach (ulong i, char c; source) { + if (c == open) + ++nest; + else if (c == close) { + if (--nest == 0) { + contents = source[0..i]; + return Result!(ulong).make(i+1); + } + } + } + return Result!(ulong).fail("Expected matching '" ~ close ~ "' but got end of file!"); + } + + +} + +/** + * Given an input string, write a stream of tokens to the output array + * Params: + * source = Source string to tokenize + * tokens = Output token stream + */ +public Result!bool tokenize(ref const(string) source, out Array!Token tokens) @trusted { + tokens.reserve(2048); + CharacterClass expectClass; + TokenState state; + + bool jump = false; + ulong jumpTo = 0; + void setJump(ulong to) @safe @nogc nothrow { + jump = true; + jumpTo = to; + } + + void insertTok(ulong i) @trusted { + final switch (expectClass) { + case CharacterClass.Decl: + const auto str = (i != 0) ? source[state.startPos..i] : source[state.startPos..$]; + const auto kwd = isKeyword(str); + const auto ty = isType(str); + const auto tok = kwd.isOk() ? kwd.unwrap() : (ty.isOk() ? ty.unwrap() : Tok.OtherValue); + + tokens.insertBack(Token(tok, str)); + state.expect = true; + state.reading = false; + break; + + case CharacterClass.Numeric: + if (i != 0) + tokens.insertBack(Token(Tok.NumericValue, source[state.startPos..i])); + else + tokens.insertBack(Token(Tok.NumericValue, source[state.startPos..$])); + state.expect = true; + state.reading = false; + break; + } + } + + bool lastTok(ref Token tok) @safe @nogc nothrow { + if (tokens.empty()) return false; + tok = tokens.back(); + return true; + } + + foreach (ulong i, char c; source) { eval: + if (jump) { + if (i != jumpTo) continue; + jump = false; + } + + if (state.expect) { + // We have a number of single char and special tokens, look for those + switch (c) { + case ' ': + tokens.insertBack(Token(Tok.Space, " ")); + continue; + + case '\r': + tokens.insertBack(Token(Tok.CharReturn, "\r")); + continue; + + case '\n': + tokens.insertBack(Token(Tok.NewLine, "\n")); + continue; + + case '\t': + tokens.insertBack(Token(Tok.Tab, "\t")); + continue; + + case '{': + tokens.insertBack(Token(Tok.OpenCurl, "{")); + string contents; + const auto loc = readBalanced(source[i+1..$], '{', '}', contents); + if (!loc.isOk()) return Result!(bool).failFrom(loc); + jump = true; + jumpTo = loc.unwrap() + i+1; + tokens.insertBack(Token(Tok.MultiLineComment, contents)); + tokens.insertBack(Token(Tok.CloseCurl, "}")); + continue; + + case '}': + tokens.insertBack(Token(Tok.CloseCurl, "}")); + continue; + + case '(': + tokens.insertBack(Token(Tok.OpenParen, "(")); + continue; + + case ')': + tokens.insertBack(Token(Tok.CloseParen, ")")); + continue; + + case '[': + tokens.insertBack(Token(Tok.OpenBrace, "[")); + continue; + + case ']': + tokens.insertBack(Token(Tok.CloseBrace, "]")); + continue; + + case '.': + tokens.insertBack(Token(Tok.Dot, ".")); + continue; + + case ',': + tokens.insertBack(Token(Tok.Comma, ",")); + continue; + + case ':': + tokens.insertBack(Token(Tok.Colon, ":")); + continue; + + case '#': + tokens.insertBack(Token(Tok.Hash, "#")); + continue; + + case '\\': + tokens.insertBack(Token(Tok.Continuation, "\\")); + continue; + + // Compare operators + case '=': + // Make sure the next token is not also '=' + if (lookAhead(source, i) != '=' && lookBehind(source, i) != '=') + tokens.insertBack(Token(Tok.Assignment, "=")); + else { + tokens.insertBack(Token(Tok.Equals, "==")); + setJump(i+2); + } + continue; + + case '!': + // Make sure the next token is not '=' + if (lookAhead(source, i) != '=') + tokens.insertBack(Token(Tok.Exclamation, "!")); + else { + tokens.insertBack(Token(Tok.NotEquals, "!=")); + setJump(i+2); + } + continue; + + case '<': + // Make sure the next token is not '=' + if (lookAhead(source, i) != '=') + tokens.insertBack(Token(Tok.OpenAngle, "<")); + else { + tokens.insertBack(Token(Tok.LEQ, "<=")); + setJump(i+2); + } + continue; + + case '>': + // Make sure the next token is not '=' + if (lookAhead(source, i) != '=') + tokens.insertBack(Token(Tok.CloseAngle, ">")); + else { + tokens.insertBack(Token(Tok.GEQ, ">=")); + setJump(i+2); + } + continue; + + // Comment + case ';': + tokens.insertBack(Token(Tok.SemiColon, ";")); + // Check for the wacky multi-line syntax + if (lookAhead(source, i) == '/') { + tokens.insertBack(Token(Tok.Slash, "/")); + const auto err = readUntil(source[i..$], "/;"); + if (!err.ok) return Result!(bool).failFrom(err); + jumpTo = err.value + i; + jump = true; + + tokens.insertBack(Token(Tok.MultiLineComment, source[i..err.value-2])); + tokens.insertBack(Token(Tok.Slash, "/")); + tokens.insertBack(Token(Tok.SemiColon, ";")); + } else { + // We are now reading a comment, read until the next line and insert that as a comment + const auto err = nextLine(source[i+1..$]); + if (!err.ok) return Result!(bool).failFrom(err); + jumpTo = err.value + i; + jump = true; + tokens.insertBack(Token(Tok.Comment, source[i+1..jumpTo])); + } + continue; + + // String literal + case '"': + string contents; + const auto err = readStringLiteral(source[i+1..$], contents); + if (!err.ok) return Result!(bool).failFrom(err); + jumpTo = err.value + i+2; + jump = true; + tokens.insertBack(Token(Tok.StringValue, contents)); + continue; + + case '-': + // Check if this is an arrow operator + if (lookAhead(source, i) == '>') { + jump = true; + jumpTo = i+2; + tokens.insertBack(Token(Tok.Arrow, "->")); + continue; + // Or part of a numeric + } else { + auto a = lookAhead(source, i); + if (!isNumeric(a) || a == 'f' || a == '-') { + tokens.insertBack(Token(Tok.Minus, "-")); + continue; + } + } + break; + + case '+': + tokens.insertBack(Token(Tok.Plus, "+")); + continue; + + case '*': + tokens.insertBack(Token(Tok.Star, "*")); + continue; + + default: break; + } + + state.expect = false; + state.reading = true; + state.startPos = i; + + if (isNumeric(c)) { + // For floats ending in 'f' - We need to sanity check + const auto prev = lookBehind(source, i); + if (isNumeric(prev) && prev != 'f') { + expectClass = CharacterClass.Numeric; + } else + if (c != 'f') + expectClass = CharacterClass.Numeric; + else + goto varChar; + + } else { +varChar: + if (isVarChar(c)) + expectClass = CharacterClass.Decl; + else + return Result!(bool).fail("Unexpected character '" ~ c ~ "'!"); + } + + } else { + final switch (expectClass) { + case CharacterClass.Decl: + if (isVarChar(c)) continue; + insertTok(i); + goto eval; // We still need to evaluate this character + + case CharacterClass.Numeric: + if (isNumeric(c)) continue; + insertTok(i); + goto eval; // We still need to evaluate this character + + } + } + } + + if (state.reading) + insertTok(0); + + return Result!(bool).make(true); +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/tokenizer/tokens.d b/CodeGen/MCM/paper/source/tokenizer/tokens.d new file mode 100644 index 0000000..c73d196 --- /dev/null +++ b/CodeGen/MCM/paper/source/tokenizer/tokens.d @@ -0,0 +1,111 @@ +module tokenizer.tokens; + +/** + * List of token types + */ +enum Tok { + OpenCurl, // { // Papyrus uses {...} as a multi-line comment + CloseCurl, // } + OpenParen, // ( + CloseParen, // ) + OpenBrace, // [ + CloseBrace, // ] + Assignment, // = + Dot, // . + Comma, // , + Arrow, // -> Not valid in normal papyrus, used by us + Colon, // : Not valid in normal papyrus, used by us + SemiColon, // ; (Used as a comment) + Quote, // " + Exclamation, // ! + Hash, // # Not valid in normal papyrus, used by us + OpenAngle, // < + CloseAngle, // > + Space, // ' ' + CharReturn, // \r + NewLine, // \n + Tab, // \t + Continuation, // \ + Slash, // / + Plus, // + + Minus, // - + Star, // * + Equals, // == + NotEquals, // != + GEQ, // >= + LEQ, // <= + + Type, // int, string, float + RealType, // real_int, real_string + Mangle, // Requires that type is string, will "mangle" the string to prevent string cache messing with it + + NumericValue, // -1, 2.5 + BoolValue, // true, false + StringValue, // Any character sequence (including escaped quotes), not including the outer quotes + OtherValue, // Any other legal character sequence given as an assignment (var name, return of a function call) + + Comment, // Any comment contents + MultiLineComment, // Curly brace or (;/.../;) contents, in normal papyrus this is a multi-line comment + + // Language keywords + kScriptName, + kExtends, + kImport, + kFunction, + kEndFunction, + kEvent, + kEndEvent, + kIf, + kElseIf, + kEndIf, + kProperty, + kEndProperty, + kState, + kEndState, + kAs, + kGlobal, + kNative, + kAuto, + kConditional, + kHidden, + kAutoReadOnly, + kSelf, + kWhile, + kEndWhile, + kLength, + kNew, + kParent, + kReturn, + + // Builtin types + tBool, + tFloat, + tInt, + tString, + + // Extended types + tNone, // None type + tLiteral, // A literal type takes the assignment contents verbatim (copy+paste) +} + +/** + * Convert a type token to a string + * Params: + * ty = Any Tok.t* token + * Returns: string + */ +string typeToString(Tok ty) @safe @nogc nothrow { + switch (ty) { + case Tok.tBool: + return "bool"; + case Tok.tInt: + return "int"; + case Tok.tFloat: + return "float"; + case Tok.tString: + return "string"; + case Tok.tLiteral: + return "literal"; + default: assert(0); + } +} \ No newline at end of file diff --git a/CodeGen/MCM/paper/source/tokenizer/utils.d b/CodeGen/MCM/paper/source/tokenizer/utils.d new file mode 100644 index 0000000..644dac4 --- /dev/null +++ b/CodeGen/MCM/paper/source/tokenizer/utils.d @@ -0,0 +1,425 @@ +module tokenizer.utils; +import tokenizer : Token, TokenStream; +import tokenizer.tokens : Tok; +import result; + +/** + * Gets a subset of tokens from the input stream contained in a pair of matching outer tokens + * Params: + * stream = Stream to search + * open = Token delimiting an opening scope + * close = Token delimiting a closing scope + * Returns: Sliced token stream + */ +Result!TokenStream getBalancedContent()(auto ref const(TokenStream) stream, Tok open, Tok close) + @trusted +{ + import std.conv : to; + + ulong depth = 1; + for (ulong i = 0; i < stream.length(); i++) { + const Token tok = stream[i]; + if (tok.type == open) + ++depth; + else if (tok.type == close) { + if (--depth == 0) { + return Result!(TokenStream).make(TokenStream(stream[0..i])); + } + } + } + return Result!(TokenStream).fail( + "Expected matching closing token '" ~ to!string(close) ~ "' but got end of stream!" + ); +} + +/** + * Checks if the given token is whitespace + * Params: + * tok = Input token + * Returns: Is whitespace + */ +bool isWhitespace(ref inout(Token) tok) @safe @nogc nothrow { + return tok.type == Tok.Space || tok.type == Tok.CharReturn || + tok.type == Tok.NewLine || tok.type == Tok.Tab; +} + +/** + * Convert the given token stream to a string + * Params: + * stream = Input stream + * Returns: Output string + */ +string streamToString()(auto ref const(TokenStream) stream) @trusted { + string output; + foreach (ref const(Token) tok; stream) { + if (tok.type == Tok.StringValue) + output ~= "\"" ~ tok.value ~ "\""; + else + output ~= tok.value; + } + + return output; +} + +/** + * Gets the current line indentation from the given position in the stream + * + * Assumes the token right before the given position is whitespace, runs until it sees a line break + * Params: + * stream = Input stream + * position = Current position in the stream + * Returns: Ok result with just the indentation whitespace or err if no valid indentation is seen + */ +Result!TokenStream getIndentation()(auto ref const (TokenStream) stream, ulong position) @trusted @nogc nothrow { + TokenStream output; + + for (ulong i = position; i >= 0; i--) { + const Token tok = stream[i]; + if (!isWhitespace(tok)) + break; + + if (tok.type == Tok.CharReturn || tok.type == Tok.NewLine) + break; + + output.insertBack(tok); + } + + if (stream.length() == 0) + return Result!(TokenStream).fail("No valid indentation"); + + return Result!(TokenStream).make(output); +} + +/** + * Checks if the stream of tokens up to the first line break is nothing but whitespace + * Params: + * stream = Input stream + * Returns: Position in the stream following the next line break, assuming the line is empty + */ +Result!ulong isEmptyLine()(auto ref const(TokenStream) stream) @trusted @nogc nothrow { + bool expectNewline = false; + for (ulong i = 0; i < stream.length(); i++) { + const Token tok = stream[i]; + + if (tok.type == Tok.NewLine) { + return Result!(ulong).make(i+1); + } else if (tok.type == Tok.CharReturn) { + expectNewline = true; + } else { + expectNewline = false; + if (tok.type == Tok.Space || tok.type == Tok.Tab) + continue; + else + return Result!(ulong).fail("Line is not empty"); + } + } + + return Result!(ulong).fail("Line is not empty or no return present"); +} + +/** + * Like `isEmptyLine`, but looks behind for a dead line + * Params: + * stream = Input stream + * Returns: Position in the stream before the last line break, assuming the line is empty + */ +Result!ulong endsOnNewLine()(auto ref const(TokenStream) stream) @trusted { + bool expectReturn = false; + for (ulong i = stream.length()-1; i >= 0; i--) { + const Token tok = stream[i]; + + if (tok.type == Tok.NewLine) { + expectReturn = true; + } else if (tok.type == Tok.CharReturn && expectReturn) { + return Result!(ulong).make(i); + } else { + expectReturn = false; + if (tok.type == Tok.Space || tok.type == Tok.Tab) + continue; + else + return Result!(ulong).fail("Line is not empty"); + } + } + + return Result!(ulong).fail("Line is not empty or no return present"); +} + +/** + * Remove leading and trailing whitespace from the input stream + * Params: + * stream = Input stream + * TemplateParams: + * onlyExtraLines = Only trim leading and trailing lines + * Returns: TokenStream + */ +TokenStream trim(bool onlyExtraLines = false)(auto ref const(TokenStream) stream) @trusted { + static if (onlyExtraLines) { + TokenStream output = TokenStream(stream[0..$]); + + ulong lastN = 0; + bool n = false; + for (ulong i = 0; i < output.length(); i++) { + const Token tok = output[i]; + if (isWhitespace(tok)) { + if (tok.type == Tok.NewLine) { + lastN = i; + n = true; + } + } else { + if (n) { + output = TokenStream(output[lastN+1..$]); + break; + } + } + } + + n = false; + for (ulong i = output.length()-1; i >= 0; i--) { + const Token tok = output[i]; + if (isWhitespace(tok)) { + if (tok.type == Tok.NewLine) { + lastN = i; + n = true; + } + } else { + if (n) { + output = TokenStream(output[0..lastN-1]); + break; + } + } + } + + return output; + + } else { + TokenStream output = TokenStream(stream[0..$]); + + for (ulong i = 0; i < output.length(); i++) { + const Token tok = output[i]; + if (!isWhitespace(tok)) { + output = TokenStream(output[i..$]); + break; + } + } + + for (ulong i = output.length()-1; i >= 0; i--) { + const Token tok = output[i]; + if (!isWhitespace(tok)) { + output = TokenStream(output[0..i+1]); + break; + } + } + + return output; + } +} + +/** + * Removes excess indentation levels, keeping only the minimum required + * Params: + * stream = Input stream + * tabWidth = Number of spaces to consider as 1 indentation layer + * Returns: TokenStream + */ +TokenStream trimIndentation()(auto ref const(TokenStream) stream, ulong tabWidth = 4) @trusted { + import std.algorithm : min; + const auto lines = toLines(stream).unwrap(); + ulong leastIndentLevel = ulong.max; + foreach (ref const(TokenStream) lineStream; lines) { + ulong levels = 0; + ulong spaces = 0; + + for (ulong i = 0; i < lineStream.length(); i++) { + const Token tok = lineStream[i]; + if (tok.type != Tok.Space && tok.type != Tok.Tab) { + leastIndentLevel = min(leastIndentLevel, levels); + break; + + } else if (tok.type == Tok.Space) { + if (++spaces == tabWidth) { + spaces = 0; + levels++; + } + } else if (tok.type == Tok.Tab) { + levels++; + } + } + } + + // Already de-tabbed + if (leastIndentLevel == 0) return TokenStream(stream[0..$]); + + // Subtract leastIndentLevel worth of tabs from each line and recombine + TokenStream output; + ulong lineIdx = 0; + foreach (ref const(TokenStream) lineStream; lines) { + lineIdx++; + ulong levels = 0; + ulong spaces = 0; + + for (ulong i = 0; i < lineStream.length(); i++) { + const Token tok = lineStream[i]; + if (tok.type == Tok.Space) { + if (++spaces == tabWidth) { + spaces = 0; + levels++; + } + } else if (tok.type == Tok.Tab) { + levels++; + } else { + assert(0); + } + + if (levels == leastIndentLevel) { + output ~= lineStream[i+1..$]; + if (lineIdx < lines.length) { + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + } + break; + } + } + } + + return output; +} + +/** + * Split the token stream on the given `sep` token - return value omits the sep in the stream + * + * This is meant for the specific case of extracting arguments from a token separated list and as such, + * enforces some rules on the input stream: + * + * A separator token may not appear twice in a row. + * The stream may not end on a separator. + * The stream may not start on a separator. + * + * TemplateParams: + * enforceTokenCount = Raise an error if maxTok+1 tokens are found without a separator between them. + * maxTok = Max number of tokens that may be seen before a separator is required if enforcing token count. + * ignoreWhitespace = Ignore whitespace in the stream. + * + * Params: + * stream = Stream to split + * sep = Token to split on + * Returns: Split token stream + */ +Result!TokenStream explode(bool enforceTokenCount = true, ulong maxTok = 1, bool ignoreWhitespace = true) + (auto ref const(TokenStream) stream, Tok sep) @trusted +{ + TokenStream output; + bool sawSep = false; + bool first = true; + + static if (enforceTokenCount) + ulong tokCount = 0; + + for (ulong i = 0; i < stream.length(); i++) { + const Token tok = stream[i]; + + static if (ignoreWhitespace) + if (isWhitespace(tok)) continue; + + if (tok.type == sep) { + if (first) + return Result!(TokenStream).fail( + "Token stream may not start on a separator!" + ); + + if (sawSep) + return Result!(TokenStream).fail( + "Duplicate '" ~ tok.value ~ "' seen in stream!" + ); + + sawSep = true; + static if (enforceTokenCount) tokCount = 0; + continue; + } + + static if (enforceTokenCount) + if (tokCount >= maxTok) + return Result!(TokenStream).fail( + "Multiple tokens in stream without separators!" + ); + + first = false; + sawSep = false; + output ~= tok; + static if (enforceTokenCount) tokCount++; + } + + if (sawSep) + return Result!(TokenStream).fail( + "Token stream may not end on a separator!" + ); + + return Result!(TokenStream).make(output); +} + +/** + * Convert the input stream into an array of streams, with each stream being 1 line + * + * Will split based on both '\n' and '\r\n' line breaks + * Params: + * stream = Stream to split into lines + * Returns: Array of token streams + */ +Result!(TokenStream[]) toLines()(auto ref const(TokenStream) stream) { + TokenStream[] output; + bool sawReturn = false; + ulong startIndex = 0; + ulong lineEndIndex = 0; + + for (auto i = 0; i < stream.length(); i++) { + const Token tok = stream[i]; + if (tok.type == Tok.CharReturn) { + if (!sawReturn) + sawReturn = true; + // We COULD see several \r's in a row - just ignore it + lineEndIndex = i; // Our index will slide along excess \r's, including any extra in the output + + } else if (tok.type == Tok.NewLine) { + if (!sawReturn) lineEndIndex = i; + + output ~= TokenStream(stream[startIndex..lineEndIndex]); + startIndex = i+1; + sawReturn = false; + } else { + sawReturn = false; + } + } + + output ~= TokenStream(stream[startIndex..$]); + + return Result!(TokenStream[]).make(output); +} + +/** + * Indent every line in the input stream with the contents of the indentation stream + * + * This can also be used to prepend something to every line in the input stream, not just add whitespace + * Params: + * stream = Input stream to apply indentation to + * indentation = Indentation stream to copy to the start of every line in `stream` + * Returns: Indented token stream + */ +Result!TokenStream indent()(auto ref const(TokenStream) stream, auto ref const(TokenStream) indentation) { + auto lines = toLines(stream); + if (!lines.isOk()) return Result!(TokenStream).failFrom(lines); + + auto lineArray = lines.unwrap(); + TokenStream output; + ulong lineIdx = 0; + foreach (ref TokenStream lineStream; lineArray) { + lineIdx++; + output ~= indentation[0..$]; + output ~= lineStream[0..$]; + + if (lineIdx < lineArray.length) { + output ~= Token(Tok.CharReturn, "\r"); + output ~= Token(Tok.NewLine, "\n"); + } + } + + return Result!(TokenStream).make(output); +} \ No newline at end of file diff --git a/CodeGen/MCM/preproc/.luacheckrc b/CodeGen/MCM/preproc/.luacheckrc deleted file mode 100644 index e80df5a..0000000 --- a/CodeGen/MCM/preproc/.luacheckrc +++ /dev/null @@ -1,29 +0,0 @@ -std = { - read_globals = { - "abstract", "class", "namespace", "extends", "implements", "from", "interface", "singleton", - "mixin", "accessor", "getter", "setter", "super", "this", "shared", "shared_block", - - "coroutine", "assert", "tostring", "tonumber", "rawget", "xpcall", "ipairs", "print", - "pcall", "gcinfo", "pairs", "package", "error", "debug", "loadfile", "rawequal", - "loadstring", "rawset", "unpack", "table", "require", "_VERSION", - "newproxy", "collectgarbage", "dofile", "next", "load", "_G", "select", - "type", "getmetatable", "setmetatable", - - "io", - "bit", - "math", - "os", - "string", - "jit", - - "arg", - "getfenv", - "setfenv", - "module" - } -} - -allow_defined = true -max_cyclomatic_complexity = 15 - -ignore = {"212/self", "131", "113"} \ No newline at end of file diff --git a/CodeGen/MCM/preproc/do_prepoc.lua b/CodeGen/MCM/preproc/do_prepoc.lua deleted file mode 100644 index af45ba9..0000000 --- a/CodeGen/MCM/preproc/do_prepoc.lua +++ /dev/null @@ -1,114 +0,0 @@ ---[[ - This is a dumb little tool I made to do some basic code generation - for papyrus, to make building the MCM easier - - Running the preprocessor: - lua do_preproc.lua path/to/my/script.psc path/to/my/output.psc -]] -package.path = "../../shared/?.lua;" - -local args = { ... } -assert( args[1] ) -assert( args[2] ) - -function string.replace( text, old, new ) - while true do - local b, e = text:find( old, 1, true ) - if not b then break end - text = text:sub( 1, b-1 ).. new.. text:sub( e+1 ) - end - return text -end - -function string.trim( s ) - return string.match( s, "^%s*(.-)%s*$" ) or s -end - -function string.split( str, sep ) - local t = {} - for s in string.gmatch( str, "([^"..sep.."]+)" ) do - table.insert( t, s ) - end - return t -end - -function table.count( t ) - local i = 0 - for _, _ in pairs( t ) do i = i + 1 end - return i -end - -lava = require "lava" -class = lava.class -lava.loadClass "preproc_array_init_list.lua" -lava.loadClass "preproc_struct.lua" -lava.loadClass "preproc_constexpr_struct.lua" -lava.loadClass "preproc_ifchain_macro_invoke.lua" -lava.loadClass "preproc_all_of_struct_impl.lua" -lava.loadClass "preproc_group_struct_macro_invoke.lua" -lava.loadClass "preproc_generate_offsetGroup.lua" -lava.loadClass "preproc_impl_offsetGroup_page.lua" - -local function readFile( strPath ) - local f = io.open( strPath, "r" ) - assert( f ) - - local t = {} - for line in f:lines() do - t[#t+1] = line - end - - local src = table.concat( t, "\n" ) - f:close() - return src -end - -local function writeFile( strPath, strData ) - local f = io.open( strPath, "w" ) - assert( f ) - f:write( strData ) - f:close() -end - -local function run() - local src = readFile(args[1]) - - -- Group generators - local groupGen = papyrus.preproc.GenerateOffsetGroup:New( src ) - local groupPageImpl = papyrus.preproc.ImplOffsetGroupPage:New( groupGen:GetParsed(), groupGen ) - - -- Run the array initializer list tool - local initList = papyrus.preproc.ArrayInitList:New( groupPageImpl:GetParsed() ) - - -- Run the parsing part of the struct tool - local structParser = papyrus.preproc.StructToVars:New( initList:GetParsed() ) - structParser:Preproc() - - -- And the constexpr struct tool - local constStructParser = papyrus.preproc.ConstexprStructToVars:New( initList:GetParsed() ) - constStructParser:Preproc() - - -- Feed the struct type info into the all of struct type macro tool - local allOfStructParser = papyrus.preproc.AllOfStructImpl:New( initList:GetParsed(), structParser, constStructParser ) - - -- Run the group macro invoke tool - local groupMacroInvoke = papyrus.preproc.GroupStructMacroInvoke:New( allOfStructParser:GetParsed() ) - - -- Feed the processed code into the if chain macro tool - local ifChainParser = papyrus.preproc.IfChainMacroInvoke:New( groupMacroInvoke:GetParsed(), constStructParser ) - - -- Now re-parse and apply the struct tool - structParser:SetSource( ifChainParser:GetParsed() ) - structParser:Preproc() - structParser:Apply() - - -- And the constexpr struct tool - constStructParser:SetSource( structParser:GetParsed() ) - constStructParser:Preproc() - constStructParser:Apply() - - -- Done - writeFile( args[2], constStructParser:GetParsed() ) -end - -run() \ No newline at end of file diff --git a/CodeGen/MCM/preproc/preproc_all_of_struct_impl.lua b/CodeGen/MCM/preproc/preproc_all_of_struct_impl.lua deleted file mode 100644 index 8e61b97..0000000 --- a/CodeGen/MCM/preproc/preproc_all_of_struct_impl.lua +++ /dev/null @@ -1,63 +0,0 @@ -do class "AllOfStructImpl" : namespace "papyrus.preproc" { - m_tblCalls = {}, - - m_strOrig = "", - m_strSource = "", - - m_strPattern = "IMPL_ALL_IMPLS_OF_STRUCT%s*%(%s*([%a%d_]+)%s*%)", - m_pStructParser = nil, - m_pConstStructParser = nil, - } - - getter "Source->m_strOrig" - getter "Parsed->m_strSource" - - function Initialize( self, strSource, pStructParser, pConstStructParser ) - self.m_strOrig = strSource - self.m_strSource = strSource - self.m_pStructParser = pStructParser - self.m_pConstStructParser = pConstStructParser - self:GenerateCalls() - self.m_strSource = self:ReplaceCalls() - end - - function GenerateCalls( self ) - for type in self.m_strSource:gmatch( self.m_strPattern ) do - local t = {} - for impl, impl_type in pairs( self.m_pStructParser:GetStructImpls() ) do - if impl_type == type then - t[#t +1] = impl - end - end - - for impl, impl_type in pairs( self.m_pConstStructParser:GetStructImpls() ) do - if impl_type.type == type then - t[#t +1] = impl - end - end - - self.m_tblCalls[#self.m_tblCalls +1] = { - type = type, - output = table.concat( t, "," ) - } - end - end - - function ReplaceCalls( self ) - local parsed = self.m_strSource - for _, call in pairs( self.m_tblCalls ) do - while true do - local str = ("IMPL_ALL_IMPLS_OF_STRUCT%s*%(%s*(_1_)%s*%)") - :replace( "_1_", call.type ) - - local s, e = parsed:find( str ) - if not s then break end - - local prefix = parsed:sub( 1, s-1 ) - local postfix = parsed:sub( e+1 ) - parsed = prefix.. call.output.. postfix - end - end - return parsed - end -end \ No newline at end of file diff --git a/CodeGen/MCM/preproc/preproc_array_init_list.lua b/CodeGen/MCM/preproc/preproc_array_init_list.lua deleted file mode 100644 index e4e1843..0000000 --- a/CodeGen/MCM/preproc/preproc_array_init_list.lua +++ /dev/null @@ -1,67 +0,0 @@ -do class "ArrayInitList" : namespace "papyrus.preproc" { - m_tblArrays = {}, - - m_strOrig = "", - m_strSource = "", - - m_strPattern = "(%s*)([%a%d_]+)%s*=%s*new%s+(%a+)%s*%[%s*%]%s*%->%s*(%b{})", - } - - getter "Source->m_strOrig" - getter "Parsed->m_strSource" - - function Initialize( self, strSource ) - self.m_strOrig = strSource - self.m_strSource = strSource - self:GenerateCalls() - self.m_strSource = self:ReplaceCalls() - end - - function GenerateCalls( self ) - for indent, decl, type, members in self.m_strSource:gmatch( self.m_strPattern ) do - indent = indent:gsub( "\n", "" ) - local decl_nice = decl:trim() - local t = {} - - local cleaned = members:gsub("\n", ""):gsub("\r", ""):gsub("\t", "") - cleaned = cleaned:sub(2, cleaned:len() -1) - for _, member in pairs( cleaned:split(",") ) do - local sz_t = #t - t[sz_t +1] = ("%s%s[%d] = %s"):format( indent, decl_nice, sz_t, member:trim() ) - end - - self.m_tblArrays[#self.m_tblArrays +1] = { - decl = decl, - decl_nice = decl_nice, - type = type, - members = members, - size = #t, - output = table.concat( t, "\n" ) - } - end - end - - function ReplaceCalls( self ) - local parsed = self.m_strSource - for _, arr in pairs( self.m_tblArrays ) do - while true do - local str = ("_1_%s*=%s*new%s+_2_%s*%[%s*%]%s*%->%s*_3_") - :replace( "_1_", arr.decl ) - :replace( "_2_", arr.type ) - :replace( "_3_", arr.members ) - - local s, e = parsed:find( str ) - if not s then break end - - local generated = ("%s = new %s[%d]\n%s"):format( - arr.decl_nice, arr.type, arr.size, arr.output - ) - - local prefix = parsed:sub( 1, s-1 ) - local postfix = parsed:sub( e+1 ) - parsed = prefix.. generated.. postfix - end - end - return parsed - end -end \ No newline at end of file diff --git a/CodeGen/MCM/preproc/preproc_constexpr_struct.lua b/CodeGen/MCM/preproc/preproc_constexpr_struct.lua deleted file mode 100644 index 8ba183c..0000000 --- a/CodeGen/MCM/preproc/preproc_constexpr_struct.lua +++ /dev/null @@ -1,253 +0,0 @@ -do class "ConstexprStructToVars" : namespace "papyrus.preproc" { - m_tblStructTypes = {}, - m_tblStructImpls = {}, - m_tblStructMemberUsage = {}, - m_tblStructMacroUsage = {}, - - m_strSource = "", - m_strOrig = "", - } - - getter "Source->m_strOrig" - getter "Parsed->m_strSource" - - getter "StructTypes->m_tblStructTypes" - getter "StructImpls->m_tblStructImpls" - - function Initialize( self, strSource ) - self:SetSource( strSource ) - end - - function SetSource( self, strSource ) - self.m_strOrig = strSource - self.m_strSource = strSource - end - - function Preproc( self ) - self.m_tblStructTypes = {} - self.m_tblStructImpls = {} - self.m_tblStructMemberUsage = {} - - self:ParseStructTypes() - self:ParseStructTypeImpls() - self:FindAndGenerateStructImplMemberUsage() - self:FindAndGenerateStructImplMacroUsage() - end - - function Apply( self ) - self.m_strSource = self:ReplaceGeneratedStructImpls() - self.m_strSource = self:ReplaceStructDecls() - self.m_strSource = self:ReplaceGeneratedStructImplMembers() - self.m_strSource = self:InsertStructImplMacros() - end - - -- Break a var decl into (type, name, value) - function ExtractVarDef( self, strVar ) - return strVar:match( "([%a%d_]+)%s+([%a%d_]+)%s*=%s*(.+)" ) - end - - function GenerateNameForStructMemberImpl( self, strStructDecl, strMember ) - local typeInfo = self.m_tblStructTypes[self.m_tblStructImpls[strStructDecl].type] - if not typeInfo.members[strMember] then - error( - "Invalid struct member - ".. strStructDecl.. "::".. strMember.. - " (Of type "..self.m_tblStructImpls[strStructDecl] ..")" - ) - end - return { - name = strStructDecl.. "_".. strMember, - type = typeInfo.members[strMember].type:sub( 6 ), - value = typeInfo.members[strMember].value, - } - end - - -- Read all struct type defs into a table - function ParseStructTypes( self ) - for def, cont in self.m_strSource:gmatch("#constexpr_struct%s+([%a%d]+)%s*(%b{})") do - local struct = { - name = def, - members = {}, - macros = {}, - } - - local cleaned, _ = cont:sub( 2, cont:len() -1 ):gsub("\t", "") - -- Match and sub out MACRO defs first - for name, impl in cleaned:gmatch("MACRO%s+([%a%d_]+)%s*=%s*(%b{})") do - struct.macros[name] = impl:sub( 2, impl:len() -1 ) - end - - while true do - local s, e = cleaned:find( "MACRO%s+([%a%d_]+)%s*=%s*(%b{})" ) - if not s then break end - - local prefix = cleaned:sub( 1, s-1 ) - local postfix = cleaned:sub( e+1 ) - cleaned = prefix.. postfix - end - - for line in cleaned:gmatch("[^\n]+") do - local type, name, value = self:ExtractVarDef( line ) - local isreal = type:sub( 1, 5 ) == "real_" - struct.members[name] = { type = type, value = value, isreal = isreal } - end - - self.m_tblStructTypes[def] = struct - end - end - - -- Read all var decls that use a struct type we've seen - function ParseStructTypeImpls( self ) - for type, name, decl in self.m_strSource:gmatch("([%a%d_]+)%s+([%a%d_]+)%s*%->%s*(%b{})") do - if self.m_tblStructTypes[type] then - local t = {} - local cleaned = decl:gsub("\r", ""):gsub("\t", "") - cleaned = cleaned:sub(2, cleaned:len() -1) - for _, member in pairs( cleaned:split("\n") ) do - local var, value = member:match("([%a%d_]+)%s*:%s*(.*)%s*") - assert( - self.m_tblStructTypes[type].members[var], - "Invalid assignment to var ".. var.. " of constexpr_struct ".. name - ) - t[var] = value - end - - self.m_tblStructImpls[name] = { - type = type, - members = t, - } - end - end - end - - -- Find all usages of myStructImpl.memberVar and make a generated name - function FindAndGenerateStructImplMemberUsage( self ) - for decl, member in self.m_strSource:gmatch("([%a%d_]+)%.([%a%d_]+)") do - if self.m_tblStructImpls[decl] and not self.m_tblStructMemberUsage[decl.."."..member] then - local typeInfo = self.m_tblStructTypes[self.m_tblStructImpls[decl].type] - assert( - typeInfo, - "Unknown var ".. member.. " of constexpr_struct ".. decl - ) - - if typeInfo.members[member].isreal then - local generated = self:GenerateNameForStructMemberImpl( decl, member ) - self.m_tblStructMemberUsage[decl.."."..member] = generated - else - -- Insert the value literal - local value = self.m_tblStructImpls[decl].members[member] - if not value then - self.m_tblStructMemberUsage[decl.."."..member] = { name = typeInfo.members[member].value } - else - self.m_tblStructMemberUsage[decl.."."..member] = { name = value } - end - end - end - end - end - - -- Find all usages of myStructImpl->!macro and generate meta data for it - function FindAndGenerateStructImplMacroUsage( self ) - local generated = {} - for indent, decl, member in self.m_strSource:gmatch("(%s*)([%a%d_]+)%->!([%a%d_]+)") do - if self.m_tblStructImpls[decl] and not self.m_tblStructMacroUsage[decl.. "->!".. member] then - local code = self.m_tblStructTypes[self.m_tblStructImpls[decl].type].macros[member] - if not code then - error("Missing code impl for macro ".. member.. " (decltype ".. decl.. ")") - end - code = code:replace( "this->", decl.."." ) - - indent = indent:gsub( "\n", "" ) - local code_t = {} - for k, line in ipairs( code:split("\n") ) do - code_t[k] = indent.. line - end - code = table.concat( code_t, "\n" ) - - for gen_decl, gen_member in code:gmatch("([%a%d_]+)%.([%a%d_]+)") do - gen_decl = gen_decl:trim() - gen_member = gen_member:trim() - - if not generated[member.."."..gen_decl.."."..gen_member] then - if self.m_tblStructImpls[gen_decl] and self.m_tblStructTypes[self.m_tblStructImpls[gen_decl].type] then - local typeInfo = self.m_tblStructTypes[self.m_tblStructImpls[gen_decl].type] - - local usage = self.m_tblStructMemberUsage[gen_decl.."."..gen_member] - if usage then - code = code:replace( gen_decl.."."..gen_member, usage.name ) - else - if typeInfo.members[gen_member].isreal then - code = code:replace( gen_decl.."."..gen_member, gen_decl.. "_".. gen_member ) - else - -- Value literal - local value = self.m_tblStructImpls[gen_decl].members[gen_member] - if not value then - value = typeInfo.members[gen_member].value - end - code = code:replace( gen_decl.."."..gen_member, value ) - end - end - - generated[member.."."..gen_decl.."."..gen_member] = true - end - end - end - - self.m_tblStructMacroUsage[decl.. "->!".. member] = code:trim() - end - end - end - - -- Replace struct decls with empty space - function ReplaceStructDecls( self ) - local parsed = self.m_strSource - for decl, _ in pairs( self.m_tblStructTypes ) do - local s, e = parsed:find( "#constexpr_struct%s+".. decl.. "%s*(%b{})" ) - local prefix = parsed:sub( 1, s-3 ) - local postfix = parsed:sub( e+1 ) - parsed = prefix.. postfix - end - return parsed - end - - function ReplaceGeneratedStructImpls( self ) - local parsed = self.m_strSource - for name, meta in pairs( self.m_tblStructImpls ) do - local s, e = parsed:find( meta.type.."%s+"..name.."%s*%->%s*(%b{})" ) - local prefix = parsed:sub( 1, s-1 ) - local postfix = parsed:sub( e+1 ) - - local generated = {} - for m_name, m_meta in pairs( self.m_tblStructTypes[meta.type].members ) do - if m_meta.isreal then - local g = self:GenerateNameForStructMemberImpl( name, m_name ) - generated[#generated+1] = ("%s %s = %s"):format( - g.type, - g.name, - g.value - ) - end - end - - parsed = prefix.. table.concat( generated, "\n" ).. postfix - end - return parsed - end - - -- Replace all instances of struct.member with the generated name - function ReplaceGeneratedStructImplMembers( self ) - local parsed = self.m_strSource - for decl, generated in pairs( self.m_tblStructMemberUsage ) do - parsed = parsed:gsub( decl, generated.name ) - end - return parsed - end - - -- Replace all instances of struct.macroMember with the macro code - function InsertStructImplMacros( self ) - local parsed = self.m_strSource - for decl, generated in pairs( self.m_tblStructMacroUsage ) do - parsed = string.replace( parsed, decl, generated ) - end - return parsed - end -end \ No newline at end of file diff --git a/CodeGen/MCM/preproc/preproc_generate_offsetGroup.lua b/CodeGen/MCM/preproc/preproc_generate_offsetGroup.lua deleted file mode 100644 index 847f1d7..0000000 --- a/CodeGen/MCM/preproc/preproc_generate_offsetGroup.lua +++ /dev/null @@ -1,348 +0,0 @@ -do class "GenerateOffsetGroup" : namespace "papyrus.preproc" { - m_tblCalls = {}, - m_tblVarIDs = { Offsets = {}, Interp = {} }, - - m_strOrig = "", - m_strSource = "", - - m_strPattern = "DECLARE_OFFSET_GROUP_CONTROLS%s*%(%s*([%a%d_]+)%s*,%s*([%a%d\"%s_]+)%s*,?%s*([%a%d_,%s]*)%s*%)", - m_pStructParser = nil, - m_pConstStructParser = nil, - } - - getter "Source->m_strOrig" - getter "Parsed->m_strSource" - getter "VarIDs->m_tblVarIDs" - - function Initialize( self, strSource ) - self.m_strOrig = strSource - self.m_strSource = strSource - self:GenerateCalls() - self.m_strSource = self:ReplaceCalls() - end - - function GenerateCalls( self ) - for type, matchingPage, flags in self.m_strSource:gmatch( self.m_strPattern ) do - local flagList = flags:gsub("\n", ""):gsub("\r", ""):gsub("\t", ""):split(",") - local flagMap = {} - for k, f in pairs(flagList) do - flagMap[f:trim()] = true - end - - self.m_tblCalls[#self.m_tblCalls +1] = { - type = type, - page = matchingPage, - flags = flags, - output = self:GenerateOffsetGroupString(type, matchingPage, flagMap) - } - end - end - - function CreateSliderSetting( self, strName, strGroup, strSubGroup, strDisplayName, strDesc, nDefault, - nMin, nMax, strPage ) - - local fmt = [[SliderSetting %s -> { - settingName: "%s" - displayName: "%s" - desc: "%s" - defaultValue: %f - min: %f - max: %f - page: %s -}]] - - local var = (strSubGroup and strSubGroup:gsub(":", "")) or "" - local varName = strGroup:lower().. "_".. strName.. var - self.m_tblVarIDs.Offsets[strGroup] = self.m_tblVarIDs.Offsets[strGroup] or {} - table.insert(self.m_tblVarIDs.Offsets[strGroup], varName) - return fmt:format( - varName, - strSubGroup and (strGroup.. strSubGroup.. ":".. strName) or (strGroup.. ":".. strName), - strDisplayName, - strDesc, - nDefault, - nMin, - nMax, - strPage - ) - end - - function CreateToggleSetting( self, strName, strGroup, strSubGroup, strDisplayName, strDesc, strPage ) - local fmt = [[ToggleSetting %s -> { - settingName: "%s" - displayName: "%s" - desc: "%s" - page: %s -}]] - local varName = strGroup:lower().. "_".. (strSubGroup or "").. strName - self.m_tblVarIDs.Interp[strGroup] = self.m_tblVarIDs.Interp[strGroup] or {} - table.insert(self.m_tblVarIDs.Interp[strGroup], varName) - return fmt:format( - varName, - strName.. strGroup.. (strSubGroup or ""), - strDisplayName, - strDesc, - strPage - ) - end - - function GenerateOffsetGroupString( self, strGroupName, strPage, tblFlags ) - local out = {} - -- Normal - table.insert(out, self:CreateSliderSetting( - "SideOffset", - strGroupName, - nil, - "Side Offset", - "The amount to move the camera to the right.", - 25, - -100, - 100, - strPage - )) - table.insert(out, self:CreateSliderSetting( - "UpOffset", - strGroupName, - nil, - "Up Offset", - "The amount to move the camera up.", - 0, - -100, - 100, - strPage - )) - table.insert(out, self:CreateSliderSetting( - "ZoomOffset", - strGroupName, - nil, - "Zoom Offset", - "The amount to offset the camera zoom by.", - 0, - -200, - 200, - strPage - )) - table.insert(out, self:CreateSliderSetting( - "FOVOffset", - strGroupName, - nil, - "FOV Offset", - "The amount to offset the camera FOV by.".. - " Note this will be clamped to a lower bound of 10 and an upper bound of 170.", - 0, - -120, - 120, - strPage - )) - - -- Ranged combat - if not tblFlags.NoRanged then - table.insert(out, self:CreateSliderSetting( - "SideOffset", - strGroupName, - "Combat:Ranged", - "Ranged Combat Side Offset", - "The amount to move the camera to the right when in ranged combat.", - 25, - -100, - 100, - strPage - )) - table.insert(out, self:CreateSliderSetting( - "UpOffset", - strGroupName, - "Combat:Ranged", - "Ranged Combat Up Offset", - "The amount to move the camera up when in ranged combat.", - 0, - -100, - 100, - strPage - )) - table.insert(out, self:CreateSliderSetting( - "ZoomOffset", - strGroupName, - "Combat:Ranged", - "Ranged Combat Zoom Offset", - "The amount to offset the camera zoom by when in ranged combat.", - 0, - -200, - 200, - strPage - )) - table.insert(out, self:CreateSliderSetting( - "FOVOffset", - strGroupName, - "Combat:Ranged", - "Ranged Combat FOV Offset", - "The amount to offset the camera FOV by when in ranged combat.".. - " Note this will be clamped to a lower bound of 10 and an upper bound of 170.", - 0, - -120, - 120, - strPage - )) - end - - -- Magic combat - if not tblFlags.NoMagic then - table.insert(out, self:CreateSliderSetting( - "SideOffset", - strGroupName, - "Combat:Magic", - "Magic Combat Side Offset", - "The amount to move the camera to the right when in magic combat.", - 25, - -100, - 100, - strPage - )) - table.insert(out, self:CreateSliderSetting( - "UpOffset", - strGroupName, - "Combat:Magic", - "Magic Combat Up Offset", - "The amount to move the camera up when in magic combat.", - 0, - -100, - 100, - strPage - )) - table.insert(out, self:CreateSliderSetting( - "ZoomOffset", - strGroupName, - "Combat:Magic", - "Magic Combat Zoom Offset", - "The amount to offset the camera zoom by when in magic combat.", - 0, - -200, - 200, - strPage - )) - table.insert(out, self:CreateSliderSetting( - "FOVOffset", - strGroupName, - "Combat:Magic", - "Magic Combat FOV Offset", - "The amount to offset the camera FOV by when in magic combat.".. - " Note this will be clamped to a lower bound of 10 and an upper bound of 170.", - 0, - -120, - 120, - strPage - )) - end - - -- Melee combat - if not tblFlags.NoMelee then - table.insert(out, self:CreateSliderSetting( - "SideOffset", - strGroupName, - "Combat:Melee", - "Melee Combat Side Offset", - "The amount to move the camera to the right when in melee combat.", - 25, - -100, - 100, - strPage - )) - table.insert(out, self:CreateSliderSetting( - "UpOffset", - strGroupName, - "Combat:Melee", - "Melee Combat Up Offset", - "The amount to move the camera up when in melee combat.", - 0, - -100, - 100, - strPage - )) - table.insert(out, self:CreateSliderSetting( - "ZoomOffset", - strGroupName, - "Combat:Melee", - "Melee Combat Zoom Offset", - "The amount to offset the camera zoom by when in melee combat.", - 0, - -200, - 200, - strPage - )) - table.insert(out, self:CreateSliderSetting( - "FOVOffset", - strGroupName, - "Combat:Melee", - "Melee Combat FOV Offset", - "The amount to offset the camera FOV by when in melee combat.".. - " Note this will be clamped to a lower bound of 10 and an upper bound of 170.", - 0, - -120, - 120, - strPage - )) - end - - -- Toggles - if not tblFlags.NoInterpToggles then - table.insert(out, self:CreateToggleSetting( - "Interp", - strGroupName, - nil, - "Enable Interpolation", - "Enables interpolation in this state.", - strPage - )) - if not tblFlags.NoRanged then - table.insert(out, self:CreateToggleSetting( - "Interp", - strGroupName, - "RangedCombat", - "Enable Ranged Interpolation", - "Enables interpolation in this state.", - strPage - )) - end - if not tblFlags.NoMagic then - table.insert(out, self:CreateToggleSetting( - "Interp", - strGroupName, - "MagicCombat", - "Enable Magic Interpolation", - "Enables interpolation in this state.", - strPage - )) - end - if not tblFlags.NoMelee then - table.insert(out, self:CreateToggleSetting( - "Interp", - strGroupName, - "MeleeCombat", - "Enable Melee Interpolation", - "Enables interpolation in this state.", - strPage - )) - end - end - return table.concat(out, "\n") - end - - function ReplaceCalls( self ) - local parsed = self.m_strSource - for _, call in pairs( self.m_tblCalls ) do - while true do - local str = ("DECLARE_OFFSET_GROUP_CONTROLS%s*%(%s*(_1_)%s*,%s*(_2_)%s*,?%s*(_3_)%s*%)") - :replace( "_1_", call.type ) - :replace( "_2_", call.page ) - :replace( "_3_", call.flags ) - - local s, e = parsed:find( str ) - if not s then break end - - local prefix = parsed:sub( 1, s-1 ) - local postfix = parsed:sub( e+1 ) - parsed = prefix.. call.output.. postfix - end - end - return parsed - end -end \ No newline at end of file diff --git a/CodeGen/MCM/preproc/preproc_group_struct_macro_invoke.lua b/CodeGen/MCM/preproc/preproc_group_struct_macro_invoke.lua deleted file mode 100644 index 2c4482c..0000000 --- a/CodeGen/MCM/preproc/preproc_group_struct_macro_invoke.lua +++ /dev/null @@ -1,60 +0,0 @@ -do class "GroupStructMacroInvoke" : namespace "papyrus.preproc" { - m_tblCalls = {}, - - m_strOrig = "", - m_strSource = "", - - m_strPattern = "(%s*)IMPL_STRUCT_MACRO_INVOKE_GROUP%s*%(%s*([%a%d_]+)%s*,%s*(%b{})%s*%)", - } - - getter "Source->m_strOrig" - getter "Parsed->m_strSource" - - function Initialize( self, strSource ) - self.m_strOrig = strSource - self.m_strSource = strSource - self:GenerateCalls() - self.m_strSource = self:ReplaceCalls() - end - - function GenerateCalls( self ) - for indent, macro, structs in self.m_strSource:gmatch( self.m_strPattern ) do - indent = indent:gsub( "\n", "" ) - - local t = {} - local first = true - local cleaned = structs:gsub("\n", ""):gsub("\r", ""):gsub("\t", "") - cleaned = cleaned:sub(2, cleaned:len() -1) - for _, struct in pairs( cleaned:split(",") ) do - struct = struct:trim() - t[#t +1] = (first and "" or indent).. struct.. "->!".. macro - first = false - end - - self.m_tblCalls[#self.m_tblCalls +1] = { - macro = macro, - structs = structs, - output = table.concat( t, "\n" ) - } - end - end - - function ReplaceCalls( self ) - local parsed = self.m_strSource - for _, call in pairs( self.m_tblCalls ) do - while true do - local str = ("IMPL_STRUCT_MACRO_INVOKE_GROUP%s*%(%s*_1_%s*,%s*_2_%s*%)") - :replace( "_1_", call.macro ) - :replace( "_2_", call.structs ) - - local s, e = parsed:find( str ) - if not s then break end - - local prefix = parsed:sub( 1, s-1 ) - local postfix = parsed:sub( e+1 ) - parsed = prefix.. call.output.. postfix - end - end - return parsed - end -end \ No newline at end of file diff --git a/CodeGen/MCM/preproc/preproc_ifchain_macro_invoke.lua b/CodeGen/MCM/preproc/preproc_ifchain_macro_invoke.lua deleted file mode 100644 index 4a9a9da..0000000 --- a/CodeGen/MCM/preproc/preproc_ifchain_macro_invoke.lua +++ /dev/null @@ -1,154 +0,0 @@ -do class "IfChainMacroInvoke" : namespace "papyrus.preproc" { - m_tblCalls = {}, - - m_strOrig = "", - m_strSource = "", - m_pConstStructParser = nil, - - m_strPattern = "(%s*)IMPL_IFCHAIN_MACRO_INVOKE%s*%(%s*([%a%d_]+)%s*,%s*([%a%d_]+)%s*,%s*([%a%d_]+)%s*,%s*(%b{})%s*,%s*([%a%d_]+)%s*%)" - } - - getter "Source->m_strOrig" - getter "Parsed->m_strSource" - - function Initialize( self, strSource, pConstStructParser ) - self.m_strOrig = strSource - self.m_strSource = strSource - self.m_pConstStructParser = pConstStructParser - self:GenerateUsage() - --self:GenerateCalls() - self.m_strSource = self:ReplaceCalls() - end - - function GenerateUsage( self ) - for indent, lhs, rhs_member, rhs_macro, structs, pageVarName in self.m_strSource:gmatch( self.m_strPattern ) do - indent = indent:gsub("\n", "") - local cleaned = structs:gsub("\n", ""):gsub("\r", ""):gsub("\t", "") - cleaned = cleaned:sub(2, cleaned:len() -1) - - local invocations = {} - for _, struct in pairs( cleaned:split(",") ) do - if self.m_pConstStructParser:GetStructImpls()[struct] then - local members = self.m_pConstStructParser:GetStructImpls()[struct].members - if members.page and members.page:len() > 0 then - invocations[members.page] = invocations[members.page] or {} - invocations[members.page][struct] = { - lhs = lhs, - member_compare = rhs_member, - macro = rhs_macro, - } - else - print( - "Warning: Member `page` was not found or defined while reading struct ".. - struct.. " for macro IMPL_IFCHAIN_MACRO_INVOKE" - ) - end - else - print( - "Warning: Type info was not found while reading struct ".. - struct.. " for macro IMPL_IFCHAIN_MACRO_INVOKE" - ) - end - end - - local output = {} - local first = true - for page, collection in pairs( invocations ) do - local case = indent.. "elseIf" - if first then - case = "if" - first = false - end - - case = case.. (" (%s == %s)"):format(pageVarName, page) - table.insert(output, case) - - local subFirst = true - for structName, context in pairs(collection) do - local case = "elseIf" - if subFirst then - case = "if" - subFirst = false - end - - table.insert(output, ("%s\t%s (%s == %s.%s)"):format( - indent, case, lhs:trim(), structName, context.member_compare:trim() - )) - table.insert(output, ("%s\t\t%s->!%s"):format( - indent, structName, context.macro - )) - end - table.insert(output, indent.. "\tendIf") - end - - table.insert(output, indent.. "endIf") - --print( table.concat(output, "\n") ) - table.insert(self.m_tblCalls, { - lhs = lhs, - rhs_member = rhs_member, - rhs_macro = rhs_macro, - structs = structs, - pageName = pageVarName, - output = table.concat(output, "\n") - }) - end - end - - function GenerateCalls( self ) - for indent, lhs, rhs_member, rhs_macro, structs, pageName in self.m_strSource:gmatch( self.m_strPattern ) do - indent = indent:gsub( "\n", "" ) - - local output = "" - local first = true - local cleaned = structs:gsub("\n", ""):gsub("\r", ""):gsub("\t", "") - cleaned = cleaned:sub(2, cleaned:len() -1) - for _, struct in pairs( cleaned:split(",") ) do - local case = indent.. "elseIf" - if first then - case = "if" - first = false - end - - output = output.. ("%s (%s == %s.%s)\n"):format( - case, - lhs:trim(), - struct:trim(), - rhs_member:trim() - ) - output = output.. ("%s\t%s->!%s\n"):format( indent, struct, rhs_macro ) - end - output = output.. indent.. "endIf" - - self.m_tblCalls[#self.m_tblCalls +1] = { - lhs = lhs, - rhs_member = rhs_member, - rhs_macro = rhs_macro, - structs = structs, - pageName = pageName, - output = output - } - end - end - - function ReplaceCalls( self ) - local parsed = self.m_strSource - for _, call in pairs( self.m_tblCalls ) do - while true do - local str = ("IMPL_IFCHAIN_MACRO_INVOKE%s*%(%s*_1_%s*,%s*_2_%s*,%s*_3_%s*,%s*_4_%s*,%s*_5_%s*%)") - :replace( "_1_", call.lhs ) - :replace( "_2_", call.rhs_member ) - :replace( "_3_", call.rhs_macro ) - :replace( "_4_", call.structs ) - :replace( "_5_", call.pageName ) - - local s, e = parsed:find( str ) - if not s then break end - - local prefix = parsed:sub( 1, s-1 ) - local postfix = parsed:sub( e+1 ) - parsed = prefix.. call.output.. postfix - end - end - return parsed - end -end \ No newline at end of file diff --git a/CodeGen/MCM/preproc/preproc_impl_offsetGroup_page.lua b/CodeGen/MCM/preproc/preproc_impl_offsetGroup_page.lua deleted file mode 100644 index 1e39a5a..0000000 --- a/CodeGen/MCM/preproc/preproc_impl_offsetGroup_page.lua +++ /dev/null @@ -1,104 +0,0 @@ -do class "ImplOffsetGroupPage" : namespace "papyrus.preproc" { - m_tblCalls = {}, - m_tblVarIDs = {}, - - m_strOrig = "", - m_strSource = "", - - m_strPattern = "(%s*)IMPL_OFFSET_GROUP_PAGE%s*%(%s*([%a%d_,%s]+)%s*%)", - m_pStructParser = nil, - m_pConstStructParser = nil, - } - - getter "Source->m_strOrig" - getter "Parsed->m_strSource" - - function Initialize( self, strSource, pOffsetGroupGenerator ) - self.m_strOrig = strSource - self.m_strSource = strSource - self.m_tblVarIDs = pOffsetGroupGenerator:GetVarIDs() - self:GenerateCalls() - self.m_strSource = self:ReplaceCalls() - end - - function GenerateCalls( self ) - for indent, type in self.m_strSource:gmatch( self.m_strPattern ) do - indent = indent:gsub("\n", "") - local firstDecl - local params = {} - for decl in type:gmatch("[%a%d_]+") do - if not firstDecl then - firstDecl = decl - end - - params[decl] = true - end - - self.m_tblCalls[#self.m_tblCalls +1] = { - type = type, - indent = indent, - output = self:GeneratePage(firstDecl, params, indent) - } - end - end - - function GeneratePage( self, strName, tblLevels, strIndent ) - local indent = "" - - local out = {} - if not tblLevels["NoSliderHeader"] then - table.insert(out, ("AddHeaderOption(\"%s Offsets\")"):format(strName)) - indent = strIndent - end - - table.insert(out, indent.. "IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, {") - local decls = {} - for _, name in ipairs(self.m_tblVarIDs.Offsets[strName] or {}) do - table.insert(decls, name) - end - table.insert(out, table.concat(decls, ",")) - - indent = strIndent - table.insert(out, "})") - - if not tblLevels["NoInterpToggles"] then - table.insert(out, indent.."SetCursorPosition(1)") - indent = strIndent - if not tblLevels["NoInterpHeader"] then - table.insert(out, indent.. "AddHeaderOption(\"Interpolation\")") - end - - table.insert(out, indent.. "IMPL_STRUCT_MACRO_INVOKE_GROUP(implControl, {") - decls = {} - for _, name in ipairs(self.m_tblVarIDs.Interp[strName] or {}) do - table.insert(decls, indent.. name) - end - table.insert(out, table.concat(decls, ",")) - table.insert(out, indent.. "})") - end - - return table.concat(out, "\n") - end - - -- We need to group invocations based on the struct page member - -- Then create an if block to compare pageName agains each unique page member found - -- THEN invoke macros under that page name - - function ReplaceCalls( self ) - local parsed = self.m_strSource - for _, call in pairs( self.m_tblCalls ) do - while true do - local str = ("IMPL_OFFSET_GROUP_PAGE%s*%(%s*(_1_)%s*%)") - :replace( "_1_", call.type ) - - local s, e = parsed:find( str ) - if not s then break end - - local prefix = parsed:sub( 1, s-1 ) - local postfix = parsed:sub( e+1 ) - parsed = prefix.. call.output.. postfix - end - end - return parsed - end -end \ No newline at end of file diff --git a/CodeGen/MCM/preproc/preproc_struct.lua b/CodeGen/MCM/preproc/preproc_struct.lua deleted file mode 100644 index 0a46cf0..0000000 --- a/CodeGen/MCM/preproc/preproc_struct.lua +++ /dev/null @@ -1,182 +0,0 @@ -do class "StructToVars" : namespace "papyrus.preproc" { - m_tblStructTypes = {}, - m_tblStructImpls = {}, - m_tblStructMemberUsage = {}, - m_tblStructMacroUsage = {}, - - m_strSource = "", - m_strOrig = "", - } - - getter "Source->m_strOrig" - getter "Parsed->m_strSource" - - getter "StructTypes->m_tblStructTypes" - getter "StructImpls->m_tblStructImpls" - - function Initialize( self, strSource ) - self:SetSource( strSource ) - end - - function SetSource( self, strSource ) - self.m_strOrig = strSource - self.m_strSource = strSource - end - - function Preproc( self ) - self.m_tblStructTypes = {} - self.m_tblStructImpls = {} - self.m_tblStructMemberUsage = {} - self.m_tblStructMacroUsage = {} - - self:ParseStructTypes() - self:ParseStructTypeImpls() - self:FindAndGenerateStructImplMemberUsage() - self:FindAndGenerateStructImplMacroUsage() - end - - function Apply( self ) - self.m_strSource = self:ReplaceGeneratedStructImpls() - self.m_strSource = self:ReplaceStructDecls() - self.m_strSource = self:ReplaceGeneratedStructImplMembers() - self.m_strSource = self:InsertStructImplMacros() - end - - -- Break a var decl into (type, name, value) - function ExtractVarDef( self, strVar ) - return strVar:match( "([%a%d_]+)%s+([%a%d_]+)%s*=%s*(.+)" ) - end - - function GenerateNameForStructMemberImpl( self, strStructDecl, strMember ) - local typeInfo = self.m_tblStructTypes[self.m_tblStructImpls[strStructDecl]] - if not typeInfo.members[strMember] then - error( "Invalid struct member - ".. strStructDecl.. "::".. strMember.. - " (Of type "..self.m_tblStructImpls[strStructDecl] ..")" - ) - end - return { - name = strStructDecl.. "_".. strMember, - type = typeInfo.members[strMember].type, - value = typeInfo.members[strMember].value, - } - end - - -- Read all struct type defs into a table - function ParseStructTypes( self ) - for def, cont in self.m_strSource:gmatch("#struct%s+([%a%d]+)%s*(%b{})") do - local struct = { - name = def, - members = {}, - macros = {}, - } - - local cleaned, _ = cont:sub( 2, cont:len() -1 ):gsub("\t", "") - -- Match and sub out MACRO defs first - for name, impl in cleaned:gmatch("MACRO%s+([%a%d_]+)%s*=%s*(%b{})") do - struct.macros[name] = impl:sub( 2, impl:len() -1 ) - end - - while true do - local s, e = cleaned:find( "MACRO%s+([%a%d_]+)%s*=%s*(%b{})" ) - if not s then break end - - local prefix = cleaned:sub( 1, s-1 ) - local postfix = cleaned:sub( e+1 ) - cleaned = prefix.. postfix - end - - for line in cleaned:gmatch("[^\n]+") do - local type, name, value = self:ExtractVarDef( line ) - struct.members[name] = { type = type, value = value } - end - - self.m_tblStructTypes[def] = struct - end - end - - -- Read all var decls that use a struct type we've seen - function ParseStructTypeImpls( self ) - for type, name in self.m_strSource:gmatch("([%a%d]+)%s+([%a%d_]+)\n") do - if self.m_tblStructTypes[type] then - self.m_tblStructImpls[name] = type - end - end - end - - -- Find all usages of myStructImpl.memberVar and make a generated name - function FindAndGenerateStructImplMemberUsage( self ) - for decl, member in self.m_strSource:gmatch("([%a%d_]+)%.([%a%d_]+)") do - if self.m_tblStructImpls[decl] and not self.m_tblStructMemberUsage[decl.."."..member] then - if self.m_tblStructTypes[self.m_tblStructImpls[decl]] then - local generated = self:GenerateNameForStructMemberImpl( decl, member ) - self.m_tblStructMemberUsage[decl.."."..member] = generated - end - end - end - end - - -- Find all usages of myStructImpl->!macro and generate meta data for it - function FindAndGenerateStructImplMacroUsage( self ) - for decl, member in self.m_strSource:gmatch("([%a%d_]+)%->!([%a%d_]+)") do - if self.m_tblStructImpls[decl] and not self.m_tblStructMacroUsage[decl.."->!"..member] then - if self.m_tblStructTypes[self.m_tblStructImpls[decl]] then - local code = self.m_tblStructTypes[self.m_tblStructImpls[decl]].macros[member] - code = code:replace( "this->", decl.."_" ) - self.m_tblStructMacroUsage[decl.. "->!".. member] = code:trim() - end - end - end - end - - -- Replace struct decls with empty space - function ReplaceStructDecls( self ) - local parsed = self.m_strSource - for decl, _ in pairs( self.m_tblStructTypes ) do - local s, e = parsed:find( "#struct%s+".. decl.. "%s*(%b{})" ) - local prefix = parsed:sub( 1, s-3 ) - local postfix = parsed:sub( e+1 ) - parsed = prefix.. postfix - end - return parsed - end - - function ReplaceGeneratedStructImpls( self ) - local parsed = self.m_strSource - for name, type in pairs( self.m_tblStructImpls ) do - local s, e = parsed:find( type.."%s+"..name.."\n" ) - local prefix = parsed:sub( 1, s-1 ) - local postfix = parsed:sub( e+1 ) - - local generated = {} - for m_name, _ in pairs( self.m_tblStructTypes[type].members ) do - local g = self:GenerateNameForStructMemberImpl( name, m_name ) - generated[#generated+1] = ("%s %s = %s"):format( - g.type, - g.name, - g.value - ) - end - - parsed = prefix.. table.concat( generated, "\n" ).. "\n".. postfix - end - return parsed - end - - -- Replace all instances of struct.member with the generated name - function ReplaceGeneratedStructImplMembers( self ) - local parsed = self.m_strSource - for decl, generated in pairs( self.m_tblStructMemberUsage ) do - parsed = parsed:gsub( decl, generated.name ) - end - return parsed - end - - -- Replace all instances of struct.macroMember with the macro code - function InsertStructImplMacros( self ) - local parsed = self.m_strSource - for decl, generated in pairs( self.m_tblStructMacroUsage ) do - parsed = string.replace( parsed, decl, generated ) - end - return parsed - end -end \ No newline at end of file diff --git a/CodeGen/MCM/run_preprocess.bat b/CodeGen/MCM/run_preprocess.bat index de48444..bcfcfc6 100644 --- a/CodeGen/MCM/run_preprocess.bat +++ b/CodeGen/MCM/run_preprocess.bat @@ -1,2 +1,2 @@ -cd preproc -lua do_prepoc.lua ../mcm.psc ../SmoothCamMCM.psc \ No newline at end of file +cd paper +paper.exe ../mcm/mcm.psc ../SmoothCamMCM.psc \ No newline at end of file diff --git a/CodeGen/gen_addrmap/run.lua b/CodeGen/gen_addrmap/run.lua index 9d88bca..c2fc02c 100644 --- a/CodeGen/gen_addrmap/run.lua +++ b/CodeGen/gen_addrmap/run.lua @@ -73,6 +73,18 @@ for _, v in ipairs(fileList) do end end + --DEFINE_MEMBER_FN_0..N + for addr in s:gmatch("DEFINE_MEMBER_FN_[%d]+%s*%(%s*[%w%d%s%*_<>]+%s*,%s*[%w%d%s%*_<>]+%s*,%s*([%dxXABCDEFabcdef0]+%s*)") do + if addr == "0" then addr = "0x00000000" end + addrs[addr] = tonumber(addr, 16) == 0 and 0 or offsets[tonumber(addr, 16)] + if not addrs[addr] then + print("WARNING: Address ".. addr.. " is not in the offsets db! Mapping this value to 0. File: ".. v) + addrs[addr] = 0 + else + numMemberFuns = numMemberFuns +1 + end + end + for addr in s:gmatch("RelocAddr%s*<%s*[%w%d%s%*_<>]+%s*>%s*[%w%d%s%*_<>]+%s*%(%s*([%dxABCDEFabcdef]+)%s*%)") do if addr == "0" then addr = "0x00000000" end addrs[addr] = tonumber(addr, 16) == 0 and 0 or offsets[tonumber(addr, 16)] diff --git a/CodeGen/shared/lava.lua b/CodeGen/shared/lava.lua deleted file mode 100644 index 54e7465..0000000 --- a/CodeGen/shared/lava.lua +++ /dev/null @@ -1,1244 +0,0 @@ -local is51 = _VERSION == "Lua 5.1" -local isJit = jit ~= nil - --- Function copies -local setmetatable = setmetatable -local getmetatable = getmetatable -local pairs = pairs -local ipairs = ipairs -local type = type -local error = error -local assert = assert -local rawget = rawget -local rawset = rawset -local getfenv = getfenv -local setfenv = setfenv -local tostring = tostring -local newproxy = newproxy -local dofile = dofile -local loadfile = loadfile -local string = { - len = string.len, - find = string.find, - sub = string.sub, - format = string.format, - rep = string.rep, - match = string.match, -} -local table = { - insert = table.insert, -} -local debug = { - getinfo = debug.getinfo, - getupvalue = debug.getupvalue, - upvaluejoin = debug.upvaluejoin, -} - -local m = { - classRegistry = {}, -- Registry of all class definitions - instanceRegistry = { -- Registry of all class instances - classes = {}, - singletons = {}, - }, - - classIndex = 0, -- Global counter for assigning class id numbers - g = is51 and getfenv() or _G, - - classTypes = { - TYPE_CLASS = 0, - TYPE_SINGLETON = 1, - TYPE_ABSTRACT = 2, - TYPE_INTERFACE = 3, - TYPE_MIXIN = 4, - } -} --- Weak values to let the garbage collector do it's thing correctly -setmetatable( m.instanceRegistry.singletons, {__mode = "v"} ) - -local utils -utils = { - trimString = function( str ) - return string.match( str, "^%s*(.-)%s*$" ) - end, - - splitString = function( str, sep ) - local ret, pos = {}, 1 - for i = 1, string.len( str ) do - local front, back = string.find( str, sep, pos, true ) - if not front then break end - ret[i] = utils.trimString( string.sub(str, pos, front -1) ) - pos = back +1 - end - ret[#ret +1] = utils.trimString( string.sub(str, pos) ) - return ret - end, - - emptyTable = function( tbl ) - for k, _ in pairs( tbl ) do - tbl[k] = nil - end - end, - - copyTable = function( tblSrc, seen ) - local copy = {} - setmetatable( copy, getmetatable(tblSrc) ) - for k, v in pairs( tblSrc ) do - if type( v ) ~= "table" then - copy[k] = v - else - seen = seen or {} - seen[tblSrc] = copy - if seen[v] then - copy[k] = seen[v] - else - copy[k] = utils.copyTable( v, seen ) - end - end - end - return copy - end, - - mergeTable = function( tblSrc, tblDest ) - for k, v in pairs( tblSrc ) do - if type( v ) == "table" and type( tblDest[k] ) == "table" then - utils.mergeTable( v, tblDest[k] ) - else - tblDest[k] = v - end - end - return tblDest - end, - - mergeTablePreserveDest = function( tblSrc, tblDest ) - for k, v in pairs( tblSrc ) do - if type( v ) == "table" and type( tblDest[k] ) == "table" then - utils.mergeTable( v, tblDest[k] ) - else - if tblDest[k] == nil then -- Don't overwrite anything in dest, only add absent things - tblDest[k] = v - end - end - end - return tblDest - end, - - mergeTableErrorConflicts = function( tblSrc, tblDest, strErrFmt, fnErrCB ) - for k, v in pairs( tblSrc ) do - if type( v ) == "table" and type( tblDest[k] ) == "table" then - utils.mergeTable( v, tblDest[k], strErrFmt ) - else - if tblDest[k] ~= nil then -- Error - if fnErrCB then fnErrCB() end - error( string.format(strErrFmt, k) ) - else - tblDest[k] = v - end - end - end - return tblDest - end, - - classGCRouter = function( pClassInstance ) - local meta = getmetatable(pClassInstance) - if meta.__removed then - return -- Lua is cleaning up fragmented userdata - end - - local v = rawget( meta.__classDef.methods, "__GC" ) - if v then - v( pClassInstance ) - end - meta.__classDef:OnGC( pClassInstance ) - end, -} - -if not is51 then - function utils.setfenv( fn, env ) - local i = 1 - while true do - local name = debug.getupvalue( fn, i ) - if name == "_ENV" then - debug.upvaluejoin( fn, i, function() - return env - end, 1 ) - break - elseif not name then - break - end - i = i +1 - end - return fn - end - - function utils.getfenv( fn ) - local i = 1 - while true do - local name, val = debug.getupvalue( fn, i ) - if name == "_ENV" then - return val - elseif not name then - break - end - i = i +1 - end - end -end - -function m:GetOrCreateNamespace( strNamespace ) - local t = m.g - for _, v in ipairs( utils.splitString(strNamespace, ".") ) do - t[v] = t[v] or {} - t = t[v] - end - return t -end - - ---[[ - Class declaration object - Created by calls to class creation methods (ex: class(), abstract(), singleton(), interface()) -]]-- -do - local classDecMT = {} - local classDecMTDef = { - __index = classDecMT, - __call = function( dec, ... ) - return m:CreateClassDefinition( dec, ... ) - end, - } - - -- A bit weird here, to allow for proper method chaining me may end up wanting to call methods like final() - -- after 'extend "" : from ""', we can't easily reconcile this so we must act as a proxy type in such instances - local extendMT = { - from = function( self, strNamespace ) - self.dec:SetParentNamespace( strNamespace ) - return self - end, - } - - local extendMTObj = { - __index = function( t, k ) - if k == "from" then - return extendMT.from - else - return rawget( t, "dec" )[k] - end - end, - __newindex = function( t, k, v ) - rawget( t, "dec" )[k] = v - end, - __call = function( t, ... ) - return classDecMTDef.__call( rawget(t, "dec"), ... ) - end, - } - - -- Pretty much the same story as above with extends, except we must 'finalize' implements() in edge case - -- (when from() is never called) - local implementsMT = { - from = function( self, strNamespace ) - self.ns = strNamespace - return self - end, - } - - local implementsMTObj = { - __index = function( t, k ) - if not rawget( t, "finalized" ) and k == "from" then - return implementsMT.from - else - -- Finalize first - if not rawget( t, "finalized" ) then - rawget( t, "dec" ):AddInterface( rawget(t, "iface"), rawget(t, "ns") ) - rawset( t, "finalized", true ) - end - - return rawget( t, "dec" )[k] - end - end, - __newindex = function( t, k, v ) - if k == "ns" then - rawset( t, k, v ) - else - rawget( t, "dec" )[k] = v - end - end, - __call = function( t, ... ) - -- Finalize first - if not rawget( t, "finalized" ) then - rawget( t, "dec" ):AddInterface( rawget(t, "iface"), rawget(t, "ns") ) - rawset( t, "finalized", true ) - end - - return classDecMTDef.__call( rawget(t, "dec"), ... ) - end, - } - - -- For mixins, more of the same - --[[local mixinMT = { - from = function( self, strNamespace ) - self.ns = strNamespace - return self - end, - }]]-- - - local mixinMTObj = { - __index = function( t, k ) - if not rawget( t, "finalized" ) and k == "from" then - return implementsMT.from - else - -- Finalize first - if not rawget( t, "finalized" ) then - rawget( t, "dec" ):AddMixin( rawget(t, "name"), rawget(t, "ns") ) - rawset( t, "finalized", true ) - end - - return rawget( t, "dec" )[k] - end - end, - __newindex = function( t, k, v ) - if k == "ns" then - rawset( t, k, v ) - else - rawget( t, "dec" )[k] = v - end - end, - __call = function( t, ... ) - -- Finalize first - if not rawget( t, "finalized" ) then - rawget( t, "dec" ):AddMixin( rawget(t, "name"), rawget(t, "ns") ) - rawset( t, "finalized", true ) - end - - return classDecMTDef.__call( rawget(t, "dec"), ... ) - end, - } - - local classDecInst = { - type = m.classTypes.TYPE_CLASS, - className = nil, - m_namespace = nil, - parentClass = nil, - parentNamespace = nil, - m_final = false, - interfaces = {}, - mixins = {}, - } - - function classDecMT:SetClassType( nClassType ) - self.type = nClassType - end - - function classDecMT:SetClassName( strName ) - self.className = strName - end - - function classDecMT:SetNamespace( strNamespace ) - self.m_namespace = strNamespace - end - - function classDecMT:SetParentClass( strClass ) - self.parentClass = strClass - end - - function classDecMT:SetParentNamespace( strNamespace ) - self.parentNamespace = strNamespace - end - - function classDecMT:AddInterface( strInterface, strNamespace ) - table.insert( self.interfaces, { - iface = strInterface, - ns = strNamespace, - } ) - end - - function classDecMT:AddMixin( strMixin, strNamespace ) - local name = (strNamespace and (strNamespace.. ".") or "").. strMixin - if self.mixins[name] then - error( string.format("Attempt to add a mixin that has already been added! (Mixin: %s)\n", name) ) - return - end - self.mixins[name] = { name = strMixin, namespace = strNamespace, path = name } - end - - function classDecMT:extends( strClassName ) - if self.type == m.classTypes.TYPE_INTERFACE then - error( "Extending an interface is not allowed.\n" ) - return - elseif self.type == m.classTypes.TYPE_MIXIN then - error( "Extending a mixin is not allowed.\n" ) - return - end - - self:SetParentClass( strClassName ) - - local extendObj = { dec = self } - setmetatable( extendObj, extendMTObj ) - return extendObj - end - - function classDecMT:namespace( strNamespace ) - self:SetNamespace( strNamespace ) - return self - end - - function classDecMT:final() - if self.type == m.classTypes.TYPE_INTERFACE then - error( "Marking an interface as final has no meaning, this is not allowed.\n" ) - return - elseif self.type == m.classTypes.TYPE_MIXIN then - error( "Marking a mixin as final has no meaning, this is not allowed.\n" ) - return - end - - self.m_final = true - return self - end - - function classDecMT:implements( strInterface ) - if self.type == m.classTypes.TYPE_INTERFACE then - error( "An interface may not implement another interface.\n" ) - return - elseif self.type == m.classTypes.TYPE_MIXIN then - error( "A mixin may not implement an interface.\n" ) - return - end - - local implementsObj = { dec = self, iface = strInterface } - setmetatable( implementsObj, implementsMTObj ) - return implementsObj - end - - function classDecMT:mixin( strMixin ) - if self.type == m.classTypes.TYPE_INTERFACE then - error( "Applying mixins to an interface is not allowed.\n" ) - return - elseif self.type == m.classTypes.TYPE_MIXIN then - error( "Applying mixins to another mixin is not allowed.\n" ) - return - end - - local mixinObj = { dec = self, name = strMixin } - setmetatable( mixinObj, mixinMTObj ) - return mixinObj - end - - function m:CreateClassDeclaration() - local inst = utils.copyTable( classDecInst ) - setmetatable( inst, classDecMTDef ) - return inst - end -end - - ---[[ - Class definition object - Created by calls to class declaration objects (declaring members at the end of the call chain via {}) -]]-- -do - local classDefMT = {} - local classDefMTDef = { - __index = classDefMT, - } - local classDefInst = { - declaration = nil, - classID = 0, - - refCount = 0, - - -- namespace and parent info - namespace = nil, - parent = nil, - parentNamespace = nil, - - instanceProto = {}, -- Final instance prototype - - -- member data - members = {}, -- Normal member vars that are copied to all new instances - - -- method data - methods = {}, - - -- interface info - interfaces = {}, - - -- mixins - mixins = { - list = {}, - methods = {}, - members = {}, - }, - - -- shared data - shared_proto = nil, - shared_instance = nil, - - -- meta methods - metaMethods = {}, - meta = nil, - - finalized = false, - } - - local metaLookup = { - ["__tostring"] = true, - ["__call"] = true, - ["__eq"] = true, - ["__unm"] = true, - ["__add"] = true, - ["__sub"] = true, - ["__mul"] = true, - ["__div"] = true, - ["__pow"] = true, - ["__concat"] = true, - ["__lt"] = true, - ["__le"] = true, - ["__len"] = true, - } - - local removedMT = { - __index = function() error( "Attempt to index a removed class!\n" ) end, - __newindex = function() error( "Attempt to index a removed class!\n" ) end, - __removed = true, - } - - function classDefMT:GetMembers() - return self.members - end - - function classDefMT:GetMethods() - return self.methods - end - - function classDefMT:GetMixins() - return self.mixins - end - - function classDefMT:GetParent() - return self.parent - end - - function classDefMT:GetNamespace() - return self.namespace - end - - function classDefMT:GetParentNamespace() - return self.parentNamespace - end - - function classDefMT:GetClassDeclaration() - return self.declaration - end - - function classDefMT:GetInstances() - if self.declaration.type ~= m.classTypes.TYPE_CLASS then return end - return m.instanceRegistry.classes[self.classID] and m.instanceRegistry.classes[self.classID] or {} - end - - function classDefMT:FindSuperMethod( pInstance, strFunc ) - local cur = pInstance - while type( cur.parent ) == "table" do - if type( cur.parent.methods[strFunc] ) == "function" then - return cur.super.methods[strFunc] - end - if cur == cur.parent then break end - cur = cur.parent - end - return - end - - function classDefMT:CallConstructor( pInstance, ... ) - if type( pInstance.Initialize ) == "function" then - pInstance:Initialize( ... ) - else - local method = self:FindSuperMethod( pInstance, "Initialize" ) - if not method then error( string.format("Class %s is missing a constructor.\n", self.declaration.className) ) - return - end - method( pInstance, ... ) - end - - return pInstance - end - - function classDefMT:IncRefCount() - self.refCount = self.refCount +1 - if self.refCount == 1 then - -- Setup shared instance data - if self.shared_proto then - utils.emptyTable( self.shared_instance ) - self.shared_instance = utils.mergeTable( self.shared_proto, self.shared_instance ) - end - end - end - - function classDefMT:DecRefCount() - assert( self.refCount > 0, "Class destructor error!\n" ) - self.refCount = self.refCount -1 - if self.refCount == 0 then - -- Clear shared instance data - if self.shared_proto and self.shared_instance then - utils.emptyTable( self.shared_instance ) - end - end - end - - local RemoveInstance - do - local def, t, meta - RemoveInstance = function( pClassInstance ) - meta = getmetatable( pClassInstance ) - if meta.__removed then return end - - def = meta.__classDef - if def.declaration.type == m.classTypes.TYPE_SINGLETON then - m.instanceRegistry.singletons[def.classID] = nil - else - t = m.instanceRegistry.classes[def.classID] - if t then - t[pClassInstance] = nil - end - end - - if meta.__removed then return end - if type( pClassInstance.OnRemove ) == "function" then - pClassInstance:OnRemove() - end - - meta.__index = removedMT.__index - meta.__newindex = removedMT.__newindex - meta.__removed = true - meta.__gc = nil - meta.__tostring = nil - meta.__call = nil - meta.__eq = nil - meta.__unm = nil - meta.__add = nil - meta.__sub = nil - meta.__mul = nil - meta.__div = nil - meta.__pow = nil - meta.__concat = nil - meta.__lt = nil - meta.__le = nil - meta.__len = nil - end - end - - do - local p - function classDefMT:OnGC( pClassInstance ) - self:DecRefCount() - p = self.parent - while p ~= nil do - p:DecRefCount() - p = p.parent - end - - RemoveInstance( pClassInstance ) - end - end - - function classDefMT:New( ... ) - if self.declaration.type == m.classTypes.TYPE_SINGLETON then - if m.instanceRegistry.singletons[self.classID] then - -- If we already have a singleton instance, return that instance - return m.instanceRegistry.singletons[self.classID] - end - end - - if self.declaration.type == m.classTypes.TYPE_ABSTRACT or - self.declaration.type == m.classTypes.TYPE_INTERFACE or - self.declaration.type == m.classTypes.TYPE_MIXIN then - error( "Cannot create an instance of an abstract, interface or mixin class.\n" ) - end - - -- Construct the instance - local meta = { - __gc = self.meta.__gc, - __tostring = self.meta.__tostring, - __call = self.meta.__call, - __eq = self.meta.__eq, - __unm = self.meta.__unm, - __add = self.meta.__add, - __sub = self.meta.__sub, - __mul = self.meta.__mul, - __div = self.meta.__div, - __pow = self.meta.__pow, - __concat = self.meta.__concat, - __lt = self.meta.__lt, - __le = self.meta.__le, - __len = self.meta.__len, - __classDef = self.meta.__classDef, - } - meta.__mt = meta - - local instance - if is51 then - -- Make a userdata proxy to get __gc functionality - local realInstance = utils.copyTable( self.instanceProto ) -- copy member vars into the instance - - instance = newproxy( true ) - local mt = getmetatable( instance ) - mt.__instance = realInstance -- Hard ref to keep the instance alive for the lifetime of the proxy - mt.__index = realInstance - mt.__newindex = realInstance - mt.__gc = meta.__gc - mt.__tostring = meta.__tostring - mt.__call = meta.__call - mt.__eq = meta.__eq - mt.__unm = meta.__unm - mt.__add = meta.__add - mt.__sub = meta.__sub - mt.__mul = meta.__mul - mt.__div = meta.__div - mt.__pow = meta.__pow - mt.__concat = meta.__concat - mt.__lt = meta.__lt - mt.__le = meta.__le - mt.__len = meta.__len - mt.__classDef = meta.__classDef - mt.__mt = mt - else - local realInstance = utils.copyTable( self.instanceProto ) -- copy member vars into the instance - -- we need a proxy table due to metatable changes in 5.2+ (__index only being invoked when a var isn't found) - instance = {} - local mt = { - __instance = realInstance, - __index = realInstance, - __newindex = realInstance, - __gc = meta.__gc, - __tostring = meta.__tostring, - __call = meta.__call, - __eq = meta.__eq, - __unm = meta.__unm, - __add = meta.__add, - __sub = meta.__sub, - __mul = meta.__mul, - __div = meta.__div, - __pow = meta.__pow, - __concat = meta.__concat, - __lt = meta.__lt, - __le = meta.__le, - __len = meta.__len, - __classDef = meta.__classDef, - } - mt.__mt = mt - setmetatable( instance, mt ) - end - - self:IncRefCount() - local p = self.parent - while p ~= nil do - p:IncRefCount() - p = p.parent - end - - -- If we are a singleton, add to the singleton list - if self.declaration.type == m.classTypes.TYPE_SINGLETON then - m.instanceRegistry.singletons[self.classID] = instance - else - -- Add the class to the instance registry - if not m.instanceRegistry.classes[self.classID] then - m.instanceRegistry.classes[self.classID] = {} - --Weak values to let the garbage collector do it's thing correctly - setmetatable( m.instanceRegistry.classes[self.classID], {__mode = "kv"} ) - end - m.instanceRegistry.classes[self.classID][instance] = instance - end - - return self:CallConstructor( instance, ... ) - end - - function classDefMT:FinalizeMixins() - for _, v in pairs( self.declaration.mixins ) do - local ns = m.g - if v.namespace then - ns = m:GetOrCreateNamespace( v.namespace ) - end - - local mixin = ns[v.name] - if not mixin then - m:CleanupFailedDef( self ) - error( string.format("Unable to find mixin '%s'\n", (v.namespace and (v.namespace.. ".") or "").. v.name) ) - return - end - - self.mixins.list[v.path] = mixin - self.mixins.members = utils.mergeTableErrorConflicts( - mixin.members, - self.mixins.members, - "Mixin member conflict! (%s)\n", - function() - m:CleanupFailedDef( self ) - end - ) - self.mixins.methods = utils.mergeTableErrorConflicts( - mixin.methods, - self.mixins.methods, - "Mixin method conflict! (%s)\n", - function() - m:CleanupFailedDef( self ) - end - ) - end - - -- Parent mixins - if self.parent then - for _, v in pairs( self.parent.declaration.mixins ) do - if self.mixins.list[v.path] then - local cns = self.declaration.m_namespace - cns = cns and (cns.. ".") or "" - - m:CleanupFailedDef( self ) - error( string.format( - "Parent class '%s' contains the same mixin as the child class '%s' (mixin '%s')\n", - self.parent.declaration.className, - cns.. self.declaration.className, - (v.namespace and (v.namespace.. ".") or "").. v.name - ) ) - end - end - end - - -- Merge mixins with class data - self.members = utils.mergeTableErrorConflicts( - self.mixins.members, - self.members, - "Mixin member conflict! (%s)\n", - function() - m:CleanupFailedDef( self ) - end - ) - self.methods = utils.mergeTableErrorConflicts( - self.mixins.methods, - self.methods, - "Mixin method conflict! (%s)\n", - function() - m:CleanupFailedDef( self ) - end - ) - end - - function classDefMT:FinalizeInterfaces() - -- Merge parent interfaces - if self.parent then - for id, iface in pairs( self.parent.interfaces ) do - local dec = m.classRegistry[iface.classID].declaration - if self.interfaces[id] then - local cns = self.declaration.m_namespace - cns = cns and (cns.. ".") or "" - - local ins = dec.m_namespace - ins = ins and (ins.. ".") or "" - - m:CleanupFailedDef( self ) - error( string.format( - "Interface re-implementation in class '%s' from parent '%s' - interface '%s'", - cns.. self.declaration.className, - self.parent.declaration.className, - ins.. dec.className - ) ) - return - end - - self.interfaces[id] = iface - end - end - - -- Check that interfaces are all implemented - for _, iface in pairs( self.interfaces ) do - for k, v in pairs( iface.methods ) do - if type( self.methods[k] ) ~= "function" or self.methods[k] == v then - local dec = m.classRegistry[iface.classID].declaration - m:CleanupFailedDef( self ) - error( string.format( - "Class '%s' uses interface '%s' but does not implement method '%s'!\n", - (self.declaration.m_namespace and (self.declaration.m_namespace.. ".") or "").. self.declaration.className, - dec.className, - k - ) ) - end - end - end - end - - function classDefMT:Finalize() - -- Register mixins - self:FinalizeMixins() - -- Interfaces - self:FinalizeInterfaces() - -- Inherit metamethods if we have any - if self.parent then - for name, _ in pairs( metaLookup ) do - if self.parent.metaMethods[name] then - if not self.metaMethods[name] then - self.metaMethods[name] = self.parent.metaMethods[name] - end - end - end - end - - -- Build our metatable - self.meta = { - __gc = utils.classGCRouter, - __tostring = self.metaMethods.__tostring, - __call = self.metaMethods.__call, - __eq = self.metaMethods.__eq, - __unm = self.metaMethods.__unm, - __add = self.metaMethods.__add, - __sub = self.metaMethods.__sub, - __mul = self.metaMethods.__mul, - __div = self.metaMethods.__div, - __pow = self.metaMethods.__pow, - __concat = self.metaMethods.__concat, - __lt = self.metaMethods.__lt, - __le = self.metaMethods.__le, - __len = self.metaMethods.__len, - __classDef = self, - } - - -- Build the instance prototype - self.instanceProto = utils.copyTable( self.members ) - self.instanceProto = utils.mergeTableErrorConflicts( - self.methods, - self.instanceProto, - "Method/member name conflict!\n", - function() - m:CleanupFailedDef( self ) - end - ) - - if not self.instanceProto.Remove then - self.instanceProto.Remove = RemoveInstance - end - - m.classRegistry[self.classID] = self - self.finalized = true - - if self.finalizer then - self.finalizer() - end - end - - function m:CleanupFailedDef( pClassDef ) - pClassDef.namespace[pClassDef.declaration.className] = nil - m.classRegistry[pClassDef.classID] = nil - m.classIndex = m.classIndex -1 - end - - function m:CreateClassDefinition( pClassDec, tblMembers ) - -- Get the namespace - local ns = m.g - if pClassDec.m_namespace then - ns = self:GetOrCreateNamespace( pClassDec.m_namespace ) - end - - -- Create a new class def - local def = utils.copyTable( classDefInst ) - setmetatable( def, classDefMTDef ) - - -- set declaration and namespace - def.classID = m.classIndex; m.classIndex = m.classIndex +1 - def.declaration = pClassDec - def.namespace = ns - - -- Get the parent namespace (if we have a parent) - if pClassDec.parentClass then - pClassDec.parentNamespace = pClassDec.parentNamespace or pClassDec.m_namespace - - if pClassDec.parentClass == pClassDec.className and pClassDec.parentNamespace == pClassDec.m_namespace then - m:CleanupFailedDef( def ) - error( string.format( - "Class %s is trying to extend itself!\n", - (pClassDec.m_namespace and (pClassDec.m_namespace.. ".") or "").. pClassDec.className - ) ) - return - end - - local parentNS = ns - if pClassDec.parentNamespace then - parentNS = self:GetOrCreateNamespace( pClassDec.parentNamespace ) - end - - -- Set parent info - def.parent = parentNS[pClassDec.parentClass] - def.parentNamespace = parentNS - - if not def.parent then - m:CleanupFailedDef( def ) - error( string.format( - "Parent class '%s' is nil - child class '%s'!\n", - pClassDec.parentClass, - (pClassDec.m_namespace and (pClassDec.m_namespace.. ".") or "").. pClassDec.className - ) ) - return - end - - -- Make sure we aren't trying to extend from a final class - if def.parent.declaration.m_final then - m:CleanupFailedDef( def ) - error( string.format( - "Attempt to extend the class '%s' is not allowed, '%s' is marked as final.\n", - pClassDec.parentClass, pClassDec.parentClass - ) ) - end - - -- Mix the parent members with our class - tblMembers = utils.mergeTablePreserveDest( parentNS[pClassDec.parentClass]:GetMembers(), tblMembers ) - -- Add parent methods - def.methods = utils.mergeTablePreserveDest( parentNS[pClassDec.parentClass]:GetMethods(), def.methods ) - end - - -- Lookup interfaces - if #pClassDec.interfaces > 0 then - for _, i in pairs( pClassDec.interfaces ) do - local namespace = i.ns and i.ns or pClassDec.m_namespace - local ins = namespace and self:GetOrCreateNamespace( namespace ) or m.g - local iface = ins[i.iface] - def.interfaces[iface.classID] = iface - end - end - - -- Set the members - def.members = tblMembers - - -- Set the definition in the class namespace - if def.namespace[pClassDec.className] ~= nil then - m:CleanupFailedDef( def ) - error( string.format("Class name conflict! Found another variable at '%s' ('%s')\n", - (pClassDec.m_namespace and (pClassDec.m_namespace.. ".") or "").. pClassDec.className, - tostring( def.namespace[pClassDec.className] ) - ) ) - return - end - def.namespace[pClassDec.className] = def - - m:HookClassEnv( def ) - end - - local function DefineAccessorFunc( pObjDef, str ) - local data = utils.splitString( str, "->" ) - local name, var = data[1], data[2] - pObjDef["Set".. name] = function( s, val ) - s[var] = val - end - pObjDef["Get".. name] = function( s ) - return s[var] - end - end - - local function DefineGetterFunc( pObjDef, str ) - local data = utils.splitString( str, "->" ) - local name, var = data[1], data[2] - pObjDef["Get".. name] = function( s ) - return s[var] - end - return pObjDef["Get".. name] - end - - local function DefineSetterFunc( pObjDef, str ) - local data = utils.splitString( str, "->" ) - local name, var = data[1], data[2] - pObjDef["Set".. name] = function( s, val ) - s[var] = val - end - return pObjDef["Set".. name] - end - - local function findMainChunk() - local i = 2 - while true do - local info = debug.getinfo( i, "nS" ) - if not info then break end - if info.what == "main" then - return i -1 - end - i = i +1 - end - end - - function m:HookClassEnv( pClassDef ) - local index = 3 -- For LuaJIT, this is always stack position 3 - if is51 and not isJit then - -- Lua 5.1 will contain extra tail calls in some cases - -- we need to find the main chunk - index = findMainChunk() - if not index then - m:CleanupFailedDef( pClassDef ) - error( "Unable to locate class environment!\n" ) - end - end - - -- If we are on Lua 5.2 or later, we use _ENV instead - local mt = is51 and getmetatable( getfenv(index) ) or getmetatable( m.__ACTIVE_CLASS_ENV ) - if mt and rawget( mt, "__isAClass" ) then - m:CleanupFailedDef( pClassDef ) - error( "Class definitions are restricted to 1 class per file/function!\n" ) - return - end - - m.__ACTIVE_CLASS_DEF = pClassDef - - local lookup - lookup = { - this = pClassDef, - super = pClassDef.parent and pClassDef.parent.methods or nil, - super_shared = pClassDef.parent and pClassDef.parent.shared_instance or nil, - accessor = function( str ) - DefineAccessorFunc( pClassDef.methods, str ) - end, - getter = function( str ) - DefineGetterFunc( pClassDef.methods, str ) - end, - setter = function( str ) - DefineSetterFunc( pClassDef.methods, str ) - end, - shared_block = function( tblData ) - if pClassDef.finalized then - error( "A shared_block can only be defined during class definition!\n" ) - return - end - - if pClassDef.shared_proto ~= nil then - m:CleanupFailedDef( pClassDef ) - error( "A class definition may only contain 1 shared block!\n" ) - return - end - pClassDef.shared_proto = tblData - pClassDef.shared_instance = {} - lookup.shared = pClassDef.shared_instance - end, - finally = function( fnCB ) - if pClassDef.finalizer then - error( "A class definition may only specify one finalizer!\n" ) - return - end - - pClassDef.finalizer = fnCB - end, - } - - local envMeta - local __index = function( _, k ) - local v = lookup[k] - return v and v or rawget( m.g, k ) - end - local __newindex = function( t, k, v ) - if type( v ) == "function" and t == envMeta then - if metaLookup[k] then - rawset( pClassDef.metaMethods, k, v ) - else - rawset( pClassDef.methods, k, v ) - end - else - if t ~= envMeta then - rawset( t, k, v ) - else - rawset( m.g, k, v ) - end - end - end - - if is51 then - envMeta = setmetatable( {}, { - __index = __index, - __newindex = __newindex, - __isAClass = true, - } ) - setfenv( index, envMeta ) - else - mt.__index = __index - mt.__newindex = __newindex - mt.__isAClass = true - envMeta = setmetatable( m.__ACTIVE_CLASS_ENV, mt ) - end - end -end - - -return { - abstract = function( strName ) - local dec = m:CreateClassDeclaration() - dec:SetClassType( m.classTypes.TYPE_ABSTRACT ) - dec:SetClassName( strName ) - return dec - end, - - singleton = function( strName ) - local dec = m:CreateClassDeclaration() - dec:SetClassType( m.classTypes.TYPE_SINGLETON ) - dec:SetClassName( strName ) - return dec - end, - - class = function( strName ) - local dec = m:CreateClassDeclaration() - dec:SetClassType( m.classTypes.TYPE_CLASS ) - dec:SetClassName( strName ) - return dec - end, - - interface = function( strName ) - local dec = m:CreateClassDeclaration() - dec:SetClassType( m.classTypes.TYPE_INTERFACE ) - dec:SetClassName( strName ) - return dec - end, - - mixin = function( strName ) - local dec = m:CreateClassDeclaration() - dec:SetClassType( m.classTypes.TYPE_MIXIN ) - dec:SetClassName( strName ) - return dec - end, - - implements = function( pClassInstance, pClassDef ) - return getmetatable( pClassInstance ).__classDef.interfaces[pClassDef.classID] and true or false - end, - - is_a = function( pClassInstance, pClassDef ) - return getmetatable( pClassInstance ).__classDef and - getmetatable( pClassInstance ).__classDef.classID == pClassDef.classID - end, - - validClass = is51 and - (function( pClassInstance ) - return type( pClassInstance ) == "userdata" and getmetatable(pClassInstance).__classDef ~= nil - end) - or - (function( pClassInstance ) - return type( pClassInstance ) == "table" and getmetatable(pClassInstance).__classDef ~= nil - end), - - loadClass = function( strFile ) - if is51 then - -- Lua 5.1/JIT will setfenv the file scope - dofile( strFile ) - m.__ACTIVE_CLASS_DEF:Finalize() - m.__ACTIVE_CLASS_DEF = nil - else - -- Lua 5.2+ must set an _ENV metatable and grab a reference to it - m.__ACTIVE_CLASS_ENV = setmetatable( {}, {__index = m.g} ) - m.__ACTIVE_CLASS_CHUNK = loadfile( strFile, nil, m.__ACTIVE_CLASS_ENV ) - m.__ACTIVE_CLASS_CHUNK() - m.__ACTIVE_CLASS_DEF:Finalize() - m.__ACTIVE_CLASS_DEF = nil - end - end, - - removeClass = function( pClassDef ) - m.classRegistry[pClassDef.classID] = nil - if pClassDef:GetInstances() then - for _, v in pairs( pClassDef:GetInstances() ) do - v:Remove() - end - end - m.instanceRegistry.classes[pClassDef.classID] = nil - m.instanceRegistry.singletons[pClassDef.classID] = nil - pClassDef.namespace[pClassDef.declaration.className] = nil - - if pClassDef.declaration.type == m.classTypes.TYPE_SINGLETON then - pClassDef:Remove() - end - end, -} \ No newline at end of file diff --git a/Deps/EABase b/Deps/EABase new file mode 160000 index 0000000..9816bb9 --- /dev/null +++ b/Deps/EABase @@ -0,0 +1 @@ +Subproject commit 9816bb911b17f9ee593ad0913a036a175aeaece7 diff --git a/Deps/EASTL b/Deps/EASTL new file mode 160000 index 0000000..95b281e --- /dev/null +++ b/Deps/EASTL @@ -0,0 +1 @@ +Subproject commit 95b281ee345372655f189ea5baa50adb3a61832a diff --git a/Package/00 Data/AVX/placeholder b/Package/00 Data/AVX/placeholder new file mode 100644 index 0000000..e69de29 diff --git a/Package/00 Data/ExtraData/SmoothCam_EyeBones_Default.txt b/Package/00 Data/ExtraData/SmoothCam_EyeBones_Default.txt new file mode 100644 index 0000000..cedba7b --- /dev/null +++ b/Package/00 Data/ExtraData/SmoothCam_EyeBones_Default.txt @@ -0,0 +1,7 @@ +// Much like the follow bones file, this file lists bones to be used for positioning the first-person camera +// This file is currently not used in release mode, the first person camera is very much work-in-progress +// and is only included in developer builds + +NPCEyeBone +NPC Head [Head] +NPC Head \ No newline at end of file diff --git a/Package/00 Data/ExtraData/SmoothCam_FollowBones_Default.txt b/Package/00 Data/ExtraData/SmoothCam_FollowBones_Default.txt new file mode 100644 index 0000000..9448d86 --- /dev/null +++ b/Package/00 Data/ExtraData/SmoothCam_FollowBones_Default.txt @@ -0,0 +1,40 @@ +// Any file in the SKSE plugins folder starting with the name "SmoothCam_FollowBones_" will be loaded and parsed by SmoothCam. +// Bone names in this file are used to select the starting camera position height for the player and are selected based on +// what matching bone name is found first. SmoothCam will search this list in descending order, so if you want to override +// a bone, place it higher up in the list (Or include your bones in a file with a name that SmoothCam will load earlier). + +// Each bone should be placed on a separate line and the name should be an exact match to what is found in the skeleton. +// You can add comment lines by starting the line with '//', but you cannot place comments anywhere on the same line as a bone! +// (You think I'm writing a proper parser for this?) + +// I've included some bone names for most of the vanilla skeletons here, with what *should* be decent locations for the +// camera to follow, starting with the highest priority bones to look for, descending to the last-resort bones. + +Camera3rd [Cam3] +NPC Head [Head] +NPC Head +DwarvenSpiderBody +ElkSpine1 +ChaurusFlyerSpine1 +Boar_Spine1 +NetchPelvis [Pelv] +DragPriestNPC Head [Head] +Canine_Spine1 +Goat_Spine3 +Horker_Ribcage +HorseSpine2 +IW Head +Mammoth Spine 4 +Mcrab_Body +Sabrecat_Ribcage[Spn4] +SlaughterfishNeck +Wisp Head +Bip01 Spine1 +Spine [Spn1] +SPINE1 +SpineUpperSpine +MainBody +NPC Spine [Spn1] +Neck1 +RabbitSpine2 +NPC COM [COM] \ No newline at end of file diff --git a/Package/00 Data/SSE/placeholder b/Package/00 Data/SSE/placeholder new file mode 100644 index 0000000..e69de29 diff --git a/Package/00 Data/SmoothCam.esl b/Package/00 Data/SmoothCam.esl new file mode 100644 index 0000000..24fa6e2 Binary files /dev/null and b/Package/00 Data/SmoothCam.esl differ diff --git a/CodeGen/MCM/SmoothCam.esp b/Package/00 Data/SmoothCam.esp similarity index 100% rename from CodeGen/MCM/SmoothCam.esp rename to Package/00 Data/SmoothCam.esp diff --git a/Package/00 Data/SmoothCamMCM.pex b/Package/00 Data/SmoothCamMCM.pex new file mode 100644 index 0000000..7ef56d2 Binary files /dev/null and b/Package/00 Data/SmoothCamMCM.pex differ diff --git a/Package/Fomod/ModuleConfig.xml b/Package/Fomod/ModuleConfig.xml new file mode 100644 index 0000000..1365c49 --- /dev/null +++ b/Package/Fomod/ModuleConfig.xml @@ -0,0 +1,62 @@ + + SmoothCam + + + + + + + For use with modern CPUs + + + + + + + + + + + For use with older CPUs (~2011 and earlier) + + + + + + + + + + + + + + + + + + + + Use the ESP version of the MCM plugin + + + + + + + + + Use the ESL version of the MCM plugin + + + + + + + + + + + + + \ No newline at end of file diff --git a/Package/Fomod/info.xml b/Package/Fomod/info.xml new file mode 100644 index 0000000..c54ac42 --- /dev/null +++ b/Package/Fomod/info.xml @@ -0,0 +1,10 @@ + + SmoothCam + mwilsnd + 1.4 + https://www.nexusmods.com/skyrimspecialedition/mods/41252 + SmoothCam is a highly configurable third-person camera, with smooth frame-interpolation and a raycasting crosshair to help you aim. + + Gameplay + + \ No newline at end of file diff --git a/README.md b/README.md index 422a6c6..77300bd 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,37 @@ # SkyrimSE-SmoothCam -SmoothCam adds a frame-interpolated third-person camera to Skyrim Special Edition. After trying other third-person camera mods I decided to try and make something better, and this is the result. If I actually achieved my goal in making a better camera is up for you to decide. +SmoothCam adds a customizable frame-interpolated third-person camera to Skyrim Special Edition. + +Notable features include: +* A raycast crosshair for archery and magic combat +* Selectable custom 3D crosshairs (Skyrim style and a dot style) +* Ability to use the HUD crosshair if not using one of the custom 3D styles +* Trajectory prediction and visualization for arrows +* Per-state offset control (X, Y, Z, FOV), further subdivided into groupings for different combat stances +* 22 different interpolation methods which may be chosen for 6 different smoothers (Primary, Local-Space, Separate Z, Offset, Zoom, FOV) +* Distance clamping control to keep your character in frame while still being able to use lazy camera smoothing +* 6 preset slots for you to save and load completely different configurations on the fly ## Compatibility SmoothCam is going to have issues with any other mod that tries to position the third-person camera (Other third-person mods, likely some lock-on mods, etc). The following mods are supported, with patches that can be enabled in the MCM: -* Improved Camera +* Improved Camera (Only with the reddit release build of Improved Camera beta 4) * Immersive First Person View * Alternate Conversation Camera * Archery Gameplay Overhaul +True Directional Movement is compatible and fully supported. + ## Runtime Requirements -SmoothCam requires SKSE64 and [Address Library](https://www.nexusmods.com/skyrimspecialedition/mods/32444). If you wish to use the MCM esp, SkyUI is also required. +SmoothCam requires SKSE64 and [Address Library](https://www.nexusmods.com/skyrimspecialedition/mods/32444). If you wish to use the MCM esp, SkyUI is also required. **SmoothCam only officially supports Skyrim Special Edition runtime 1.5.97**, other versions are not tested for full functionality and are unsupported. #### Notice: Depending on the age of your processor (~2011 and earlier), you might need to use the SSE version (Streaming SIMD Extensions). Most people can use the normal version without issue. ## Installing -If using one of the pre-compiled releases, just copy the `Data` folder in the archive into your skyrim folder or install via a mod manager. +If using one of the pre-compiled releases, use a mod manager and follow the prompts to select the DLL and plugin types you want. If doing a manual install, install either the AVX or SSE version of the DLL and the contents of the `ExtraData` folder to `Data/SKSE/Plugins`. Copy the pex script to `Data/Scripts` and copy either the esp or esl variant of the plugin into `Data`. -If installing after a build, copy `SmoothCam.dll` to `Data/SKSE/Plugins` (along with the address library database file and `ExtraData/SmoothCam_FollowBones_Default.txt`), `SmoothCam.esp` to `Data/` and `SmoothCamMCM.pex` to `Data/Scripts`. Enable the esp file if you wish to use the MCM, otherwise the module will generate a json file in the plugins folder you can edit to manually configure the camera. +If installing after a build, copy `SmoothCam.dll` to `Data/SKSE/Plugins` (along with the address library database file and `ExtraData/SmoothCam_FollowBones_Default.txt`), `SmoothCam.esp/esl` to `Data/` and `SmoothCamMCM.pex` to `Data/Scripts`. Enable the esp file if you wish to use the MCM, otherwise the module will generate a json file in the plugins folder you can edit to manually configure the camera. ## Building To build the project, clone the repo (remember to include `--recurse-submodules`), then run `make_vs2019.bat` or `make_vs2017.bat`. @@ -29,66 +41,115 @@ Once premake has finished, open the generated Visual Studio solution, select `De Built files are placed in `SmoothCam/bin//SmoothCam`. -To build the papyrus script, you'll need `lua` on the system path. To run the code generation just run `MCM/run_preprocess.bat` which will generate `SmoothCamMCM.psc`. -From there just compile the generated code like any normal papyrus script. - To generate the offset map, run `lua run.lua "path/to/skse/src"` from the directory `CodeGen/gen_addrmap`. You will need `offsets.txt` in the same folder, which can be exported from address library. +To build the papyrus script for the MCM, navigate to `CodeGen/MCM` and run `run_preprocess.bat` to generate the output file `SmoothCamMCM.psc`. From there just compile the generated code like any normal papyrus script. + To generate crosshair model file headers, navigate to `CodeGen/ModelBaker` and run `ModelBaker "path/to/input.ply" "output_name". -To build the model baker, you will need dub, DMD or LDC, a copy of assimp which you have compiled as a static library and optionally VSCode with the code-d extension. Make a folder in `ModelBaker` named `compiled_libs` (or `compiled_libs_debug` if making a debug build) and copy all assimp libraries to that location. +To compile the MCM tool (`paper`) and crosshair model converter (`ModelBaker`), you will need dub, DMD or LDC and optionally VSCode with the code-d extension. Both projects have build targets pre-configured for VSCode. +For `ModelBaker`, you will also need a copy of assimp which you have compiled as a static library. Make a folder in `ModelBaker` named `compiled_libs` (or `compiled_libs_debug` if making a debug build) and copy all assimp libraries to that location. +Crosshair models use vertex colors rather than textures, therefore use of the stanford ply model format as input to the converter tool is recommended. + +## FAQ +> My crosshair/sneak meter flickers randomly. + +If you have Archery Gameplay Overhaul installed, you need to disable "Bow Camera" and "Bow Crosshair" in AGO's MCM. Also enable the compatibility for AGO in SmoothCam's MCM. This can also be caused by HUD mods which hide or move around elements like the crosshair. If possible, disabling these features should resolve the issue. + +> I think SmoothCam is causing me to crash. + +As of version 1.4, you can enable the general setting "Enable Crash Dump Handler". Select this setting and then restart the game. If you do crash while SmoothCam code is running, you will get a message box informing you of the crash and explaining where to find the crash dump file (SmoothCam_AppCrash.mdmp, in the same folder as SkyrimSE.exe). Make a new github issue and share this mini-dump file so I can try to help fix the problem. This won't catch every single possible cause of SmoothCam potentially causing a crash, but it should catch >99% of them. You can also share .NET Script Framework crash logs in your github issue. + +> Under "Compatibility Options", Improved Camera beta4 shows as either "NOT DETECTED" or "VERSION MISTMATCH" but I have it installed? + +You must install the correct version for compatibility features to work with Improved Camera correctly. Only the reddit release DLL of beta 4 is supported. + +> My camera got stuck in a strange state and I can't rotate my camera anymore, what gives? + +This is a pretty rare issue, I've only had it happen once myself. You can try the general option "Force camera to thirdperson" which may work. The problem appears to be caused by the camera getting stuck in the "Furniture" camera state. + +> The Plugin Info section shows "D3D11 Hooked" is FALSE, or "DLL Version" does not match "MCM Script Version" + +This usually means the SKSE plugin failed to load. Check your SKSE log file for any error messages, along with SmoothCam's log file in the same folder. You can try installing the non-AVX version of SmoothCam (SSE version) which may resolve the issue if your CPU doesn't support the AVX instruction set. If the versions don't match, you might have an outdated version of either the script or DLL. Reinstalling the mod fresh should resolve that issue. + +> The SmoothCam MCM looks correct and functional, but the camera isn't working. + +Two causes: Either the general setting "Disable SmoothCam" is enabled, or you used the "coc" console command from the main menu without loading a save or starting a new game. + +> Camera movement is very jittery when rotating around the player character. + +This is most commonly caused by local-space interpolation when you have a low frame rate. You can eliminate the jitter by keeping local space interpolation enabled and setting the method to "linear" and follow rate to "1.0". + +> I used the "Load Next Preset" hotkey and now it doesn't work anymore. + +Currently, you must set this keybinding in every preset. When you load your next preset if you haven't set this key binding it will be unloaded (or set to a different key). + +> My camera randomly swapped to the other side of my character. + +You likely pressed the "Shoulder Swap" key unintentionally. You can find this key binding under the misc section in the Following tab. + ## Configuration The easiest way to configure the camera is by using the SmoothCam MCM menu. If you do not have SkyUI, or wish to manually edit the json settings, here is a description of each value: Setting | Description --- | --- -enableInterp | Enables camera smoothing globally -use3DBowAimCrosshair | Enables the raycasted crosshair while aiming with a bow -use3DMagicCrosshair | Enables the raycasted crosshair when magic is equiped -hideNonCombatCrosshair | Hides the crosshair when no weapon is drawn -hideCrosshairMeleeCombat | Hides the crosshair when melee weapons are drawn +use3DBowAimCrosshair | Use the raycasting crosshair when aiming with a bow +use3DMagicCrosshair | Use the raycasting crosshair when aiming with combat magic +hideNonCombatCrosshair | Hide the crosshair when not in a combat stance +hideCrosshairMeleeCombat| Hide the crosshair when in a melee-only combat stance enableCrosshairSizeManip | Enable size manipulation of the crosshair -crosshairNPCHitGrowSize | When the 3D crosshair is over an NPC, grow the size of the crosshair by this amount +crosshairNPCHitGrowSize | When the 3D crosshair is over an actor, grow the size of the crosshair by this amount crosshairMinDistSize | Sets the size of the 3D crosshair when the player's aim ray is at the maximum distance crosshairMaxDistSize | Sets the size of the 3D crosshair when the player's aim ray is at the minimum distance -useWorldCrosshair | Use a custom 3D rendered crosshair instead of the HUD crosshair -worldCrosshairDepthTest | Enable depth testing when using the world crosshair -worldCrosshairType | The style of crosshair to use when using the world crosshair mode +useWorldCrosshair | Use the world-space 3D crosshair model rather than the HUD crosshair +worldCrosshairDepthTest | Enables depth-testing (occlusion) of the world-space crosshair +worldCrosshairType | The crosshair style to use when using the world-space crosshair +stealthMeterXOffset | Offset the stealth meter by this value along the X axis +stealthMeterYOffset | Offset the stealth meter by this value along the Y axis +offsetStealthMeter | Offset the stealth meter when the world-space crosshair is active +alwaysOffsetStealthMeter | Always offset the stealth meter useArrowPrediction | Enable arrow trajectory prediction -drawArrowArc | Draw an arc when using arrow prediction -arrowArcColor | The color of the arrow arc -maxArrowPredictionRange | The maximum range to run arrow trajectory prediction code -disableDeltaTime | Disables frame time factoring in the smoothing math -compatIC | Enables compat code for dealing with issue when running the Improved Camera mod -compatACC | Enables compat code for dealing with issues when running the Alternate Conversation Camera mod -compatIFPV | Enables compat code for dealing with issues when running the Immersive First Person View mod -compatAGO | Enables compat code for dealing with issues when running the Archery Gameplay Overhaul mod -shoulderSwapKey | The key code used for swapping the X axis when pressed -swapXClamping | When swapping the shoulder/X axis, also flips the distance clamping X values -currentScalar | A value from 0 - 21: The scalar function to use for interpolation +drawArrowArc | When using arrow prediction, draws the predicted trajectory as an arc +arrowArcColor | The RGBA value to use for coloring the trajectory arc +maxArrowPredictionRange | The maximum range to allow trajectory prediction to run - Very long ranges can cause lag as trajectory computation is on the expensive side +disableDeltaTime | Removes frame-time from the interpolation math. This may or may not result in a smoother camera motion depending on your system and frame rate +nextPresetKey | Key code to use for cycling to the next preset. All presets need this key set or your key will be unloaded or set to something different when switching +shoulderSwapKey | Key code to toggle shoulder swapping +swapXClamping | When swapping shoulders, will also swap the X axis of your distance clamping configuration +modDisabled | When enabled, disables SmoothCam and allows the normal game camera to run +modToggleKey | Key code to use for toggling modDisabled +customZOffset | A custom Z height offset to use with the accompanying toggle key +applyZOffsetKey | Key code to toggle the customZOffset +enableCrashDumps | When enabled, SmoothCam will install a vectored exception handler at game startup and catch crashes that happen while SmoothCam code is present in the call stack. Writes a mini-dump (mdmp) to help find the cause. +compatIC | Enables Improved Camera beta 4 compatibility +compatIFPV | Enables Immersive First Person View compatibility +compatAGO | Enables Archery Gameplay Overhaul compatibility +compatACC | Enables Alternate Conversation Camera compatibility +enableInterp | Enables world position interpolation +currentScalar | Selectable scalar method to use for world interpolation minCameraFollowDistance | The distance the camera follows the player from when at the lowest zoom level minCameraFollowRate | The amount of camera latency when the camera is close to the player (lower = more latency) maxCameraFollowRate | The amount of camera latency when the camera is far from the player (higher = less latency) zoomMul | The maximum zoom-out distance from the player (added to MinFollowDistance) zoomMaxSmoothingDistance | The distance from camera to player at which MaxCameraFollowRate is used for smoothing (when distance is small, MinCameraFollowRate is used, otherwise is mixed with MaxCameraFollowRate) This results in the camera being more "lazy" when close to the player and more snappy when further away -separateLocalInterp | Enable separate local-space interpolation settings -separateLocalScalar | A value from 0 - 21: The scalar function to use for local-space interpolation -localScalarRate | The constant smoothing rate for local-space interpolation -separateZInterp | Enable the separate Z interpolation settings +separateLocalInterp | Enables interpolation separately for character-local space +separateLocalScalar | Selectable scalar method to use for separate local interpolation +localScalarRate | The smoothing rate to use for separate local interpolation +separateZInterp | Enables interpolation separately for world position on the Z axis +separateZScalar | Selectable scalar method to use for separate Z interpolation separateZMaxSmoothingDistance | The distance from camera to player at which separateZMaxFollowRate is used for smoothing (when distance is small, separateZMinFollowRate is used, otherwise is mixed with separateZMaxFollowRate) This results in the camera being more "lazy" when close to the player and more snappy when further away separateZMinFollowRate | The amount of camera latency when the camera is close to the player (lower = more latency) separateZMaxFollowRate | The amount of camera latency when the camera is far from the player (higher = less latency) -separateZScalar | A value from 0 - 21: The scalar function to use for Z interpolation -enableOffsetInterpolation | Enable smoothing of camera offset state transitions -offsetScalar | A value from 0 - 21: The scalar method to use for offset transition smoothing -offsetInterpDurationSecs | The smoothing duration to use when the camera changes offsets (In seconds) -enableZoomInterpolation | Enable smoothing of camera zoom state transitions -zoomScalar | A value from 0 - 21: The scalar method to use for zoom transition smoothing -zoomInterpDurationSecs | The smoothing duration to use when the camera changes zoom distance (In seconds) -enableFOVInterpolation | Enable smoothing of camera fov state transitions -fovScalar | A value from 0 - 21: The scalar method to use for fov transition smoothing -fovInterpDurationSecs | The smoothing duration to use when the camera changes fov (In seconds) +enableOffsetInterpolation | Enables interpolation for offset group transitions +offsetScalar | Selectable scalar method to use for offset interpolation +offsetInterpDurationSecs | Duration smoothing between changes in offset should take place +enableZoomInterpolation | Enables interpolation for zoom transitions +zoomScalar | Selectable scalar method to use for zoom interpolation +zoomInterpDurationSecs | Duration smoothing between changes in zoom should take place +enableFOVInterpolation | Enables interpolation for FOV transitions +fovScalar | Selectable scalar method to use for FOV interpolation +fovInterpDurationSecs | Duration smoothing between changes in FOV should take place cameraDistanceClampXEnable | Enable distance clamping on the local X axis cameraDistanceClampXMin | The minimum value the camera position may reach on the X axis cameraDistanceClampXMax | The maximum value the camera position may reach on the X axis diff --git a/SmoothCam/include/addrlib/offsets.h b/SmoothCam/include/addrlib/offsets.h index 7e0a822..1d9182c 100644 --- a/SmoothCam/include/addrlib/offsets.h +++ b/SmoothCam/include/addrlib/offsets.h @@ -1,5 +1,6 @@ #pragma once #include +#include namespace Offsets { // Generated code from code_gen/gen_addrmap @@ -26,6 +27,8 @@ namespace Offsets { { 0x0019C3B0, 14678 }, { 0x0019CE40, 14693 }, { 0x001A1730, 14809 }, + { 0x001C60A0, 15491 }, + { 0x001C61A0, 15494 }, { 0x001D2050, 0 }, { 0x001D66E0, 15757 }, { 0x001D6860, 15758 }, @@ -155,6 +158,9 @@ namespace Offsets { { 0x00878850, 50650 }, { 0x00879770, 50657 }, { 0x00879B40, 50659 }, + { 0x00880140, 50750 }, + { 0x00885400, 50823 }, + { 0x00885C40, 50839 }, { 0x00887750, 50882 }, { 0x00887970, 50884 }, { 0x008879A0, 50885 }, @@ -195,6 +201,7 @@ namespace Offsets { { 0x00C52750, 68835 }, { 0x00C529A0, 68839 }, { 0x00C56B50, 68900 }, + { 0x00C57A60, 68936 }, { 0x00C59690, 68971 }, { 0x00C598F0, 68972 }, { 0x00C59AE0, 68973 }, @@ -205,6 +212,7 @@ namespace Offsets { { 0x00C68D20, 69335 }, { 0x00C6BC70, 69406 }, { 0x00C72180, 69562 }, + { 0x00C72300, 69564 }, { 0x00C76060, 69636 }, { 0x00C76340, 69638 }, { 0x00C76480, 69639 }, @@ -215,12 +223,28 @@ namespace Offsets { { 0x00C767B0, 69647 }, { 0x00C76820, 69648 }, { 0x00C7EB60, 69804 }, + { 0x00C91440, 70356 }, { 0x00D2ECA0, 74034 }, { 0x00D74B60, 75638 }, { 0x00EBE150, 79937 }, { 0x00EBF9C0, 79949 }, { 0x00EC30F0, 80059 }, { 0x00EC31D0, 80061 }, + { 0x00EC83A0, 80197 }, + { 0x00EC9490, 80207 }, + { 0x00EC9BD0, 80214 }, + { 0x00EC9C20, 80216 }, + { 0x00EC9F30, 80218 }, + { 0x00ECA150, 80222 }, + { 0x00ECA570, 80230 }, + { 0x00ECA620, 80231 }, + { 0x00ECA860, 80233 }, + { 0x00ECB080, 80244 }, + { 0x00ECB0E0, 80245 }, + { 0x00ECB300, 80248 }, + { 0x00ECBDB0, 80263 }, + { 0x00ECCBA0, 80268 }, + { 0x00ECCCF0, 80270 }, { 0x00ECD7F0, 80282 }, { 0x00ECD850, 80283 }, { 0x00ECD8A0, 80284 }, @@ -232,6 +256,8 @@ namespace Offsets { { 0x00ECE790, 80302 }, { 0x00ED3A50, 80446 }, { 0x00ED6AC0, 80520 }, + { 0x00F45350, 82382 }, + { 0x00F45820, 82383 }, { 0x00F483C0, 82467 }, { 0x0122DAD0, 97379 }, { 0x01233670, 97462 }, @@ -250,6 +276,7 @@ namespace Offsets { { 0x012507F0, 97923 }, { 0x01250C80, 97925 }, { 0x01250ED0, 97927 }, + { 0x0128F900, 98844 }, { 0x01291C30, 98893 }, { 0x01291D40, 98897 }, { 0x01295C30, 98986 }, @@ -363,24 +390,27 @@ namespace Offsets { { 0x058FEAB4, 0 } }); - VersionDb& GetDB(); + std::unique_ptr& GetDB(); bool Initialize(); + void ReleaseDB() noexcept; #ifdef _DEBUG void DumpDatabaseTextFile(); #endif + void CacheID(uintptr_t ofsID) noexcept; + template - T Get(uintptr_t id) { - return reinterpret_cast(GetDB().FindAddressById(id)); + T Get(uintptr_t id) noexcept { + return reinterpret_cast(reinterpret_cast(GetOffset(id))); } template - T GetVersionAddress(uintptr_t addr) { + T GetVersionAddress(uintptr_t addr) noexcept { return Get(addrMap.at(addr)); } constexpr uintptr_t GetByVersionAddr(uintptr_t addr); - uintptr_t GetVersionAddress(uintptr_t addr); - uintptr_t GetOffset(uintptr_t id); + uintptr_t GetVersionAddress(uintptr_t addr) noexcept; + uintptr_t GetOffset(uintptr_t id) noexcept; } \ No newline at end of file diff --git a/SmoothCam/include/addrlib/skse_macros.h b/SmoothCam/include/addrlib/skse_macros.h index 5443866..1f92666 100644 --- a/SmoothCam/include/addrlib/skse_macros.h +++ b/SmoothCam/include/addrlib/skse_macros.h @@ -37,10 +37,10 @@ struct AddrNotInMapWarning { #define DEFINE_MEMBER_FN_LONG(className, functionName, retnType, address, ...) \ typedef retnType (className::* _##functionName##_type)(__VA_ARGS__); \ - static constexpr const AddrNotInMapWarning _##functionName##_adr = {}; \ + static constexpr AddrNotInMapWarning _##functionName##_adr; \ inline _##functionName##_type * _##functionName##_GetPtr(void) \ { \ - static const uintptr_t adr = Offsets::GetVersionAddress(address) + RelocationManager::s_baseAddr; \ + static const uintptr_t adr = Offsets::GetVersionAddress(address); \ return (_##functionName##_type *)&adr; \ } @@ -77,83 +77,92 @@ struct AddrNotInMapWarning { // Using the original implementation does very broken things in a Release build #define FORCE_INLINE __forceinline -#define DEFINE_MEMBER_FN_0(fnName, retnType, addr) \ - FORCE_INLINE retnType fnName() { \ - struct empty_struct {}; \ - typedef retnType(empty_struct::*_##fnName##_type)(); \ - const static uintptr_t address = addr + RelocationManager::s_baseAddr; \ - _##fnName##_type fn = *(_##fnName##_type*)&address; \ - return (reinterpret_cast(this)->*fn)(); \ +#define DEFINE_MEMBER_FN_0(fnName, retnType, addr) \ + FORCE_INLINE retnType fnName() { \ + struct empty_struct {}; \ + typedef retnType(empty_struct::*_##fnName##_type)(); \ + static constexpr AddrNotInMapWarning _##functionName##_adr = {}; \ + const static uintptr_t address = Offsets::GetVersionAddress(addr); \ + _##fnName##_type fn = *(_##fnName##_type*)&address; \ + return (reinterpret_cast(this)->*fn)(); \ } -#define DEFINE_MEMBER_FN_1(fnName, retnType, addr, ...) \ - template \ - FORCE_INLINE retnType fnName(T1 && t1) { \ - struct empty_struct {}; \ - typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ - const static uintptr_t address = addr + RelocationManager::s_baseAddr; \ - _##fnName##_type fn = *(_##fnName##_type*)&address; \ - return (reinterpret_cast(this)->*fn)(t1); \ +#define DEFINE_MEMBER_FN_1(fnName, retnType, addr, ...) \ + template \ + FORCE_INLINE retnType fnName(T1 && t1) { \ + struct empty_struct {}; \ + typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ + static constexpr AddrNotInMapWarning _##functionName##_adr = {}; \ + const static uintptr_t address = Offsets::GetVersionAddress(addr); \ + _##fnName##_type fn = *(_##fnName##_type*)&address; \ + return (reinterpret_cast(this)->*fn)(t1); \ } -#define DEFINE_MEMBER_FN_2(fnName, retnType, addr, ...) \ - template \ - FORCE_INLINE retnType fnName(T1 && t1, T2 && t2) { \ - struct empty_struct {}; \ - typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ - const static uintptr_t address = addr + RelocationManager::s_baseAddr; \ - _##fnName##_type fn = *(_##fnName##_type*)&address; \ - return (reinterpret_cast(this)->*fn)(t1, t2); \ +#define DEFINE_MEMBER_FN_2(fnName, retnType, addr, ...) \ + template \ + FORCE_INLINE retnType fnName(T1 && t1, T2 && t2) { \ + struct empty_struct {}; \ + typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ + static constexpr AddrNotInMapWarning _##functionName##_adr = {}; \ + const static uintptr_t address = Offsets::GetVersionAddress(addr); \ + _##fnName##_type fn = *(_##fnName##_type*)&address; \ + return (reinterpret_cast(this)->*fn)(t1, t2); \ } -#define DEFINE_MEMBER_FN_3(fnName, retnType, addr, ...) \ - template \ - FORCE_INLINE retnType fnName(T1 && t1, T2 && t2, T3 && t3) { \ - struct empty_struct {}; \ - typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ - const static uintptr_t address = addr + RelocationManager::s_baseAddr; \ - _##fnName##_type fn = *(_##fnName##_type*)&address; \ - return (reinterpret_cast(this)->*fn)(t1, t2, t3); \ +#define DEFINE_MEMBER_FN_3(fnName, retnType, addr, ...) \ + template \ + FORCE_INLINE retnType fnName(T1 && t1, T2 && t2, T3 && t3) { \ + struct empty_struct {}; \ + typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ + static constexpr AddrNotInMapWarning _##functionName##_adr = {}; \ + const static uintptr_t address = Offsets::GetVersionAddress(addr); \ + _##fnName##_type fn = *(_##fnName##_type*)&address; \ + return (reinterpret_cast(this)->*fn)(t1, t2, t3); \ } -#define DEFINE_MEMBER_FN_4(fnName, retnType, addr, ...) \ - template \ - FORCE_INLINE retnType fnName(T1 && t1, T2 && t2, T3 && t3, T4 && t4) { \ - struct empty_struct {}; \ - typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ - const static uintptr_t address = addr + RelocationManager::s_baseAddr; \ - _##fnName##_type fn = *(_##fnName##_type*)&address; \ - return (reinterpret_cast(this)->*fn)(t1, t2, t3, t4); \ +#define DEFINE_MEMBER_FN_4(fnName, retnType, addr, ...) \ + template \ + FORCE_INLINE retnType fnName(T1 && t1, T2 && t2, T3 && t3, T4 && t4) { \ + struct empty_struct {}; \ + typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ + static constexpr AddrNotInMapWarning _##functionName##_adr = {}; \ + const static uintptr_t address = Offsets::GetVersionAddress(addr); \ + _##fnName##_type fn = *(_##fnName##_type*)&address; \ + return (reinterpret_cast(this)->*fn)(t1, t2, t3, t4); \ } -#define DEFINE_MEMBER_FN_5(fnName, retnType, addr, ...) \ - template \ - FORCE_INLINE retnType fnName(T1 && t1, T2 && t2, T3 && t3, T4 && t4, T5 && t5) { \ - struct empty_struct {}; \ - typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ - const static uintptr_t address = addr + RelocationManager::s_baseAddr; \ - _##fnName##_type fn = *(_##fnName##_type*)&address; \ - return (reinterpret_cast(this)->*fn)(t1, t2, t3, t4, t5); \ +#define DEFINE_MEMBER_FN_5(fnName, retnType, addr, ...) \ + template \ + FORCE_INLINE retnType fnName(T1 && t1, T2 && t2, T3 && t3, T4 && t4, T5 && t5) { \ + struct empty_struct {}; \ + typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ + static constexpr AddrNotInMapWarning _##functionName##_adr = {}; \ + const static uintptr_t address = Offsets::GetVersionAddress(addr); \ + _##fnName##_type fn = *(_##fnName##_type*)&address; \ + return (reinterpret_cast(this)->*fn)(t1, t2, t3, t4, t5); \ } -#define DEFINE_MEMBER_FN_6(fnName, retnType, addr, ...) \ - template \ - FORCE_INLINE retnType fnName(T1 && t1, T2 && t2, T3 && t3, T4 && t4, T5 && t5, T6 && t6) { \ - struct empty_struct {}; \ - typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ - const static uintptr_t address = addr + RelocationManager::s_baseAddr; \ - _##fnName##_type fn = *(_##fnName##_type*)&address; \ - return (reinterpret_cast(this)->*fn)(t1, t2, t3, t4, t5, t6); \ +#define DEFINE_MEMBER_FN_6(fnName, retnType, addr, ...) \ + template \ + FORCE_INLINE retnType fnName(T1 && t1, T2 && t2, T3 && t3, T4 && t4, T5 && t5, T6 && t6) { \ + struct empty_struct {}; \ + typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ + static constexpr AddrNotInMapWarning _##functionName##_adr = {}; \ + const static uintptr_t address = Offsets::GetVersionAddress(addr); \ + _##fnName##_type fn = *(_##fnName##_type*)&address; \ + return (reinterpret_cast(this)->*fn)(t1, t2, t3, t4, t5, t6); \ } -#define DEFINE_MEMBER_FN_7(fnName, retnType, addr, ...) \ +#define DEFINE_MEMBER_FN_7(fnName, retnType, addr, ...) \ template \ FORCE_INLINE retnType fnName(T1 && t1, T2 && t2, T3 && t3, T4 && t4, T5 && t5, T6 && t6, T7 && t7) { \ struct empty_struct {}; \ typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ - const static uintptr_t address = addr + RelocationManager::s_baseAddr; \ + static constexpr AddrNotInMapWarning _##functionName##_adr = {}; \ + const static uintptr_t address = Offsets::GetVersionAddress(addr); \ _##fnName##_type fn = *(_##fnName##_type*)&address; \ return (reinterpret_cast(this)->*fn)(t1, t2, t3, t4, t5, t6, t7); \ } -#define DEFINE_MEMBER_FN_8(fnName, retnType, addr, ...) \ +#define DEFINE_MEMBER_FN_8(fnName, retnType, addr, ...) \ template \ FORCE_INLINE retnType fnName(T1 && t1, T2 && t2, T3 && t3, T4 && t4, T5 && t5, T6 && t6, T7 && t7, T8 && t8) { \ struct empty_struct {}; \ typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ - const static uintptr_t address = addr + RelocationManager::s_baseAddr; \ + static constexpr AddrNotInMapWarning _##functionName##_adr = {}; \ + const static uintptr_t address = Offsets::GetVersionAddress(addr); \ _##fnName##_type fn = *(_##fnName##_type*)&address; \ return (reinterpret_cast(this)->*fn)(t1, t2, t3, t4, t5, t6, t7, t8); \ } @@ -162,7 +171,8 @@ struct AddrNotInMapWarning { FORCE_INLINE retnType fnName(T1 && t1, T2 && t2, T3 && t3, T4 && t4, T5 && t5, T6 && t6, T7 && t7, T8 && t8, T9 && t9) { \ struct empty_struct {}; \ typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ - const static uintptr_t address = addr + RelocationManager::s_baseAddr; \ + static constexpr AddrNotInMapWarning _##functionName##_adr = {}; \ + const static uintptr_t address = Offsets::GetVersionAddress(addr); \ _##fnName##_type fn = *(_##fnName##_type*)&address; \ return (reinterpret_cast(this)->*fn)(t1, t2, t3, t4, t5, t6, t7, t8, t9); \ } @@ -171,7 +181,8 @@ struct AddrNotInMapWarning { FORCE_INLINE retnType fnName(T1 && t1, T2 && t2, T3 && t3, T4 && t4, T5 && t5, T6 && t6, T7 && t7, T8 && t8, T9 && t9, T10 && t10) { \ struct empty_struct {}; \ typedef retnType(empty_struct::*_##fnName##_type)(__VA_ARGS__); \ - const static uintptr_t address = addr + RelocationManager::s_baseAddr; \ + static constexpr AddrNotInMapWarning _##functionName##_adr = {}; \ + const static uintptr_t address = Offsets::GetVersionAddress(addr); \ _##fnName##_type fn = *(_##fnName##_type*)&address; \ return (reinterpret_cast(this)->*fn)(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10); \ } diff --git a/SmoothCam/include/arrow_fixes.h b/SmoothCam/include/arrow_fixes.h index 0ad1b4c..7bda973 100644 --- a/SmoothCam/include/arrow_fixes.h +++ b/SmoothCam/include/arrow_fixes.h @@ -1,25 +1,52 @@ #pragma once +#ifdef DEBUG +#include "render/d3d_context.h" +#include "render/line_drawer.h" +#endif namespace ArrowFixes { struct LaunchData { void* vtbl; + + NiPoint3 normal; NiPoint3 unkVec1; - float pad1; - uintptr_t unk1; BGSProjectile* projectile; PlayerCharacter* player; - uintptr_t unk2; + NiAVObject* avObject; TESObjectWEAP* weapon; TESAmmo* ammo; float spawnYaw; float spawnPitch; - uintptr_t unk3; + uintptr_t refCounted; uintptr_t unk4; uintptr_t unk5; }; + struct UnkData { + // Not sure what is going on in any of this + void* unk0; + BGSEncounterZone* zone; + uintptr_t unk1; + void* unk2; + + struct UnkData1 { + BSTEventSink evCleared; + const char* name; + int unk0; + BSTEventSink evShoutMastered; + }; + + UnkData1* unk3; + int unk4; + }; + bool Attach(); + +#ifdef DEBUG + void Draw(Render::D3DContext& ctx); + void Shutdown(); +#endif } \ No newline at end of file diff --git a/SmoothCam/include/basicdetour.h b/SmoothCam/include/basicdetour.h index 4e0c61e..da5af39 100644 --- a/SmoothCam/include/basicdetour.h +++ b/SmoothCam/include/basicdetour.h @@ -4,6 +4,11 @@ class BasicDetour { public: BasicDetour(void** orig, void* detour) noexcept; + ~BasicDetour() noexcept; + BasicDetour(const BasicDetour&) = delete; + BasicDetour(BasicDetour&&) noexcept = delete; + BasicDetour& operator=(const BasicDetour&) = delete; + BasicDetour& operator=(BasicDetour&&) noexcept = delete; bool Attach() noexcept; void Detach() noexcept; @@ -11,6 +16,194 @@ class BasicDetour { private: void** fnOrig = nullptr; void* fnDetour = nullptr; + bool attached = false; +}; + +// Some more sugar +template +class TypedDetour { + public: + explicit TypedDetour(T orig, T detour) noexcept : fnOrig(orig), fnDetour(detour) {} + explicit TypedDetour(uintptr_t offsetID, T detour) noexcept : fnDetour(detour) { + fnOrig = Offsets::Get(offsetID); + assert(fnOrig); + } + + ~TypedDetour() { + if (attached) Detach(); + } + + TypedDetour(const TypedDetour&) = delete; + TypedDetour(TypedDetour&&) noexcept = delete; + TypedDetour& operator=(const TypedDetour&) = delete; + TypedDetour& operator=(TypedDetour&&) noexcept = delete; + + bool Attach() noexcept { + assert(!attached); + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + + attached = DetourAttach(reinterpret_cast(&fnOrig), reinterpret_cast(fnDetour)) + == NO_ERROR; + + if (attached) + DetourTransactionCommit(); + else + DetourTransactionAbort(); + + return attached; + } + + void Detach() noexcept { + assert(attached); + DetourTransactionBegin(); + DetourUpdateThread(GetCurrentThread()); + DetourDetach(reinterpret_cast(&fnOrig), reinterpret_cast(fnDetour)); + DetourTransactionCommit(); + } + + T GetBase() noexcept { + return fnOrig; + } + + private: + T fnOrig = nullptr; + T fnDetour = nullptr; + bool attached = false; +}; + +// For polyhook too, why not +template +class VTableDetour { + public: + explicit VTableDetour(T* target) noexcept : target(target) {} + ~VTableDetour() noexcept { + if (attached) + Detach(); + } + + VTableDetour(const VTableDetour&) = delete; + VTableDetour(VTableDetour&&) noexcept = delete; + VTableDetour& operator=(const VTableDetour&) = delete; + VTableDetour& operator=(VTableDetour&&) noexcept = delete; + + template + void Add(uint16_t index, U fnDetour) noexcept { + assert(!attached); + redirects.insert({ index, reinterpret_cast(fnDetour) }); + } + + void Add(PLH::VFuncMap&& methods) noexcept { + assert(!attached); + redirects = eastl::move(methods); + } + + template + U GetBase(uint16_t index) noexcept { + return reinterpret_cast(originals.at(index)); + } + + // @NOTE: Because of the union hack, Attach MUST *always* be called, otherwise you risk attempting to run + // a destructor on an uninitialized object. Yes, this is nasty. + bool Attach() noexcept { + assert(!attached); + + new (&hooks) PLH::VFuncSwapHook( + reinterpret_cast(target), + redirects, + &originals + ); + + attached = hooks.hook(); + return attached; + } + + void Detach() noexcept { + assert(attached); + hooks.unHook(); + attached = false; + } + + private: + T* target = nullptr; + PLH::VFuncMap redirects; + PLH::VFuncMap originals; + + // Dumb hack for dumb reasons + union { + PLH::VFuncSwapHook hooks; + }; + + bool attached = false; +}; + +// And hooking multiple polymorphic instances +template +class PolymorphicVTableDetour { + public: + explicit PolymorphicVTableDetour() {}; + ~PolymorphicVTableDetour() { + if (attached) + Detach(); + }; + + PolymorphicVTableDetour(const PolymorphicVTableDetour&) = delete; + PolymorphicVTableDetour(PolymorphicVTableDetour&&) noexcept = delete; + PolymorphicVTableDetour& operator=(const PolymorphicVTableDetour&) = delete; + PolymorphicVTableDetour& operator=(PolymorphicVTableDetour&&) noexcept = delete; + + template + void Add(T* target, uint16_t index, U fnDetour) noexcept { + assert(!attached); + for (auto i = 0; i < targets.size(); ++i) { + if (targets[i] == target) { + redirects[i].insert({ index, reinterpret_cast(fnDetour) }); + return; + } + } + + targets.push_back(target); + redirects.push_back({ { index, reinterpret_cast(fnDetour) } }); + originals.push_back(); + } + + template + U GetBase(T* target, uint16_t index) noexcept { + for (auto i = 0; i < targets.size(); ++i) + if (targets[i] == target) + return reinterpret_cast(originals[i].at(index)); + return nullptr; + } + + bool Attach() noexcept { + assert(!attached); + for (auto i = 0; i < targets.size(); ++i) { + hooks.push_back_uninitialized(); + new (&hooks.back()) PLH::VFuncSwapHook( + reinterpret_cast(targets[i]), + redirects[i], + &originals[i] + ); + + if (!hooks.back().hook()) + return false; + } + attached = true; + return true; + } + + void Detach() noexcept { + assert(attached); + for (auto& it : hooks) + it.unHook(); + attached = false; + } + + private: + eastl::fixed_vector targets; + eastl::fixed_vector redirects; + eastl::fixed_vector originals; + eastl::fixed_vector hooks; bool attached = false; }; \ No newline at end of file diff --git a/SmoothCam/include/camera.h b/SmoothCam/include/camera.h index 35b6b91..357d52f 100644 --- a/SmoothCam/include/camera.h +++ b/SmoothCam/include/camera.h @@ -1,19 +1,12 @@ #pragma once -#include "camera_state.h" -#include "camera_states/thirdperson.h" -#include "camera_states/thirdperson_combat.h" -#include "camera_states/thirdperson_horse.h" -#include "crosshair.h" - -#ifdef WITH_CHARTS -# include "render/line_graph.h" -# include "render/cbuffer.h" -# include "render/ninode_tree_display.h" -# include "render/state_overlay.h" -#endif +#include "camera_states/base_first.h" +#include "camera_states/base_third.h" +#include "trackir/trackir.h" namespace Camera { - typedef void(*UpdateWorldToScreenMtx)(NiCamera*); + class Thirdperson; + class Firstperson; + class Camera; // Helps describe more about the current camera and player state // Used for selecting camera offsets @@ -39,37 +32,86 @@ namespace Camera { MAX_STATE }; - // Used to select which scalar function type should be run - enum class ScalarSelector { - Normal, - SepZ, - LocalSpace, - }; - - enum class MenuID { + enum class MenuID : uint8_t { None, DialogMenu, LoadingMenu, MistMenu, FaderMenu, LoadWaitSpinner, + MapMenu, + InventoryMenu, + }; + + enum class CameraID : uint8_t { + None, + Firstperson, + Thirdperson + }; + + // Interface to different cameras + class ICamera { + public: + ICamera(Camera* baseCamera, CameraID id) : m_camera(baseCamera), m_id(id) {}; + virtual ~ICamera() {}; + + // Called when we are switching to this camera + virtual void OnBegin(PlayerCharacter* player, CorrectedPlayerCamera* camera, ICamera* lastState) noexcept = 0; + // Called when we are done using this camera + virtual void OnEnd(PlayerCharacter* player, CorrectedPlayerCamera* camera, ICamera* newState) noexcept = 0; + + // Runs before the internal game camera logic + // Return true when changing the camera state + virtual bool OnPreGameUpdate(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState) = 0; + // Selects the correct update method and positions the camera + virtual void OnUpdateCamera(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState) = 0; + // Render crosshair objects + virtual void Render(Render::D3DContext& ctx) noexcept = 0; + + // Called when the player toggles the POV + virtual void OnTogglePOV(const ButtonEvent* ev) noexcept = 0; + // Called when any other key is pressed + virtual bool OnKeyPress(const ButtonEvent* ev) noexcept = 0; + // Called when a menu of interest is opening or closing + virtual bool OnMenuOpenClose(MenuID id, const MenuOpenCloseEvent* const ev) noexcept = 0; + + // Triggers when the camera action state changes + virtual void OnCameraActionStateTransition(const PlayerCharacter* player, const CameraActionState newState, + const CameraActionState oldState) noexcept = 0; + // Triggers when the camera state changes + virtual void OnCameraStateTransition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, + const GameState::CameraState newState, const GameState::CameraState oldState) noexcept = 0; + + public: + const CameraID m_id = CameraID::None; + + protected: + Camera* m_camera = nullptr; + }; - class SmoothCamera { + // Low level camera, holding both the thirdperson and firstperson cameras + class Camera { public: - SmoothCamera(); - ~SmoothCamera(); - SmoothCamera(const SmoothCamera&) = delete; - SmoothCamera(SmoothCamera&&) noexcept = delete; - SmoothCamera& operator=(const SmoothCamera&) = delete; - SmoothCamera& operator=(SmoothCamera&&) noexcept = delete; + Camera(); + ~Camera(); + Camera(const Camera&) = delete; + Camera(Camera&&) noexcept = delete; + Camera& operator=(const Camera&) = delete; + Camera& operator=(Camera&&) noexcept = delete; public: // Runs before the internal game camera logic // Return true when changing the camera state - bool PreGameUpdate(PlayerCharacter* player, CorrectedPlayerCamera* camera, BSTSmartPointer& nextState); + bool PreGameUpdate(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState); // Selects the correct update method and positions the camera - void UpdateCamera(PlayerCharacter* player, CorrectedPlayerCamera* camera, BSTSmartPointer& nextState); + void UpdateCamera(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState); + // Render crosshair objects + void Render(Render::D3DContext& ctx); // Called when the player toggles the POV void OnTogglePOV(const ButtonEvent* ev) noexcept; @@ -86,110 +128,61 @@ namespace Camera { // Set the camera world position void SetPosition(const glm::vec3& pos, const CorrectedPlayerCamera* camera) noexcept; - // Returns the full world-space camera target postion for the current player state - glm::vec3 GetCurrentCameraTargetWorldPosition(const TESObjectREFR* ref, const CorrectedPlayerCamera* camera) const; - // Returns the target world-space position for the camera, with local offsets and rotations applied. This is the goal - // position ignoring any interpolation - void GetCameraGoalPosition(const CorrectedPlayerCamera* camera, glm::vec3& world, glm::vec3& local); - // Return the euler angles for the player's current aim - glm::vec2 GetAimRotation(const TESObjectREFR* ref, const CorrectedPlayerCamera * camera) const; - // Return the camera rotation - const mmath::Rotation& GetCameraRotation() const noexcept; + // Return the current frustum + const NiFrustum& GetFrustum() const noexcept; // Get the object the camera is currently focused on NiPointer GetCurrentCameraTarget(const CorrectedPlayerCamera* camera) noexcept; // Returns true if a loading screen is active bool InLoadingScreen() const noexcept; + // Option for the MCM - Allow forcing the camera to a new state + // Adding this after observing a bug - Camera got stuck in kCameraState_Furniture + void SetShouldForceCameraState(bool force, uint8_t newCameraState) noexcept; + + // Get the thirdperson camera + Thirdperson* GetThirdpersonCamera() noexcept; + // Get the firstperson camera + Firstperson* GetFirstpersonCamera() noexcept; private: - // Returns true if interpolation is allowed in the current state - bool IsInterpAllowed(const PlayerCharacter* player) const noexcept; - // Update skyrim's screen projection matrix - void UpdateInternalWorldToScreenMatrix(const CorrectedPlayerCamera* camera) noexcept; - // Update the internal rotation - void UpdateInternalRotation(CorrectedPlayerCamera* camera) noexcept; + void UpdateInternalWorldToScreenMatrix(const mmath::Rotation& rot, const CorrectedPlayerCamera* camera) noexcept; // Updates our POV state to the true value the game expects for each state const bool UpdateCameraPOVState(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) noexcept; // Returns the current camera state for use in selecting an update method - const GameState::CameraState UpdateCurrentCameraState(const PlayerCharacter* player, const CorrectedPlayerCamera* camera); + const GameState::CameraState UpdateCurrentCameraState(const PlayerCharacter* player, + const CorrectedPlayerCamera* camera) noexcept; // Returns the current camera action state for use in the selected update method - const CameraActionState UpdateCurrentCameraActionState(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) noexcept; + const CameraActionState UpdateCurrentCameraActionState(const PlayerCharacter* player, + const CorrectedPlayerCamera* camera) noexcept; -#ifdef _DEBUG - // Triggers when the camera action state changes, for debugging + // Triggers when the camera action state changes void OnCameraActionStateTransition(const PlayerCharacter* player, const CameraActionState newState, const CameraActionState oldState) const noexcept; -#endif // Triggers when the camera state changes - void OnCameraStateTransition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, const GameState::CameraState newState, - const GameState::CameraState oldState); - - /// Camera position calculations - // Find a node to use as the world position for following - NiAVObject* FindFollowBone(const TESObjectREFR* ref) const noexcept; - // Returns the zoom value set from the given camera state - float GetCurrentCameraZoom(const CorrectedPlayerCamera* camera, const GameState::CameraState currentState) const noexcept; - // Returns an offset group for the current player movement state - const Config::OffsetGroup* GetOffsetForState(const CameraActionState state) const noexcept; - - // Selects the right offset from an offset group for the player's weapon state - float GetActiveWeaponStateZoomOffset(const PlayerCharacter* player, const Config::OffsetGroup* group) const noexcept; - // Selects the right offset from an offset group for the player's weapon state - float GetActiveWeaponStateUpOffset(const PlayerCharacter* player, const Config::OffsetGroup* group) const noexcept; - // Selects the right offset from an offset group for the player's weapon state - float GetActiveWeaponStateSideOffset(const PlayerCharacter* player, const Config::OffsetGroup* group) const noexcept; - // Selects the right offset from an offset group for the player's weapon state - float GetActiveWeaponStateFOVOffset(const PlayerCharacter* player, const Config::OffsetGroup* group) const noexcept; - - //Returns the camera zoom for the current player state - float GetCurrentCameraZoomOffset(const PlayerCharacter* player) const noexcept; - // Returns the camera height for the current player state - float GetCurrentCameraHeight(const PlayerCharacter* player) const noexcept; - // Returns the ideal camera distance for the current zoom level - float GetCurrentCameraDistance(const CorrectedPlayerCamera* camera) const noexcept; - // Returns the camera side offset for the current player state - float GetCurrentCameraSideOffset(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) const noexcept; - // Returns the camera FOV offset for the current player state - float GetCurrentCameraFOVOffset(const PlayerCharacter* player) const noexcept; - - // Returns the full local-space camera offset for the current player state, FOV is packed in .w - glm::vec4 GetCurrentCameraOffset(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) const noexcept; - - // Returns the current smoothing scalar to use for the given distance to the player - double GetCurrentSmoothingScalar(const float distance, ScalarSelector method = ScalarSelector::Normal) const; - // Returns the user defined distance clamping vector pair - std::tuple GetDistanceClamping() const noexcept; - // Returns the camera's current zoom level - Camera must extend ThirdPersonState - float GetCameraZoomScalar(const CorrectedPlayerCamera* camera, uint16_t cameraState) const noexcept; - - // Offset the gmae FOV by the given amount - void SetFOVOffset(float fov) noexcept; + void OnCameraStateTransition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, + const GameState::CameraState newState, const GameState::CameraState oldState); NiPointer GetNiCamera(CorrectedPlayerCamera* camera) const noexcept; - // Render crosshair objects - void Render(Render::D3DContext& ctx); - private: - struct { - mutable BSFixedString head = "NPC Head [Head]"; - mutable BSFixedString spine1 = "NPC Spine1 [Spn1]"; - } Strings; - - // User config - Config::UserConfig* config = nullptr; - - // The ref object the camera is focused on - Actor* currentFocusObject = nullptr; - // NiCamera - NiPointer cameraNi = nullptr; - - // Crosshair manager - std::unique_ptr crosshair; - - // All camera state instances - std::array, static_cast(GameState::CameraState::MAX_STATE)> cameraStates; - + Config::UserConfig* config = nullptr; // User config + Actor* currentFocusObject = nullptr; // The ref object the camera is focused on + NiPointer cameraNi = nullptr; // Active NiCamera + ICamera* activeCamera = nullptr; // And the active camera + + eastl::unique_ptr cameraThird; // Third person camera + eastl::unique_ptr cameraFirst; // First person camera + + bool ranLastFrame = false; // Did the camera run last frame? + bool povIsThird = false; // Our current POV state + bool povWasPressed = false; // Change POV was pressed + bool wasLoading = false; // True if we saw the loading screen menu + int8_t loadScreenDepth = 0; // If not 0, we are in a loading screen sequence + NiFrustum frustum; // Our current view frustum + mmath::NiMatrix44 worldToScaleform; // Our current worldToScreen matrix for the hud +#ifdef DEVELOPER + TrackIR::TrackingSnapshot trackIRData; // If using TrackIR, the current data from the tracker +#endif // The last and current camera state GameState::CameraState currentState = GameState::CameraState::Unknown; GameState::CameraState lastState = GameState::CameraState::Unknown; @@ -198,84 +191,21 @@ namespace Camera { CameraActionState currentActionState = CameraActionState::Unknown; CameraActionState lastActionState = CameraActionState::Unknown; - // The current rotation of the camera in both euler angles and in quaternion form - mmath::Rotation rotation; - // The last position of the camera we set - mmath::Position lastPosition; - // Our most current camera position we set - mmath::Position currentPosition; // Whatever position was last set before any camera update code is executed for the frame glm::vec3 gameLastActualPosition = { 0.0f, 0.0f, 0.0f }; - // Our current worldToScreen matrix for the hud - mmath::NiMatrix44 worldToScaleform; - // Our current view frustum - NiFrustum frustum; - - // Data to be saved and restored across certain state transitions - struct { - // Yaw rotation for the horse state - We need to restore this after moving from tween->horse - float horseYaw = 0.0f; - // ACC writes over pitch rotation of the player with an incorrect value (are we causing that?) - // Store pitch when entering dialog and restore it after - float accPitch = 0.0f; - } stateCopyData; - - // Our current offset group and offset position, set by the offset transition states - struct { - const Config::OffsetGroup* currentGroup = nullptr; - glm::vec3 position = { 0.0f, 0.0f, 0.0f }; - float fov = 0.0f; - } offsetState; - // Transition groups for smoothing offset and zoom switches - using OffsetTransition = mmath::TransitionGroup; - using ZoomTransition = mmath::TransitionGroup; - // Smooth x, y components of the active offset group - OffsetTransition offsetTransitionState; - // Smooth z of the active offset group - ZoomTransition zoomTransitionState; - // Smooth FOV of the active offset group - ZoomTransition fovTransitionState; + bool wantNewCameraState = false; // If true, we are to switch to a new camera state next update + uint8_t wantNewState = CorrectedPlayerCamera::kCameraState_ThirdPerson2; // The state to switch to - // Set on first execution to perform setup - bool firstFrame = false; - // Should we be in third person? - bool povIsThird = false; - // Was the POV key pressed this frame? - bool povWasPressed = false; - // Is the dialog menu open? - bool dialogMenuOpen = false; - // Was the dialog menu open last frame? - bool wasDialogMenuOpen = false; - // -1 when we have swapped shoulders - int shoulderSwap = 1; - // If not 0, we are in a loading screen sequence - uint8_t loadScreenDepth = 0; - -#ifdef WITH_CHARTS - std::unique_ptr perFrameOrtho; - std::unique_ptr worldPosTargetGraph; - std::unique_ptr offsetPosGraph; - std::unique_ptr offsetTargetPosGraph; - std::unique_ptr localSpaceGraph; - std::unique_ptr rotationGraph; - std::unique_ptr tpsRotationGraph; - std::unique_ptr computeTimeGraph; - std::unique_ptr refTreeDisplay; - std::unique_ptr stateOverlay; - glm::mat4 orthoMatrix; - float lastProfSnap = 0.0f; - - enum class DisplayMode : uint8_t { - None, - Graphs, - NodeTree, - StateOverlay - }; - DisplayMode curDebugMode = DisplayMode::None; - bool dbgKeyDown = false; -#endif + // When using cycle preset hotkeys, this will point at our current preset ID. + // We could store the preset slot in the config structure, but that would cause compat issues with older + // save files and somewhat prevent renaming preset files outside of the game. + // Just start at slot 0 every time and keep track of it here. + int currentPresetIndex = 0; - friend class State::BaseCameraState; + friend class State::BaseThird; + friend class State::BaseFirst; + friend class Thirdperson; + friend class Firstperson; }; } \ No newline at end of file diff --git a/SmoothCam/include/camera_states/base_first.h b/SmoothCam/include/camera_states/base_first.h new file mode 100644 index 0000000..6da9644 --- /dev/null +++ b/SmoothCam/include/camera_states/base_first.h @@ -0,0 +1,11 @@ +#pragma once + +namespace Camera { + class Firstperson; + enum class CameraState : uint8_t; + enum class CameraActionState : uint8_t; + + namespace State { + class BaseFirst {}; + } +} \ No newline at end of file diff --git a/SmoothCam/include/camera_state.h b/SmoothCam/include/camera_states/base_third.h similarity index 77% rename from SmoothCam/include/camera_state.h rename to SmoothCam/include/camera_states/base_third.h index 5856d3e..7ea7742 100644 --- a/SmoothCam/include/camera_state.h +++ b/SmoothCam/include/camera_states/base_third.h @@ -2,40 +2,43 @@ #include "game_state.h" namespace Camera { - class SmoothCamera; + class Thirdperson; enum class CameraState : uint8_t; enum class CameraActionState : uint8_t; namespace State { /* The base camera state - exposes higher level methods for operating on the camera */ - class BaseCameraState { + class __declspec(novtable) BaseThird { public: - BaseCameraState(Camera::SmoothCamera* camera) noexcept; - BaseCameraState(const BaseCameraState&) = delete; - BaseCameraState(BaseCameraState&&) noexcept = delete; - BaseCameraState& operator=(const BaseCameraState&) = delete; - BaseCameraState& operator=(BaseCameraState&&) noexcept = delete; + BaseThird(Thirdperson* camera) noexcept; + BaseThird(const BaseThird&) = delete; + BaseThird(BaseThird&&) noexcept = delete; + BaseThird& operator=(const BaseThird&) = delete; + BaseThird& operator=(BaseThird&&) noexcept = delete; public: - virtual ~BaseCameraState(); - virtual void OnBegin(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* fromState) = 0; - virtual void OnEnd(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* nextState) = 0; - virtual void Update(PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera) = 0; + virtual ~BaseThird(); + virtual void OnBegin(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* fromState) noexcept = 0; + virtual void OnEnd(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* nextState) noexcept = 0; + virtual void Update(PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera) + noexcept = 0; protected: // Hand of internal state to the next state - void StateHandOff(BaseCameraState* nextState) const noexcept; + void StateHandOff(BaseThird* nextState) const noexcept; // Returns the current camera state GameState::CameraState GetCameraState() const noexcept; // Returns the current camera action state - Camera::CameraActionState GetCameraActionState() const noexcept; + CameraActionState GetCameraActionState() const noexcept; // Get the last camera position mmath::Position& GetLastCameraPosition() const noexcept; // Get the current camera position mmath::Position& GetCameraPosition() const noexcept; + // Get the current frustum + const NiFrustum& GetFrustum() const noexcept; // Sets the camera world position void SetCameraPosition(const glm::vec3& pos, const PlayerCharacter* player, const CorrectedPlayerCamera* playerCamera) noexcept; @@ -78,7 +81,7 @@ namespace Camera { const Config::UserConfig* const GetConfig() const noexcept; protected: - Camera::SmoothCamera* const camera = nullptr; + Thirdperson* const camera = nullptr; private: // Smooth world position when moving from an interp state to a disabled one diff --git a/SmoothCam/include/camera_states/thirdperson.h b/SmoothCam/include/camera_states/thirdperson.h deleted file mode 100644 index 81f4ad4..0000000 --- a/SmoothCam/include/camera_states/thirdperson.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -#include "camera_state.h" - -namespace Camera { - namespace State { - class ThirdpersonState : public BaseCameraState { - public: - ThirdpersonState(Camera::SmoothCamera* camera) noexcept; - ThirdpersonState(const ThirdpersonState&) = delete; - ThirdpersonState(ThirdpersonState&&) noexcept = delete; - ThirdpersonState& operator=(const ThirdpersonState&) = delete; - ThirdpersonState& operator=(ThirdpersonState&&) noexcept = delete; - - public: - virtual void OnBegin(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* fromState) override; - virtual void OnEnd(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* nextState) override; - virtual void Update(PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera) override; - }; - } -} \ No newline at end of file diff --git a/SmoothCam/include/camera_states/thirdperson/thirdperson.h b/SmoothCam/include/camera_states/thirdperson/thirdperson.h new file mode 100644 index 0000000..9f7e3db --- /dev/null +++ b/SmoothCam/include/camera_states/thirdperson/thirdperson.h @@ -0,0 +1,25 @@ +#pragma once +#include "camera_states/base_third.h" + +namespace Camera { + class Thirdperson; + + namespace State { + class ThirdpersonState : public BaseThird { + public: + ThirdpersonState(Thirdperson* camera) noexcept; + ThirdpersonState(const ThirdpersonState&) = delete; + ThirdpersonState(ThirdpersonState&&) noexcept = delete; + ThirdpersonState& operator=(const ThirdpersonState&) = delete; + ThirdpersonState& operator=(ThirdpersonState&&) noexcept = delete; + + public: + virtual void OnBegin(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* fromState) noexcept override; + virtual void OnEnd(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* nextState) noexcept override; + virtual void Update(PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera) + noexcept override; + }; + } +} \ No newline at end of file diff --git a/SmoothCam/include/camera_states/thirdperson/thirdperson_combat.h b/SmoothCam/include/camera_states/thirdperson/thirdperson_combat.h new file mode 100644 index 0000000..271e270 --- /dev/null +++ b/SmoothCam/include/camera_states/thirdperson/thirdperson_combat.h @@ -0,0 +1,25 @@ +#pragma once +#include "camera_states/base_third.h" + +namespace Camera { + class Thirdperson; + + namespace State { + class ThirdpersonCombatState : public BaseThird { + public: + ThirdpersonCombatState(Thirdperson* camera) noexcept; + ThirdpersonCombatState(const ThirdpersonCombatState&) = delete; + ThirdpersonCombatState(ThirdpersonCombatState&&) noexcept = delete; + ThirdpersonCombatState& operator=(const ThirdpersonCombatState&) = delete; + ThirdpersonCombatState& operator=(ThirdpersonCombatState&&) noexcept = delete; + + public: + virtual void OnBegin(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* fromState) noexcept override; + virtual void OnEnd(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* nextState) noexcept override; + virtual void Update(PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera) + noexcept override; + }; + } +} \ No newline at end of file diff --git a/SmoothCam/include/camera_states/thirdperson/thirdperson_horse.h b/SmoothCam/include/camera_states/thirdperson/thirdperson_horse.h new file mode 100644 index 0000000..dbc8201 --- /dev/null +++ b/SmoothCam/include/camera_states/thirdperson/thirdperson_horse.h @@ -0,0 +1,25 @@ +#pragma once +#include "camera_states/base_third.h" + +namespace Camera { + class Thirdperson; + + namespace State { + class ThirdpersonHorseState : public BaseThird { + public: + ThirdpersonHorseState(Thirdperson* camera) noexcept; + ThirdpersonHorseState(const ThirdpersonHorseState&) = delete; + ThirdpersonHorseState(ThirdpersonHorseState&&) noexcept = delete; + ThirdpersonHorseState& operator=(const ThirdpersonHorseState&) = delete; + ThirdpersonHorseState& operator=(ThirdpersonHorseState&&) noexcept = delete; + + public: + virtual void OnBegin(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* fromState) noexcept override; + virtual void OnEnd(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* nextState) noexcept override; + virtual void Update(PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera) + noexcept override; + }; + } +} \ No newline at end of file diff --git a/SmoothCam/include/camera_states/thirdperson_combat.h b/SmoothCam/include/camera_states/thirdperson_combat.h deleted file mode 100644 index a7ac848..0000000 --- a/SmoothCam/include/camera_states/thirdperson_combat.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -#include "camera_state.h" - -namespace Camera { - namespace State { - class ThirdpersonCombatState : public BaseCameraState { - public: - ThirdpersonCombatState(Camera::SmoothCamera* camera) noexcept; - ThirdpersonCombatState(const ThirdpersonCombatState&) = delete; - ThirdpersonCombatState(ThirdpersonCombatState&&) noexcept = delete; - ThirdpersonCombatState& operator=(const ThirdpersonCombatState&) = delete; - ThirdpersonCombatState& operator=(ThirdpersonCombatState&&) noexcept = delete; - - public: - virtual void OnBegin(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* fromState) override; - virtual void OnEnd(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* nextState) override; - virtual void Update(PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera) override; - }; - } -} \ No newline at end of file diff --git a/SmoothCam/include/camera_states/thirdperson_horse.h b/SmoothCam/include/camera_states/thirdperson_horse.h deleted file mode 100644 index 5c7e5fa..0000000 --- a/SmoothCam/include/camera_states/thirdperson_horse.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -#include "camera_state.h" - -namespace Camera { - namespace State { - class ThirdpersonHorseState : public BaseCameraState { - public: - ThirdpersonHorseState(Camera::SmoothCamera* camera) noexcept; - ThirdpersonHorseState(const ThirdpersonHorseState&) = delete; - ThirdpersonHorseState(ThirdpersonHorseState&&) noexcept = delete; - ThirdpersonHorseState& operator=(const ThirdpersonHorseState&) = delete; - ThirdpersonHorseState& operator=(ThirdpersonHorseState&&) noexcept = delete; - - public: - virtual void OnBegin(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* fromState) override; - virtual void OnEnd(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* nextState) override; - virtual void Update(PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera) override; - }; - } -} \ No newline at end of file diff --git a/SmoothCam/include/config.h b/SmoothCam/include/config.h index 1b2dbd6..72b463d 100644 --- a/SmoothCam/include/config.h +++ b/SmoothCam/include/config.h @@ -8,7 +8,7 @@ namespace Config { using json = nlohmann::json; constexpr auto MaxPresetSlots = 6; - enum class ScalarMethods { + enum class ScalarMethods : uint8_t { LINEAR, QUAD_IN, QUAD_OUT, QUAD_INOUT, CUBIC_IN, CUBIC_OUT, CUBIC_INOUT, @@ -25,13 +25,13 @@ namespace Config { Dot }; - enum class LoadStatus { + enum class LoadStatus : uint8_t { OK, MISSING, FAILED }; - enum class OffsetGroupID { + enum class OffsetGroupID : uint8_t { Standing, Walking, Running, @@ -49,72 +49,65 @@ namespace Config { }; constexpr auto scalarMethods = mapbox::eternal::hash_map({ - { "linear", ScalarMethods::LINEAR }, - { "quadraticEaseIn", ScalarMethods::QUAD_IN }, - { "quadraticEaseOut", ScalarMethods::QUAD_OUT }, - { "quadraticEaseInOut", ScalarMethods::QUAD_INOUT }, - { "cubicEaseIn", ScalarMethods::CUBIC_IN }, - { "cubicEaseOut", ScalarMethods::CUBIC_OUT }, - { "cubicEaseInOut", ScalarMethods::CUBIC_INOUT }, - { "quarticEaseIn", ScalarMethods::QUART_IN }, - { "quarticEaseOut", ScalarMethods::QUART_OUT }, - { "quarticEaseInOut", ScalarMethods::QUART_INOUT }, - { "quinticEaseIn", ScalarMethods::QUINT_IN }, - { "quinticEaseOut", ScalarMethods::QUINT_OUT }, - { "quinticEaseInOut", ScalarMethods::QUINT_INOUT }, - { "sineEaseIn", ScalarMethods::SINE_IN }, - { "sineEaseOut", ScalarMethods::SINE_OUT }, - { "sineEaseInOut", ScalarMethods::SINE_INOUT }, - { "circularEaseIn", ScalarMethods::CIRC_IN }, - { "circularEaseOut", ScalarMethods::CIRC_OUT }, - { "circularEaseInOut", ScalarMethods::CIRC_INOUT }, - { "exponentialEaseIn", ScalarMethods::EXP_IN }, - { "exponentialEaseOut", ScalarMethods::EXP_OUT }, - { "exponentialEaseInOut", ScalarMethods::EXP_INOUT }, + { "LINEAR", ScalarMethods::LINEAR }, + { "QUADRATICEASEIN", ScalarMethods::QUAD_IN }, + { "QUADRATICEASEOUT", ScalarMethods::QUAD_OUT }, + { "QUADRATICEASEINOUT", ScalarMethods::QUAD_INOUT }, + { "CUBICEASEIN", ScalarMethods::CUBIC_IN }, + { "CUBICEASEOUT", ScalarMethods::CUBIC_OUT }, + { "CUBICEASEINOUT", ScalarMethods::CUBIC_INOUT }, + { "QUARTICEASEIN", ScalarMethods::QUART_IN }, + { "QUARTICEASEOUT", ScalarMethods::QUART_OUT }, + { "QUARTICEASEINOUT", ScalarMethods::QUART_INOUT }, + { "QUINTICEASEIN", ScalarMethods::QUINT_IN }, + { "QUINTICEASEOUT", ScalarMethods::QUINT_OUT }, + { "QUINTICEASEINOUT", ScalarMethods::QUINT_INOUT }, + { "SINEEASEIN", ScalarMethods::SINE_IN }, + { "SINEEASEOUT", ScalarMethods::SINE_OUT }, + { "SINEEASEINOUT", ScalarMethods::SINE_INOUT }, + { "CIRCULAREASEIN", ScalarMethods::CIRC_IN }, + { "CIRCULAREASEOUT", ScalarMethods::CIRC_OUT }, + { "CIRCULAREASEINOUT", ScalarMethods::CIRC_INOUT }, + { "EXPONENTIALEASEIN", ScalarMethods::EXP_IN }, + { "EXPONENTIALEASEOUT", ScalarMethods::EXP_OUT }, + { "EXPONENTIALEASEINOUT", ScalarMethods::EXP_INOUT }, }); constexpr auto scalarMethodRevLookup = mapbox::eternal::map({ - { ScalarMethods::LINEAR, "linear" }, - { ScalarMethods::QUAD_IN, "quadraticEaseIn" }, - { ScalarMethods::QUAD_OUT, "quadraticEaseOut" }, - { ScalarMethods::QUAD_INOUT, "quadraticEaseInOut" }, - { ScalarMethods::CUBIC_IN, "cubicEaseIn" }, - { ScalarMethods::CUBIC_OUT, "cubicEaseOut" }, - { ScalarMethods::CUBIC_INOUT, "cubicEaseInOut" }, - { ScalarMethods::QUART_IN, "quarticEaseIn" }, - { ScalarMethods::QUART_OUT, "quarticEaseOut" }, - { ScalarMethods::QUART_INOUT, "quarticEaseInOut" }, - { ScalarMethods::QUINT_IN, "quinticEaseIn" }, - { ScalarMethods::QUINT_OUT, "quinticEaseOut" }, - { ScalarMethods::QUINT_INOUT, "quinticEaseInOut" }, - { ScalarMethods::SINE_IN, "sineEaseIn" }, - { ScalarMethods::SINE_OUT, "sineEaseOut" }, - { ScalarMethods::SINE_INOUT, "sineEaseInOut" }, - { ScalarMethods::CIRC_IN, "circularEaseIn" }, - { ScalarMethods::CIRC_OUT, "circularEaseOut" }, - { ScalarMethods::CIRC_INOUT, "circularEaseInOut" }, - { ScalarMethods::EXP_IN, "exponentialEaseIn" }, - { ScalarMethods::EXP_OUT, "exponentialEaseOut" }, - { ScalarMethods::EXP_INOUT, "exponentialEaseInOut" }, + { ScalarMethods::LINEAR, "LINEAR" }, + { ScalarMethods::QUAD_IN, "QUADRATICEASEIN" }, + { ScalarMethods::QUAD_OUT, "QUADRATICEASEOUT" }, + { ScalarMethods::QUAD_INOUT, "QUADRATICEASEINOUT" }, + { ScalarMethods::CUBIC_IN, "CUBICEASEIN" }, + { ScalarMethods::CUBIC_OUT, "CUBICEASEOUT" }, + { ScalarMethods::CUBIC_INOUT, "CUBICEASEINOUT" }, + { ScalarMethods::QUART_IN, "QUARTICEASEIN" }, + { ScalarMethods::QUART_OUT, "QUARTICEASEOUT" }, + { ScalarMethods::QUART_INOUT, "QUARTICEASEINOUT" }, + { ScalarMethods::QUINT_IN, "QUINTICEASEIN" }, + { ScalarMethods::QUINT_OUT, "QUINTICEASEOUT" }, + { ScalarMethods::QUINT_INOUT, "QUINTICEASEINOUT" }, + { ScalarMethods::SINE_IN, "SINEEASEIN" }, + { ScalarMethods::SINE_OUT, "SINEEASEOUT" }, + { ScalarMethods::SINE_INOUT, "SINEEASEINOUT" }, + { ScalarMethods::CIRC_IN, "CIRCULAREASEIN" }, + { ScalarMethods::CIRC_OUT, "CIRCULAREASEOUT" }, + { ScalarMethods::CIRC_INOUT, "CIRCULAREASEINOUT" }, + { ScalarMethods::EXP_IN, "EXPONENTIALEASEIN" }, + { ScalarMethods::EXP_OUT, "EXPONENTIALEASEOUT" }, + { ScalarMethods::EXP_INOUT, "EXPONENTIALEASEINOUT" }, }); constexpr auto crosshairTypeLookup = mapbox::eternal::hash_map({ - { "Skyrim", CrosshairType::Skyrim }, - { "Dot", CrosshairType::Dot } + { "SKYRIM", CrosshairType::Skyrim }, + { "DOT", CrosshairType::Dot } }); constexpr auto crosshairTypeRevLookup = mapbox::eternal::map({ - { CrosshairType::Skyrim, "Skyrim" }, - { CrosshairType::Dot, "Dot" } + { CrosshairType::Skyrim, "SKYRIM" }, + { CrosshairType::Dot, "DOT" } }); - typedef struct gameConf { - float f3PArrowTiltUpAngle = 2.5f; - float f3PBoltTiltUpAngle = 2.5f; - float fNearDistance = 15.0f; - float fMinCurrentZoom = -0.200000003; - } GameConfig; - typedef struct offsetGroup { float sideOffset = 25.0; float upOffset = 0.0; @@ -173,8 +166,12 @@ namespace Config { float crosshairMinDistSize = 16.0f; float crosshairMaxDistSize = 24.0f; bool useWorldCrosshair = false; - bool worldCrosshairDepthTest = true; + bool worldCrosshairDepthTest = false; CrosshairType worldCrosshairType = CrosshairType::Skyrim; + float stealthMeterXOffset = 0.0f; + float stealthMeterYOffset = 0.0f; + bool offsetStealthMeter = false; + bool alwaysOffsetStealthMeter = false; // Arrow prediction bool useArrowPrediction = false; @@ -184,8 +181,15 @@ namespace Config { // Misc bool disableDeltaTime = false; + int nextPresetKey = -1; int shoulderSwapKey = -1; bool swapXClamping = true; + bool modDisabled = false; + int modToggleKey = -1; + float customZOffset = 0.0f; + int applyZOffsetKey = -1; + bool zOffsetActive = false; // @NOSAVE + bool enableCrashDumps = false; // Compat bool compatIC = false; @@ -197,8 +201,8 @@ namespace Config { bool enableInterp = true; ScalarMethods currentScalar = ScalarMethods::SINE_IN; float minCameraFollowDistance = 64.0f; - float minCameraFollowRate = 0.15f; - float maxCameraFollowRate = 0.4f; + float minCameraFollowRate = 0.25f; + float maxCameraFollowRate = 0.66f; float zoomMul = 500.0f; float zoomMaxSmoothingDistance = 650.0f; @@ -277,17 +281,21 @@ namespace Config { // Returns true if ok, otherwise does nothing bool LoadPreset(int slot); // Returns true if ok, otherwise does nothing - LoadStatus LoadPresetName(int slot, std::string& name); + LoadStatus LoadPresetName(int slot, eastl::string& name); // Returns the name of the saved preset or "Slot " if no preset is found BSFixedString GetPresetSlotName(int slot); // Get the file path for the given preset slot - std::wstring GetPresetPath(int slot); + eastl::wstring GetPresetPath(int slot); // Load the list of bones for the camera to follow void LoadBonePriorities(); - using BoneList = std::vector; + using BoneList = eastl::vector; // Get the follow bone list BoneList& GetBonePriorities() noexcept; - const GameConfig* const GetGameConfig(); +#ifdef DEVELOPER + void LoadEyeBonePriorities(); + // And the bone list for firstperson + BoneList& GetEyeBonePriorities() noexcept; +#endif } \ No newline at end of file diff --git a/SmoothCam/include/crosshair.h b/SmoothCam/include/crosshair.h index 9e06bb7..c46f005 100644 --- a/SmoothCam/include/crosshair.h +++ b/SmoothCam/include/crosshair.h @@ -14,57 +14,91 @@ namespace Crosshair { ~Manager(); // Get a normal vector pointing in the direction of the crosshair - glm::vec3 GetCrosshairTargetNormal(const glm::vec2& aimRotation, float pitchMod = 0.0f); + glm::vec3 GetCrosshairTargetNormal(const glm::vec2& aimRotation, float pitchMod = 0.0f) const noexcept; // Updates the screen position of the crosshair for correct aiming void UpdateCrosshairPosition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, - const glm::vec2& aimRotation, mmath::NiMatrix44& worldToScaleform); + const glm::vec2& aimRotation, mmath::NiMatrix44& worldToScaleform) noexcept; // Set the 3D crosshair position - void SetCrosshairPosition(const glm::dvec2& pos); + void SetCrosshairPosition(const glm::dvec2& pos) noexcept; + + // Set the stealth meter position (eye crosshair) + void SetStealthMeterPosition(const glm::vec2& pos) noexcept; // Center the position of the crosshair - void CenterCrosshair(); + void CenterCrosshair() noexcept; + + // Center the position of the stealth meter + void CenterStealthMeter() noexcept; // Set the size of the 3D crosshair - void SetCrosshairSize(const glm::dvec2& size); + void SetCrosshairSize(const glm::dvec2& size) noexcept; // Set the crosshair size back to default - void SetDefaultSize(); + void SetDefaultSize() noexcept; // Show or hide the crosshair - void SetCrosshairEnabled(bool enabled); + void SetCrosshairEnabled(bool enabled) noexcept; // Select the type of 3D crosshair to render with - void Set3DCrosshairType(Config::CrosshairType type); + void Set3DCrosshairType(Config::CrosshairType type) noexcept; // When loading cells, the crosshair appears to re-enable, which means our cache ends up - // in a conflicting state - This method will invalidate the cache, such that the need call + // in a conflicting state - This method will invalidate the cache, such that the new call // to SetCrosshairEnabled actually invokes the scaleform function. - void InvalidateEnablementCache(); + void InvalidateEnablementCache() noexcept; + + // Update state each frame + void Update(PlayerCharacter* player, CorrectedPlayerCamera* camera) noexcept; // Render crosshair objects void Render(Render::D3DContext& ctx, const glm::vec3& cameraPosition, const glm::vec2& cameraRotation, const NiFrustum& frustum) noexcept; + // Reset mutations, hard reset = true to clear crosshair related user settings + void Reset(bool hard = false) noexcept; + private: + // Returns true if the manager has captured the base crosshair data correctly + // If returning false, the manager is not allowed to modify the crosshair + bool IsCrosshairDataValid() const noexcept; + // Read initial values for the crosshair during startup - void ReadInitialCrosshairInfo(); + void ReadInitialCrosshairInfo() noexcept; // Simulate a time slice of projectile physics - void TickProjectilePath(glm::vec3& position, glm::vec3& vel, float gravity, float dt) noexcept; + void TickProjectilePath(glm::vec3& position, glm::vec3& vel, const glm::vec3& gravity, float mass, float dt) noexcept; // Compute the initial impulse vector for the given projectile glm::vec3 ComputeProjectileVelocityVector(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, - const TESAmmo* ammo, float gravity, const glm::vec2& aimRotation) noexcept; + const TESAmmo* ammo, const glm::vec2& aimRotation) noexcept; // Cast a curved ray for the currently equipped projectile bool ProjectilePredictionCurve(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, const glm::vec2& aimRotation, const glm::vec3& startPos, glm::vec3& hitPos, bool& hitCharacter) noexcept; + public: + struct CurrentCrosshairData { + glm::dvec2 ofs = { 0.0, 0.0 }; + glm::dvec2 position = { 0.0, 0.0 }; + glm::dvec2 scale = { 1.0, 1.0 }; + + glm::dvec2 stealthMeterOfs = { 0.0, 0.0 }; + glm::dvec2 stealthMeterPosition = { 0.0, 0.0 }; + + bool enabled = true; + bool invalidated = false; + bool alertMode = false; + bool stealthMeterMutated = false; + }; + private: - BSFixedString weapon = "WEAPON"; - BSFixedString magic = "NPC Head MagicNode [Hmag]"; + struct { + mutable BSFixedString weapon = "WEAPON"; + mutable BSFixedString arrowName = "Arrow:0"; + mutable BSFixedString magic = "NPC Head MagicNode [Hmag]"; + } Strings; // Crosshair metrics read at game start before we mess with them struct { @@ -75,21 +109,20 @@ namespace Crosshair { double yScale = 0.0; double xCenter = 0.0; double yCenter = 0.0; + + double stealthXOff = 0.0; + double stealthYOff = 0.0; } baseCrosshairData; // The last state we left the crosshair in - struct { - glm::dvec2 position = { 0.0, 0.0 }; - glm::dvec2 scale = { 1.0, 1.0 }; - bool enabled = true; - bool invalidated = false; - } currentCrosshairData; + CurrentCrosshairData currentCrosshairData; // Renderable objects struct { // Data which should really only change once each frame struct VSMatricesCBuffer { glm::mat4 matProjView = glm::identity(); + glm::vec4 tint = { 1.0f, 1.0f, 1.0f, 1.0f }; float curTime = 0.0f; float pad[3] = { 0.0f, 0.0f, 0.0f }; }; @@ -99,21 +132,21 @@ namespace Crosshair { struct VSPerObjectCBuffer { glm::mat4 model = glm::identity(); }; - static_assert(sizeof(VSMatricesCBuffer) % 16 == 0); + static_assert(sizeof(VSPerObjectCBuffer) % 16 == 0); VSMatricesCBuffer cbufPerFrameStaging = {}; VSPerObjectCBuffer cbufPerObjectStaging = {}; - std::shared_ptr cbufPerFrame; - std::shared_ptr cbufPerObject; + eastl::shared_ptr cbufPerFrame; + eastl::shared_ptr cbufPerObject; - std::unique_ptr curCrosshair; + eastl::unique_ptr curCrosshair; Config::CrosshairType crosshairType = Config::CrosshairType::None; bool drawCrosshair = false; bool hitCharacter = false; // Resouces for drawing the arrow prediction arc Render::LineList arrowTailSegments; - std::unique_ptr tailDrawer; + eastl::unique_ptr tailDrawer; // D3D expects resources to be released in a certain order void release() { diff --git a/SmoothCam/include/crosshair/base.h b/SmoothCam/include/crosshair/base.h index 81dd547..06cbef5 100644 --- a/SmoothCam/include/crosshair/base.h +++ b/SmoothCam/include/crosshair/base.h @@ -11,26 +11,26 @@ namespace Crosshair { class Base { public: Base() = default; - virtual ~Base() {}; + virtual ~Base() noexcept {}; Base(const Base&) = delete; Base(Base&&) noexcept = delete; Base& operator=(const Base&) = delete; Base& operator=(Base&&) noexcept = delete; // Create any needed 3D assets for rendering - virtual void Create3D(Render::D3DContext& ctx, std::shared_ptr& perObjectBuf) = 0; + virtual void Create3D(Render::D3DContext& ctx, eastl::shared_ptr& perObjectBuf) noexcept = 0; // Render the crosshair - virtual void Render(Render::D3DContext& ctx, float curTime, float deltaTime, bool allowDepthTesting) = 0; + virtual void Render(Render::D3DContext& ctx, float curTime, float deltaTime, bool allowDepthTesting) noexcept = 0; // Set the world position - virtual void SetPosition(const glm::vec3& pos); + virtual void SetPosition(const glm::vec3& pos) noexcept; // Set the rotation - virtual void SetRotation(const glm::vec3& rot); + virtual void SetRotation(const glm::vec3& rot) noexcept; // Set the scale - virtual void SetScale(const glm::vec3& s); + virtual void SetScale(const glm::vec3& s) noexcept; // Update the transform matrix - virtual void UpdateTransform(); + virtual void UpdateTransform() noexcept; // Get the world position glm::vec3 GetPosition() const noexcept; @@ -42,7 +42,7 @@ namespace Crosshair { glm::mat4 GetTransform() const noexcept; protected: - std::shared_ptr perObjectBuffer; + eastl::shared_ptr perObjectBuffer; glm::mat4 transform = glm::identity(); glm::vec3 position = { 0.0f, 0.0f, 0.0f }; glm::vec3 rotation = { 0.0f, 0.0f, 0.0f }; diff --git a/SmoothCam/include/crosshair/dot.h b/SmoothCam/include/crosshair/dot.h index 250a255..51453f0 100644 --- a/SmoothCam/include/crosshair/dot.h +++ b/SmoothCam/include/crosshair/dot.h @@ -9,16 +9,16 @@ namespace Crosshair { class Dot : public Base { public: Dot() = default; - virtual ~Dot() {}; + virtual ~Dot() noexcept {}; Dot(const Dot&) = delete; Dot(Dot&&) noexcept = delete; Dot& operator=(const Dot&) = delete; Dot& operator=(Dot&&) noexcept = delete; - virtual void Create3D(Render::D3DContext& ctx, std::shared_ptr& perObjectBuf); - virtual void Render(Render::D3DContext& ctx, float curTime, float deltaTime, bool allowDepthTesting); + virtual void Create3D(Render::D3DContext& ctx, eastl::shared_ptr& perObjectBuf) noexcept override; + virtual void Render(Render::D3DContext& ctx, float curTime, float deltaTime, bool allowDepthTesting) noexcept override; private: - std::unique_ptr meshDrawer; + eastl::unique_ptr meshDrawer; }; } \ No newline at end of file diff --git a/SmoothCam/include/crosshair/skyrim.h b/SmoothCam/include/crosshair/skyrim.h index 7eddddd..6bad69c 100644 --- a/SmoothCam/include/crosshair/skyrim.h +++ b/SmoothCam/include/crosshair/skyrim.h @@ -9,16 +9,16 @@ namespace Crosshair { class Skyrim : public Base { public: Skyrim() = default; - virtual ~Skyrim() {}; + virtual ~Skyrim() noexcept {}; Skyrim(const Skyrim&) = delete; Skyrim(Skyrim&&) noexcept = delete; Skyrim& operator=(const Skyrim&) = delete; Skyrim& operator=(Skyrim&&) noexcept = delete; - virtual void Create3D(Render::D3DContext& ctx, std::shared_ptr& perObjectBuf); - virtual void Render(Render::D3DContext& ctx, float curTime, float deltaTime, bool allowDepthTesting); + virtual void Create3D(Render::D3DContext& ctx, eastl::shared_ptr& perObjectBuf) noexcept override; + virtual void Render(Render::D3DContext& ctx, float curTime, float deltaTime, bool allowDepthTesting) noexcept override; private: - std::unique_ptr meshDrawer; + eastl::unique_ptr meshDrawer; }; } \ No newline at end of file diff --git a/SmoothCam/include/debug/ICommand.h b/SmoothCam/include/debug/ICommand.h new file mode 100644 index 0000000..06b6805 --- /dev/null +++ b/SmoothCam/include/debug/ICommand.h @@ -0,0 +1,12 @@ +#pragma once +#ifdef _DEBUG +namespace Debug { + class ICommand { + public: + virtual ~ICommand() = 0; + virtual void Run(const eastl::string& args) noexcept = 0; + virtual const eastl::string_view GetHelpString() const noexcept = 0; + virtual const eastl::string& GetName() const noexcept = 0; + }; +} +#endif \ No newline at end of file diff --git a/SmoothCam/include/debug/commands/dump_addrlib_db.h b/SmoothCam/include/debug/commands/dump_addrlib_db.h new file mode 100644 index 0000000..1ae601c --- /dev/null +++ b/SmoothCam/include/debug/commands/dump_addrlib_db.h @@ -0,0 +1,20 @@ +#pragma once +#ifdef _DEBUG +#include "debug/ICommand.h" + +namespace Debug { + class DumpAddrLibDB : public ICommand { + public: + virtual ~DumpAddrLibDB() override; + virtual void Run(const eastl::string& args) noexcept override; + virtual const eastl::string_view GetHelpString() const noexcept override; + virtual const eastl::string& GetName() const noexcept override { + return commandName; + }; + + protected: + const eastl::string commandName = "dump_addr_db"; + const eastl::string helpMsg = "Dump the offsets database for runtime 1.5.97\n"; + }; +} +#endif \ No newline at end of file diff --git a/SmoothCam/include/debug/commands/dump_game_ini.h b/SmoothCam/include/debug/commands/dump_game_ini.h new file mode 100644 index 0000000..6742a60 --- /dev/null +++ b/SmoothCam/include/debug/commands/dump_game_ini.h @@ -0,0 +1,20 @@ +#pragma once +#ifdef _DEBUG +#include "debug/ICommand.h" + +namespace Debug { + class DumpGameINI : public ICommand { + public: + virtual ~DumpGameINI() override; + virtual void Run(const eastl::string& args) noexcept override; + virtual const eastl::string_view GetHelpString() const noexcept override; + virtual const eastl::string& GetName() const noexcept override { + return commandName; + }; + + protected: + const eastl::string commandName = "dump_game_ini"; + const eastl::string helpMsg = "Dump all current values in Skyrim.ini as loaded in memory\n"; + }; +} +#endif \ No newline at end of file diff --git a/SmoothCam/include/debug/commands/dump_game_perfs_ini.h b/SmoothCam/include/debug/commands/dump_game_perfs_ini.h new file mode 100644 index 0000000..9f884ae --- /dev/null +++ b/SmoothCam/include/debug/commands/dump_game_perfs_ini.h @@ -0,0 +1,20 @@ +#pragma once +#ifdef _DEBUG +#include "debug/ICommand.h" + +namespace Debug { + class DumpGamePerfsINI : public ICommand { + public: + virtual ~DumpGamePerfsINI() override; + virtual void Run(const eastl::string& args) noexcept override; + virtual const eastl::string_view GetHelpString() const noexcept override; + virtual const eastl::string& GetName() const noexcept override { + return commandName; + }; + + protected: + const eastl::string commandName = "dump_game_perfs_ini"; + const eastl::string helpMsg = "Dump all current values in SkyrimPerfs.ini as loaded in memory\n"; + }; +} +#endif \ No newline at end of file diff --git a/SmoothCam/include/debug/commands/get_setting.h b/SmoothCam/include/debug/commands/get_setting.h new file mode 100644 index 0000000..fb4e2fb --- /dev/null +++ b/SmoothCam/include/debug/commands/get_setting.h @@ -0,0 +1,20 @@ +#pragma once +#ifdef _DEBUG +#include "debug/ICommand.h" + +namespace Debug { + class GetSetting : public ICommand { + public: + virtual ~GetSetting() override; + virtual void Run(const eastl::string& args) noexcept override; + virtual const eastl::string_view GetHelpString() const noexcept override; + virtual const eastl::string& GetName() const noexcept override { + return commandName; + }; + + protected: + const eastl::string commandName = "get_setting"; + const eastl::string helpMsg = "Get the value of the named setting\n"; + }; +} +#endif \ No newline at end of file diff --git a/SmoothCam/include/debug/commands/help.h b/SmoothCam/include/debug/commands/help.h new file mode 100644 index 0000000..8680681 --- /dev/null +++ b/SmoothCam/include/debug/commands/help.h @@ -0,0 +1,20 @@ +#pragma once +#ifdef _DEBUG +#include "debug/ICommand.h" + +namespace Debug { + class Help : public ICommand { + public: + virtual ~Help() override; + virtual void Run(const eastl::string& args) noexcept override; + virtual const eastl::string_view GetHelpString() const noexcept override; + virtual const eastl::string& GetName() const noexcept override { + return commandName; + }; + + protected: + const eastl::string commandName = "help"; + const eastl::string helpMsg = "Displays a list of all commands\n"; + }; +} +#endif \ No newline at end of file diff --git a/SmoothCam/include/debug/commands/set_setting.h b/SmoothCam/include/debug/commands/set_setting.h new file mode 100644 index 0000000..b9dda9e --- /dev/null +++ b/SmoothCam/include/debug/commands/set_setting.h @@ -0,0 +1,20 @@ +#pragma once +#ifdef _DEBUG +#include "debug/ICommand.h" + +namespace Debug { + class SetSetting : public ICommand { + public: + virtual ~SetSetting() override; + virtual void Run(const eastl::string& args) noexcept override; + virtual const eastl::string_view GetHelpString() const noexcept override; + virtual const eastl::string& GetName() const noexcept override { + return commandName; + }; + + protected: + const eastl::string commandName = "set_setting"; + const eastl::string helpMsg = "Set the value of the named setting\n"; + }; +} +#endif \ No newline at end of file diff --git a/SmoothCam/include/debug/console.h b/SmoothCam/include/debug/console.h new file mode 100644 index 0000000..7e4f1eb --- /dev/null +++ b/SmoothCam/include/debug/console.h @@ -0,0 +1,19 @@ +#pragma once + +#ifdef _DEBUG +#include +#define DebugPrint(...) \ + {std::lock_guard lock(Debug::GetTerminalLock()); \ + printf(__VA_ARGS__);} + +namespace Debug { + // Locking for stdout may or may not be required, but better to be on the safe side + std::mutex& GetTerminalLock() noexcept; + + void StartREPL(FILE* outSteam = stdout) noexcept; + void CommandPump() noexcept; +} + +#else +#define DebugPrint(...) +#endif \ No newline at end of file diff --git a/SmoothCam/include/debug/eh.h b/SmoothCam/include/debug/eh.h new file mode 100644 index 0000000..cda306a --- /dev/null +++ b/SmoothCam/include/debug/eh.h @@ -0,0 +1,27 @@ +#ifdef EMIT_MINIDUMPS +#pragma comment(lib, "Dbghelp.lib") + +namespace Debug { + // Create a MiniDumpScope at the start of a scope where you want to catch + // fatal crashes - a mdmp will be written if a crash does happen. + class MiniDumpScope { + public: + MiniDumpScope() noexcept; + ~MiniDumpScope() noexcept; + MiniDumpScope(const MiniDumpScope&) = delete; + MiniDumpScope(MiniDumpScope&&) noexcept = delete; + MiniDumpScope& operator=(const MiniDumpScope&) = delete; + MiniDumpScope& operator=(MiniDumpScope&&) noexcept = delete; + }; + + // Install the vectored exception handler which will handle crashes in our code section + // and execute a mini dump + void InstallMiniDumpHandler() noexcept; + // Remove the exception handler + void RemoveMiniDumpHandler() noexcept; +} +#else +namespace Debug { + class MiniDumpScope {}; +} +#endif \ No newline at end of file diff --git a/SmoothCam/include/debug/registry.h b/SmoothCam/include/debug/registry.h new file mode 100644 index 0000000..bd4d6e5 --- /dev/null +++ b/SmoothCam/include/debug/registry.h @@ -0,0 +1,26 @@ +#pragma once +#ifdef _DEBUG +namespace Debug { + class ICommand; + + class CommandRegistry { + public: + static CommandRegistry* Get() noexcept; + void Register(eastl::unique_ptr&& command) noexcept; + ICommand* Find(const eastl::string& name) const noexcept; + + using CommandTable = eastl::unordered_map>; + const CommandTable& GetCommands() const noexcept; + + private: + CommandRegistry() = default; + ~CommandRegistry() = default; + CommandRegistry(const CommandRegistry&) = delete; + CommandRegistry(CommandRegistry&&) noexcept = delete; + CommandRegistry& operator=(const CommandRegistry&) = delete; + CommandRegistry& operator=(CommandRegistry&&) noexcept = delete; + + CommandTable registry; + }; +} +#endif \ No newline at end of file diff --git a/SmoothCam/include/detours.h b/SmoothCam/include/detours.h index d9e34d7..f2ad615 100644 --- a/SmoothCam/include/detours.h +++ b/SmoothCam/include/detours.h @@ -1,31 +1,5 @@ #pragma once -namespace Camera { - class SmoothCamera; -} - namespace Detours { bool Attach(); - - typedef void(__thiscall* CameraOnUpdate)(TESCameraState*, BSTSmartPointer&); - class CameraStateDetour { - public: - CameraStateDetour(TESCameraState* pThis, uint16_t idx, uint64_t fnDetour, PLH::VFuncMap& origVFuncs) { - hook = std::make_unique( - (uint64_t)pThis, - PLH::VFuncMap{ - { idx, fnDetour }, - }, - &origVFuncs - ); - - if (!hook->hook()) { - _ERROR("Failed to place detour on target virtual function(TESCameraState Update), this error is fatal."); - FatalError(L"Failed to place detour on target virtual function(TESCameraState Update), this error is fatal."); - } - } - - private: - std::unique_ptr hook; - }; } \ No newline at end of file diff --git a/SmoothCam/include/firstperson.h b/SmoothCam/include/firstperson.h new file mode 100644 index 0000000..284ef0b --- /dev/null +++ b/SmoothCam/include/firstperson.h @@ -0,0 +1,61 @@ +#pragma once +#include "camera.h" +#include "camera_states/base_first.h" + +namespace Camera { + // All firstperson only logic + class Firstperson : public ICamera { + public: + Firstperson(Camera* baseCamera); + Firstperson(const Firstperson&) = delete; + Firstperson(Firstperson&&) noexcept = delete; + Firstperson& operator=(const Firstperson&) = delete; + Firstperson& operator=(Firstperson&&) noexcept = delete; + + public: + virtual ~Firstperson(); + + // Called when we are switching to this camera + virtual void OnBegin(PlayerCharacter* player, CorrectedPlayerCamera* camera, ICamera* lastState) noexcept override; + // Called when we are done using this camera + virtual void OnEnd(PlayerCharacter* player, CorrectedPlayerCamera* camera, ICamera* newState) noexcept override; + + // Runs before the internal game camera logic + // Return true when changing the camera state + virtual bool OnPreGameUpdate(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState) override; + // Selects the correct update method and positions the camera + virtual void OnUpdateCamera(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState) override; + // Render crosshair objects + virtual void Render(Render::D3DContext& ctx) noexcept override; + + // Called when the player toggles the POV + virtual void OnTogglePOV(const ButtonEvent* ev) noexcept override; + // Called when any other key is pressed + virtual bool OnKeyPress(const ButtonEvent* ev) noexcept override; + // Called when a menu of interest is opening or closing + virtual bool OnMenuOpenClose(MenuID id, const MenuOpenCloseEvent* const ev) noexcept override; + + // Triggers when the camera action state changes + virtual void OnCameraActionStateTransition(const PlayerCharacter* player, const CameraActionState newState, + const CameraActionState oldState) noexcept override; + // Triggers when the camera state changes + virtual void OnCameraStateTransition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, + const GameState::CameraState newState, const GameState::CameraState oldState) noexcept override; + + private: + // Enables the thirdperson skeleton and hides the face gen head node + void ToggleThirdpersonSkeleton(bool show) noexcept; + // Hide the player's head + void HidePlayerHead(bool hide) noexcept; + + private: + struct { + mutable BSFixedString headPositionTarget = "NPCEyeBone"; + mutable BSFixedString faceNode = "BSFaceGenNiNodeSkinned"; + } Strings; + + Config::UserConfig* config = nullptr; + }; +} \ No newline at end of file diff --git a/SmoothCam/include/game_state.h b/SmoothCam/include/game_state.h index 0add7bf..108fe87 100644 --- a/SmoothCam/include/game_state.h +++ b/SmoothCam/include/game_state.h @@ -67,10 +67,53 @@ namespace GameState { MAX_STATE, }; + // When drawing an arrow, the game stores the amount of time passed since + // starting the draw. This is used for arrow shot power calculations and + // is stored in this format within a small array at PlayerCharacter::BA0. + // Array appears to be used as a stack, with the most current draw time always being + // the last entry. Size is reset when an arrow is fired but amusingly grows forever if + // the player draws and cancels arrows repeatedly. + // + // Additionally, the eagle eye perk appears to mess with this timer, forcing it to a low value + // causing arrows to be fired with less force - this appears to be a bug with the game. + struct UnkBowDrawnTimerEntry { + float bowDrawTime = 0.0f; + float unk1 = 0.0f; // Haven't looked much into the following data, unsure what it is + float unk2 = 0.0f; + float unk3 = 0.0f; + }; + + template + struct SSA { + union Data { + T* ptr; + char ssa[sizeof(T) * S]; + T obj[S]; + }; + + uint32_t capacity : 31; + uint32_t local : 1; + Data data; + uint32_t size; + + inline const T& at(size_t index) const noexcept { + if (local == 1) { + return data.obj[index]; + } else { + return data.ptr[index]; + } + } + + inline const T& top() const noexcept { + return at(size - 1); + } + }; + using PlayerArrayBA0 = SSA; + // Returns the bits for player->actorState->flags04 which appear to convey movement info - const std::bitset<32> GetPlayerMovementBits(const Actor* player) noexcept; + const eastl::bitset<32> GetPlayerMovementBits(const Actor* player) noexcept; // Returns the bits for player->actorState->flags08 which appear to convey action info - const std::bitset<32> GetPlayerActionBits(const Actor* player) noexcept; + const eastl::bitset<32> GetPlayerActionBits(const Actor* player) noexcept; // Check if the camera is near the player's head (for first person mods) const bool IC_InFirstPersonState(const TESObjectREFR* player, const CorrectedPlayerCamera* camera) noexcept; @@ -105,14 +148,17 @@ namespace GameState { const bool IsInBleedoutCamera(const CorrectedPlayerCamera* camera) noexcept; // Returns true if the player is riding a dragon const bool IsInDragonCamera(const CorrectedPlayerCamera* camera) noexcept; - + // Get the current camera state const CameraState GetCameraState(const Actor* player, const CorrectedPlayerCamera* canera) noexcept; - /// Player action states const bool IsWeaponDrawn(const Actor* player) noexcept; // Get an equipped weapon const TESObjectWEAP* GetEquippedWeapon(const Actor* player, bool leftHand = false) noexcept; + // Get currently equipped ammo + const TESAmmo* GetCurrentAmmo(const Actor* player) noexcept; + // Get the amount of time the player has been holding an arrow back in the bow + float GetCurrentBowDrawTimer(const PlayerCharacter* player) noexcept; // Returns true if the player is holding an enchanted item that counts as 'magic' in the given hand bool IsUsingMagicItem(const Actor* player, bool leftHand = false) noexcept; // Returns true if the given spell counts as "combat" magic diff --git a/SmoothCam/include/havok/hkpCastCollector.h b/SmoothCam/include/havok/hkpCastCollector.h index 29a5abe..b14c210 100644 --- a/SmoothCam/include/havok/hkpCastCollector.h +++ b/SmoothCam/include/havok/hkpCastCollector.h @@ -54,9 +54,9 @@ class hkpCastCollector { const uint64_t m = 1ULL << static_cast(hitResult.hit->flags & 0x7F); constexpr uint64_t filter = 0x40122716; if ((m & filter) != 0) { - results.push_back(hitResult); // We only want further hits to be closer than this m_earlyOutHitFraction = hitResult.hitFraction; + results.push_back(eastl::move(hitResult)); } } } @@ -78,7 +78,7 @@ class hkpCastCollector { hkpCollidable* m_rootCollidable = nullptr; uint64_t unk2 = 0; - std::vector results; + eastl::vector results; }; typedef __declspec(align(16)) struct hkpRayCastInfo { diff --git a/SmoothCam/include/main.h b/SmoothCam/include/main.h index 50e6d0e..3ecf48c 100644 --- a/SmoothCam/include/main.h +++ b/SmoothCam/include/main.h @@ -6,4 +6,37 @@ #include "render/d3d_context.h" #ifdef WITH_D2D # include "render/d2d.h" -#endif \ No newline at end of file +#endif + +constexpr eastl::array idsToCache({ + // arrow fixes + 42537, 42536, 12204, 514905, 514725, + 49866, 42998, 43008, 42928, + + // camera + 69271, + + // crosshair + 80230, 80233, 505066, 515530, 505064, 505070, 505072, + + // firstperson + 39401, 100421, 100424, 100854, + + // main + 75446, + + // physics + 18536, + + // raycast + 32270, 76160, + + // thirdperson + 49816, 527997, 49858, + + // render/common + 513786, + + // render/d3d_context + 524728, 524730, 75713, 75694, 524768, 524998, +}); \ No newline at end of file diff --git a/SmoothCam/include/mmath.h b/SmoothCam/include/mmath.h index 5c72007..8d2680f 100644 --- a/SmoothCam/include/mmath.h +++ b/SmoothCam/include/mmath.h @@ -1,7 +1,7 @@ #pragma once namespace Config { - enum class ScalarMethods; + enum class ScalarMethods : uint8_t; } namespace mmath { diff --git a/SmoothCam/include/pch.h b/SmoothCam/include/pch.h index 383bdfb..804d553 100644 --- a/SmoothCam/include/pch.h +++ b/SmoothCam/include/pch.h @@ -1,26 +1,48 @@ #pragma once -#include #include #include +#include #include -#include -#include -#include -#include #include -#include -#include #include +#include #include +#include +void* operator new[](size_t size, const char* pName, int flags, unsigned debugFlags, const char* file, int line); +void* operator new[](size_t size, size_t alignment, size_t alignmentOffset, const char* pName, int flags, + unsigned debugFlags, const char* file, int line); + +extern "C" { + int __cdecl Vsnprintf8(char* p, size_t n, const char* pFormat, va_list arguments); +} + +#define EASTL_EASTDC_VSNPRINTF 0 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "code_analysis.h" // Ignore all warnings from external code SILENCE_CODE_ANALYSIS; # define WIN32_LEAN_AND_MEAN # define NOMINMAX -# undef CreateFont # include +# include +# undef CreateFont +# undef NO_DATA // Compat when building in unicode mode with SKSE // (ModInfo.fileData is of type WIN32_FIND_DATA which changes based on mbcs or unicode) @@ -41,6 +63,9 @@ SILENCE_CODE_ANALYSIS; # include # include # include +# include +# include +# include # include # include # include @@ -48,6 +73,7 @@ SILENCE_CODE_ANALYSIS; # include # include # include +# include # include # include @@ -64,8 +90,7 @@ SILENCE_CODE_ANALYSIS; # undef WIN32_FIND_DATA # define WIN32_FIND_DATA WIN32_FIND_DATAW # endif -//# include - + // @TODO: Use Polyhook2's function detouring instead, drop microsoft detours # include <../Detours/include/detours.h> # include @@ -115,11 +140,22 @@ RESTORE_CODE_ANALYSIS; #pragma warning( disable : 26409 ) // avoid naked new and delete - required for papyrus registration #pragma warning( disable : 26486 ) // invalid pointer passing - also busted? +#include "util.h" +#include "debug/console.h" + #include "addrlib/offsets.h" #include "timer.h" +// Enable in-progress features +//#define DEVELOPER + +// Enable an exception handler that writes minidumps when smoothcam code crashes +#define EMIT_MINIDUMPS + +// Enable Direct2D code //#define WITH_D2D #ifdef WITH_D2D +// Enable debug overlays # define WITH_CHARTS # ifdef WITH_CHARTS @@ -134,6 +170,7 @@ RESTORE_CODE_ANALYSIS; #include "skyrimSE/bhkSimpleShapePhantom.h" #include "skyrimSE/bhkWorld.h" #include "skyrimSE/PlayerCamera.h" +#include "skyrimSE/FirstPersonState.h" #include "skyrimSE/ThirdPersonState.h" #include "skyrimSE/PlayerCameraTransitionState.h" #include "skyrimSE/HorseCameraState.h" diff --git a/SmoothCam/include/physics.h b/SmoothCam/include/physics.h index ea10973..db47bd7 100644 --- a/SmoothCam/include/physics.h +++ b/SmoothCam/include/physics.h @@ -1,6 +1,10 @@ #pragma once namespace Physics { - hkp3AxisSweep* GetBroadphase(const bhkWorld* physicsWorld); + // Get the spatial partitioning structure used by the collision engine of havok + hkp3AxisSweep* GetBroadphase(bhkWorld* physicsWorld); + // Get the physics world for the given cell bhkWorld* GetWorld(const TESObjectCELL* parentCell); + // Get the gravity vector for the current world space using `ref` + glm::vec3 GetGravityVector(const TESObjectREFR* ref); } \ No newline at end of file diff --git a/SmoothCam/include/render/cbuffer.h b/SmoothCam/include/render/cbuffer.h index 1da8fcb..b2c5672 100644 --- a/SmoothCam/include/render/cbuffer.h +++ b/SmoothCam/include/render/cbuffer.h @@ -26,6 +26,8 @@ namespace Render { size_t Size() const noexcept; // Get the buffer usage it was created with D3D11_USAGE Usage() const noexcept; + // Get the naked resource + winrt::com_ptr& GetBuffer() noexcept; private: winrt::com_ptr buffer; diff --git a/SmoothCam/include/render/d2d.h b/SmoothCam/include/render/d2d.h index 943f8ad..a633e33 100644 --- a/SmoothCam/include/render/d2d.h +++ b/SmoothCam/include/render/d2d.h @@ -14,6 +14,17 @@ namespace Render { class VertexBuffer; class Shader; + enum class StrokeStyle : uint8_t { + Solid, // Solid + RoundedSolid, // Solid, Rounded ends + RoundedSolidRounded, // Solid, Rounded ends, rounded joins + Dashed, // Dashed + RoundedDashed, // Dashed, Rounded ends + RoundedDashedRounded, // Dashed, Rounded ends, rounded joins + RoundedDashedRoundedSmooth, // Dashed, Rounded ends, rounded joins, rounded dashes + STYLE_MAX, + }; + typedef struct ColorBrushKey { glm::vec4 color; @@ -56,15 +67,49 @@ namespace Render { void WriteToBackbuffer(D3DContext& ctx) noexcept; // Get the direct write instance - std::unique_ptr& GetDWrite() noexcept; + eastl::unique_ptr& GetDWrite() noexcept; // Get a color brush for use with D2D/DWrite winrt::com_ptr GetColorBrush(const glm::vec4& color) noexcept; // Draw a line void DrawLine(const glm::vec2& p1, const glm::vec2& p2, const glm::vec4& color, float thickness = 1.0f) noexcept; + // Draw an ellipse + void DrawEllipse(const glm::vec2& center, const glm::vec2& extents, const glm::vec4& color, + StrokeStyle style = StrokeStyle::Solid, const float stroke = 1.0f) noexcept; private: void CreateRenderTarget(D3DContext& ctx, D3DContext& renderingCtx); + // Get a stroke style + template + winrt::com_ptr GetStrokeStyle( + const float* dashes, + D2D1_CAP_STYLE endStyle = D2D1_CAP_STYLE::D2D1_CAP_STYLE_FLAT, + D2D1_CAP_STYLE dashEndStyle = D2D1_CAP_STYLE::D2D1_CAP_STYLE_FLAT, + D2D1_LINE_JOIN segmentStyle = D2D1_LINE_JOIN::D2D1_LINE_JOIN_MITER, + D2D1_DASH_STYLE dashStyle = D2D1_DASH_STYLE::D2D1_DASH_STYLE_SOLID) noexcept + { + D2D1_STROKE_STYLE_PROPERTIES1 props; + props.dashCap = dashEndStyle; + props.dashOffset = 0.0f; + props.dashStyle = dashStyle; + props.endCap = endStyle; + props.lineJoin = segmentStyle; + props.miterLimit = 1.0f; + props.startCap = endStyle; + props.transformType = D2D1_STROKE_TRANSFORM_TYPE::D2D1_STROKE_TRANSFORM_TYPE_NORMAL; + winrt::com_ptr style; + + if constexpr (count == 0) { + if (!SUCCEEDED(factory->CreateStrokeStyle(&props, nullptr, 0, style.put()))) + FatalError(L"SmoothCam: Failed to create brush stroke style"); + } else { + if (!SUCCEEDED(factory->CreateStrokeStyle(&props, dashes, count, style.put()))) + FatalError(L"SmoothCam: Failed to create brush stroke style"); + } + + return style; + } + D3DContext d2dContext; winrt::com_ptr factory; @@ -73,25 +118,27 @@ namespace Render { winrt::com_ptr context; winrt::com_ptr bitmap; - std::unique_ptr dwrite; - std::shared_ptr colorBuffer; - std::shared_ptr sharedColorBuffer; + eastl::unique_ptr dwrite; + eastl::shared_ptr colorBuffer; + eastl::shared_ptr sharedColorBuffer; winrt::com_ptr dxgiBackBuffer; - std::unique_ptr colorSRV; - std::unique_ptr vboFullscreen; - std::shared_ptr fullScreenVS; - std::shared_ptr fullScreenPS; + eastl::unique_ptr colorSRV; + eastl::unique_ptr vboFullscreen; + eastl::shared_ptr fullScreenVS; + eastl::shared_ptr fullScreenPS; winrt::com_ptr workQuery; - std::unordered_map< + eastl::unordered_map< ColorBrushKey, winrt::com_ptr, ColorBrushHasher, ColorBrushCompare > colorBrushes; + winrt::com_ptr strokeStyles[static_cast(StrokeStyle::STYLE_MAX)]; + friend class DWrite; }; } diff --git a/SmoothCam/include/render/d3d_context.h b/SmoothCam/include/render/d3d_context.h index 2f3455a..45cd63e 100644 --- a/SmoothCam/include/render/d3d_context.h +++ b/SmoothCam/include/render/d3d_context.h @@ -3,17 +3,150 @@ #include #pragma comment(lib, "D3D11.lib") #pragma comment(lib, "d3dcompiler.lib") +#include "util.h" namespace Render { - typedef struct _D3DContext { + struct D3DContext { // @Note: we don't want to refCount the swap chain - Let skyrim manage the lifetime. // As long as the game is running, we have a valid swapchain. IDXGISwapChain* swapChain = nullptr; - winrt::com_ptr device; + winrt::com_ptr device = nullptr; winrt::com_ptr context; // Size of the output window in pixels glm::vec2 windowSize = {}; - } D3DContext; + HWND hWnd = nullptr; + }; + + // 143025f00:524728 + struct D3D11Resources { + int32_t unk0; // 0x0 + int32_t unk4; // 0x4 + int32_t unk8; // 0x8 + int32_t unkC; // 0xC + int32_t unk10; // 0x10 + int32_t unk14; // 0x14 + int32_t unk18; // 0x18 + int32_t unk1C; // 0x1C + uintptr_t unk20; // 0x20 + uintptr_t unk28; // 0x28 + uintptr_t unk30; // 0x30 + ID3D11Device* device; // 0x38 + ID3D11DeviceContext* ctx; // 0x40 + HWND window; // 0x48 + DWORD windowX; // 0x50 + DWORD windowY; // 0x54 + DWORD windowW; // 0x58 + DWORD windowH; // 0x5C + IDXGISwapChain* swapChain; // 0x60 + uintptr_t unk68; // 0x68 + uintptr_t unk70; // 0x70 + ID3D11RenderTargetView* unkRTV78; // 0x78 + ID3D11ShaderResourceView* unkSRV80; // 0x80 + uintptr_t unk88; // 0x88 + uintptr_t unk90; // 0x90 + uintptr_t unk98; // 0x98 + uintptr_t unkA0; // 0xA0 + uintptr_t unkA8; // 0xA8 + uintptr_t unkB0; // 0xB0 + }; + static_assert(offsetof(D3D11Resources, unkC) == 0xC); + static_assert(offsetof(D3D11Resources, unk1C) == 0x1C); + static_assert(offsetof(D3D11Resources, device) == 0x38); + static_assert(offsetof(D3D11Resources, swapChain) == 0x60); + static_assert(offsetof(D3D11Resources, unkB0) == 0xB0); + + // 12: cbuffer12 Buffer 1196 0 - 4096 41 Variables, 656 bytes needed, 720 provided + struct CBuffer12 { + // Lots of duplicates + glm::mat4 unk0; + glm::mat4 proj0; + glm::mat4 projView0; + glm::mat4 projView1; + glm::mat4 projView2; + glm::mat4 frustum0; + glm::mat4 proj1; + glm::mat4 unk0Transposed; + glm::mat4 unk2; + glm::mat4 frustum1; + glm::mat4 pad8; + glm::vec4 pad9; + + static ID3D11Buffer* Get() { + // DAT_143027e88 + return *Offsets::Get(524768); + } + + static void Set(ID3D11Buffer* buf) { + *Offsets::Get(524768) = buf; + } + }; + static_assert(sizeof(CBuffer12) == 720); + static_assert(sizeof(CBuffer12) % 16 == 0); + + typedef struct BatchRenderCommand { + using BSEffectShader = void; + BSEffectShader* shader; + BSEffectShaderProperty* properties; + BSGeometry* geometry; + } BatchRenderCommand; + + typedef struct GBuffer { + NiSourceTexture* projectedNoise; + NiSourceTexture* projectedDiffuse; + NiSourceTexture* projectedNormal; + NiSourceTexture* projectedNormalDetail; + + typedef struct WrappedCameraData { + NiCamera* camera; + // @Note: This might just be junk on the stack and not actually + // part of the return type - That said messing with these matrices + // does end up being transmitted to cbuffer 12 + uintptr_t pad0; + glm::vec4 dc0; + glm::vec4 dc1; + glm::vec4 dc2; + glm::vec4 dc3; + glm::vec4 dc4; + glm::vec4 dc5; + glm::vec4 dc6; + glm::mat4 proj; + glm::mat4 projView0; + glm::mat4 projView1; + glm::mat4 projView2; + glm::mat4 projView3; + } WrappedCameraData; + + // Called by UpdateGPUCameraData + WrappedCameraData* CameraSwap(NiCamera* inCamera, byte flags = 0x0); + // Compute new contents of cbuffer 12 and update on the GPU + void UpdateGPUCameraData(NiCamera* inCamera, byte flags = 0x0); + + static GBuffer* Get() { + // DAT_14302c890 + static auto gbuffer = Offsets::Get(524998); + return gbuffer; + } + + // Other locations of interest: + // FUN_1412e3520:100421 + // typedef void(*DrawEarlyZPass)(bool, bool); + + // FUN_1412e3e70:100424 + // typedef void(*DrawGBuffer)(char param_1); + + // BSBatchRenderer::RenderBatch::FUN_141308440:100854 + // typedef void(*RenderStuff)(Render::BatchRenderCommand* cmd, uint32_t id, bool unk0, uint32_t unk1); + + // SkyrimSE.exe+0x00d6cbc7 <- Draw + // SkyrimSE.exe+0x00c7cb14 <- FUN_140c7ca70_RenderSomething::vtable.NiSkinPartition + // SkyrimSE.exe+0x00c6ba5f <- FUN_140c6b9f0::vtable.BSDismemberSkinInstance::Draw (One of many) + // SkyrimSE.exe+0x01308a97 <- CALL qword ptr [RAX + 0x128] CommonLibSSE : BSDismemberSkinInstance->Unk_25 + // SkyrimSE.exe+ <- ? + // SkyrimSE.exe+0x013082d3 <- BSBatchRenderer::RenderBatch::FUN_141308440 + // SkyrimSE.exe+0x012ccf4f <- BSShaderAccumulator::FUN_141308030 + // SkyrimSE.exe+ <- ? + // SkyrimSE.exe+0x012c1672 <- NiCamera::FUN_140d7bab0:UpdateCameraDataAndCBuffer12 + } GBuffer; // The present hook typedef HRESULT(*D3D11Present)(IDXGISwapChain*, UINT, UINT); @@ -34,7 +167,7 @@ namespace Render { // Add a new function for drawing during the present hook using DrawFunc = std::function; - void OnPresent(DrawFunc&&callback); + void OnPresent(DrawFunc&& callback); // Set the depth state void SetDepthState(D3DContext& ctx, bool writeEnable, bool testEnable, D3D11_COMPARISON_FUNC testFunc); @@ -63,8 +196,11 @@ namespace Render { {} size_t Hash() const { - return std::hash()(write) ^ std::hash()(test) ^ - std::hash()(mode); + size_t seed = 0; + Util::HashCombine(seed, write); + Util::HashCombine(seed, test); + Util::HashCombine(seed, mode); + return seed; } bool operator==(const DSStateKey& other) const { @@ -124,15 +260,24 @@ namespace Render { } size_t HashRTBlendDesc(const D3D11_RENDER_TARGET_BLEND_DESC& rtDesc) const { - return std::hash()(rtDesc.BlendEnable) ^ std::hash()(rtDesc.SrcBlend) ^ std::hash()(rtDesc.DestBlend) - ^ std::hash()(rtDesc.SrcBlendAlpha) ^ std::hash()(rtDesc.DestBlendAlpha) - ^ std::hash()(rtDesc.BlendOp) ^ std::hash()(rtDesc.BlendOpAlpha) - ^ std::hash()(rtDesc.RenderTargetWriteMask); + size_t seed = 0; + Util::HashCombine(seed, rtDesc.BlendEnable); + Util::HashCombine(seed, rtDesc.SrcBlend); + Util::HashCombine(seed, rtDesc.DestBlend); + Util::HashCombine(seed, rtDesc.SrcBlendAlpha); + Util::HashCombine(seed, rtDesc.DestBlendAlpha); + Util::HashCombine(seed, rtDesc.BlendOp); + Util::HashCombine(seed, rtDesc.BlendOpAlpha); + Util::HashCombine(seed, rtDesc.RenderTargetWriteMask); + return seed; } size_t Hash() const { - return std::hash()(desc.AlphaToCoverageEnable) ^ std::hash()(desc.IndependentBlendEnable) ^ - HashRTBlendDesc(desc.RenderTarget[0]); // @NOTE: We only work with the main RT right now + size_t seed = 0; + Util::HashCombine(seed, desc.AlphaToCoverageEnable); + Util::HashCombine(seed, desc.IndependentBlendEnable); + Util::HashCombine(seed, HashRTBlendDesc(desc.RenderTarget[0])); // @NOTE: We only work with the main RT right now + return seed; } bool RTBlendDescEq(const D3D11_RENDER_TARGET_BLEND_DESC& other) const { @@ -196,11 +341,18 @@ namespace Render { RasterStateKey(D3D11_RASTERIZER_DESC desc) : desc(desc) {} size_t Hash() const { - return std::hash()(desc.FillMode) ^ std::hash()(desc.CullMode) ^ - std::hash()(desc.FrontCounterClockwise) ^ std::hash()(desc.DepthBias) ^ - std::hash()(desc.DepthBiasClamp) ^ std::hash()(desc.SlopeScaledDepthBias) ^ - std::hash()(desc.DepthClipEnable) ^ std::hash()(desc.ScissorEnable) ^ - std::hash()(desc.MultisampleEnable) ^ std::hash()(desc.AntialiasedLineEnable); + size_t seed = 0; + Util::HashCombine(seed, desc.FillMode); + Util::HashCombine(seed, desc.CullMode); + Util::HashCombine(seed, desc.FrontCounterClockwise); + Util::HashCombine(seed, desc.DepthBias); + Util::HashCombine(seed, desc.DepthBiasClamp); + Util::HashCombine(seed, desc.SlopeScaledDepthBias); + Util::HashCombine(seed, desc.DepthClipEnable); + Util::HashCombine(seed, desc.ScissorEnable); + Util::HashCombine(seed, desc.MultisampleEnable); + Util::HashCombine(seed, desc.AntialiasedLineEnable); + return seed; } bool operator==(const RasterStateKey& other) const { diff --git a/SmoothCam/include/render/dwrite.h b/SmoothCam/include/render/dwrite.h index 792c33f..1f57c05 100644 --- a/SmoothCam/include/render/dwrite.h +++ b/SmoothCam/include/render/dwrite.h @@ -15,10 +15,10 @@ namespace Render { ~DWrite(); // Generate and return a text layout - winrt::com_ptr& GetLayout(const std::wstring_view& text, float maxWidth, float maxHeight); + winrt::com_ptr& GetLayout(const eastl::wstring_view& text, float maxWidth, float maxHeight); // Draw text - void Write(const std::wstring_view& text, float maxWidth, float maxHeight, const glm::vec2& pos, + void Write(const eastl::wstring_view& text, float maxWidth, float maxHeight, const glm::vec2& pos, const glm::vec4& color) noexcept; // Draw using a layout @@ -26,7 +26,7 @@ namespace Render { const glm::vec4& color) noexcept; // Get the width and height of a text input, were it to be drawn - glm::vec2 GetTextSize(const std::wstring_view& text, float maxWidth, float maxHeight) noexcept; + glm::vec2 GetTextSize(const eastl::wstring_view& text, float maxWidth, float maxHeight) noexcept; // Get the size from a layout glm::vec2 GetTextSize(winrt::com_ptr& layout) noexcept; diff --git a/SmoothCam/include/render/gradbox.h b/SmoothCam/include/render/gradbox.h index e80cff1..9c96ccc 100644 --- a/SmoothCam/include/render/gradbox.h +++ b/SmoothCam/include/render/gradbox.h @@ -1,4 +1,5 @@ #pragma once +#ifdef WITH_D2D #include "render/d3d_context.h" namespace Render { @@ -24,13 +25,14 @@ namespace Render { bool backgroundDirty = false; glm::vec4 bgColor1 = { 0.05f, 0.05f, 0.05f, 0.7f }; glm::vec4 bgColor2 = { 0.1f, 0.1f, 0.1f, 0.7f }; - std::shared_ptr vsBackground; - std::shared_ptr psBackground; + eastl::shared_ptr vsBackground; + eastl::shared_ptr psBackground; - std::vector backgroundVerts; - std::unique_ptr vboBackground; + eastl::vector backgroundVerts; + eastl::unique_ptr vboBackground; glm::uvec2 bgSize = { 0, 0 }; glm::ivec2 bgPos = { 0, 0 }; }; -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/SmoothCam/include/render/line_drawer.h b/SmoothCam/include/render/line_drawer.h index 20b8653..7016e49 100644 --- a/SmoothCam/include/render/line_drawer.h +++ b/SmoothCam/include/render/line_drawer.h @@ -18,7 +18,7 @@ namespace Render { Line(Point&& start, Point&& end) : start(start), end(end) {}; } Line; - using LineList = std::vector; + using LineList = eastl::vector; // Number of points we can submit in a single draw call constexpr size_t LineDrawPointBatchSize = 64; @@ -38,11 +38,11 @@ namespace Render { void Submit(const LineList& lines) noexcept; protected: - std::shared_ptr vs; - std::shared_ptr ps; + eastl::shared_ptr vs; + eastl::shared_ptr ps; private: - std::array, NumBuffers> vbo; + eastl::array, NumBuffers> vbo; void CreateObjects(D3DContext& ctx); void DrawBatch(uint32_t bufferIndex, LineList::const_iterator& begin, LineList::const_iterator& end); diff --git a/SmoothCam/include/render/line_graph.h b/SmoothCam/include/render/line_graph.h index 0fdc2f9..a5fef6a 100644 --- a/SmoothCam/include/render/line_graph.h +++ b/SmoothCam/include/render/line_graph.h @@ -23,7 +23,7 @@ namespace Render { void SetLineThickness(float amount) noexcept; // Set the name of the chart - void SetName(const std::wstring& n); + void SetName(const eastl::wstring& n); // Add a point to the given plot id void AddPoint(uint8_t plotID, float value) noexcept; @@ -31,12 +31,12 @@ namespace Render { void Draw(D3DContext& ctx) noexcept; private: - using PlotList = std::vector; - std::vector plots; - std::vector plotRanges; - std::vector plotColors; + using PlotList = eastl::vector; + eastl::vector plots; + eastl::vector plotRanges; + eastl::vector plotColors; - std::wstring name; + eastl::wstring name; uint8_t numPlots; uint32_t maxPoints; diff --git a/SmoothCam/include/render/mesh_drawer.h b/SmoothCam/include/render/mesh_drawer.h index 1b56415..e40dc41 100644 --- a/SmoothCam/include/render/mesh_drawer.h +++ b/SmoothCam/include/render/mesh_drawer.h @@ -7,13 +7,13 @@ namespace Render { typedef struct MeshCreateInfo { Model::Mesh* mesh = nullptr; - std::shared_ptr vs; - std::shared_ptr ps; + eastl::shared_ptr vs; + eastl::shared_ptr ps; } MeshCreateInfo; class MeshDrawer { public: - explicit MeshDrawer(MeshCreateInfo& info, std::shared_ptr& perObjectBuffer, D3DContext& ctx); + explicit MeshDrawer(MeshCreateInfo& info, eastl::shared_ptr& perObjectBuffer, D3DContext& ctx); ~MeshDrawer(); MeshDrawer(const MeshDrawer&) = delete; MeshDrawer(MeshDrawer&&) noexcept = delete; @@ -24,15 +24,15 @@ namespace Render { void Submit(glm::mat4& modelMatrix) noexcept; // Set the shaders used by the mesh for rendering - void SetShaders(std::shared_ptr& vs, std::shared_ptr& ps); + void SetShaders(eastl::shared_ptr& vs, eastl::shared_ptr& ps); private: D3DContext context; - std::unique_ptr vbo; - std::shared_ptr cbufPerObject; - std::shared_ptr vs; - std::shared_ptr ps; + eastl::unique_ptr vbo; + eastl::shared_ptr cbufPerObject; + eastl::shared_ptr vs; + eastl::shared_ptr ps; - void CreateObjects(std::vector& vertices, D3DContext& ctx); + void CreateObjects(eastl::vector& vertices, D3DContext& ctx); }; } \ No newline at end of file diff --git a/SmoothCam/include/render/model.h b/SmoothCam/include/render/model.h index be6f1e7..6d5dacd 100644 --- a/SmoothCam/include/render/model.h +++ b/SmoothCam/include/render/model.h @@ -24,12 +24,12 @@ namespace Render { typedef struct Mesh { MeshHeader header = {}; - std::vector vertices = {}; + eastl::vector vertices = {}; } Mesh; typedef struct Model { ModelHeader header = {}; - std::vector meshes = {}; + eastl::vector meshes = {}; } Model; bool Load(const uint8_t* location, Model& output); diff --git a/SmoothCam/include/render/ninode_tree_display.h b/SmoothCam/include/render/ninode_tree_display.h index b72b75b..fb675a4 100644 --- a/SmoothCam/include/render/ninode_tree_display.h +++ b/SmoothCam/include/render/ninode_tree_display.h @@ -22,14 +22,12 @@ namespace Render { void Draw(D3DContext& ctx, NiNode* node) noexcept; private: - void DrawNode(D3DContext& ctx, NiNode* no, float indent, float line, float& maxWidth, uint32_t level); - uint32_t width = 0; uint32_t height = 0; uint32_t xPos = 0; uint32_t yPos = 0; - StringBuilder builder; + StringBuilder builder; wchar_t* buffer = nullptr; size_t bufSize = 0; }; diff --git a/SmoothCam/include/render/render_target.h b/SmoothCam/include/render/render_target.h index 1a8b11a..95b8c2b 100644 --- a/SmoothCam/include/render/render_target.h +++ b/SmoothCam/include/render/render_target.h @@ -9,7 +9,7 @@ namespace Render { DXGI_FORMAT format; D3D11_RTV_DIMENSION dimensions = D3D11_RTV_DIMENSION_TEXTURE2D; D3D11_TEX2D_RTV texture2D; - std::shared_ptr texture; + eastl::shared_ptr texture; } RenderTargetCreateInfo; class RenderTarget { @@ -26,11 +26,11 @@ namespace Render { // Clear the target void Clear(Render::D3DContext& ctx, glm::vec4& color) noexcept; // Get an SRV for reading the target in shaders - std::unique_ptr& GetColorSRV() noexcept; + eastl::unique_ptr& GetColorSRV() noexcept; private: winrt::com_ptr rtv; - std::shared_ptr texture; - std::unique_ptr srvColor; + eastl::shared_ptr texture; + eastl::unique_ptr srvColor; }; } \ No newline at end of file diff --git a/SmoothCam/include/render/shader.h b/SmoothCam/include/render/shader.h index 9110fde..37ed517 100644 --- a/SmoothCam/include/render/shader.h +++ b/SmoothCam/include/render/shader.h @@ -1,6 +1,7 @@ #pragma once #include "render/d3d_context.h" #include +#include "render/shaders/shader_decl.h" namespace Render { class VertexBuffer; @@ -11,15 +12,29 @@ namespace Render { }; struct ShaderCreateInfo { - std::string source; - std::string entryName = "main"; - std::string version; + Shaders::ShaderDecl source; + eastl::string entryName = "main"; + eastl::string version; PipelineStage stage; - ShaderCreateInfo(std::string&& source, PipelineStage stage, - std::string&& entryName = "main", std::string&& version = "5_0") + ShaderCreateInfo(const Shaders::ShaderDecl& source, PipelineStage stage, + eastl::string&& entryName = "main", eastl::string&& version = "5_0") : source(source), entryName(entryName), version(version), stage(stage) {} + + size_t Hash() const { + size_t seed = source.uid; + Util::HashCombine(seed, entryName); + Util::HashCombine(seed, version); + Util::HashCombine(seed, stage); + return seed; + } + + bool operator==(const ShaderCreateInfo& other) const { + return + stage == other.stage && version == other.version && + entryName == other.entryName && source.uid == other.source.uid; + } }; class Shader { @@ -49,7 +64,7 @@ namespace Render { ID3D11PixelShader* fragment; } program; - bool Compile(const std::string& source, const std::string& entryName, const std::string& version) noexcept; + bool Compile(const eastl::string& source, const eastl::string& entryName, const eastl::string& version) noexcept; friend class VertexBuffer; }; diff --git a/SmoothCam/include/render/shader_cache.h b/SmoothCam/include/render/shader_cache.h new file mode 100644 index 0000000..b3f9a9d --- /dev/null +++ b/SmoothCam/include/render/shader_cache.h @@ -0,0 +1,42 @@ +#pragma once +#include "render/shader.h" + +namespace Render { + class ShaderCache { + public: + ShaderCache(const ShaderCache&) = delete; + ShaderCache(ShaderCache&&) noexcept = delete; + ShaderCache& operator=(const ShaderCache&) = delete; + ShaderCache& operator=(ShaderCache&&) noexcept = delete; + + static ShaderCache& Get() noexcept { + static ShaderCache cache; + return cache; + } + + void Release() noexcept; + + eastl::shared_ptr Load(const ShaderCreateInfo& info, Render::D3DContext& ctx) noexcept; + + struct SCIHasher { + size_t operator()(const ShaderCreateInfo& key) const { + return key.Hash(); + } + }; + + struct SCICompare { + size_t operator()(const ShaderCreateInfo& k1, const ShaderCreateInfo& k2) const { + return k1 == k2; + } + }; + + private: + ShaderCache() noexcept; + + private: + eastl::unordered_map< + ShaderCreateInfo, eastl::weak_ptr, + SCIHasher, SCICompare + > shaders; + }; +} \ No newline at end of file diff --git a/SmoothCam/include/render/shaders/draw_fullscreen_texture.h b/SmoothCam/include/render/shaders/draw_fullscreen_texture.h index b8eafb3..f090afd 100644 --- a/SmoothCam/include/render/shaders/draw_fullscreen_texture.h +++ b/SmoothCam/include/render/shaders/draw_fullscreen_texture.h @@ -1,8 +1,11 @@ #pragma once +#include "render/shaders/shader_decl.h" namespace Render { namespace Shaders { - constexpr const auto DrawFullscreenTextureVS = R"( + constexpr ShaderDecl DrawFullscreenTextureVS = { + 2, + R"( struct VS_INPUT { float3 vPos : POS; float2 vUV : UV; @@ -19,9 +22,11 @@ VS_OUTPUT main(VS_INPUT input) { output.vUV = input.vUV; return output; } - )"; + )" }; - constexpr const auto DrawFullscreenTexturePS = R"( + constexpr ShaderDecl DrawFullscreenTexturePS = { + 3, + R"( struct PS_INPUT { float4 vPos : SV_POSITION; float2 uv : COLOR0; @@ -39,6 +44,6 @@ PS_OUTPUT main(PS_INPUT input) { output.color = tex.Sample(texSampler, input.uv); return output; } - )"; + )" }; } } \ No newline at end of file diff --git a/SmoothCam/include/render/shaders/shader_decl.h b/SmoothCam/include/render/shaders/shader_decl.h new file mode 100644 index 0000000..c6705fe --- /dev/null +++ b/SmoothCam/include/render/shaders/shader_decl.h @@ -0,0 +1,10 @@ +#pragma once + +namespace Render { + namespace Shaders { + typedef struct ShaderDecl { + uint8_t uid = 0; + const char* source = nullptr; + } ShaderDecl; + } +} \ No newline at end of file diff --git a/SmoothCam/include/render/shaders/shader_vertex_color.h b/SmoothCam/include/render/shaders/vertex_color_screen.h similarity index 79% rename from SmoothCam/include/render/shaders/shader_vertex_color.h rename to SmoothCam/include/render/shaders/vertex_color_screen.h index 11fc366..0fbfddb 100644 --- a/SmoothCam/include/render/shaders/shader_vertex_color.h +++ b/SmoothCam/include/render/shaders/vertex_color_screen.h @@ -1,8 +1,11 @@ #pragma once +#include "render/shaders/shader_decl.h" namespace Render { namespace Shaders { - constexpr const auto VertexColorPassThruVS = R"( + constexpr ShaderDecl VertexColorScreenVS = { + 0, + R"( struct VS_INPUT { float4 vPos : POS; float4 vColor : COL; @@ -26,9 +29,11 @@ VS_OUTPUT main(VS_INPUT input) { output.vColor = input.vColor; return output; } - )"; + )" }; - constexpr const auto VertexColorPassThruPS = R"( + constexpr ShaderDecl VertexColorScreenPS = { + 1, + R"( struct PS_INPUT { float4 pos : SV_POSITION; float4 color : COLOR0; @@ -43,6 +48,6 @@ PS_OUTPUT main(PS_INPUT input) { output.color = input.color; return output; } - )"; + )" }; } } \ No newline at end of file diff --git a/SmoothCam/include/render/shaders/vertex_color.h b/SmoothCam/include/render/shaders/vertex_color_world.h similarity index 75% rename from SmoothCam/include/render/shaders/vertex_color.h rename to SmoothCam/include/render/shaders/vertex_color_world.h index d65b114..0c19282 100644 --- a/SmoothCam/include/render/shaders/vertex_color.h +++ b/SmoothCam/include/render/shaders/vertex_color_world.h @@ -1,8 +1,11 @@ #pragma once +#include "render/shaders/shader_decl.h" namespace Render { namespace Shaders { - constexpr const auto VertexColorVS = R"( + constexpr ShaderDecl VertexColorWorldVS = { + 4, + R"( struct VS_INPUT { float3 vPos : POS; float2 vUV : UV; @@ -39,9 +42,11 @@ VS_OUTPUT main(VS_INPUT input) { return output; } - )"; + )" }; - constexpr const auto VertexColorPS = R"( + constexpr ShaderDecl VertexColorWorldPS = { + 5, + R"( struct PS_INPUT { float4 pos : SV_POSITION; float2 uv : COLOR0; @@ -55,14 +60,16 @@ struct PS_OUTPUT { cbuffer PerFrame : register(b1) { float4x4 matProjView; + float4 tint; float curTime; }; PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT output; - output.color = input.color; + // scale tint.rgb by color.xyz so that black colors remain unchanged + output.color = float4(lerp(input.color.xyz, tint.xyz * input.color.xyz, 0.5f), input.color.w); return output; } - )"; + )" }; } } \ No newline at end of file diff --git a/SmoothCam/include/render/srv.h b/SmoothCam/include/render/srv.h index f348cf7..86418f2 100644 --- a/SmoothCam/include/render/srv.h +++ b/SmoothCam/include/render/srv.h @@ -9,7 +9,7 @@ namespace Render { DXGI_FORMAT format = DXGI_FORMAT::DXGI_FORMAT_UNKNOWN; D3D11_SRV_DIMENSION dimensions = D3D11_SRV_DIMENSION::D3D10_1_SRV_DIMENSION_TEXTURE2D; D3D11_TEX2D_SRV texture2D = {}; - std::shared_ptr texture; + eastl::shared_ptr texture; } SRVCreateInfo; class SRV { @@ -26,6 +26,6 @@ namespace Render { private: winrt::com_ptr srv; - std::shared_ptr texture; + eastl::shared_ptr texture; }; } \ No newline at end of file diff --git a/SmoothCam/include/render/state_overlay.h b/SmoothCam/include/render/state_overlay.h index e839acf..066a229 100644 --- a/SmoothCam/include/render/state_overlay.h +++ b/SmoothCam/include/render/state_overlay.h @@ -4,13 +4,13 @@ #include "render/gradbox.h" namespace Camera { - class SmoothCamera; + class Thirdperson; } namespace Render { class StateOverlay : public GradBox { public: - explicit StateOverlay(uint32_t width, uint32_t height, Camera::SmoothCamera* camera, D3DContext& ctx); + explicit StateOverlay(uint32_t width, uint32_t height, Camera::Thirdperson* camera, D3DContext& ctx); ~StateOverlay(); StateOverlay(const StateOverlay&) = delete; StateOverlay(StateOverlay&&) noexcept = delete; @@ -22,15 +22,15 @@ namespace Render { // Set the size of the graph void SetSize(uint32_t w, uint32_t h) noexcept; // Draw the chart - void Draw(const Config::OffsetGroup* curGroup, D3DContext& ctx) noexcept; + void Draw(const Actor* focus, const Config::OffsetGroup* curGroup, D3DContext& ctx) noexcept; private: - void DrawBitset32(const std::wstring& name, const std::bitset<32>& bits, + void DrawBitset32(const eastl::wstring& name, const eastl::bitset<32>& bits, const glm::vec2& pos, D3DContext& ctx) noexcept; - void DrawBool(const std::wstring& name, bool value, const glm::vec2& pos, + void DrawBool(const eastl::wstring& name, bool value, const glm::vec2& pos, D3DContext& ctx) noexcept; - Camera::SmoothCamera* camera = nullptr; + Camera::Thirdperson* camera = nullptr; uint32_t width = 0; uint32_t height = 0; uint32_t xPos = 0; diff --git a/SmoothCam/include/render/vertex_buffer.h b/SmoothCam/include/render/vertex_buffer.h index ef30105..695ec8d 100644 --- a/SmoothCam/include/render/vertex_buffer.h +++ b/SmoothCam/include/render/vertex_buffer.h @@ -3,7 +3,7 @@ #include "render/shader.h" namespace Render { - using IALayout = std::vector; + using IALayout = eastl::vector; struct VertexBufferCreateInfo { size_t elementSize = 0; size_t numElements = 0; @@ -11,7 +11,7 @@ namespace Render { D3D11_PRIMITIVE_TOPOLOGY topology = D3D11_PRIMITIVE_TOPOLOGY::D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST; D3D11_USAGE bufferUsage = D3D11_USAGE_IMMUTABLE; uint32_t cpuAccessFlags = 0; - std::shared_ptr vertexProgram; + eastl::shared_ptr vertexProgram; IALayout iaLayout = {}; }; @@ -35,7 +35,7 @@ namespace Render { // Unmap the buffer void Unmap() noexcept; // Create the input assembler layout - void CreateIALayout(const IALayout& layout, const std::shared_ptr& vertexProgram) noexcept; + void CreateIALayout(const IALayout& layout, const eastl::shared_ptr& vertexProgram) noexcept; private: uint32_t stride; diff --git a/SmoothCam/include/skyrimSE/FirstPersonState.h b/SmoothCam/include/skyrimSE/FirstPersonState.h new file mode 100644 index 0000000..a054220 --- /dev/null +++ b/SmoothCam/include/skyrimSE/FirstPersonState.h @@ -0,0 +1,27 @@ +#pragma once + +class CorrectedFirstPersonState : public TESCameraState { + public: + CorrectedFirstPersonState(); + virtual ~CorrectedFirstPersonState(); + + PlayerInputHandler inputHandler; + NiPoint3 lastPosition; + NiPoint3 vel; + NiPoint3 damping; + uint32_t pad54; + NiAVObject* cameraObj; + NiNode* fovControl; + float sittingRotation; + float unk6C; + float unk70; + float currentPitchOfs; + float targetPitchOfs; + float unk7C; + uint32_t unk80; + bool cameraOverride; + bool cameraPitchOverride; + uint16_t unk86; + uint64_t unk88; +}; +static_assert(sizeof(CorrectedFirstPersonState) == 0x90); \ No newline at end of file diff --git a/SmoothCam/include/skyrimSE/bhkWorld.h b/SmoothCam/include/skyrimSE/bhkWorld.h index bbe82a0..19dcd23 100644 --- a/SmoothCam/include/skyrimSE/bhkWorld.h +++ b/SmoothCam/include/skyrimSE/bhkWorld.h @@ -2,6 +2,28 @@ struct hkpRayCastInfo; +class hkpWorld { + public: + uintptr_t unk0; + uintptr_t unk1; + uintptr_t unk2; + uintptr_t unk3; + glm::vec4 gravity; + uintptr_t unk4; + uintptr_t unk5; + uintptr_t unk6; + uintptr_t unk7; + uintptr_t unk8; + uintptr_t unk9; + uintptr_t unk10; + uintptr_t unk11; + uintptr_t unk12; + uintptr_t unk13; + uintptr_t unk14; + hkp3AxisSweep* broadphase; +}; +static_assert(offsetof(hkpWorld, broadphase) == 0x88); + class bhkWorld { public: virtual void unk1(); // 0x0 ==> FUN_140dac670 @@ -43,7 +65,7 @@ class bhkWorld { virtual void unk37(); // 0x120 ==> LAB_140c52b10 virtual void unk38(); // 0x128 ==> thunk_FUN_140ddc2a0 virtual void unk39(); // 0x130 ==> FUN_140dab5f0 - virtual intptr_t unk40() const; // 0x138 ==> LAB_140da6b60 + virtual hkpWorld* GetHavokWorld();// 0x138 ==> LAB_140da6b60 virtual intptr_t unk41() const; // 0x140 ==> LAB_140da6b70 virtual void unk42(); // 0x148 ==> LAB_14029f590 virtual void unk43(); // 0x150 ==> LAB_1402a1da0 @@ -59,4 +81,5 @@ class bhkWorld { virtual void unk53(); // 0x1A0 ==> FUN_140da79c0 virtual void unk54(); // 0x1A8 ==> FUN_140da79f0 virtual void unk55(); // 0x1B0 ==> FUN_140da7af0 -}; \ No newline at end of file +}; + diff --git a/SmoothCam/include/string_builder.h b/SmoothCam/include/string_builder.h index beaf10c..465d743 100644 --- a/SmoothCam/include/string_builder.h +++ b/SmoothCam/include/string_builder.h @@ -42,6 +42,6 @@ struct StringBuilder { private: T str = {}; - std::vector items = {}; + eastl::vector items = {}; size_t size_ = 0; }; \ No newline at end of file diff --git a/SmoothCam/include/thirdperson.h b/SmoothCam/include/thirdperson.h new file mode 100644 index 0000000..a57078c --- /dev/null +++ b/SmoothCam/include/thirdperson.h @@ -0,0 +1,229 @@ +#pragma once +#include "camera.h" +#include "camera_states/base_third.h" +#include "camera_states/thirdperson/thirdperson.h" +#include "camera_states/thirdperson/thirdperson_combat.h" +#include "camera_states/thirdperson/thirdperson_horse.h" +#include "crosshair.h" + +#ifdef WITH_CHARTS +# include "render/cbuffer.h" +# include "render/line_graph.h" +# include "render/state_overlay.h" +# include "render/ninode_tree_display.h" +#endif + +namespace Camera { + // Used to select which scalar function type should be run + enum class ScalarSelector { + Normal, + SepZ, + LocalSpace, + }; + + // All thirdperson only logic (Aim rotation, interp, crosshair, so on) + class Thirdperson : public ICamera { + public: + Thirdperson(Camera* baseCamera); + Thirdperson(const Thirdperson&) = delete; + Thirdperson(Thirdperson&&) noexcept = delete; + Thirdperson& operator=(const Thirdperson&) = delete; + Thirdperson& operator=(Thirdperson&&) noexcept = delete; + + public: + virtual ~Thirdperson(); + + // Called when we are switching to this camera + virtual void OnBegin(PlayerCharacter* player, CorrectedPlayerCamera* camera, ICamera* lastState) noexcept override; + // Called when we are done using this camera + virtual void OnEnd(PlayerCharacter* player, CorrectedPlayerCamera* camera, ICamera* newState) noexcept override; + + // Runs before the internal game camera logic + // Return true when changing the camera state + virtual bool OnPreGameUpdate(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState) override; + // Selects the correct update method and positions the camera + virtual void OnUpdateCamera(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState) override; + // Render crosshair objects + virtual void Render(Render::D3DContext& ctx) noexcept override; + + // Called when the player toggles the POV + virtual void OnTogglePOV(const ButtonEvent* ev) noexcept override; + // Called when any other key is pressed + virtual bool OnKeyPress(const ButtonEvent* ev) noexcept override; + // Called when a menu of interest is opening or closing + virtual bool OnMenuOpenClose(MenuID id, const MenuOpenCloseEvent* const ev) noexcept override; + + // Triggers when the camera action state changes + virtual void OnCameraActionStateTransition(const PlayerCharacter* player, const CameraActionState newState, + const CameraActionState oldState) noexcept override; + // Triggers when the camera state changes + virtual void OnCameraStateTransition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, + const GameState::CameraState newState, const GameState::CameraState oldState) noexcept override; + + // Returns the full world-space camera target postion for the current player state + glm::vec3 GetCurrentCameraTargetWorldPosition(const TESObjectREFR* ref, const CorrectedPlayerCamera* camera) const; + // Returns the target world-space position for the camera, with local offsets and rotations applied. This is the goal + // position ignoring any interpolation + void GetCameraGoalPosition(const CorrectedPlayerCamera* camera, glm::vec3& world, glm::vec3& local); + // Return the euler angles for the player's current aim + glm::vec2 GetAimRotation(const TESObjectREFR* ref, const CorrectedPlayerCamera * camera) const; + // Return the camera rotation + const mmath::Rotation& GetCameraRotation() const noexcept; + // Set the camera world position + void SetPosition(const glm::vec3& pos, const CorrectedPlayerCamera* camera) noexcept; + // Get the crosshair manager + Crosshair::Manager* GetCrosshairManager() noexcept; + + private: + // Set the camera to the goal position and invalidate interp state + void MoveToGoalPosition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) noexcept; + // Update the internal rotation + void UpdateInternalRotation(CorrectedPlayerCamera* camera) noexcept; + // Find a node to use as the world position for following + NiAVObject* FindFollowBone(const TESObjectREFR* ref) const noexcept; + + // Returns the zoom value set from the given camera state + float GetCurrentCameraZoom(const CorrectedPlayerCamera* camera, + const GameState::CameraState currentState) const noexcept; + // Returns the camera's current zoom level - Camera must extend ThirdPersonState + float GetCameraZoomScalar(const CorrectedPlayerCamera* camera, uint16_t cameraState) const noexcept; + // Returns true if interpolation is allowed in the current state + bool IsInterpAllowed(const PlayerCharacter* player) const noexcept; + + // Returns an offset group for the current player movement state + const Config::OffsetGroup* GetOffsetForState(const CameraActionState state) const noexcept; + + // Selects the right offset from an offset group for the player's weapon state + float GetActiveWeaponStateZoomOffset(const PlayerCharacter* player, const Config::OffsetGroup* group) const noexcept; + // Selects the right offset from an offset group for the player's weapon state + float GetActiveWeaponStateUpOffset(const PlayerCharacter* player, const Config::OffsetGroup* group) const noexcept; + // Selects the right offset from an offset group for the player's weapon state + float GetActiveWeaponStateSideOffset(const PlayerCharacter* player, const Config::OffsetGroup* group) const noexcept; + // Selects the right offset from an offset group for the player's weapon state + float GetActiveWeaponStateFOVOffset(const PlayerCharacter* player, const Config::OffsetGroup* group) const noexcept; + + //Returns the camera zoom for the current player state + float GetCurrentCameraZoomOffset(const PlayerCharacter* player) const noexcept; + // Returns the camera height for the current player state + float GetCurrentCameraHeight(const PlayerCharacter* player) const noexcept; + // Returns the camera side offset for the current player state + float GetCurrentCameraSideOffset(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) const noexcept; + // Returns the camera FOV offset for the current player state + float GetCurrentCameraFOVOffset(const PlayerCharacter* player) const noexcept; + + // Returns the ideal camera distance for the current zoom level + float GetCurrentCameraDistance(const CorrectedPlayerCamera* camera) const noexcept; + // Returns the full local-space camera offset for the current player state, FOV is packed in .w + glm::vec4 GetCurrentCameraOffset(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) const noexcept; + // Returns the current smoothing scalar to use for the given distance to the player + double GetCurrentSmoothingScalar(const float distance, ScalarSelector method = ScalarSelector::Normal) const; + // Returns the user defined distance clamping vector pair + std::tuple GetDistanceClamping() const noexcept; + + // Offset the gmae FOV by the given amount + void SetFOVOffset(float fov, bool force = false) noexcept; + // Track the thirdperson state for a POV switch and update as required + bool UpdatePOVSwitchState(CorrectedPlayerCamera* camera, uint16_t cameraState) noexcept; + + private: + struct { + mutable BSFixedString head = "NPC Head [Head]"; + mutable BSFixedString spine1 = "NPC Spine1 [Spn1]"; + } Strings; + + // User config + Config::UserConfig* config = nullptr; + // Crosshair manager + eastl::unique_ptr crosshair; + // All camera state instances + eastl::array, static_cast(GameState::CameraState::MAX_STATE)> cameraStates; + // Current actor being followed + Actor* currentFocusObject = nullptr; + + // The current rotation of the camera in both euler angles and in quaternion form + mmath::Rotation rotation; + // The last position of the camera we set + mmath::Position lastPosition; + // Our most current camera position we set + mmath::Position currentPosition; + + // Data to be saved and restored across certain state transitions + struct { + // Yaw rotation for the horse state - We need to restore this after moving from tween->horse + float horseYaw = 0.0f; + // ACC writes over pitch rotation of the player with an incorrect value (are we causing that?) + // Store pitch when entering dialog and restore it after + float accPitch = 0.0f; + } stateCopyData; + + // Our current offset group and offset position, set by the offset transition states + struct { + const Config::OffsetGroup* currentGroup = nullptr; + glm::vec3 position = { 0.0f, 0.0f, 0.0f }; + float fov = 0.0f; + } offsetState; + + // Transition groups for smoothing offset and zoom switches + using OffsetTransition = mmath::TransitionGroup; + using ZoomTransition = mmath::TransitionGroup; + using POVTransition = mmath::FixedTransitionGoal; + // Smooth x, y components of the active offset group + OffsetTransition offsetTransitionState; + // Smooth z of the active offset group + ZoomTransition zoomTransitionState; + // Smooth FOV of the active offset group + ZoomTransition fovTransitionState; + // Smooth POV switching + struct { + double startTime = 0.0; + float lastValue = 0.0f; + bool running = false; + } povTransitionState; + + // Set on first execution to perform setup + bool firstFrame = false; + // Was the POV key pressed this frame? + bool povWasPressed = false; + // Last read zoom value from the third person state + float lastZoomValue = -0.2f; + // Is the dialog menu open? + bool dialogMenuOpen = false; + // Was the dialog menu open last frame? + bool wasDialogMenuOpen = false; + // -1 when we have swapped shoulders + int shoulderSwap = 1; + + // Debug overlays +#ifdef WITH_CHARTS + eastl::unique_ptr perFrameBuffer; + + eastl::unique_ptr graph_worldPosTarget; + eastl::unique_ptr graph_offsetPos; + eastl::unique_ptr graph_targetOffsetPos; + eastl::unique_ptr graph_localSpace; + eastl::unique_ptr graph_rotation; + eastl::unique_ptr graph_tpsRotation; + eastl::unique_ptr graph_computeTime; + + eastl::unique_ptr focusTargetNodeTree; + eastl::unique_ptr stateOverlay; + + enum class DisplayMode : uint8_t { + None, + Graphs, + NodeTree, + StateOverlay + }; + DisplayMode curDebugMode = DisplayMode::None; + bool dbgKeyDown = false; + float lastProfSnap = 0.0f; + glm::mat4 orthoMatrix = {}; + + friend class Render::StateOverlay; +#endif + + friend class State::BaseThird; + }; +} \ No newline at end of file diff --git a/SmoothCam/include/trackir/trackir.h b/SmoothCam/include/trackir/trackir.h new file mode 100644 index 0000000..9234744 --- /dev/null +++ b/SmoothCam/include/trackir/trackir.h @@ -0,0 +1,118 @@ +#pragma once +#ifdef DEVELOPER +namespace TrackIR { + constexpr uint16_t NPQUERYVERSION = 1040; + + constexpr uint8_t NPSTATUS_REMOTEACTIVE = 0; + constexpr uint8_t NPSTATUS_REMOTEDISABLED = 1; + + constexpr uint8_t NPVERSIONMAJOR = 1; + constexpr uint8_t NPVERSIONMINOR = 2; + + constexpr uint16_t NPRoll = 1; + constexpr uint16_t NPPitch = 2; + constexpr uint16_t NPYaw = 4; + + constexpr uint16_t NPControl = 8; + + constexpr uint16_t NPX = 16; + constexpr uint16_t NPY = 32; + constexpr uint16_t NPZ = 64; + + constexpr uint16_t NPRawX = 128; + constexpr uint16_t NPRawY = 256; + constexpr uint16_t NPRawZ = 512; + + constexpr uint16_t NPDeltaX = 1024; + constexpr uint16_t NPDeltaY = 2048; + constexpr uint16_t NPDeltaZ = 4096; + + constexpr uint16_t NPSmoothX = 8192; + constexpr uint16_t NPSmoothY = 16384; + constexpr uint16_t NPSmoothZ = 32768; + + constexpr float NPMaxValue = 16383.0f; + constexpr float NPMinValue = -16383.0f; + + constexpr float MaxRotation = 180.0f; + + enum class NPResult : uint8_t { + OK = 0, + DEVICE_NOT_PRESENT, + UNSUPPORTED_OS, + INVALID_ARG, + DLL_NOT_FOUND, + NO_DATA, + INTERNAL_DATA + }; + +#pragma pack(push, 1) + typedef struct tagTrackIRSignature { + char DllSignature[200]; + char AppSignature[200]; + } SIGNATUREDATA, *LPTRACKIRSIGNATURE; + + typedef struct tagTrackIRData { + uint16_t wNPStatus; + uint16_t wPFrameSignature; + uint32_t dwNPIOData; + + float fNPRoll; + float fNPPitch; + float fNPYaw; + float fNPX; + float fNPY; + float fNPZ; + float fNPRawX; + float fNPRawY; + float fNPRawZ; + float fNPDeltaX; + float fNPDeltaY; + float fNPDeltaZ; + float fNPSmoothX; + float fNPSmoothY; + float fNPSmoothZ; + } TRACKIRDATA, *LPTRACKIRDATA; +#pragma pack(pop) + + typedef NPResult(__stdcall *PF_NOTIFYCALLBACK)(uint16_t, uint16_t); + typedef NPResult(__stdcall *PF_NP_REGISTERWINDOWHANDLE)(HWND); + typedef NPResult(__stdcall *PF_NP_UNREGISTERWINDOWHANDLE)(void); + typedef NPResult(__stdcall *PF_NP_REGISTERPROGRAMPROFILEID)(uint16_t); + typedef NPResult(__stdcall *PF_NP_QUERYVERSION)(uint16_t*); + typedef NPResult(__stdcall *PF_NP_REQUESTDATA)(uint16_t); + typedef NPResult(__stdcall *PF_NP_GETSIGNATURE)(LPTRACKIRSIGNATURE); + typedef NPResult(__stdcall *PF_NP_GETDATA)(LPTRACKIRDATA); + typedef NPResult(__stdcall *PF_NP_STARTCURSOR)(void); + typedef NPResult(__stdcall *PF_NP_STOPCURSOR)(void); + typedef NPResult(__stdcall *PF_NP_RECENTER)(void); + typedef NPResult(__stdcall *PF_NP_STARTDATATRANSMISSION)(void); + typedef NPResult(__stdcall *PF_NP_STOPDATATRANSMISSION)(void); + + NPResult __stdcall RegisterWindowHandle(HWND hWnd); + NPResult __stdcall UnregisterWindowHandle(void); + NPResult __stdcall RegisterProgramProfileID(uint16_t wPPID); + NPResult __stdcall QueryVersion(uint16_t* pwVersion); + NPResult __stdcall RequestData(uint16_t wDataReq); + NPResult __stdcall GetSignature(LPTRACKIRSIGNATURE pSignature); + NPResult __stdcall GetData(LPTRACKIRDATA pTID); + NPResult __stdcall StartCursor(void); + NPResult __stdcall StopCursor(void); + NPResult __stdcall ReCenter(void); + NPResult __stdcall StartDataTransmission(void); + NPResult __stdcall StopDataTransmission(void); + + NPResult Init(LPTSTR pszDLLPath); + NPResult GetDLLLocation(LPTSTR pszPath); + + NPResult Initialize(HWND hWnd); + void Shutdown(); + bool IsRunning() noexcept; + + struct TrackingSnapshot { + glm::vec3 pos = { 0.0f, 0.0f, 0.0f }; + glm::vec3 rot = { 0.0f, 0.0f, 0.0f }; + }; + TrackingSnapshot GetTrackingData() noexcept; +} +#endif \ No newline at end of file diff --git a/SmoothCam/include/util.h b/SmoothCam/include/util.h new file mode 100644 index 0000000..4426675 --- /dev/null +++ b/SmoothCam/include/util.h @@ -0,0 +1,63 @@ +#pragma once + +namespace Util { + // @Note: We are tagging the 3 0 bits (caused by pointer alignment rules) + // Tags must be cleared before being dereferenced. + template + T* TagPointer(T* ptr, uint8_t tag) noexcept { + constexpr uintptr_t mask = 0xFFFFFFFFFFFFFFFC; + const auto addr = reinterpret_cast(ptr); + return reinterpret_cast((addr & mask) | tag); + } + + template + T* ClearPointerTag(T* ptr) noexcept { + constexpr uintptr_t mask = 0xFFFFFFFFFFFFFFFC; + const auto addr = reinterpret_cast(ptr); + return reinterpret_cast(addr & mask); + } + + template + uint8_t GetPointerTag(T* ptr) noexcept { + constexpr uintptr_t tagMask = 0x0000000000000003; + const auto addr = reinterpret_cast(ptr); + return static_cast(addr & tagMask); + } + + template + inline void HashCombine(size_t& seed, const T& v) noexcept { + eastl::hash h; + seed ^= h(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + } + + inline eastl::string UpperCase(const eastl::string& str) noexcept { + eastl::string result; + result.reserve(str.length()); + + for (auto it = str.cbegin(); it != str.cend(); it++) { + const auto ch = *it; + if (ch >= 0x61 && ch < 0x7B) + result.push_back(ch - 0x20); + else + result.push_back(ch); + } + + return result; + } + + inline eastl::string UpperCase(const BSFixedString& str) noexcept { + eastl::string result; + const auto len = std::strlen(str.c_str()); + result.reserve(len); + + for (auto i = 0; i < len; i++) { + const auto ch = str.c_str()[i]; + if (ch >= 0x61 && ch < 0x7B) + result.push_back(ch - 0x20); + else + result.push_back(ch); + } + + return result; + } +} \ No newline at end of file diff --git a/SmoothCam/resource/Resource.rc b/SmoothCam/resource/Resource.rc index 3e3c68a..e7eaa8b 100644 --- a/SmoothCam/resource/Resource.rc +++ b/SmoothCam/resource/Resource.rc @@ -52,7 +52,7 @@ END // VS_VERSION_INFO VERSIONINFO - FILEVERSION 1,0,0,0 + FILEVERSION 1,4,0,0 PRODUCTVERSION 1,0,0,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG @@ -69,9 +69,9 @@ BEGIN BLOCK "040904b0" BEGIN VALUE "FileDescription", "SkyrimSE Smooth Cam" - VALUE "FileVersion", "1.0.0.0" + VALUE "FileVersion", "1.4.0.0" VALUE "InternalName", "SmoothCam.dll" - VALUE "LegalCopyright", "Copyright (C) 2020" + VALUE "LegalCopyright", "Copyright (C) 2020-2021" VALUE "OriginalFilename", "SmoothCam.dll" VALUE "ProductName", "SmoothCam" VALUE "ProductVersion", "1.0.0.0" diff --git a/SmoothCam/source/addrlib/offsets.cpp b/SmoothCam/source/addrlib/offsets.cpp index 1fac202..f6a5e34 100644 --- a/SmoothCam/source/addrlib/offsets.cpp +++ b/SmoothCam/source/addrlib/offsets.cpp @@ -1,23 +1,41 @@ #include "addrlib/offsets.h" #include "addrlib/skse_macros.h" -#include -VersionDb& Offsets::GetDB() { - static VersionDb db; +static bool dbLoaded = false; +static std::unique_ptr db; +static eastl::unordered_map cache; + +std::unique_ptr& Offsets::GetDB() { return db; } bool Offsets::Initialize() { - return GetDB().Load(); + db = std::make_unique(); + auto status = db->Load(); + if (status) { + dbLoaded = true; + for (const auto& it : addrMap) + CacheID(it.second); + } + return status; +} + +void Offsets::ReleaseDB() noexcept { + dbLoaded = false; + db.reset(); +} + +void Offsets::CacheID(uintptr_t ofsID) noexcept { + cache.insert({ ofsID, reinterpret_cast(db->FindAddressById(ofsID)) }); } #ifdef _DEBUG void Offsets::DumpDatabaseTextFile() { - if (!GetDB().Load(1, 5, 97, 0)) { + if (!db->Load(1, 5, 97, 0)) { FatalError(L"Failed to load offset database."); } - GetDB().Dump("offsets.txt"); + db->Dump("offsets.txt"); } #endif @@ -25,12 +43,14 @@ constexpr uintptr_t Offsets::GetByVersionAddr(uintptr_t addr) { return addrMap.at(addr); } -uintptr_t Offsets::GetVersionAddress(uintptr_t addr) { +uintptr_t Offsets::GetVersionAddress(uintptr_t addr) noexcept { return GetOffset(addrMap.at(addr)); } -uintptr_t Offsets::GetOffset(uintptr_t id) { - uintptr_t ret; - GetDB().FindOffsetById(id, ret); - return ret; +uintptr_t Offsets::GetOffset(uintptr_t id) noexcept { + if (dbLoaded) { + return reinterpret_cast(db->FindAddressById(id)); + } else { + return cache.find(id)->second; + } } \ No newline at end of file diff --git a/SmoothCam/source/arrow_fixes.cpp b/SmoothCam/source/arrow_fixes.cpp index e402c0f..444f45b 100644 --- a/SmoothCam/source/arrow_fixes.cpp +++ b/SmoothCam/source/arrow_fixes.cpp @@ -1,27 +1,111 @@ #include "arrow_fixes.h" #include "game_state.h" #include "camera.h" +#include "thirdperson.h" +#include "debug/eh.h" + +extern eastl::unique_ptr g_theCamera; + +#ifdef DEBUG +// Draw the flight path of the last fired projectile for debug help +// INSERT toggles the overlay + +static eastl::unique_ptr segmentDrawer; +std::mutex segmentLock; +static eastl::vector> points; +static Render::LineList segments; +static bool drawOverlay = false; + + +typedef uintptr_t(*UpdateTraceArrowProjectile)(SkyrimSE::ArrowProjectile*, NiPoint3*, NiPoint3*); +using TickArrowFlightPath = TypedDetour; +static eastl::unique_ptr detUpdateTraceArrowProjectile; +static uintptr_t mUpdateTraceArrowProjectile(SkyrimSE::ArrowProjectile* arrow, NiPoint3* to, NiPoint3* from) { + if (arrow->shooter == 0x00100000) { + std::lock_guard lock(segmentLock); + points.push_back(eastl::make_tuple(glm::vec3{ from->x, from->y, from->z }, glm::vec3{ to->x, to->y, to->z })); + } + return detUpdateTraceArrowProjectile->GetBase()(arrow, to, from); +} + + +typedef UInt32(*MaybeSpawnArrow)(uint32_t* arrowHandle, ArrowFixes::LaunchData* launchData, + uintptr_t param_3, uintptr_t** param_4); +using ArrowSpawnFunc = TypedDetour; +static eastl::unique_ptr detArrowSpawn; +static UInt32 mArrowSpawn(uint32_t* arrowHandle, ArrowFixes::LaunchData* launchData, + uintptr_t param_3, uintptr_t** param_4) +{ + auto ret = detArrowSpawn->GetBase()(arrowHandle, launchData, param_3, param_4); + NiPointer ref; + UInt32 rc = *arrowHandle; + (*LookupREFRByHandle)(rc, ref); + auto asArrow = reinterpret_cast(ref.get()); + + if (asArrow->shooter == 0x00100000) { + std::lock_guard lock(segmentLock); + points.clear(); + } + + return ret; +} + +static bool insertWasDown = false; +void ArrowFixes::Draw(Render::D3DContext& ctx) { + if (GetAsyncKeyState(VK_INSERT) && !insertWasDown) { + insertWasDown = true; + drawOverlay = !drawOverlay; + } else if (!GetAsyncKeyState(VK_INSERT)) { + insertWasDown = false; + } + + if (!drawOverlay) return; + + std::lock_guard lock(segmentLock); + segments.clear(); + for (const auto& [l1, l2] : points) { + segments.emplace_back( + Render::Point( + Render::ToRenderScale(l1), + { 1.0f, 0.0f, 0.0f, 1.0f } + ), + Render::Point( + Render::ToRenderScale(l2), + { 1.0f, 0.0f, 0.0f, 1.0f } + ) + ); + } + + segmentDrawer->Submit(segments); +} +#endif -extern std::shared_ptr g_theCamera; //FUN_14084b430:49866 typedef void(*FactorCameraOffset)(CorrectedPlayerCamera* camera, NiPoint3& pos, bool fac); -static FactorCameraOffset fnFactorCameraOffset; -static std::unique_ptr detFactorCameraOffset; +using FactorCameraOffsetDetour = TypedDetour; +static eastl::unique_ptr detFactorCameraOffset; static void mFactorCameraOffset(CorrectedPlayerCamera* camera, NiPoint3& pos, bool fac) { + const auto mdmp = Debug::MiniDumpScope(); + // SSE Engine Fixes will call GetEyeVector with factorCameraOffset = true // We appear to screw this computation up as a side effect of correcting the interaction crosshair // So, yeah. Just fix it here. - if (fac) { - fnFactorCameraOffset(camera, pos, fac); + if (fac || Config::GetCurrentConfig()->modDisabled) { + detFactorCameraOffset->GetBase()(camera, pos, fac); return; } // Only run in states we care about - static const auto ply = *g_thePlayer; + const auto ply = *g_thePlayer; + if (!ply) { + detFactorCameraOffset->GetBase()(camera, pos, fac); + return; + } + if (!GameState::IsThirdPerson(ply, camera) && !GameState::IsInHorseCamera(ply, camera) && !GameState::IsInDragonCamera(camera)) { - fnFactorCameraOffset(camera, pos, fac); + detFactorCameraOffset->GetBase()(camera, pos, fac); return; } @@ -30,25 +114,29 @@ static void mFactorCameraOffset(CorrectedPlayerCamera* camera, NiPoint3& pos, bo } typedef void(*UpdateArrowFlightPath)(SkyrimSE::ArrowProjectile* arrow); -static UpdateArrowFlightPath fnUpdateArrowFlightPath; -static std::unique_ptr detArrowFlightPath; +using UpdateArrowFlightPathDetour = TypedDetour; +static eastl::unique_ptr detArrowFlightPath; static void mUpdateArrowFlightPath(SkyrimSE::ArrowProjectile* arrow) { - if (!g_theCamera) - return fnUpdateArrowFlightPath(arrow); + const auto mdmp = Debug::MiniDumpScope(); + + if (!g_theCamera || Config::GetCurrentConfig()->modDisabled) + return detArrowFlightPath->GetBase()(arrow); const auto camera = CorrectedPlayerCamera::GetSingleton(); const auto ply = *g_thePlayer; // Only correct our own arrows UInt32 ref = arrow->shooter; + if (ref == *g_invalidRefHandle) + return detArrowFlightPath->GetBase()(arrow); + NiPointer out; (*LookupREFRByHandle)(ref, out); // On horseback, our camera actually follows the horse and not the player // Compare with the player directly - // @TODO: When refactoring the camera to follow the active input ref, change this check too if (reinterpret_cast(out.get()) != reinterpret_cast(ply)) - return fnUpdateArrowFlightPath(arrow); + return detArrowFlightPath->GetBase()(arrow); // @Note: Don't run our trajectory correction while in a kill move // Only run it in the primary states we care about @@ -56,35 +144,28 @@ static void mUpdateArrowFlightPath(SkyrimSE::ArrowProjectile* arrow) { if (state != GameState::CameraState::ThirdPerson && state != GameState::CameraState::ThirdPersonCombat && state != GameState::CameraState::Horseback && state != GameState::CameraState::IronSights) { - return fnUpdateArrowFlightPath(arrow); + return detArrowFlightPath->GetBase()(arrow); } - const auto tps = reinterpret_cast( - camera->cameraStates[CorrectedPlayerCamera::kCameraState_ThirdPerson2] - ); - if (!tps) return - fnUpdateArrowFlightPath(arrow); - // Get camera aim rotation - auto rot = g_theCamera->GetAimRotation(ply, camera); + auto rot = g_theCamera->GetThirdpersonCamera()->GetAimRotation(ply, camera); + // Compute arrow velocity vector typedef float(*GetAFloat)(SkyrimSE::ArrowProjectile*); - - // Doesn't look like gravity - might be a scalar applied to gravity? - // Does the same thing as equippedWeapon->gameData.speed - const auto s2 = Offsets::Get(42537)(arrow); - // Scalar, 0-1 how long you held back the arrow - const auto power = Offsets::Get(42536)(arrow); + const auto s2 = Offsets::Get(42537)(arrow); // Does the same thing as equippedWeapon->gameData.speed + const auto power = Offsets::Get(42536)(arrow); // Scalar, 0-1 how long you held back the arrow (arrow->unk188) // Not sure what this is looking for, but do it anyways if ((~(byte)(arrow->flags >> 0x1f) & 1) != 0) { - // Assume otherwise is a magic projectile + // Assume otherwise is a magic projectile if (s2 != 1.0f) { + static auto arrowTilt = (*g_iniSettingCollection)->Get("f3PArrowTiltUpAngle:Combat"); // Add tilt angle - if (GameState::IsUsingCrossbow(*g_thePlayer)) { - rot.x -= glm::radians(Config::GetGameConfig()->f3PBoltTiltUpAngle); - } else if (GameState::IsUsingBow(*g_thePlayer)) { - rot.x -= glm::radians(Config::GetGameConfig()->f3PArrowTiltUpAngle); + if (GameState::IsUsingBow(ply) || GameState::IsUsingCrossbow(ply)) { + float tilt = 2.5f; + if (arrowTilt) + tilt = arrowTilt->data.f32; + rot.x -= glm::radians(tilt); } } } else { @@ -112,63 +193,67 @@ static void mUpdateArrowFlightPath(SkyrimSE::ArrowProjectile* arrow) { pitchSinVel }; - // Still don't know what this all is doing - // Takes a lock, gets some globals, adds an x,y offset to velocity - typedef void(*LookupFun)(uint32_t*, uintptr_t*); + // Appears to look for the shooter, then add the shooter's velocity to the arrow + typedef void(*LookupFun)(uint32_t*, TESObjectREFR**); static auto lookupFunc = Offsets::Get(12204); - uint32_t local_res8 = arrow->shooter; - uintptr_t local_res10 = 0; - lookupFunc(&local_res8, &local_res10); // FUN_1401329d0 - - auto uVar3 = local_res10; + uint32_t shooterID = arrow->shooter; + TESObjectREFR* shooterRef = nullptr; + lookupFunc(&shooterID, &shooterRef); - const uintptr_t DAT_142eff7d8 = Offsets::Get(514905); - const uintptr_t DAT_142ec5c60 = Offsets::Get(514725); - if (((local_res10 != 0) && (local_res10 == DAT_142eff7d8)) && - (*(int*)(DAT_142ec5c60 + 0x20) != 4)) + const TESObjectREFR* DAT_142eff7d8 = *Offsets::Get(514905); + const auto unkData = Offsets::Get(514725); + if (((shooterRef != nullptr) && (shooterRef == DAT_142eff7d8)) && (unkData->unk4 != 4)) { - NiPoint3 local_70; - //(**(code **)(*ThePlayer + 0x430))(ThePlayer, &local_70); - typedef void(__thiscall PlayerCharacter::* Unk)(NiPoint3&); - (*g_thePlayer->*reinterpret_cast(&PlayerCharacter::Unk_86))(local_70); - arrow->velocityVector.x = local_70.x + arrow->velocityVector.x; - arrow->velocityVector.y = local_70.y + arrow->velocityVector.y; + NiPoint3 shooterVelocity; + typedef void(__thiscall TESObjectREFR::* GetLinearVelocity)(NiPoint3&); // GetLinearVelocity, So says CommonLib + (ply->*reinterpret_cast(&TESObjectREFR::Unk_86))(shooterVelocity); + arrow->velocityVector.x = shooterVelocity.x + arrow->velocityVector.x; + arrow->velocityVector.y = shooterVelocity.y + arrow->velocityVector.y; } // Like other handle refcounters, arg1 = 0, release rc if arg2 != nullptr - local_res8 = 0; - lookupFunc(&local_res8, &local_res10); + shooterID = 0; + lookupFunc(&shooterID, &shooterRef); } bool ArrowFixes::Attach() { - { - //FUN_14084b430:FactorCameraOffset:GetEyeVector - fnFactorCameraOffset = Offsets::Get(49866); - detFactorCameraOffset = std::make_unique( - reinterpret_cast(&fnFactorCameraOffset), - mFactorCameraOffset - ); + detFactorCameraOffset = eastl::make_unique(49866, mFactorCameraOffset); + if (!detFactorCameraOffset->Attach()) { + _ERROR("Failed to place detour on target function(49,866), this error is fatal."); + FatalError(L"Failed to place detour on target function(49,866), this error is fatal."); + } - if (!detFactorCameraOffset->Attach()) { - _ERROR("Failed to place detour on target function(49,866), this error is fatal."); - FatalError(L"Failed to place detour on target function(49,866), this error is fatal."); - } + detArrowFlightPath = eastl::make_unique(42998, mUpdateArrowFlightPath); + if (!detArrowFlightPath->Attach()) { + _ERROR("Failed to place detour on target function(42,998), this error is fatal."); + FatalError(L"Failed to place detour on target function(42,998), this error is fatal."); } - { - //140750150::UpdateArrowFlightPath - fnUpdateArrowFlightPath = Offsets::Get(42998); - detArrowFlightPath = std::make_unique( - reinterpret_cast(&fnUpdateArrowFlightPath), - mUpdateArrowFlightPath - ); +#ifdef DEBUG + detUpdateTraceArrowProjectile = eastl::make_unique(43008, mUpdateTraceArrowProjectile); + if (!detUpdateTraceArrowProjectile->Attach()) { + _ERROR("Failed to place detour on target function(43,008), this error is fatal."); + FatalError(L"Failed to place detour on target function(43,008), this error is fatal."); + } - if (!detArrowFlightPath->Attach()) { - _ERROR("Failed to place detour on target function(42,998), this error is fatal."); - FatalError(L"Failed to place detour on target function(42,998), this error is fatal."); - } + detArrowSpawn = eastl::make_unique(42928, mArrowSpawn); + if (!detArrowSpawn->Attach()) { + _ERROR("Failed to place detour on target function(42,928), this error is fatal."); + FatalError(L"Failed to place detour on target function(42,928), this error is fatal."); } + if (Render::HasContext()) { + segmentDrawer = eastl::make_unique(Render::GetContext()); + } +#endif + return true; -} \ No newline at end of file +} + +#ifdef DEBUG +void ArrowFixes::Shutdown() { + std::lock_guard lock(segmentLock); + segmentDrawer.reset(); +} +#endif \ No newline at end of file diff --git a/SmoothCam/source/basicdetour.cpp b/SmoothCam/source/basicdetour.cpp index 5300c1f..515d145 100644 --- a/SmoothCam/source/basicdetour.cpp +++ b/SmoothCam/source/basicdetour.cpp @@ -5,6 +5,11 @@ BasicDetour::BasicDetour(void** old, void* replacement) noexcept : attached(fals fnDetour = replacement; } +BasicDetour::~BasicDetour() noexcept { + if (attached) + Detach(); +} + bool BasicDetour::Attach() noexcept { DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); diff --git a/SmoothCam/source/camera.cpp b/SmoothCam/source/camera.cpp index 960ddc8..e862ba1 100644 --- a/SmoothCam/source/camera.cpp +++ b/SmoothCam/source/camera.cpp @@ -1,120 +1,78 @@ #include "camera.h" #include "crosshair.h" +#ifdef DEVELOPER +#include "trackir/trackir.h" +#endif +#include "firstperson.h" +#include "thirdperson.h" -Camera::SmoothCamera::SmoothCamera() : config(Config::GetCurrentConfig()) { - cameraStates[static_cast(GameState::CameraState::ThirdPerson)] = - std::move(std::make_unique(this)); - cameraStates[static_cast(GameState::CameraState::ThirdPersonCombat)] = - std::move(std::make_unique(this)); - cameraStates[static_cast(GameState::CameraState::Horseback)] = - std::move(std::make_unique(this)); - - crosshair = std::make_unique(); - - if (Render::HasContext()) { - Render::OnPresent(std::bind(&Camera::SmoothCamera::Render, this, std::placeholders::_1)); - -#ifdef WITH_CHARTS - refTreeDisplay = std::make_unique(600, 1080, Render::GetContext()); - stateOverlay = std::make_unique(600, 128, this, Render::GetContext()); - - worldPosTargetGraph = std::make_unique(3, 128, 600, 128, Render::GetContext()); - offsetPosGraph = std::make_unique(3, 128, 600, 128, Render::GetContext()); - offsetTargetPosGraph = std::make_unique(3, 128, 600, 128, Render::GetContext()); - localSpaceGraph = std::make_unique(3, 128, 600, 128, Render::GetContext()); - rotationGraph = std::make_unique(2, 128, 600, 128, Render::GetContext()); - tpsRotationGraph = std::make_unique(4, 128, 600, 128, Render::GetContext()); - computeTimeGraph = std::make_unique(2, 128, 600, 128, Render::GetContext()); - - const auto xColor = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); - const auto yColor = glm::vec4(0.0f, 1.0f, 0.0f, 1.0f); - const auto zColor = glm::vec4(0.0f, 0.0f, 1.0f, 1.0f); - worldPosTargetGraph->SetLineColor(0, xColor); - worldPosTargetGraph->SetLineColor(1, yColor); - worldPosTargetGraph->SetLineColor(2, zColor); - worldPosTargetGraph->SetName(L"World Pos"); - - localSpaceGraph->SetLineColor(0, xColor); - localSpaceGraph->SetLineColor(1, yColor); - localSpaceGraph->SetLineColor(2, zColor); - localSpaceGraph->SetPosition(0, 128); - localSpaceGraph->SetName(L"Local Space"); - - offsetPosGraph->SetLineColor(0, xColor); - offsetPosGraph->SetLineColor(1, yColor); - offsetPosGraph->SetLineColor(2, zColor); - offsetPosGraph->SetPosition(0, 256); - offsetPosGraph->SetName(L"Offset Pos"); - - offsetTargetPosGraph->SetLineColor(0, xColor); - offsetTargetPosGraph->SetLineColor(1, yColor); - offsetTargetPosGraph->SetLineColor(2, zColor); - offsetTargetPosGraph->SetPosition(0, 384); - offsetTargetPosGraph->SetName(L"Target Offset Pos"); - - rotationGraph->SetLineColor(0, xColor); - rotationGraph->SetLineColor(1, yColor); - rotationGraph->SetPosition(0, 512); - rotationGraph->SetName(L"Current Rotation (Pitch, Yaw)"); - - tpsRotationGraph->SetLineColor(0, xColor); - tpsRotationGraph->SetLineColor(1, yColor); - tpsRotationGraph->SetLineColor(2, zColor); - tpsRotationGraph->SetLineColor(3, { 1.0f, 1.0f, 0.0f, 1.0f }); - tpsRotationGraph->SetPosition(0, 640); - tpsRotationGraph->SetName(L"TPS Rotation (Yaw1, Yaw2, Yaw), camera->lookYaw"); +#include "debug/eh.h" - computeTimeGraph->SetLineColor(0, { 1.0f, 0.266f, 0.984f, 1.0f }); - computeTimeGraph->SetLineColor(1, { 1.0f, 0.541f, 0.218f, 1.0f }); - computeTimeGraph->SetName(L"Compute Time (Camera::Update()), Frame Time , seconds"); - computeTimeGraph->SetPosition(0, 768); +Camera::Camera::Camera() : config(Config::GetCurrentConfig()) { + cameraFirst = eastl::make_unique(this); + cameraThird = eastl::make_unique(this); - Render::CBufferCreateInfo cbuf; - cbuf.bufferUsage = D3D11_USAGE::D3D11_USAGE_DYNAMIC; - cbuf.cpuAccessFlags = D3D11_CPU_ACCESS_WRITE; - cbuf.size = sizeof(glm::mat4); - cbuf.initialData = &orthoMatrix; - perFrameOrtho = std::make_unique(cbuf, Render::GetContext()); -#endif - } + if (Render::HasContext()) + Render::OnPresent(std::bind(&Camera::Camera::Render, this, std::placeholders::_1)); } -Camera::SmoothCamera::~SmoothCamera() { - crosshair.reset(); +Camera::Camera::~Camera() { + activeCamera = nullptr; + cameraFirst.reset(); + cameraThird.reset(); } // Called when the player toggles the POV -void Camera::SmoothCamera::OnTogglePOV(const ButtonEvent* ev) noexcept { +void Camera::Camera::OnTogglePOV(const ButtonEvent* ev) noexcept { povIsThird = !povIsThird; povWasPressed = true; + if (activeCamera) + activeCamera->OnTogglePOV(ev); } -void Camera::SmoothCamera::OnKeyPress(const ButtonEvent* ev) noexcept { +void Camera::Camera::OnKeyPress(const ButtonEvent* ev) noexcept { auto code = ev->keyMask; - if (code <= 0x6 && ev->deviceType == kDeviceType_Mouse) { + if (code <= 0x6 && ev->deviceType == kDeviceType_Mouse) code += 0x100; + + // Cycle next preset + if (config->nextPresetKey >= 0 && code == config->nextPresetKey && ev->timer <= 0.000001f) { + // wrap to 0..5 + if (++currentPresetIndex > 5) currentPresetIndex = 0; + + // Try and load from currentIndex, up to n slots wrapping back to startIndex + const auto startIndex = currentPresetIndex; + while (!Config::LoadPreset(currentPresetIndex)) { + if (++currentPresetIndex > 5) currentPresetIndex = 0; + if (currentPresetIndex == startIndex) break; // No valid preset to load + } + + return; + } else if (config->modToggleKey >= 0 && code == config->modToggleKey && ev->timer <= 0.000001f) { + // Mod enable/disable + config->modDisabled = !config->modDisabled; } - if (config->shoulderSwapKey >= 0 && config->shoulderSwapKey == code && ev->timer <= 0.000001f) - shoulderSwap = shoulderSwap == 1 ? -1 : 1; + + if (activeCamera && activeCamera->OnKeyPress(ev)) return; } -void Camera::SmoothCamera::OnMenuOpenClose(MenuID id, const MenuOpenCloseEvent* const ev) noexcept { - switch (id) { - case MenuID::DialogMenu: { - if (dialogMenuOpen && !ev->opening) wasDialogMenuOpen = true; - dialogMenuOpen = ev->opening; +void Camera::Camera::OnMenuOpenClose(MenuID id, const MenuOpenCloseEvent* const ev) noexcept { + if (activeCamera && activeCamera->OnMenuOpenClose(id, ev)) return; - if (dialogMenuOpen && config->compatACC) { - stateCopyData.accPitch = (*g_thePlayer)->rot.x; - } + switch (id) { + case MenuID::InventoryMenu: + case MenuID::DialogMenu: + case MenuID::MapMenu: break; - } + case MenuID::LoadingMenu: + wasLoading = true; + [[fallthrough]]; default: { loadScreenDepth = glm::clamp( loadScreenDepth + (ev->opening ? 1 : -1), 0, - 255 + 127 ); break; } @@ -122,28 +80,29 @@ void Camera::SmoothCamera::OnMenuOpenClose(MenuID id, const MenuOpenCloseEvent* } // Updates our POV state to the true value the game expects for each state -const bool Camera::SmoothCamera::UpdateCameraPOVState(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) noexcept { +const bool Camera::Camera::UpdateCameraPOVState(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) noexcept { povIsThird = GameState::IsInAutoVanityCamera(camera) || - GameState::IsInCameraTransition(camera) || GameState::IsInUsingObjectCamera(camera) || GameState::IsInKillMove(camera) || - GameState::IsInBleedoutCamera(camera) || GameState::IsInFurnitureCamera(camera) || - GameState::IsInHorseCamera(player, camera) || GameState::IsInDragonCamera(camera) || GameState::IsThirdPerson(player, camera); + GameState::IsInCameraTransition(camera) || GameState::IsInUsingObjectCamera(camera) || + GameState::IsInKillMove(camera) || GameState::IsInBleedoutCamera(camera) || + GameState::IsInFurnitureCamera(camera) || GameState::IsInHorseCamera(player, camera) || + GameState::IsInDragonCamera(camera) || GameState::IsThirdPerson(player, camera); return povIsThird; } -#pragma region Camera state updates -const GameState::CameraState Camera::SmoothCamera::GetCurrentCameraState() const noexcept { +const GameState::CameraState Camera::Camera::GetCurrentCameraState() const noexcept { return currentState; } -const Camera::CameraActionState Camera::SmoothCamera::GetCurrentCameraActionState() const noexcept { +const Camera::CameraActionState Camera::Camera::GetCurrentCameraActionState() const noexcept { return currentActionState; } // Returns the current camera state for use in selecting an update method -const GameState::CameraState Camera::SmoothCamera::UpdateCurrentCameraState(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) { +const GameState::CameraState Camera::Camera::UpdateCurrentCameraState(const PlayerCharacter* player, + const CorrectedPlayerCamera* camera) noexcept +{ GameState::CameraState newState = GameState::CameraState::Unknown; const auto tps = reinterpret_cast(camera->cameraState); - const auto minZoom = Config::GetGameConfig()->fMinCurrentZoom; // Improved camera - Sitting if (config->compatIC && GameState::IC_InFirstPersonState(player, camera) && @@ -189,7 +148,7 @@ const GameState::CameraState Camera::SmoothCamera::UpdateCurrentCameraState(cons } // Returns the current camera action state for use in the selected update method -const Camera::CameraActionState Camera::SmoothCamera::UpdateCurrentCameraActionState(const PlayerCharacter* player, +const Camera::CameraActionState Camera::Camera::UpdateCurrentCameraActionState(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) noexcept { CameraActionState newState = CameraActionState::Unknown; @@ -242,529 +201,53 @@ const Camera::CameraActionState Camera::SmoothCamera::UpdateCurrentCameraActionS if (newState != currentActionState) { lastActionState = currentActionState; currentActionState = newState; -#ifdef _DEBUG OnCameraActionStateTransition(player, newState, lastActionState); -#endif } return newState; } -#ifdef _DEBUG // Triggers when the camera action state changes, for debugging -void Camera::SmoothCamera::OnCameraActionStateTransition(const PlayerCharacter* player, +void Camera::Camera::OnCameraActionStateTransition(const PlayerCharacter* player, const CameraActionState newState, const CameraActionState oldState) const noexcept { - // For debugging + if (activeCamera) + activeCamera->OnCameraActionStateTransition(player, newState, oldState); } -#endif // Triggers when the camera state changes -void Camera::SmoothCamera::OnCameraStateTransition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, +void Camera::Camera::OnCameraStateTransition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, const GameState::CameraState newState, const GameState::CameraState oldState) { - auto& oldCameraState = cameraStates.at(static_cast(oldState)); - auto& newCameraState = cameraStates.at(static_cast(newState)); - - switch (oldState) { - case GameState::CameraState::UsingObject: // We also want to do this for this case - case GameState::CameraState::FirstPerson: { - // We want to invalidate any interpolation state when exiting first-person - // Set both current and last positions to the target third-person position - const auto pov = UpdateCameraPOVState(player, camera); - const auto actionState = UpdateCurrentCameraActionState(player, camera); - offsetState.currentGroup = GetOffsetForState(actionState); - - auto ofs = GetCurrentCameraOffset(player, camera); - offsetTransitionState.currentPosition = offsetTransitionState.lastPosition = { - ofs.x, ofs.z - }; - zoomTransitionState.currentPosition = zoomTransitionState.lastPosition = ofs.y; - fovTransitionState.currentPosition = fovTransitionState.lastPosition = ofs.w; - - GetCameraGoalPosition(camera, currentPosition.world, currentPosition.local); - lastPosition = currentPosition; - break; - } - case GameState::CameraState::ThirdPerson: { - oldCameraState->OnEnd(player, currentFocusObject, camera, newCameraState.get()); - break; - } - case GameState::CameraState::ThirdPersonCombat: { - oldCameraState->OnEnd(player, currentFocusObject, camera, newCameraState.get()); - break; - } - case GameState::CameraState::Horseback: { - oldCameraState->OnEnd(player, currentFocusObject, camera, newCameraState.get()); - - if (newState == GameState::CameraState::Tweening) { - // The game clears horseYaw to 0 when going from tween back to horseback, which results in a rotation jump - // once the horse starts moving again. We can store the horse yaw value here to restore later. - stateCopyData.horseYaw = reinterpret_cast( - camera->cameraStates[CorrectedPlayerCamera::kCameraState_Horse] - )->horseYaw; - - } else if (newState == GameState::CameraState::ThirdPerson) { - // We need to smooth out the camera position - const auto pov = UpdateCameraPOVState(player, camera); - const auto actionState = UpdateCurrentCameraActionState(player, camera); - offsetState.currentGroup = GetOffsetForState(actionState); - - auto ofs = GetCurrentCameraOffset(player, camera); - offsetTransitionState.currentPosition = offsetTransitionState.lastPosition = { - ofs.x, ofs.z - }; - zoomTransitionState.currentPosition = zoomTransitionState.lastPosition = ofs.y; - fovTransitionState.currentPosition = fovTransitionState.lastPosition = ofs.w; - - GetCameraGoalPosition(camera, currentPosition.world, currentPosition.local); - lastPosition = currentPosition; - } - - break; - } - case GameState::CameraState::Tweening: - case GameState::CameraState::Transitioning: { - // Don't copy positions from these states - break; - } - default: - // Store the position for smoothing in the new state - lastPosition.world = currentPosition.world = glm::vec3{ - camera->cameraNode->m_worldTransform.pos.x, - camera->cameraNode->m_worldTransform.pos.y, - camera->cameraNode->m_worldTransform.pos.z - }; - break; - } - - switch (newState) { - case GameState::CameraState::ThirdPerson: { - newCameraState->OnBegin(player, currentFocusObject, camera, oldCameraState.get()); - break; - } - case GameState::CameraState::ThirdPersonCombat: { - newCameraState->OnBegin(player, currentFocusObject, camera, oldCameraState.get()); - break; - } - case GameState::CameraState::Horseback: { - newCameraState->OnBegin(player, currentFocusObject, camera, oldCameraState.get()); - - if (oldState == GameState::CameraState::Tweening) { - reinterpret_cast( - camera->cameraStates[CorrectedPlayerCamera::kCameraState_Horse] - )->horseYaw = stateCopyData.horseYaw; - } - - break; - } - case GameState::CameraState::Free: { - // Forward our position to the free cam state - auto state = reinterpret_cast(camera->cameraState); - state->unk30[0] = lastPosition.world.x; - state->unk30[1] = lastPosition.world.y; - state->unk30[2] = lastPosition.world.z; - - const auto quat = rotation.ToNiQuat(); - - //FUN_140848880((longlong)ppuVar3,local_18); - typedef void(__fastcall* UpdateFreeCamTransform)(FreeCameraState*, const NiQuaternion&); - Offsets::Get(49816)(state, quat); - } - [[fallthrough]]; - default: { - // Do this once here on transition to a new state we don't run in - crosshair->SetCrosshairEnabled(true); - crosshair->CenterCrosshair(); - crosshair->SetDefaultSize(); - break; - } - } -} -#pragma endregion - -#pragma region Camera position calculations -// Returns the zoom value set from the given camera state -float Camera::SmoothCamera::GetCurrentCameraZoom(const CorrectedPlayerCamera* camera, const GameState::CameraState currentState) const noexcept { - switch (currentState) { - case GameState::CameraState::ThirdPerson: - case GameState::CameraState::ThirdPersonCombat: { - return GetCameraZoomScalar(camera, PlayerCamera::kCameraState_ThirdPerson2); - } - case GameState::CameraState::Horseback: { - return GetCameraZoomScalar(camera, PlayerCamera::kCameraState_Horse); - } - case GameState::CameraState::Dragon: { - return GetCameraZoomScalar(camera, PlayerCamera::kCameraState_Dragon); - } - case GameState::CameraState::Bleedout: { - return GetCameraZoomScalar(camera, PlayerCamera::kCameraState_Bleedout); - } - default: - return 0.0f; - } -} - -// Returns an offset group for the current player movement state -const Config::OffsetGroup* Camera::SmoothCamera::GetOffsetForState(const CameraActionState state) const noexcept { - switch (state) { - case CameraActionState::VampireLord: { - return &config->vampireLord; - } - case CameraActionState::Werewolf: { - return &config->werewolf; - } - case CameraActionState::DisMounting: { - return &config->standing; // Better when dismounting - } - case CameraActionState::Sleeping: { - return &config->sitting; - } - case CameraActionState::Sitting: { - return &config->sitting; - } - case CameraActionState::Sneaking: { - return &config->sneaking; - } - case CameraActionState::Aiming: { - return &config->bowAim; - } - case CameraActionState::Swimming: { - return &config->swimming; - } - case CameraActionState::Sprinting: { - return &config->sprinting; - } - case CameraActionState::Walking: { - return &config->walking; - } - case CameraActionState::Running: { - return &config->running; - } - case CameraActionState::Standing: { - return &config->standing; - } - default: { - return &config->standing; - } - } -} - -float Camera::SmoothCamera::GetActiveWeaponStateZoomOffset(const PlayerCharacter* player, const Config::OffsetGroup* group) const noexcept { - if (!GameState::IsWeaponDrawn(player)) return group->zoomOffset; - if (GameState::IsRangedWeaponDrawn(player)) { - return group->combatRangedZoomOffset; - } - if (GameState::IsMagicDrawn(player)) { - return group->combatMagicZoomOffset; - } - return group->combatMeleeZoomOffset; -} - -// Selects the right offset from an offset group for the player's weapon state -float Camera::SmoothCamera::GetActiveWeaponStateUpOffset(const PlayerCharacter* player, const Config::OffsetGroup* group) const noexcept { - if (!GameState::IsWeaponDrawn(player)) return group->upOffset; - if (GameState::IsRangedWeaponDrawn(player)) { - return group->combatRangedUpOffset; - } - if (GameState::IsMagicDrawn(player)) { - return group->combatMagicUpOffset; - } - return group->combatMeleeUpOffset; -} - -// Selects the right offset from an offset group for the player's weapon state -float Camera::SmoothCamera::GetActiveWeaponStateSideOffset(const PlayerCharacter* player, const Config::OffsetGroup* group) const noexcept { - if (!GameState::IsWeaponDrawn(player)) return group->sideOffset; - if (GameState::IsRangedWeaponDrawn(player)) { - return group->combatRangedSideOffset; - } - if (GameState::IsMagicDrawn(player)) { - return group->combatMagicSideOffset; - } - return group->combatMeleeSideOffset; -} - -// Selects the right offset from an offset group for the player's weapon state -float Camera::SmoothCamera::GetActiveWeaponStateFOVOffset(const PlayerCharacter* player, const Config::OffsetGroup* group) const noexcept { - if (!GameState::IsWeaponDrawn(player)) return group->fovOffset; - if (GameState::IsRangedWeaponDrawn(player)) { - return group->combatRangedFOVOffset; - } - if (GameState::IsMagicDrawn(player)) { - return group->combatMagicFOVOffset; - } - return group->combatMeleeFOVOffset; -} - - -float Camera::SmoothCamera::GetCurrentCameraZoomOffset(const PlayerCharacter* player) const noexcept { - switch (currentState) { - case GameState::CameraState::Horseback: { - if (GameState::IsBowDrawn(player)) { - return config->bowAim.horseZoomOffset; - } else { - return GetActiveWeaponStateZoomOffset(player, &config->horseback); - } - } - default: - break; - } - - switch (currentActionState) { - case CameraActionState::DisMounting: - case CameraActionState::Sleeping: - case CameraActionState::Sitting: - case CameraActionState::Swimming: { - return offsetState.currentGroup->zoomOffset; - } - case CameraActionState::Aiming: { - if (GameState::IsSneaking(player)) - return config->bowAim.combatMeleeZoomOffset; - return offsetState.currentGroup->zoomOffset; - } - case CameraActionState::Sneaking: - case CameraActionState::VampireLord: - case CameraActionState::Werewolf: - case CameraActionState::Sprinting: - case CameraActionState::Walking: - case CameraActionState::Running: - case CameraActionState::Standing: { - return GetActiveWeaponStateZoomOffset(player, offsetState.currentGroup); - } - default: { - break; - } - } - return 0.0f; -} - -// Returns the camera height for the current player state -float Camera::SmoothCamera::GetCurrentCameraHeight(const PlayerCharacter* player) const noexcept { - switch (currentState) { - case GameState::CameraState::Horseback: { - if (GameState::IsBowDrawn(player)) { - return config->bowAim.horseUpOffset; - } else { - return GetActiveWeaponStateUpOffset(player, &config->horseback); - } - } - default: - break; - } - - switch (currentActionState) { - case CameraActionState::DisMounting: - case CameraActionState::Sleeping: - case CameraActionState::Sitting: - case CameraActionState::Swimming: { - return offsetState.currentGroup->upOffset; - } - case CameraActionState::Aiming: { - if (GameState::IsSneaking(player)) - return config->bowAim.combatMeleeUpOffset; - return offsetState.currentGroup->upOffset; - } - case CameraActionState::Sneaking: - case CameraActionState::VampireLord: - case CameraActionState::Werewolf: - case CameraActionState::Sprinting: - case CameraActionState::Walking: - case CameraActionState::Running: - case CameraActionState::Standing: { - return GetActiveWeaponStateUpOffset(player, offsetState.currentGroup); - } - default: { - break; - } - } - return 0.0f; -} - -// Returns the camera side offset for the current player state -float Camera::SmoothCamera::GetCurrentCameraSideOffset(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) const noexcept { - switch (currentState) { - case GameState::CameraState::Horseback: { - if (GameState::IsBowDrawn(player)) { - return config->bowAim.horseSideOffset * shoulderSwap; - } else { - return GetActiveWeaponStateSideOffset(player, &config->horseback) * shoulderSwap; - } - } - default: - break; - } - - switch (currentActionState) { - case CameraActionState::DisMounting: - case CameraActionState::Sleeping: - case CameraActionState::Sitting: - case CameraActionState::Swimming: { - return offsetState.currentGroup->sideOffset * shoulderSwap; - } - case CameraActionState::Aiming: { - if (GameState::IsSneaking(player)) - return config->bowAim.combatMeleeSideOffset * shoulderSwap; - return offsetState.currentGroup->sideOffset * shoulderSwap; - } - case CameraActionState::Sneaking: - case CameraActionState::VampireLord: - case CameraActionState::Werewolf: - case CameraActionState::Sprinting: - case CameraActionState::Walking: - case CameraActionState::Running: - case CameraActionState::Standing: { - return GetActiveWeaponStateSideOffset(player, offsetState.currentGroup) * shoulderSwap; - } - default: { - break; - } - } - return 0.0f; -} - -float Camera::SmoothCamera::GetCurrentCameraFOVOffset(const PlayerCharacter* player) const noexcept { - switch (currentState) { - case GameState::CameraState::Horseback: { - if (GameState::IsBowDrawn(player)) { - return config->bowAim.horseFOVOffset; - } else { - return GetActiveWeaponStateFOVOffset(player, &config->horseback); - } - } - default: - break; - } - - switch (currentActionState) { - case CameraActionState::DisMounting: - case CameraActionState::Sleeping: - case CameraActionState::Sitting: - case CameraActionState::Swimming: { - return offsetState.currentGroup->fovOffset; - } - case CameraActionState::Aiming:{ - if (GameState::IsSneaking(player)) - return config->bowAim.combatMeleeFOVOffset; - return offsetState.currentGroup->fovOffset; - } - case CameraActionState::Sneaking: - case CameraActionState::VampireLord: - case CameraActionState::Werewolf: - case CameraActionState::Sprinting: - case CameraActionState::Walking: - case CameraActionState::Running: - case CameraActionState::Standing: { - return GetActiveWeaponStateFOVOffset(player, offsetState.currentGroup); - } - default: { - break; - } - } - return 0.0f; -} - -// Returns the ideal camera distance for the current zoom level -float Camera::SmoothCamera::GetCurrentCameraDistance(const CorrectedPlayerCamera* camera) const noexcept { - return -(config->minCameraFollowDistance + (GetCurrentCameraZoom(camera, currentState) * config->zoomMul)); -} - -// Returns the full local-space camera offset for the current player state, FOV is packed in .w -glm::vec4 Camera::SmoothCamera::GetCurrentCameraOffset(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) const noexcept { - return { - GetCurrentCameraSideOffset(player, camera), - GetCurrentCameraDistance(camera) + GetCurrentCameraZoomOffset(player), - GetCurrentCameraHeight(player), - GetCurrentCameraFOVOffset(player) - }; -} - -NiAVObject* Camera::SmoothCamera::FindFollowBone(const TESObjectREFR* ref) const noexcept { - if (!ref->loadedState || !ref->loadedState->node) return nullptr; - auto& boneList = Config::GetBonePriorities(); - - for (auto it = boneList.begin(); it != boneList.end(); it++) { - auto node = ref->loadedState->node->GetObjectByName(&it->data); - if (node) return node; - } - - return nullptr; -} - -// Returns the full world-space camera target postion for the current player state -glm::vec3 Camera::SmoothCamera::GetCurrentCameraTargetWorldPosition(const TESObjectREFR* ref, - const CorrectedPlayerCamera* camera) const -{ - if (ref->loadedState && ref->loadedState->node && Strings.spine1.data) { - NiAVObject* node; - if (currentState == GameState::CameraState::Horseback) { - node = ref->loadedState->node->GetObjectByName(&Strings.spine1.data); - } else { - node = FindFollowBone(ref); - } - - if (node) { - return glm::vec3( - ref->pos.x, - ref->pos.y, - node->m_worldTransform.pos.z - ); - } - } - - return { - ref->pos.x, - ref->pos.y, - ref->pos.z - }; + if (activeCamera) + activeCamera->OnCameraStateTransition(player, camera, newState, oldState); } -void Camera::SmoothCamera::GetCameraGoalPosition(const CorrectedPlayerCamera* camera, glm::vec3& world, glm::vec3& local) { - const auto cameraLocal = offsetState.position; - auto translated = rotation.ToRotationMatrix() * glm::vec4( - cameraLocal.x, - cameraLocal.y, - 0.0f, - 1.0f - ); - translated.z += cameraLocal.z; - - local = static_cast(translated); - world = - GetCurrentCameraTargetWorldPosition(currentFocusObject, camera) + - local; -} - -void Camera::SmoothCamera::SetPosition(const glm::vec3& pos, const CorrectedPlayerCamera* camera) noexcept { +void Camera::Camera::SetPosition(const glm::vec3& pos, const CorrectedPlayerCamera* camera) noexcept { auto cameraNode = camera->cameraNode; - currentPosition.SetWorldPosition(pos); #ifdef _DEBUG - if (!mmath::IsValid(currentPosition.world)) { + if (!mmath::IsValid(pos)) { __debugbreak(); - // Oops, go ahead and clear both - lastPosition.SetWorldPosition(gameLastActualPosition); - currentPosition.SetWorldPosition(gameLastActualPosition); return; } #endif + cameraNode->m_localTransform.pos = cameraNode->m_worldTransform.pos = cameraNi->m_worldTransform.pos = - currentPosition.ToNiPoint3(); + { pos.x, pos.y, pos.z }; if (currentState == GameState::CameraState::ThirdPerson || currentState == GameState::CameraState::ThirdPersonCombat) { auto state = reinterpret_cast(camera->cameraState); - state->translation = currentPosition.ToNiPoint3(); + state->translation = cameraNode->m_localTransform.pos; } - - // Update world to screen matrices - UpdateInternalWorldToScreenMatrix(camera); } -void Camera::SmoothCamera::UpdateInternalWorldToScreenMatrix(const CorrectedPlayerCamera* camera) noexcept { +void Camera::Camera::UpdateInternalWorldToScreenMatrix(const mmath::Rotation& rot, const CorrectedPlayerCamera* camera) noexcept { // Run the THT to get a world to scaleform matrix auto lastRotation = cameraNi->m_worldTransform.rot; - cameraNi->m_worldTransform.rot = rotation.THT(); + cameraNi->m_worldTransform.rot = rot.THT(); // Force the game to compute the matrix for us + typedef void(*UpdateWorldToScreenMtx)(NiCamera*); static auto toScreenFunc = Offsets::Get(69271); toScreenFunc(cameraNi); // Grab it @@ -775,132 +258,11 @@ void Camera::SmoothCamera::UpdateInternalWorldToScreenMatrix(const CorrectedPlay toScreenFunc(cameraNi); } -// Returns the current smoothing scalar to use for the given distance to the player -double Camera::SmoothCamera::GetCurrentSmoothingScalar(const float distance, ScalarSelector method) const { - Config::ScalarMethods scalarMethod; - - // Work in FP64 here to eek out some more precision - // Avoid a divide-by-zero error by clamping to this lower bound - constexpr const double minZero = 0.000000000001; - - double scalar = 1.0; - double interpValue = 1.0; - double remapped = 1.0; - - if (method == ScalarSelector::SepZ) { - const auto max = static_cast(config->separateZMaxSmoothingDistance); - scalar = glm::clamp(glm::max(1.0 - (max - distance), minZero) / max, 0.0, 1.0); - remapped = mmath::Remap( - scalar, 0.0, 1.0, static_cast(config->separateZMinFollowRate), static_cast(config->separateZMaxFollowRate) - ); - scalarMethod = config->separateZScalar; - } else if (method == ScalarSelector::LocalSpace) { - remapped = distance; - scalarMethod = config->separateLocalScalar; - } else { - const auto max = static_cast(config->zoomMaxSmoothingDistance); - scalar = glm::clamp(glm::max(1.0 - (max - distance), minZero) / max, 0.0, 1.0); - remapped = mmath::Remap( - scalar, 0.0, 1.0, static_cast(config->minCameraFollowRate), static_cast(config->maxCameraFollowRate) - ); - scalarMethod = config->currentScalar; - } - - if (!config->disableDeltaTime) { - const double delta = glm::max(GameTime::GetFrameDelta(), minZero); - const double fps = 1.0 / delta; - const double mul = -fps * glm::log2(1.0 - remapped); - interpValue = glm::clamp(1.0 - glm::exp2(-mul * delta), 0.0, 1.0); - } else { - interpValue = remapped; - } - - return mmath::RunScalarFunction(scalarMethod, interpValue); -} - -// Returns the user defined distance clamping vector pair -std::tuple Camera::SmoothCamera::GetDistanceClamping() const noexcept { - float minsX = config->cameraDistanceClampXMin; - float maxsX = config->cameraDistanceClampXMax; - - if (config->swapXClamping && shoulderSwap < 1) { - std::swap(minsX, maxsX); - maxsX *= -1.0f; - minsX *= -1.0f; - } - - return std::tie( - glm::vec3{ minsX, config->cameraDistanceClampYMin, config->cameraDistanceClampZMin }, - glm::vec3{ maxsX, config->cameraDistanceClampYMax, config->cameraDistanceClampZMax } - ); +const NiFrustum& Camera::Camera::GetFrustum() const noexcept { + return frustum; } -// Returns true if interpolation is allowed in the current state -bool Camera::SmoothCamera::IsInterpAllowed(const PlayerCharacter* player) const noexcept { - auto ofs = offsetState.currentGroup; - if (currentState == GameState::CameraState::Horseback) { - if (GameState::IsBowDrawn(player)) { - return config->bowAim.interpHorseback; - } else { - ofs = &config->horseback; - } - } - - if (GameState::IsSneaking(player)) { - if (GameState::IsBowDrawn(player)) { - return config->bowAim.interpMeleeCombat; - } - } - - if (!GameState::IsWeaponDrawn(player)) return ofs->interp; - if (GameState::IsRangedWeaponDrawn(player)) { - return ofs->interpRangedCombat; - } - if (GameState::IsMagicDrawn(player)) { - return ofs->interpMagicCombat; - } - return ofs->interpMeleeCombat; -} -#pragma endregion - -#pragma region Camera getters -glm::vec2 Camera::SmoothCamera::GetAimRotation(const TESObjectREFR* ref, const CorrectedPlayerCamera* camera) const { - if (GameState::IsInHorseCamera(ref, camera)) { - // In horse camera, we need to clamp our local y axis - const auto yaw = ref->rot.z + glm::pi(); // convert to 0-tau - const auto yawLocal = glm::mod(rotation.euler.y - yaw, glm::pi() * 2.0f); // confine to tau - const auto clampedLocal = glm::clamp(yawLocal, mmath::half_pi, mmath::half_pi*3.0f); // clamp it to faceforward zone - - return { - rotation.euler.x, - yaw + clampedLocal - }; - } else { - const auto tps = reinterpret_cast(camera->cameraStates[CorrectedPlayerCamera::kCameraState_ThirdPerson2]); - if (tps && tps->freeRotationEnabled) { - // In free rotation mode aim yaw is locked to player rotation - return { - rotation.euler.x, - ref->rot.z - }; - } - } - - return rotation.euler; -} - -const mmath::Rotation& Camera::SmoothCamera::GetCameraRotation() const noexcept { - return rotation; -} - -// Returns the camera's current zoom level - Camera must extend ThirdPersonState -float Camera::SmoothCamera::GetCameraZoomScalar(const CorrectedPlayerCamera* camera, uint16_t cameraState) const noexcept { - const auto state = reinterpret_cast(camera->cameraStates[cameraState]); - if (!state) return 0.0f; - return state->cameraZoom + (Config::GetGameConfig()->fMinCurrentZoom *-1); -} - -NiPointer Camera::SmoothCamera::GetCurrentCameraTarget(const CorrectedPlayerCamera* camera) noexcept { +NiPointer Camera::Camera::GetCurrentCameraTarget(const CorrectedPlayerCamera* camera) noexcept { auto ctrls = PlayerControls::GetSingleton(); if (!ctrls) return *g_thePlayer; @@ -925,71 +287,26 @@ NiPointer Camera::SmoothCamera::GetCurrentCameraTarget(const CorrectedPla return *g_thePlayer; } -bool Camera::SmoothCamera::InLoadingScreen() const noexcept { +bool Camera::Camera::InLoadingScreen() const noexcept { return loadScreenDepth != 0; } -#pragma endregion -void Camera::SmoothCamera::UpdateInternalRotation(CorrectedPlayerCamera* camera) noexcept { - const auto tps = reinterpret_cast(camera->cameraState); - if (!tps) return; - - if (config->compatACC && wasDialogMenuOpen) { - auto pl = *g_thePlayer; - if (pl) { - tps->rotation = rotation.ToNiQuat(); - tps->yaw1 = tps->yaw2 = rotation.euler.y; - camera->lookYaw = rotation.euler.y; - pl->rot.x = stateCopyData.accPitch; - } - - } else { - // Alright, just lie and force the game to compute yaw for us - // We get some jitter when things like magic change our character, not sure why yet @TODO - auto last = tps->freeRotationEnabled; - auto pl = *g_thePlayer; - tps->freeRotationEnabled = true; - tps->UpdateRotation(); - rotation.SetQuaternion(tps->rotation); - tps->freeRotationEnabled = last; - tps->UpdateRotation(); - } +void Camera::Camera::SetShouldForceCameraState(bool force, uint8_t newCameraState) noexcept { + wantNewCameraState = force; + wantNewState = newCameraState; } -void Camera::SmoothCamera::SetFOVOffset(float fov) noexcept { - // This is an offset value applied to any call to SetFOV. - // Used internally in the game by things like zooming with the bow. - // Appears to be set by the game every frame so we can just add our offset on top like this. - const auto baseValue = CorrectedPlayerCamera::GetSingleton()->worldFOV; - const auto currentOffset = *Offsets::Get(527997); - - // We never want the FOV to go negative or past 180, so clamp it to a sane-ish range - // (10-170) - const auto actualFov = baseValue + currentOffset; - constexpr auto fovMax = 170.0f; - constexpr auto fovMin = 10.0f; - - // Break early if we are already past our limit - if (actualFov >= fovMax) return; - if (actualFov <= fovMin) return; - - // Compute the maximal range we can offset before hitting the limit - const auto fullOffset = currentOffset + fov; - const auto f = baseValue + fullOffset; - auto finalOffset = fov; - - if (f > fovMax) { - auto delta = f - fovMax; - finalOffset -= delta; - } else if (f < fovMin) { - auto delta = fovMin - f; - finalOffset += delta; - } +// Get the thirdperson camera +Camera::Thirdperson* Camera::Camera::GetThirdpersonCamera() noexcept { + return cameraThird.get(); +} - *Offsets::Get(527997) += finalOffset; +// Get the firstperson camera +Camera::Firstperson* Camera::Camera::GetFirstpersonCamera() noexcept { + return cameraFirst.get(); } -NiPointer Camera::SmoothCamera::GetNiCamera(CorrectedPlayerCamera* camera) const noexcept { +NiPointer Camera::Camera::GetNiCamera(CorrectedPlayerCamera* camera) const noexcept { // Do other things parent stuff to the camera node? Better safe than sorry I guess if (camera->cameraNode->m_children.m_size == 0) return nullptr; for (auto i = 0; i < camera->cameraNode->m_children.m_size; i++) { @@ -1000,157 +317,33 @@ NiPointer Camera::SmoothCamera::GetNiCamera(CorrectedPlayerCamera* cam return nullptr; } -void Camera::SmoothCamera::Render(Render::D3DContext& ctx) { +void Camera::Camera::Render(Render::D3DContext& ctx) { if (InLoadingScreen()) return; - crosshair->Render(ctx, currentPosition.world, rotation.euler, frustum); - -#ifdef WITH_CHARTS - if (!currentFocusObject) return; - - if (GetAsyncKeyState(VK_UP)) { - if (!dbgKeyDown) { - switch (curDebugMode) { - case DisplayMode::None: - curDebugMode = DisplayMode::Graphs; - break; - case DisplayMode::Graphs: - curDebugMode = DisplayMode::NodeTree; - break; - case DisplayMode::NodeTree: - curDebugMode = DisplayMode::StateOverlay; - break; - case DisplayMode::StateOverlay: - curDebugMode = DisplayMode::None; - break; - } - dbgKeyDown = true; - } - } else { - dbgKeyDown = false; - } - - const auto& size = ctx.windowSize; - orthoMatrix = glm::ortho(0.0f, size.x, size.y, 0.0f); - perFrameOrtho->Update(&orthoMatrix, 0, sizeof(glm::mat4), ctx); - perFrameOrtho->Bind(Render::PipelineStage::Vertex, 1, ctx); - - switch (curDebugMode) { - case DisplayMode::None: - break; - - case DisplayMode::Graphs: { - const auto worldPos = GetCurrentCameraTargetWorldPosition(currentFocusObject, CorrectedPlayerCamera::GetSingleton()); - worldPosTargetGraph->AddPoint(0, lastPosition.world.x); - worldPosTargetGraph->AddPoint(1, lastPosition.world.y); - worldPosTargetGraph->AddPoint(2, lastPosition.world.z); - - localSpaceGraph->AddPoint(0, lastPosition.local.x); - localSpaceGraph->AddPoint(1, lastPosition.local.y); - localSpaceGraph->AddPoint(2, lastPosition.local.z); - - offsetPosGraph->AddPoint(0, offsetState.position.x); - offsetPosGraph->AddPoint(1, offsetState.position.y); - offsetPosGraph->AddPoint(2, offsetState.position.z); - - const auto ofsPos = GetCurrentCameraOffset(*g_thePlayer, CorrectedPlayerCamera::GetSingleton()); - offsetTargetPosGraph->AddPoint(0, ofsPos.x); - offsetTargetPosGraph->AddPoint(1, ofsPos.y); - offsetTargetPosGraph->AddPoint(2, ofsPos.z); - - rotationGraph->AddPoint(0, rotation.euler.x); - rotationGraph->AddPoint(1, rotation.euler.y); - - if (GameState::IsThirdPerson(currentFocusObject, CorrectedPlayerCamera::GetSingleton())) { - auto tps = reinterpret_cast(CorrectedPlayerCamera::GetSingleton()->cameraState); - tpsRotationGraph->AddPoint(0, tps->yaw1); - tpsRotationGraph->AddPoint(1, tps->yaw2); - tpsRotationGraph->AddPoint(2, tps->yaw); - tpsRotationGraph->AddPoint(3, CorrectedPlayerCamera::GetSingleton()->lookYaw); - } - - computeTimeGraph->AddPoint(0, lastProfSnap); - computeTimeGraph->AddPoint(1, static_cast(GameTime::GetFrameDelta())); - - worldPosTargetGraph->Draw(ctx); - localSpaceGraph->Draw(ctx); - offsetPosGraph->Draw(ctx); - offsetTargetPosGraph->Draw(ctx); - rotationGraph->Draw(ctx); - tpsRotationGraph->Draw(ctx); - computeTimeGraph->Draw(ctx); - - break; - } - case DisplayMode::NodeTree: { - refTreeDisplay->SetPosition(0, 0); - refTreeDisplay->SetSize(size.x, size.y); - if (currentFocusObject->loadedState->node) - refTreeDisplay->Draw(ctx, currentFocusObject->loadedState->node); - break; - } - case DisplayMode::StateOverlay: { - stateOverlay->SetPosition((size.x / 2) - 300, size.y - 600); - stateOverlay->SetSize(600, 400); - stateOverlay->Draw(offsetState.currentGroup, ctx); - break; - } - } -#endif + if (activeCamera) + activeCamera->Render(ctx); } // Use this method to snatch modifications done by mods that run after us // Called before the internal game method runs which will overwrite most of that -bool Camera::SmoothCamera::PreGameUpdate(PlayerCharacter* player, CorrectedPlayerCamera* camera, +bool Camera::Camera::PreGameUpdate(PlayerCharacter* player, CorrectedPlayerCamera* camera, BSTSmartPointer& nextState) { - const auto state = GameState::GetCameraState(player, camera); - if (state == GameState::CameraState::Transitioning) { - auto cstate = reinterpret_cast( - camera->cameraStates[CorrectedPlayerCamera::kCameraState_Transition] - ); - auto horse = camera->cameraStates[CorrectedPlayerCamera::kCameraState_Horse]; - auto tps = camera->cameraStates[CorrectedPlayerCamera::kCameraState_ThirdPerson2]; - if (cstate->fromState == horse && cstate->toState == tps) { - if (!(config->compatIFPV && GameState::IFPV_InFirstPersonState(player, camera)) && - !(config->compatIC && GameState::IC_InFirstPersonState(player, camera))) - { - // Getting off a horse, MURDER THIS STATE - // @TODO: Write our own transition for this state - It still looks nicer with it disabled anyways though - if (nextState.ptr) { - InterlockedDecrement(&nextState.ptr->refCount.m_refCount); - } - - InterlockedIncrement(&cstate->toState->refCount.m_refCount); - nextState.ptr = cstate->toState; - - auto tpsState = reinterpret_cast( - camera->cameraStates[CorrectedPlayerCamera::kCameraState_ThirdPerson2] - ); - - auto horseState = reinterpret_cast( - camera->cameraStates[CorrectedPlayerCamera::kCameraState_Horse] - ); - - // Just copy a whole wack ton of stuff - tpsState->offsetVector = horseState->offsetVector; - tpsState->translation = horseState->translation; - tpsState->zoom = horseState->zoom; - tpsState->cameraZoom = horseState->cameraZoom; - tpsState->cameraLastZoom = horseState->cameraLastZoom; - tpsState->fOverShoulderPosX = horseState->fOverShoulderPosX; - tpsState->fOverShoulderCombatAddY = horseState->fOverShoulderCombatAddY; - tpsState->fOverShoulderPosZ = horseState->fOverShoulderPosZ; - tpsState->controllerNode->m_worldTransform = tpsState->controllerNode->m_oldWorldTransform; - - currentPosition.SetWorldPosition(tpsState->cameraNode->m_worldTransform.pos); - currentPosition.SetLocalPosition(glm::vec3{ 0.0f, 0.0f, 0.0f }); - lastPosition = currentPosition; - - return true; - } + // Force the camera to a new state + if (wantNewCameraState) { + auto wantState = camera->cameraStates[wantNewState]; + if (wantState && camera->cameraState != wantState) { + if (nextState.ptr) + InterlockedDecrement(&nextState.ptr->refCount.m_refCount); + InterlockedIncrement(&wantState->refCount.m_refCount); + nextState.ptr = wantState; } + wantNewCameraState = false; + return true; } + if (activeCamera && activeCamera->OnPreGameUpdate(player, camera, nextState)) + return true; + // Store the last actual position the game used for rendering cameraNi = GetNiCamera(camera); if (cameraNi) { @@ -1165,15 +358,16 @@ bool Camera::SmoothCamera::PreGameUpdate(PlayerCharacter* player, CorrectedPlaye } // Selects the correct update method and positions the camera -void Camera::SmoothCamera::UpdateCamera(PlayerCharacter* player, CorrectedPlayerCamera* camera, - BSTSmartPointer& nextState) { -#ifdef WITH_CHARTS - Profiler prof; -#endif - - // Invalidate while we are in a loading screen state - if (InLoadingScreen()) - crosshair->InvalidateEnablementCache(); +void Camera::Camera::UpdateCamera(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState) +{ + // Check if the user has turned the camera off + if (config->modDisabled) { + if (ranLastFrame) + cameraThird->GetCrosshairManager()->Reset(); + ranLastFrame = false; + return; + } // If we don't have an NiCamera, something else is likely very wrong if (!cameraNi) return; @@ -1183,153 +377,47 @@ void Camera::SmoothCamera::UpdateCamera(PlayerCharacter* player, CorrectedPlayer // We can pass the naked pointer around for the lifetime of the update function safely, // up until refHandle's destructor call currentFocusObject = refHandle.get(); - if (!currentFocusObject) { - lastPosition.SetWorldPosition(gameLastActualPosition); - currentPosition.SetWorldPosition(gameLastActualPosition); + if (!currentFocusObject) { cameraNi = nullptr; return; } - // Update our current crosshair selection if required - if (Render::HasContext() && config->useWorldCrosshair) { - crosshair->Set3DCrosshairType(config->worldCrosshairType); - } - - // Update states & effects - // Fetch the active camera state - auto state = UpdateCurrentCameraState(player, camera); + // Update states + const auto state = UpdateCurrentCameraState(player, camera); const auto pov = UpdateCameraPOVState(player, camera); const auto actionState = UpdateCurrentCameraActionState(player, camera); - offsetState.currentGroup = GetOffsetForState(actionState); - const auto currentOffset = GetCurrentCameraOffset(player, camera); - const auto curTime = GameTime::CurTime(); - - // Don't continue to update transition states if we aren't running update code - bool shouldRun = false; - switch (state) { - case GameState::CameraState::ThirdPerson: - case GameState::CameraState::ThirdPersonCombat: - case GameState::CameraState::Horseback: - shouldRun = true; - break; - default: break; - } - - // And set our current reference position - if (wasDialogMenuOpen && config->compatACC) { - offsetTransitionState.currentPosition = offsetTransitionState.lastPosition = { - currentOffset.x, currentOffset.z - }; - zoomTransitionState.currentPosition = zoomTransitionState.lastPosition = currentOffset.y; - fovTransitionState.currentPosition = fovTransitionState.lastPosition = currentOffset.w; - - GetCameraGoalPosition(camera, currentPosition.world, currentPosition.local); - lastPosition = currentPosition; - } else { - currentPosition.SetRef(currentFocusObject->pos, currentFocusObject->rot); - } // Set our frustum frustum = cameraNi->m_frustum; - // Perform a bit of setup to smooth out camera loading - if (!firstFrame) { - GetCameraGoalPosition(camera, currentPosition.world, currentPosition.local); - lastPosition = currentPosition; - firstFrame = true; - } - - // Save our last position - lastPosition = currentPosition; - - if (!shouldRun || (config->compatACC && dialogMenuOpen)) { - SetFOVOffset(0.0f); - - } else { - // Update transition states - mmath::UpdateTransitionState( - curTime, - config->enableOffsetInterpolation, - config->offsetInterpDurationSecs, - config->offsetScalar, - offsetTransitionState, - { currentOffset.x, currentOffset.z } - ); - - if (!povWasPressed) { - mmath::UpdateTransitionState( - curTime, - config->enableZoomInterpolation, - config->zoomInterpDurationSecs, - config->zoomScalar, - zoomTransitionState, - currentOffset.y - ); - } else { - zoomTransitionState.lastPosition = zoomTransitionState.currentPosition = - zoomTransitionState.targetPosition = currentOffset.y; - } - - mmath::UpdateTransitionState( - curTime, - config->enableFOVInterpolation, - config->fovInterpDurationSecs, - config->fovScalar, - fovTransitionState, - currentOffset.w - ); - - offsetState.position = { - offsetTransitionState.currentPosition.x, - zoomTransitionState.currentPosition, - offsetTransitionState.currentPosition.y - }; - offsetState.fov = fovTransitionState.currentPosition; - SetFOVOffset(offsetState.fov); - - // Now run the active camera state - switch (state) { - case GameState::CameraState::ThirdPerson: { - UpdateInternalRotation(camera); - cameraStates.at(static_cast(GameState::CameraState::ThirdPerson))->Update(player, currentFocusObject, camera); - break; - } - case GameState::CameraState::ThirdPersonCombat: { - UpdateInternalRotation(camera); - cameraStates.at(static_cast(GameState::CameraState::ThirdPersonCombat))->Update(player, currentFocusObject, camera); - break; - } - case GameState::CameraState::Horseback: { - UpdateInternalRotation(camera); - cameraStates.at(static_cast(GameState::CameraState::Horseback))->Update(player, currentFocusObject, camera); - break; - } + // Now based on POV state, select the active camera + ICamera* nextCamera = nullptr; + if (camera->cameraState == camera->cameraStates[CorrectedPlayerCamera::kCameraState_FirstPerson] || + (config->compatIC && GameState::IC_InFirstPersonState(player, camera)) || + (config->compatIFPV && GameState::IFPV_InFirstPersonState(player, camera))) + { + nextCamera = cameraFirst.get(); + // Only query TrackIR in first person mode +#ifdef DEVELOPER + if (TrackIR::IsRunning()) + trackIRData = TrackIR::GetTrackingData(); +#endif + } else if (pov) + nextCamera = cameraThird.get(); - // Here just for my own reference that these are unused (for now) - case GameState::CameraState::FirstPerson: - case GameState::CameraState::KillMove: - case GameState::CameraState::Tweening: - case GameState::CameraState::Transitioning: - case GameState::CameraState::UsingObject: - case GameState::CameraState::Vanity: - case GameState::CameraState::Free: - case GameState::CameraState::IronSights: - case GameState::CameraState::Furniture: - case GameState::CameraState::Bleedout: - case GameState::CameraState::Dragon: - case GameState::CameraState::Unknown: - default: { - break; - } - } + if (activeCamera != nextCamera) { + if (activeCamera) activeCamera->OnEnd(player, camera, nextCamera); + if (nextCamera) + nextCamera->OnBegin(player, camera, activeCamera); + activeCamera = nextCamera; } - povWasPressed = false; - wasDialogMenuOpen = false; - cameraNi = nullptr; + // Update the camera + if (activeCamera) + activeCamera->OnUpdateCamera(player, camera, nextState); -#ifdef WITH_CHARTS - lastProfSnap = prof.Snap(); -#endif + cameraNi = nullptr; + wasLoading = false; + ranLastFrame = true; } \ No newline at end of file diff --git a/SmoothCam/source/camera_states/base_first.cpp b/SmoothCam/source/camera_states/base_first.cpp new file mode 100644 index 0000000..80facc3 --- /dev/null +++ b/SmoothCam/source/camera_states/base_first.cpp @@ -0,0 +1 @@ +// TODO... maybe \ No newline at end of file diff --git a/SmoothCam/source/camera_state.cpp b/SmoothCam/source/camera_states/base_third.cpp similarity index 71% rename from SmoothCam/source/camera_state.cpp rename to SmoothCam/source/camera_states/base_third.cpp index ae53184..cade023 100644 --- a/SmoothCam/source/camera_state.cpp +++ b/SmoothCam/source/camera_states/base_third.cpp @@ -1,40 +1,45 @@ -#include "camera_state.h" +#include "camera_states/base_third.h" #include "camera.h" +#include "thirdperson.h" #include "raycast.h" #include "crosshair.h" -Camera::State::BaseCameraState::BaseCameraState(Camera::SmoothCamera* camera) noexcept : camera(camera) {} +Camera::State::BaseThird::BaseThird(Thirdperson* camera) noexcept : camera(camera) {} -Camera::State::BaseCameraState::~BaseCameraState() {} +Camera::State::BaseThird::~BaseThird() {} -void Camera::State::BaseCameraState::StateHandOff(BaseCameraState* nextState) const noexcept { +void Camera::State::BaseThird::StateHandOff(BaseThird* nextState) const noexcept { if (!nextState) return; nextState->interpSmoother = interpSmoother; nextState->wasInterpLastFrame = wasInterpLastFrame; } // Returns the current camera state -GameState::CameraState Camera::State::BaseCameraState::GetCameraState() const noexcept { - return camera->currentState; +GameState::CameraState Camera::State::BaseThird::GetCameraState() const noexcept { + return camera->m_camera->GetCurrentCameraState(); } // Returns the current camera action state -Camera::CameraActionState Camera::State::BaseCameraState::GetCameraActionState() const noexcept { - return camera->currentActionState; +Camera::CameraActionState Camera::State::BaseThird::GetCameraActionState() const noexcept { + return camera->m_camera->GetCurrentCameraActionState(); } // Get the last camera position -mmath::Position& Camera::State::BaseCameraState::GetLastCameraPosition() const noexcept { +mmath::Position& Camera::State::BaseThird::GetLastCameraPosition() const noexcept { return camera->lastPosition; } // Get the current camera position -mmath::Position& Camera::State::BaseCameraState::GetCameraPosition() const noexcept { +mmath::Position& Camera::State::BaseThird::GetCameraPosition() const noexcept { return camera->currentPosition; } +const NiFrustum& Camera::State::BaseThird::GetFrustum() const noexcept { + return camera->m_camera->GetFrustum(); +} + // Sets the camera position -void Camera::State::BaseCameraState::SetCameraPosition(const glm::vec3& pos, const PlayerCharacter* player, +void Camera::State::BaseThird::SetCameraPosition(const glm::vec3& pos, const PlayerCharacter* player, const CorrectedPlayerCamera* playerCamera) noexcept { camera->SetPosition(pos, playerCamera); @@ -42,7 +47,7 @@ void Camera::State::BaseCameraState::SetCameraPosition(const glm::vec3& pos, con } // Performs a ray cast and returns a new position based on the result -glm::vec3 Camera::State::BaseCameraState::ComputeRaycast(const glm::vec3& rayStart, const glm::vec3& rayEnd) { +glm::vec3 Camera::State::BaseThird::ComputeRaycast(const glm::vec3& rayStart, const glm::vec3& rayEnd) { constexpr float hullSize = 15.0f; const auto rayStart4 = glm::vec4(rayStart.x, rayStart.y, rayStart.z, 0.0f); const auto rayEnd4 = glm::vec4(rayEnd.x, rayEnd.y, rayEnd.z, 0.0f); @@ -53,11 +58,11 @@ glm::vec3 Camera::State::BaseCameraState::ComputeRaycast(const glm::vec3& raySta } // Clamps the camera position based on offset clamp settings -glm::vec3 Camera::State::BaseCameraState::ComputeOffsetClamping(const TESObjectREFR* ref, +glm::vec3 Camera::State::BaseThird::ComputeOffsetClamping(const TESObjectREFR* ref, const glm::vec3& cameraLocalOffset, const glm::vec3& cameraWorldTarget, const glm::vec3& cameraPosition) const { // This is the position we ideally want to be in, before interpolation - const auto expectedPosition = cameraWorldTarget + static_cast(cameraLocalOffset); + const auto expectedPosition = cameraWorldTarget + cameraLocalOffset; // This extracts the interpolation vector from the camera position const auto interpVector = cameraPosition - expectedPosition; @@ -65,11 +70,11 @@ glm::vec3 Camera::State::BaseCameraState::ComputeOffsetClamping(const TESObjectR glm::vec3 forward, right, up, coef; mmath::DecomposeToBasis( interpVector, - { ref->rot.x, ref->rot.y, ref->rot.z }, + { camera->GetCameraRotation().euler.x, ref->rot.y, camera->GetCameraRotation().euler.y }, forward, right, up, coef ); - const auto& [mins, maxs] = camera->GetDistanceClamping(); + auto& [mins, maxs] = camera->GetDistanceClamping(); // Now we can do whatever we want to the interp vector in axis aligned space if (camera->config->cameraDistanceClampXEnable) @@ -83,13 +88,13 @@ glm::vec3 Camera::State::BaseCameraState::ComputeOffsetClamping(const TESObjectR return (forward * coef.x) + (right * coef.y) + (up * coef.z) + expectedPosition; } -glm::vec3 Camera::State::BaseCameraState::ComputeOffsetClamping(const TESObjectREFR* ref, const glm::vec3& cameraWorldTarget, +glm::vec3 Camera::State::BaseThird::ComputeOffsetClamping(const TESObjectREFR* ref, const glm::vec3& cameraWorldTarget, const glm::vec3& cameraPosition) const { glm::vec3 forward, right, up, coef; mmath::DecomposeToBasis( cameraPosition - cameraWorldTarget, - { ref->rot.x, ref->rot.y, ref->rot.z }, + { camera->GetCameraRotation().euler.x, ref->rot.y, camera->GetCameraRotation().euler.y }, forward, right, up, coef ); @@ -104,11 +109,13 @@ glm::vec3 Camera::State::BaseCameraState::ComputeOffsetClamping(const TESObjectR coef.z = glm::clamp(coef.z, mins.z, maxs.z); // Now recompose with the new coefficients and add back the world position - return (forward * coef.x) + (right * coef.y) + (up * coef.z) + cameraWorldTarget; + auto mod = (forward * coef.x) + (right * coef.y) + (up * coef.z) + cameraWorldTarget; + auto d = glm::distance(mod, cameraPosition); + return mod; } -void Camera::State::BaseCameraState::UpdateCrosshair(const PlayerCharacter* player, const CorrectedPlayerCamera* playerCamera) const { - if (camera->InLoadingScreen()) return; +void Camera::State::BaseThird::UpdateCrosshair(const PlayerCharacter* player, const CorrectedPlayerCamera* playerCamera) const { + if (camera->m_camera->InLoadingScreen()) return; auto use3D = false; if (GameState::IsRangedWeaponDrawn(player)) { @@ -141,32 +148,32 @@ void Camera::State::BaseCameraState::UpdateCrosshair(const PlayerCharacter* play } // Updates the 3D crosshair position, performing all logic internally -void Camera::State::BaseCameraState::UpdateCrosshairPosition(const PlayerCharacter* player, const CorrectedPlayerCamera* playerCamera) const { - if (camera->InLoadingScreen()) return; +void Camera::State::BaseThird::UpdateCrosshairPosition(const PlayerCharacter* player, const CorrectedPlayerCamera* playerCamera) const { + if (camera->m_camera->InLoadingScreen()) return; camera->crosshair->UpdateCrosshairPosition( player, playerCamera, camera->GetAimRotation(camera->currentFocusObject, playerCamera), - camera->worldToScaleform + camera->m_camera->worldToScaleform ); } // Returns the euler rotation of the camera -mmath::Rotation& Camera::State::BaseCameraState::GetCameraRotation() const noexcept { +mmath::Rotation& Camera::State::BaseThird::GetCameraRotation() const noexcept { return camera->rotation; } // Returns the local offsets to apply to the camera -glm::vec3 Camera::State::BaseCameraState::GetCameraOffsetStatePosition() const noexcept { +glm::vec3 Camera::State::BaseThird::GetCameraOffsetStatePosition() const noexcept { return camera->offsetState.position; } // Returns the world position to apply local offsets to -glm::vec3 Camera::State::BaseCameraState::GetCameraWorldPosition(const TESObjectREFR* ref, const CorrectedPlayerCamera* playerCamera) const { +glm::vec3 Camera::State::BaseThird::GetCameraWorldPosition(const TESObjectREFR* ref, const CorrectedPlayerCamera* playerCamera) const { return camera->GetCurrentCameraTargetWorldPosition(ref, playerCamera); } // Performs all camera offset math using the view rotation matrix and local offsets, returns a local position -glm::vec3 Camera::State::BaseCameraState::GetTransformedCameraLocalPosition() const { +glm::vec3 Camera::State::BaseThird::GetTransformedCameraLocalPosition() const { const auto cameraLocal = GetCameraOffsetStatePosition(); auto translated = camera->rotation.ToRotationMatrix() * glm::vec4( cameraLocal.x, @@ -179,13 +186,7 @@ glm::vec3 Camera::State::BaseCameraState::GetTransformedCameraLocalPosition() co } // Interpolates the given position -glm::vec3 Camera::State::BaseCameraState::UpdateInterpolatedLocalPosition(const glm::vec3& rot) { - if (camera->currentPosition.local == glm::vec3(0.0f)) { - // Store the first valid position for future interpolation - camera->currentPosition.SetLocalPosition(rot); - return rot; - } - +glm::vec3 Camera::State::BaseThird::UpdateInterpolatedLocalPosition(const glm::vec3& rot) { const auto pos = mmath::Interpolate( camera->lastPosition.local, rot, camera->GetCurrentSmoothingScalar( @@ -198,7 +199,7 @@ glm::vec3 Camera::State::BaseCameraState::UpdateInterpolatedLocalPosition(const } // Interpolates the given position, stores last interpolated position -glm::vec3 Camera::State::BaseCameraState::UpdateInterpolatedWorldPosition(const PlayerCharacter* player, const glm::vec3& fromPos, +glm::vec3 Camera::State::BaseThird::UpdateInterpolatedWorldPosition(const PlayerCharacter* player, const glm::vec3& fromPos, const glm::vec3& pos, const float distance) { if (!GetConfig()->enableInterp) { @@ -259,7 +260,7 @@ glm::vec3 Camera::State::BaseCameraState::UpdateInterpolatedWorldPosition(const } } -void Camera::State::BaseCameraState::ApplyLocalSpaceGameOffsets(const PlayerCharacter* player, const CorrectedPlayerCamera* playerCamera) { +void Camera::State::BaseThird::ApplyLocalSpaceGameOffsets(const PlayerCharacter* player, const CorrectedPlayerCamera* playerCamera) { auto state = reinterpret_cast(playerCamera->cameraState); const auto& euler = camera->rotation.euler; @@ -290,11 +291,11 @@ void Camera::State::BaseCameraState::ApplyLocalSpaceGameOffsets(const PlayerChar } // Returns true if the player is moving -bool Camera::State::BaseCameraState::IsPlayerMoving(const PlayerCharacter* player) const noexcept { +bool Camera::State::BaseThird::IsPlayerMoving(const PlayerCharacter* player) const noexcept { return (GameState::IsWalking(player) || GameState::IsRunning(player) || GameState::IsSprinting(player)); } // Returns the user config -const Config::UserConfig* const Camera::State::BaseCameraState::GetConfig() const noexcept { +const Config::UserConfig* const Camera::State::BaseThird::GetConfig() const noexcept { return camera->config; } \ No newline at end of file diff --git a/SmoothCam/source/camera_states/thirdperson.cpp b/SmoothCam/source/camera_states/thirdperson/thirdperson.cpp similarity index 50% rename from SmoothCam/source/camera_states/thirdperson.cpp rename to SmoothCam/source/camera_states/thirdperson/thirdperson.cpp index 5d75c43..9672ef4 100644 --- a/SmoothCam/source/camera_states/thirdperson.cpp +++ b/SmoothCam/source/camera_states/thirdperson/thirdperson.cpp @@ -1,35 +1,32 @@ -#include "camera_states/thirdperson.h" -#include "camera.h" +#include "camera_states/thirdperson/thirdperson.h" +#include "thirdperson.h" /* * The base thirdperson camera logic - used when no weapon is drawn. */ +Camera::State::ThirdpersonState::ThirdpersonState(Thirdperson* camera) noexcept : BaseThird(camera) {} -Camera::State::ThirdpersonState::ThirdpersonState(Camera::SmoothCamera* camera) noexcept : BaseCameraState(camera) { - -} - -void Camera::State::ThirdpersonState::OnBegin(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* fromState) +void Camera::State::ThirdpersonState::OnBegin(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* fromState) noexcept { } -void Camera::State::ThirdpersonState::OnEnd(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* nextState) +void Camera::State::ThirdpersonState::OnEnd(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* nextState) noexcept { StateHandOff(nextState); } -void Camera::State::ThirdpersonState::Update(PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera) { +void Camera::State::ThirdpersonState::Update(PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera) + noexcept +{ // Get our computed local-space xyz offset. const auto cameraLocal = GetCameraOffsetStatePosition(); // Get the base world position for the camera which we will offset with the local-space values. const auto worldTarget = GetCameraWorldPosition(cameraRef, camera); // Transform the camera offsets based on the computed view matrix const auto transformedLocalPos = GetTransformedCameraLocalPosition(); - // Define the starting point for our raycast - const auto start = worldTarget + glm::vec3(0.0f, 0.0f, cameraLocal.z); glm::vec3 preFinalPos; if (GetConfig()->separateLocalInterp) { @@ -48,7 +45,7 @@ void Camera::State::ThirdpersonState::Update(PlayerCharacter* player, const TESO // Compute offset clamping if enabled preFinalPos = ComputeOffsetClamping(cameraRef, worldTarget, lerpedWorldPos) + loc; - + } else { // Combined case @@ -65,10 +62,40 @@ void Camera::State::ThirdpersonState::Update(PlayerCharacter* player, const TESO preFinalPos = ComputeOffsetClamping(cameraRef, transformedLocalPos, worldTarget, lerpedWorldPos); } - // Cast our ray and update the camera position - const auto finalPos = ComputeRaycast(start, preFinalPos); - // Set the position + glm::vec3 f, s, u, coef; + mmath::DecomposeToBasis( + preFinalPos, + { GetCameraRotation().euler.x, 0.0f, GetCameraRotation().euler.y }, + f, s, u, coef + ); + + // Cast from the player out towards the X offset, rotated + // The end position of that ray becomes the origin for our primary distance ray + // for camera collision. + // Doing this ensures the origin of our collision ray is never occluded - We just + // need to make sure the hull size for our origin test ray is larger than the primary + // collision ray. + constexpr auto hullSize = 30.0f; + + // Ray origin from the player, plus Z offset + const auto playerOrigin = worldTarget + glm::vec3(0.0f, 0.0f, cameraLocal.z); + // Towards the +X offset + auto rayXOrigin = playerOrigin + (f * cameraLocal.x); + const auto resultPtoX = Raycast::CastRay(glm::vec4(playerOrigin, 0.0f), glm::vec4(rayXOrigin, 0.0f), hullSize); + + // And if we hit, that hit position becomes the new origin for the next ray + if (resultPtoX.hit) + rayXOrigin = resultPtoX.hitPos + (resultPtoX.rayNormal * glm::min(resultPtoX.rayLength, hullSize)); + + // Otherwise, we just cast from +X offset like normal as nothing is overlapping + // Cast from origin towards the camera, Get back final position + const auto finalPos = ComputeRaycast(rayXOrigin, preFinalPos); + + // Set the position to the ray hit position + // This sets the rendering position of the camera, and applies game offsets SetCameraPosition(finalPos, player, camera); + // And store our world position BEFORE collision + GetCameraPosition().SetWorldPosition(preFinalPos); // Update crosshair visibility UpdateCrosshair(player, camera); } \ No newline at end of file diff --git a/SmoothCam/source/camera_states/thirdperson/thirdperson_combat.cpp b/SmoothCam/source/camera_states/thirdperson/thirdperson_combat.cpp new file mode 100644 index 0000000..6b4fa2c --- /dev/null +++ b/SmoothCam/source/camera_states/thirdperson/thirdperson_combat.cpp @@ -0,0 +1,100 @@ +#include "camera_states/thirdperson/thirdperson_combat.h" +#include "thirdperson.h" + +Camera::State::ThirdpersonCombatState::ThirdpersonCombatState(Thirdperson* camera) noexcept : BaseThird(camera) { + +} + +void Camera::State::ThirdpersonCombatState::OnBegin(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* fromState) noexcept +{ + +} + +void Camera::State::ThirdpersonCombatState::OnEnd(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* nextState) noexcept +{ + StateHandOff(nextState); +} + +void Camera::State::ThirdpersonCombatState::Update(PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera) + noexcept +{ + // Get our computed local-space xyz offset. + const auto cameraLocal = GetCameraOffsetStatePosition(); + // Get the base world position for the camera which we will offset with the local-space values. + const auto worldTarget = GetCameraWorldPosition(cameraRef, camera); + // Transform the camera offsets based on the computed view matrix + const auto transformedLocalPos = GetTransformedCameraLocalPosition(); + + glm::vec3 preFinalPos; + if (GetConfig()->separateLocalInterp) { + // Handle separate local-space interpolation + const auto loc = UpdateInterpolatedLocalPosition(transformedLocalPos); + + const auto& last = GetLastCameraPosition(); + // Last offset position from ref + const auto lastWorld = last.world - last.local; + + // And the world target + const auto lerpedWorldPos = UpdateInterpolatedWorldPosition( + player, lastWorld, worldTarget, + glm::length(lastWorld - worldTarget) + ); + + // Compute offset clamping if enabled + preFinalPos = ComputeOffsetClamping(cameraRef, worldTarget, lerpedWorldPos) + loc; + + } else { + // Combined case + + // Add the final local space transformation to the player postion + const auto targetWorldPos = worldTarget + transformedLocalPos; + + // Now lerp it based on camera distance to player position + const auto lerpedWorldPos = UpdateInterpolatedWorldPosition( + player, GetLastCameraPosition().world, targetWorldPos, + glm::length(targetWorldPos - worldTarget) + ); + + // Compute offset clamping if enabled + preFinalPos = ComputeOffsetClamping(cameraRef, transformedLocalPos, worldTarget, lerpedWorldPos); + } + + glm::vec3 f, s, u, coef; + mmath::DecomposeToBasis( + preFinalPos, + { GetCameraRotation().euler.x, 0.0f, GetCameraRotation().euler.y }, + f, s, u, coef + ); + + // Cast from the player out towards the X offset, rotated + // The end position of that ray becomes the origin for our primary distance ray + // for camera collision. + // Doing this ensures the origin of our collision ray is never occluded - We just + // need to make sure the hull size for our origin test ray is larger than the primary + // collision ray. + constexpr auto hullSize = 30.0f; + + // Ray origin from the player, plus Z offset + const auto playerOrigin = worldTarget + glm::vec3(0.0f, 0.0f, cameraLocal.z); + // Towards the +X offset + auto rayXOrigin = playerOrigin + (f * cameraLocal.x); + const auto resultPtoX = Raycast::CastRay(glm::vec4(playerOrigin, 0.0f), glm::vec4(rayXOrigin, 0.0f), hullSize); + + // And if we hit, that hit position becomes the new origin for the next ray + if (resultPtoX.hit) + rayXOrigin = resultPtoX.hitPos + (resultPtoX.rayNormal * glm::min(resultPtoX.rayLength, hullSize)); + + // Otherwise, we just cast from +X offset like normal as nothing is overlapping + // Cast from origin towards the camera, Get back final position + const auto finalPos = ComputeRaycast(rayXOrigin, preFinalPos); + + // Set the position to the ray hit position + // This sets the rendering position of the camera, and applies game offsets + SetCameraPosition(finalPos, player, camera); + // And store our world position BEFORE collision + GetCameraPosition().SetWorldPosition(preFinalPos); + // Update crosshair visibility + UpdateCrosshair(player, camera); +} \ No newline at end of file diff --git a/SmoothCam/source/camera_states/thirdperson_horse.cpp b/SmoothCam/source/camera_states/thirdperson/thirdperson_horse.cpp similarity index 52% rename from SmoothCam/source/camera_states/thirdperson_horse.cpp rename to SmoothCam/source/camera_states/thirdperson/thirdperson_horse.cpp index 2644a1d..21578d1 100644 --- a/SmoothCam/source/camera_states/thirdperson_horse.cpp +++ b/SmoothCam/source/camera_states/thirdperson/thirdperson_horse.cpp @@ -1,23 +1,25 @@ -#include "camera_states/thirdperson_horse.h" -#include "camera.h" +#include "camera_states/thirdperson/thirdperson_horse.h" +#include "thirdperson.h" -Camera::State::ThirdpersonHorseState::ThirdpersonHorseState(Camera::SmoothCamera* camera) noexcept : BaseCameraState(camera) { +Camera::State::ThirdpersonHorseState::ThirdpersonHorseState(Thirdperson* camera) noexcept : BaseThird(camera) { } -void Camera::State::ThirdpersonHorseState::OnBegin(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* fromState) +void Camera::State::ThirdpersonHorseState::OnBegin(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* fromState) noexcept { } -void Camera::State::ThirdpersonHorseState::OnEnd(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* nextState) +void Camera::State::ThirdpersonHorseState::OnEnd(const PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera, + BaseThird* nextState) noexcept { StateHandOff(nextState); } -void Camera::State::ThirdpersonHorseState::Update(PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera) { +void Camera::State::ThirdpersonHorseState::Update(PlayerCharacter* player, const Actor* cameraRef, const CorrectedPlayerCamera* camera) + noexcept +{ // Get our computed local-space xyz offset. const auto cameraLocal = GetCameraOffsetStatePosition(); // Get the base world position for the camera which we will offset with the local-space values. @@ -61,10 +63,38 @@ void Camera::State::ThirdpersonHorseState::Update(PlayerCharacter* player, const preFinalPos = ComputeOffsetClamping(cameraRef, transformedLocalPos, worldTarget, lerpedWorldPos); } - // Cast our ray and update the camera position - const auto finalPos = ComputeRaycast(start, preFinalPos); + glm::vec3 f, s, u, coef; + mmath::DecomposeToBasis( + preFinalPos, + { GetCameraRotation().euler.x, 0.0f, GetCameraRotation().euler.y }, + f, s, u, coef + ); + + // Cast from the player out towards the X offset, rotated + // The end position of that ray becomes the origin for our primary distance ray + // for camera collision. + // Doing this ensures the origin of our collision ray is never occluded - We just + // need to make sure the hull size for our origin test ray is larger than the primary + // collision ray. + constexpr auto hullSize = 30.0f; + + // Ray origin from the player, plus Z offset + const auto playerOrigin = worldTarget + glm::vec3(0.0f, 0.0f, cameraLocal.z); + // Towards the +X offset + auto rayXOrigin = playerOrigin + (f * cameraLocal.x); + const auto resultPtoX = Raycast::CastRay(glm::vec4(playerOrigin, 0.0f), glm::vec4(rayXOrigin, 0.0f), hullSize); + + // And if we hit, that hit position becomes the new origin for the next ray + if (resultPtoX.hit) + rayXOrigin = resultPtoX.hitPos + (resultPtoX.rayNormal * glm::min(resultPtoX.rayLength, hullSize)); + + // Otherwise, we just cast from +X offset like normal as nothing is overlapping + // Cast from origin towards the camera, Get back final position + const auto finalPos = ComputeRaycast(rayXOrigin, preFinalPos); + // Set the position SetCameraPosition(finalPos, player, camera); + GetCameraPosition().SetWorldPosition(preFinalPos); // Update crosshair visibility UpdateCrosshair(player, camera); } \ No newline at end of file diff --git a/SmoothCam/source/camera_states/thirdperson_combat.cpp b/SmoothCam/source/camera_states/thirdperson_combat.cpp deleted file mode 100644 index 71cd7d6..0000000 --- a/SmoothCam/source/camera_states/thirdperson_combat.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "camera_states/thirdperson_combat.h" -#include "camera.h" - -Camera::State::ThirdpersonCombatState::ThirdpersonCombatState(Camera::SmoothCamera* camera) noexcept : BaseCameraState(camera) { - -} - -void Camera::State::ThirdpersonCombatState::OnBegin(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* fromState) -{ - -} - -void Camera::State::ThirdpersonCombatState::OnEnd(const PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera, - BaseCameraState* nextState) -{ - StateHandOff(nextState); -} - -void Camera::State::ThirdpersonCombatState::Update(PlayerCharacter* player, const TESObjectREFR* cameraRef, const CorrectedPlayerCamera* camera) { - // Get our computed local-space xyz offset. - const auto cameraLocal = GetCameraOffsetStatePosition(); - // Get the base world position for the camera which we will offset with the local-space values. - const auto worldTarget = GetCameraWorldPosition(cameraRef, camera); - // Transform the camera offsets based on the computed view matrix - const auto transformedLocalPos = GetTransformedCameraLocalPosition(); - // Define the starting point for our raycast - const auto start = worldTarget + glm::vec3(0.0f, 0.0f, cameraLocal.z); - - glm::vec3 preFinalPos; - if (GetConfig()->separateLocalInterp) { - // Handle separate local-space interpolation - const auto loc = UpdateInterpolatedLocalPosition(transformedLocalPos); - - const auto& last = GetLastCameraPosition(); - // Last offset position from ref - const auto lastWorld = last.world - last.local; - - // And the world target - const auto lerpedWorldPos = UpdateInterpolatedWorldPosition( - player, lastWorld, worldTarget, - glm::length(lastWorld - worldTarget) - ); - - // Compute offset clamping if enabled - preFinalPos = ComputeOffsetClamping(cameraRef, worldTarget, lerpedWorldPos) + loc; - - } else { - // Combined case - - // Add the final local space transformation to the player postion - const auto targetWorldPos = worldTarget + transformedLocalPos; - - // Now lerp it based on camera distance to player position - const auto lerpedWorldPos = UpdateInterpolatedWorldPosition( - player, GetLastCameraPosition().world, targetWorldPos, - glm::length(targetWorldPos - worldTarget) - ); - - // Compute offset clamping if enabled - preFinalPos = ComputeOffsetClamping(cameraRef, transformedLocalPos, worldTarget, lerpedWorldPos); - } - - // Cast our ray and update the camera position - const auto finalPos = ComputeRaycast(start, preFinalPos); - // Set the position - SetCameraPosition(finalPos, player, camera); - // Update crosshair visibility - UpdateCrosshair(player, camera); -} \ No newline at end of file diff --git a/SmoothCam/source/config.cpp b/SmoothCam/source/config.cpp index 8aea76e..92a4643 100644 --- a/SmoothCam/source/config.cpp +++ b/SmoothCam/source/config.cpp @@ -1,5 +1,4 @@ Config::UserConfig currentConfig; -Config::GameConfig gameConfig; #define CREATE_JSON_VALUE(obj, member) {#member, obj.member} #define VALUE_FROM_JSON(obj, member) \ @@ -90,6 +89,10 @@ void Config::to_json(json& j, const UserConfig& obj) { CREATE_JSON_VALUE(obj, useWorldCrosshair), CREATE_JSON_VALUE(obj, worldCrosshairDepthTest), CREATE_JSON_VALUE(obj, worldCrosshairType), + CREATE_JSON_VALUE(obj, stealthMeterXOffset), + CREATE_JSON_VALUE(obj, stealthMeterYOffset), + CREATE_JSON_VALUE(obj, offsetStealthMeter), + CREATE_JSON_VALUE(obj, alwaysOffsetStealthMeter), // Arrow prediction CREATE_JSON_VALUE(obj, useArrowPrediction), @@ -99,8 +102,14 @@ void Config::to_json(json& j, const UserConfig& obj) { // Misc CREATE_JSON_VALUE(obj, disableDeltaTime), + CREATE_JSON_VALUE(obj, nextPresetKey), CREATE_JSON_VALUE(obj, shoulderSwapKey), CREATE_JSON_VALUE(obj, swapXClamping), + CREATE_JSON_VALUE(obj, modDisabled), + CREATE_JSON_VALUE(obj, modToggleKey), + CREATE_JSON_VALUE(obj, customZOffset), + CREATE_JSON_VALUE(obj, applyZOffsetKey), + CREATE_JSON_VALUE(obj, enableCrashDumps), // Comapt CREATE_JSON_VALUE(obj, compatACC), @@ -184,6 +193,10 @@ void Config::from_json(const json& j, UserConfig& obj) { VALUE_FROM_JSON(obj, useWorldCrosshair) VALUE_FROM_JSON(obj, worldCrosshairDepthTest) VALUE_FROM_JSON(obj, worldCrosshairType) + VALUE_FROM_JSON(obj, stealthMeterXOffset) + VALUE_FROM_JSON(obj, stealthMeterYOffset) + VALUE_FROM_JSON(obj, offsetStealthMeter) + VALUE_FROM_JSON(obj, alwaysOffsetStealthMeter) // Arrow prediction VALUE_FROM_JSON(obj, useArrowPrediction) @@ -193,8 +206,14 @@ void Config::from_json(const json& j, UserConfig& obj) { // Misc VALUE_FROM_JSON(obj, disableDeltaTime) + VALUE_FROM_JSON(obj, nextPresetKey) VALUE_FROM_JSON(obj, shoulderSwapKey) VALUE_FROM_JSON(obj, swapXClamping) + VALUE_FROM_JSON(obj, modDisabled) + VALUE_FROM_JSON(obj, modToggleKey) + VALUE_FROM_JSON(obj, customZOffset) + VALUE_FROM_JSON(obj, applyZOffsetKey) + VALUE_FROM_JSON(obj, enableCrashDumps) // Compat VALUE_FROM_JSON(obj, compatACC) @@ -325,43 +344,18 @@ void Config::ReadConfigFile() { SaveCurrentConfig(); } - wchar_t path[MAX_PATH]; - if (!SUCCEEDED(SHGetFolderPath(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, path))) { - _WARNING("Failed to locate My Documents folder, using defualt game config values."); - } else { - wchar_t buf[16]; - const auto inipath = std::wstring(path) + L"\\My Games\\Skyrim Special Edition\\Skyrim.ini"; - if (GetPrivateProfileString(L"Combat", L"f3PArrowTiltUpAngle", L"2.5", buf, 16, inipath.c_str()) != 0) { - wchar_t* end; - gameConfig.f3PArrowTiltUpAngle = std::wcstof(buf, &end); - } - - if (GetPrivateProfileString(L"Combat", L"f3PBoltTiltUpAngle", L"2.5", buf, 16, inipath.c_str()) != 0) { - wchar_t* end; - gameConfig.f3PBoltTiltUpAngle = std::wcstof(buf, &end); - } - - if (GetPrivateProfileString(L"Display", L"fNearDistance", L"15.0", buf, 16, inipath.c_str()) != 0) { - wchar_t* end; - gameConfig.fNearDistance = std::wcstof(buf, &end); - } - - if (GetPrivateProfileString(L"Camera", L"fMinCurrentZoom", L"-0.200000003", buf, 16, inipath.c_str()) != 0) { - wchar_t* end; - gameConfig.fMinCurrentZoom = std::wcstof(buf, &end); - } - } - // Load bone data LoadBonePriorities(); - +#ifdef DEVELOPER + LoadEyeBonePriorities(); +#endif currentConfig = cfg; } void Config::SaveCurrentConfig() { std::ofstream os(L"Data/SKSE/Plugins/SmoothCam.json"); Config::json j = currentConfig; - os << j << std::endl; + os << std::setw(4) << j << std::endl; } Config::UserConfig* Config::GetCurrentConfig() noexcept { @@ -382,9 +376,10 @@ BSFixedString Config::SaveConfigAsPreset(int slot, const BSFixedString& name) { p.name = { name.c_str() }; p.config = currentConfig; - std::ofstream os(GetPresetPath(slot)); + const auto path = GetPresetPath(slot); + std::ofstream os(path.c_str()); Config::json j = p; - os << j << std::endl; + os << std::setw(4) << j << std::endl; return { "" }; } @@ -393,7 +388,8 @@ bool Config::LoadPreset(int slot) { if (slot >= MaxPresetSlots) return false; Preset p; - std::ifstream is(GetPresetPath(slot)); + const auto path = GetPresetPath(slot); + std::ifstream is(path.c_str()); if (is.good()) { try { Config::json j; @@ -414,11 +410,12 @@ bool Config::LoadPreset(int slot) { return true; } -Config::LoadStatus Config::LoadPresetName(int slot, std::string& name) { +Config::LoadStatus Config::LoadPresetName(int slot, eastl::string& name) { if (slot >= MaxPresetSlots) return LoadStatus::FAILED; Preset p; - std::ifstream is(GetPresetPath(slot)); + const auto path = GetPresetPath(slot); + std::ifstream is(path.c_str()); if (is.good()) { try { Config::json j; @@ -434,7 +431,7 @@ Config::LoadStatus Config::LoadPresetName(int slot, std::string& name) { return LoadStatus::MISSING; } - name = { p.name }; + name = p.name.c_str(); return LoadStatus::OK; } @@ -442,7 +439,7 @@ BSFixedString Config::GetPresetSlotName(int slot) { if (slot >= MaxPresetSlots) return { "ERROR: Preset index out of range" }; - std::string userName; + eastl::string userName; const auto code = LoadPresetName(slot, userName); if (code == LoadStatus::OK) return { userName.c_str() }; @@ -452,17 +449,13 @@ BSFixedString Config::GetPresetSlotName(int slot) { return { "Empty" }; } -std::wstring Config::GetPresetPath(int slot) { - std::wstring slotName(L"Data/SKSE/Plugins/SmoothCamPreset"); - slotName.append(std::to_wstring(slot)); +eastl::wstring Config::GetPresetPath(int slot) { + eastl::wstring slotName(L"Data/SKSE/Plugins/SmoothCamPreset"); + slotName.append(std::to_wstring(slot).c_str()); slotName.append(L".json"); return slotName; } -const Config::GameConfig* const Config::GetGameConfig() { - return &gameConfig; -} - void trimString(std::string& outStr) { while(outStr.size() && isspace(outStr.front())) outStr.erase(outStr.begin()); @@ -473,29 +466,39 @@ void trimString(std::string& outStr) { static Config::BoneList bonePriorities = {}; void Config::LoadBonePriorities() { - const std::string search = "SmoothCam_FollowBones_"; - for (const auto& v : std::filesystem::directory_iterator("Data/SKSE/Plugins")) { - if (!v.is_regular_file()) continue; - const auto name = v.path().filename().string(); - - if (name.length() < search.length()) continue; - if (search.compare(0, search.length(), name.substr(0, search.length())) == 0) { - std::string path = "Data/SKSE/Plugins/"; - path.append(name); - - std::ifstream ifs(path); - if (!ifs.good()) continue; - - std::string line; - while (std::getline(ifs, line)) { - trimString(line); - if (line.length() == 0) continue; - if (line.rfind("//", 0) == 0) continue; - - bonePriorities.emplace_back(line.c_str()); + // @Issue:35: std::filesystem::directory_iterator throws on unicode paths + const std::wstring path = L"\\\\?\\Data\\SKSE\\Plugins\\SmoothCam_FollowBones_*.txt"; + WIN32_FIND_DATA data; + auto hf = FindFirstFileEx( + path.c_str(), + FINDEX_INFO_LEVELS::FindExInfoStandard, + &data, + FINDEX_SEARCH_OPS::FindExSearchLimitToDirectories, + nullptr, + 0 + ); + + if (hf != INVALID_HANDLE_VALUE) + do { + std::wstring filePath = L"Data/SKSE/Plugins/"; + filePath.append(data.cFileName); + std::ifstream ifs(filePath); + if (ifs.good()) { + std::string line; + while (std::getline(ifs, line)) { + trimString(line); + if (line.length() == 0) continue; + if (line.rfind("//", 0) == 0) continue; + + bonePriorities.emplace_back(line.c_str()); + } } - } - } + + if (!FindNextFile(hf, &data)) { + FindClose(hf); + break; + } + } while (hf != INVALID_HANDLE_VALUE); if (bonePriorities.size() == 0) { WarningPopup(LR"(SmoothCam: Did not find any bone names to follow while loading! Is SmoothCam_FollowBones_Default.txt present in the SKSE plugins directory? @@ -507,4 +510,53 @@ To prevent this warning ensure a bone list file is present with at least 1 bone Config::BoneList& Config::GetBonePriorities() noexcept { return bonePriorities; -} \ No newline at end of file +} + +#ifdef DEVELOPER +static Config::BoneList eyeBonePriorities = {}; +void Config::LoadEyeBonePriorities() { + const std::wstring path = L"\\\\?\\Data\\SKSE\\Plugins\\SmoothCam_EyeBones_*.txt"; + WIN32_FIND_DATA data; + auto hf = FindFirstFileEx( + path.c_str(), + FINDEX_INFO_LEVELS::FindExInfoStandard, + &data, + FINDEX_SEARCH_OPS::FindExSearchLimitToDirectories, + nullptr, + 0 + ); + + if (hf != INVALID_HANDLE_VALUE) + do { + std::wstring filePath = L"Data/SKSE/Plugins/"; + filePath.append(data.cFileName); + std::ifstream ifs(filePath); + if (ifs.good()) { + std::string line; + while (std::getline(ifs, line)) { + trimString(line); + if (line.length() == 0) continue; + if (line.rfind("//", 0) == 0) continue; + + eyeBonePriorities.emplace_back(line.c_str()); + } + } + + if (!FindNextFile(hf, &data)) { + FindClose(hf); + break; + } + } while (hf != INVALID_HANDLE_VALUE); + + if (eyeBonePriorities.size() == 0) { + WarningPopup(LR"(SmoothCam: Did not find any bone names to follow while loading! Is SmoothCam_EyeBones_Default.txt present in the SKSE plugins directory? +Will fall back to default first-person camera bone. +To prevent this warning ensure a bone list file is present with at least 1 bone defined within and that SmoothCam is able to load it.)"); + eyeBonePriorities.emplace_back("NPCEyeBone"); + } +} + +Config::BoneList& Config::GetEyeBonePriorities() noexcept { + return eyeBonePriorities; +} +#endif \ No newline at end of file diff --git a/SmoothCam/source/crosshair.cpp b/SmoothCam/source/crosshair.cpp index 71c6ff6..67d9e1a 100644 --- a/SmoothCam/source/crosshair.cpp +++ b/SmoothCam/source/crosshair.cpp @@ -1,9 +1,86 @@ -#include "crosshair.h" - +#include "crosshair.h" #include "crosshair/skyrim.h" #include "crosshair/dot.h" +static Crosshair::Manager::CurrentCrosshairData g_crosshairData; + +// 80230 : gotoAndStop +typedef uintptr_t(__fastcall* CrosshairInvoke)(GFxMovieView** param1, uint64_t* param2, const char* name, uint64_t param4); +static eastl::unique_ptr> detCrosshairInvoke; +uintptr_t __fastcall mCrosshairInvoke(GFxMovieView** param1, uint64_t* param2, const char* name, uint64_t param4) { + auto ret = detCrosshairInvoke->GetBase()(param1, param2, name, param4); + if (!name) return ret; + + // @Note: This method is called right before "GFxInvoke" below + if (strcmp(name, "Alert") == 0) { + g_crosshairData.alertMode = true; + } else if (strcmp(name, "Normal") == 0) { + g_crosshairData.alertMode = false; + } + + return ret; +} + +//{ 0x00ECA860, 80233 }, +typedef bool(__thiscall *GFxInvoke)(void* pThis, void* obj, GFxValue* result, const char* name, GFxValue* args, UInt32 numArgs, bool isDisplayObj); +static eastl::unique_ptr> detGFxInvoke; +bool __fastcall mGFxInvoke(void* pThis, void* obj, GFxValue* result, const char* name, GFxValue* args, UInt32 numArgs, bool isDisplayObj) { + const auto ret = detGFxInvoke->GetBase()(pThis, obj, result, name, args, numArgs, isDisplayObj); + + if (name && strcmp(name, "ValidateCrosshair") == 0) { + // Getting spammed on by conjuration magic mode - (ノಠ益ಠ)ノ彡┻━┻ + // @Note: I really don't like these more invasive detours - finding a way around this should be a priority + // We need this currently because ValidateCrosshair will desync our crosshair state and mess things up + auto menu = MenuManager::GetSingleton()->GetMenu(&UIStringHolder::GetSingleton()->hudMenu); + GFxValue instance; + if (menu && menu->view && menu->view->GetVariable(&instance, "_root.HUDMovieBaseInstance.Crosshair")) { + GFxValue result; + GFxValue args[2]; + args[0].SetString("SetCrosshairEnabled"); + args[1].SetBool(g_crosshairData.enabled); + menu->view->Invoke("call", &result, static_cast(args), 2); + + GFxValue member; + if (instance.GetMember("_x", &member)) { + member.SetNumber(g_crosshairData.ofs.x); + instance.SetMember("_x", &member); + } + + if (instance.GetMember("_y", &member)) { + member.SetNumber(g_crosshairData.ofs.y); + instance.SetMember("_y", &member); + } + + if (instance.GetMember("_width", &member)) { + member.SetNumber(g_crosshairData.scale.x); + instance.SetMember("_width", &member); + } + + if (instance.GetMember("_height", &member)) { + member.SetNumber(g_crosshairData.scale.y); + instance.SetMember("_height", &member); + } + + menu->view->SetVariable("_root.HUDMovieBaseInstance.Crosshair", &instance, 2); + } + } + + return ret; +} + Crosshair::Manager::Manager() { + detGFxInvoke = eastl::make_unique>(80233, mGFxInvoke); + if (!detGFxInvoke->Attach()) { + _ERROR("Failed to place detour on target function(80,233), this error is fatal."); + FatalError(L"Failed to place detour on target function(80,233), this error is fatal."); + } + + detCrosshairInvoke = eastl::make_unique>(80230, mCrosshairInvoke); + if (!detCrosshairInvoke->Attach()) { + _ERROR("Failed to place detour on target function(80,230), this error is fatal."); + FatalError(L"Failed to place detour on target function(80,230), this error is fatal."); + } + ReadInitialCrosshairInfo(); if (!Render::HasContext()) return; @@ -15,7 +92,7 @@ Crosshair::Manager::Manager() { perObj.cpuAccessFlags = D3D11_CPU_ACCESS_FLAG::D3D11_CPU_ACCESS_WRITE; perObj.size = sizeof(decltype(renderables.cbufPerObjectStaging)); perObj.initialData = &renderables.cbufPerObjectStaging; - renderables.cbufPerObject = std::make_shared(perObj, ctx); + renderables.cbufPerObject = eastl::make_shared(perObj, ctx); // Per-frame data, shared among many objects (view, projection) Render::CBufferCreateInfo perFrane; @@ -23,17 +100,17 @@ Crosshair::Manager::Manager() { perFrane.cpuAccessFlags = D3D11_CPU_ACCESS_FLAG::D3D11_CPU_ACCESS_WRITE; perFrane.size = sizeof(decltype(renderables.cbufPerFrameStaging)); perFrane.initialData = &renderables.cbufPerFrameStaging; - renderables.cbufPerFrame = std::make_shared(perFrane, ctx); + renderables.cbufPerFrame = eastl::make_shared(perFrane, ctx); // Create our line drawer for the crosshair tail - renderables.tailDrawer = std::make_unique(ctx); + renderables.tailDrawer = eastl::make_unique(ctx); } Crosshair::Manager::~Manager() { renderables.release(); } -glm::vec3 Crosshair::Manager::GetCrosshairTargetNormal(const glm::vec2& aimRotation, float pitchMod) { +glm::vec3 Crosshair::Manager::GetCrosshairTargetNormal(const glm::vec2& aimRotation, float pitchMod) const noexcept { return mmath::GetViewVector( glm::vec3(0.0, 1.0, 0.0), aimRotation.x - pitchMod, @@ -41,24 +118,34 @@ glm::vec3 Crosshair::Manager::GetCrosshairTargetNormal(const glm::vec2& aimRotat ); } -void Crosshair::Manager::TickProjectilePath(glm::vec3& position, glm::vec3& vel, float gravity, float dt) noexcept { - // Not really sure what skyrim's gravity actually is, but this works (close enough) - constexpr auto magic = 650.0f; - const auto velNormal = glm::normalize(vel); - glm::vec3 gravityVector = { 0, 0, gravity * magic }; - vel -= gravityVector * dt; +void Crosshair::Manager::TickProjectilePath(glm::vec3& position, glm::vec3& vel, const glm::vec3& gravity, + float mass, float dt) noexcept +{ + // @Note: what i"m calling mass is called "gravity" in the CK - appears to be a scalar on gravity + // There is ALSO a gravityFactor in the nif file for the arrow + + // From the iron arrow nif file + // Until I can figure out a way to read these values at runtime, assume they don't change + constexpr auto linDamp = 0.099609f; + constexpr auto gravityFactor = 1.0f; + + const auto magic = glm::vec3{ 0.0f, 0.0f, 59.0f }; + glm::vec3 gravityVector = gravity * gravityFactor * mass * magic; + const auto step = gravityVector -(vel * linDamp); + + vel += step * dt; position += vel * dt; } glm::vec3 Crosshair::Manager::ComputeProjectileVelocityVector(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, - const TESAmmo* ammo, float gravity, const glm::vec2& aimRotation) noexcept + const TESAmmo* ammo, const glm::vec2& aimRotation) noexcept { // Read the tilt angle float tilt = 0.0f; - if (GameState::IsUsingCrossbow(player)) - tilt = glm::radians(Config::GetGameConfig()->f3PBoltTiltUpAngle); - else if (GameState::IsUsingBow(player)) - tilt = glm::radians(Config::GetGameConfig()->f3PArrowTiltUpAngle); + if (GameState::IsUsingBow(player) || GameState::IsUsingCrossbow(player)) { + static auto arrowTilt = (*g_iniSettingCollection)->Get("f3PArrowTiltUpAngle:Combat"); + tilt = glm::radians(arrowTilt->data.f32); + } // Get the rotation for the arrow auto arrowRot = aimRotation; @@ -67,26 +154,46 @@ glm::vec3 Crosshair::Manager::ComputeProjectileVelocityVector(const PlayerCharac arrowRot.x *= -1; // Read required metrics for the shot + + // Replicates the function at 42537 //const auto s2 = Offsets::Get(42537)(arrow); - const auto s2 = GameState::GetEquippedWeapon(player)->gameData.speed; - // (DAT_141de0dc0 - param_2) * _DAT_142f01438 + 1.00000000; - static auto pF1 = Offsets::Get(505066); - static auto pF2 = Offsets::Get(515530); - auto f1 = *pF1; - auto f2 = *pF2; - const auto launchSpeed = (f1 - s2) * f2 + 1.0f; - const auto speed = ammo->settings.projectile->data.speed; + const auto s2 = [](const TESObjectWEAP* wep) { + if (!wep) return 1.0f; + + auto speed = wep->gameData.speed; + const auto DAT_141de0da8 = *Offsets::Get(505064); + const auto DAT_141de0dc0 = *Offsets::Get(505066); + const auto DAT_142f01438 = *Offsets::Get(515530); + + if (DAT_141de0da8 <= speed) + speed = DAT_141de0da8; + + if (speed <= DAT_141de0dc0) + speed = DAT_141de0dc0; + + return (DAT_141de0dc0 - speed) * DAT_142f01438 + 1.0f; + }(GameState::GetEquippedWeapon(player)); - //const auto power = Offsets::Get(42536)(arrow); - // Let's assume a full power shot - constexpr auto power = 1.0f; + // Replicates the function at 42536 + // const auto power = Offsets::Get(42536)(arrow); + const auto power = [](float unk188_ArrowDrawTimer) { + // @Note: The parameter is projectile->unk188 + float fVar1 = unk188_ArrowDrawTimer; // FUN_14074dc80(arrowProjectile); + + const auto DAT_141de0df0 = *Offsets::Get(505070); + const auto DAT_141de0e08 = *Offsets::Get(505072); + + return ((fVar1 - DAT_141de0df0) / (1.0 - DAT_141de0df0)) * (1.0 - DAT_141de0e08) + DAT_141de0e08; + }(GameState::GetCurrentBowDrawTimer(player)); + + const auto speed = ammo->settings.projectile->data.speed; // arrowProjectile->arrowUnk18C // Appears to always be one constexpr auto arrowUnk18C = 1.0f; // Now calculate the initial velocity vector for the arrow if we were to fire it right now const auto arrowFireSpeed = speed * power; - const auto velScalar = launchSpeed * arrowFireSpeed * arrowUnk18C; + const auto velScalar = s2 * arrowFireSpeed * arrowUnk18C; const auto pitchCos = glm::cos(arrowRot.x); const auto pitchSin = glm::sin(arrowRot.x); const auto yawCos = glm::cos(arrowRot.y); @@ -104,30 +211,28 @@ bool Crosshair::Manager::ProjectilePredictionCurve(const PlayerCharacter* player const glm::vec2& aimRotation, const glm::vec3& startPos, glm::vec3& hitPos, bool& hitCharacter) noexcept { // Get the ammo we are using - typedef TESAmmo*(__thiscall PlayerCharacter::* GetAmmo)() const; - const auto ammo = (player->*reinterpret_cast(&PlayerCharacter::Unk_9E))(); + const auto ammo = GameState::GetCurrentAmmo(player); if (ammo == nullptr) return false; - // Compute impulse - const auto gravity = *reinterpret_cast(&ammo->settings.projectile->data.unk04); - glm::vec3 velocityVector = ComputeProjectileVelocityVector(player, camera, ammo, gravity, aimRotation); - const auto config = Config::GetCurrentConfig(); + const auto gravity = Physics::GetGravityVector(player); + const auto mass = *reinterpret_cast(&ammo->settings.projectile->data.unk04); - // And simulate the arc, trace in discrete slices + // Compute impulse + glm::vec3 velocityVector = ComputeProjectileVelocityVector(player, camera, ammo, aimRotation); // Max number of segments we want to simulate constexpr auto segCount = 128; - constexpr float timeStep = 1.0f / 16.0f; // The slower the timestep, the more we decimate the arc + constexpr float timeStep = 1.0f / 20.0f; // The slower the timestep, the more we decimate the arc glm::vec3 lastPos = startPos; glm::vec3 curPos = startPos; uint8_t entries = 0; - std::array, segCount> points; + eastl::array, segCount> points; bool hit = false; for (auto i = 0; i < segCount; i++) { - TickProjectilePath(curPos, velocityVector, gravity, timeStep); + TickProjectilePath(curPos, velocityVector, gravity, mass, timeStep); const auto origin = glm::vec4(lastPos.x, lastPos.y, lastPos.z, 0.0f); const auto endPoint = glm::vec4(curPos.x, curPos.y, curPos.z, 0.0f); const auto rayLength = glm::length(endPoint - origin); @@ -175,20 +280,16 @@ bool Crosshair::Manager::ProjectilePredictionCurve(const PlayerCharacter* player const auto alpha2 = dt2 <= 0.0f ? 0.0f : dt2 / max; renderables.arrowTailSegments.emplace_back( Render::Point( - l1 * Render::RenderScale, + Render::ToRenderScale(l1), { - col.r, - col.g, - col.b, + col.r, col.g, col.b, (1.0f - glm::clamp(alpha, 0.0f, 1.0f)) * col.a } ), Render::Point( - l2 * Render::RenderScale, + Render::ToRenderScale(l2), { - col.r, - col.g, - col.b, + col.r, col.g, col.b, (1.0f - glm::clamp(alpha2, 0.0f, 1.0f)) * col.a } ) @@ -200,7 +301,7 @@ bool Crosshair::Manager::ProjectilePredictionCurve(const PlayerCharacter* player } void Crosshair::Manager::UpdateCrosshairPosition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, - const glm::vec2& aimRotation, mmath::NiMatrix44& worldToScaleform) + const glm::vec2& aimRotation, mmath::NiMatrix44& worldToScaleform) noexcept { if (!player->loadedState || !player->loadedState->node) return; @@ -211,11 +312,11 @@ void Crosshair::Manager::UpdateCrosshairPosition(const PlayerCharacter* player, bool hit = false; bool hitCharacter = false; - if (GameState::IsBowDrawn(player) && weapon.data) { - const auto handNode = DYNAMIC_CAST(player->loadedState->node->GetObjectByName(&weapon.data), NiAVObject, NiNode); + if (GameState::IsBowDrawn(player) && Strings.weapon.data && Strings.arrowName.data) { + const auto handNode = DYNAMIC_CAST(player->loadedState->node->GetObjectByName(&Strings.weapon.data), NiAVObject, NiNode); if (handNode && handNode->m_children.m_size > 0 && handNode->m_children.m_data) { - const auto arrow = handNode->m_children.m_data[0]; + const auto arrow = handNode->GetObjectByName(&Strings.arrowName.data); if (arrow != nullptr) { const auto pos = glm::vec3{ arrow->m_worldTransform.pos.x, @@ -233,10 +334,9 @@ void Crosshair::Manager::UpdateCrosshairPosition(const PlayerCharacter* player, } else { // Classic method float fac = 0.0f; - if (GameState::IsUsingCrossbow(player)) { - fac = glm::radians(Config::GetGameConfig()->f3PBoltTiltUpAngle) * 0.5f; - } else if (GameState::IsUsingBow(player)) { - fac = glm::radians(Config::GetGameConfig()->f3PArrowTiltUpAngle) * 0.5f; + if (GameState::IsUsingBow(player) || GameState::IsUsingCrossbow(player)) { + static auto arrowTilt = (*g_iniSettingCollection)->Get("f3PArrowTiltUpAngle:Combat"); + fac = glm::radians(arrowTilt->data.f32) * 0.5f; } const auto n = GetCrosshairTargetNormal(aimRotation, fac); @@ -250,13 +350,13 @@ void Crosshair::Manager::UpdateCrosshairPosition(const PlayerCharacter* player, } } } - } else if (GameState::IsMagicDrawn(player) && weapon.data) { + } else if (GameState::IsMagicDrawn(player) && Strings.magic.data) { NiPoint3 niOrigin = { 0.01f, 0.01f, 0.01f }; glm::vec3 normal = { 0.0f, 1.00f, 0.0f }; - const auto handNode = DYNAMIC_CAST(player->loadedState->node->GetObjectByName(&magic.data), NiAVObject, NiNode); + const auto handNode = DYNAMIC_CAST(player->loadedState->node->GetObjectByName(&Strings.magic.data), NiAVObject, NiNode); if (handNode) - niOrigin = { handNode->m_worldTransform.pos.x,handNode->m_worldTransform.pos.y,handNode->m_worldTransform.pos.z }; + niOrigin = { handNode->m_worldTransform.pos.x, handNode->m_worldTransform.pos.y, handNode->m_worldTransform.pos.z }; normal = GetCrosshairTargetNormal(aimRotation); // Cast the aim ray @@ -294,18 +394,16 @@ void Crosshair::Manager::UpdateCrosshairPosition(const PlayerCharacter* player, renderables.drawCrosshair = true; renderables.hitCharacter = hitCharacter; renderables.curCrosshair->SetPosition(hitPos); - // We want the crosshair to face the player - const glm::vec3 bilboard = { + renderables.curCrosshair->SetRotation({ mmath::half_pi - aimRotation.x, aimRotation.y * -1.0f, 0.0f - }; + }); - renderables.curCrosshair->SetRotation(bilboard); return; } - + auto pt = NiPoint3( hitPos.x, hitPos.y, @@ -342,7 +440,11 @@ void Crosshair::Manager::UpdateCrosshairPosition(const PlayerCharacter* player, } } -void Crosshair::Manager::ReadInitialCrosshairInfo() { +bool Crosshair::Manager::IsCrosshairDataValid() const noexcept { + return baseCrosshairData.captured; +} + +void Crosshair::Manager::ReadInitialCrosshairInfo() noexcept { auto menu = MenuManager::GetSingleton()->GetMenu(&UIStringHolder::GetSingleton()->hudMenu); if (!menu || !menu->view) return; @@ -363,9 +465,16 @@ void Crosshair::Manager::ReadInitialCrosshairInfo() { baseCrosshairData.xCenter = mmath::Remap(0.5f, 0.0f, 1.0f, rect.left, rect.right); baseCrosshairData.yCenter = mmath::Remap(0.5f, 0.0f, 1.0f, rect.top, rect.bottom); + menu->view->GetVariable(&va, "_root.HUDMovieBaseInstance.StealthMeterInstance._x"); + baseCrosshairData.stealthXOff = va.GetNumber(); + + menu->view->GetVariable(&va, "_root.HUDMovieBaseInstance.StealthMeterInstance._y"); + baseCrosshairData.stealthYOff = va.GetNumber(); + baseCrosshairData.captured = true; + currentCrosshairData.enabled = true; - currentCrosshairData.position = { + currentCrosshairData.position = currentCrosshairData.stealthMeterPosition = { baseCrosshairData.xCenter, baseCrosshairData.yCenter }; @@ -373,14 +482,19 @@ void Crosshair::Manager::ReadInitialCrosshairInfo() { baseCrosshairData.xScale, baseCrosshairData.yScale }; - currentCrosshairData.enabled = true; + + g_crosshairData = currentCrosshairData; + g_crosshairData.ofs = { baseCrosshairData.xOff, baseCrosshairData.yOff }; + g_crosshairData.stealthMeterOfs = { baseCrosshairData.stealthXOff, baseCrosshairData.stealthYOff }; } -void Crosshair::Manager::SetCrosshairPosition(const glm::dvec2& pos) { +void Crosshair::Manager::SetCrosshairPosition(const glm::dvec2& pos) noexcept { + if (!IsCrosshairDataValid()) return; if (currentCrosshairData.position == pos) return; auto menu = MenuManager::GetSingleton()->GetMenu(&UIStringHolder::GetSingleton()->hudMenu); - if (menu && menu->view) { + GFxValue instance; + if (menu && menu->view && menu->view->GetVariable(&instance, "_root.HUDMovieBaseInstance.Crosshair")) { const auto rect = menu->view->GetVisibleFrameRect(); const auto half_x = pos.x - ((static_cast(rect.right) + static_cast(rect.left)) * 0.5); @@ -390,47 +504,95 @@ void Crosshair::Manager::SetCrosshairPosition(const glm::dvec2& pos) { const auto x = half_x + baseCrosshairData.xOff; const auto y = half_y + baseCrosshairData.yOff; - GFxValue va; - va.SetNumber(x); - menu->view->SetVariable("_root.HUDMovieBaseInstance.CrosshairInstance._x", &va, 0); - va.SetNumber(y); - menu->view->SetVariable("_root.HUDMovieBaseInstance.CrosshairInstance._y", &va, 0); + GFxValue member; + if (instance.GetMember("_x", &member)) { + member.SetNumber(x); + instance.SetMember("_x", &member); + } + if (instance.GetMember("_y", &member)) { + member.SetNumber(y); + instance.SetMember("_y", &member); + } + + menu->view->SetVariable("_root.HUDMovieBaseInstance.Crosshair", &instance, 2); currentCrosshairData.position = pos; + g_crosshairData.position = pos; + g_crosshairData.ofs = { x, y }; } } -void Crosshair::Manager::CenterCrosshair() { +void Crosshair::Manager::SetStealthMeterPosition(const glm::vec2& pos) noexcept { + if (!IsCrosshairDataValid()) return; + auto menu = MenuManager::GetSingleton()->GetMenu(&UIStringHolder::GetSingleton()->hudMenu); + GFxValue instance; + if (menu && menu->view && menu->view->GetVariable(&instance, "_root.HUDMovieBaseInstance.StealthMeterInstance")) { + GFxValue member; + if (instance.GetMember("_x", &member)) { + member.SetNumber(baseCrosshairData.stealthXOff + static_cast(pos.x)); + instance.SetMember("_x", &member); + } + + if (instance.GetMember("_y", &member)) { + member.SetNumber(baseCrosshairData.stealthYOff + static_cast(pos.y)); + instance.SetMember("_y", &member); + } + + menu->view->SetVariable("_root.HUDMovieBaseInstance.StealthMeterInstance", &instance, 2); + currentCrosshairData.stealthMeterPosition = pos; + g_crosshairData.stealthMeterPosition = pos; + currentCrosshairData.stealthMeterMutated = true; + } +} + +void Crosshair::Manager::CenterCrosshair() noexcept { + if (!IsCrosshairDataValid()) return; SetCrosshairPosition({ baseCrosshairData.xCenter, baseCrosshairData.yCenter }); } -void Crosshair::Manager::SetCrosshairSize(const glm::dvec2& size) { +void Crosshair::Manager::CenterStealthMeter() noexcept { + if (!IsCrosshairDataValid()) return; + SetStealthMeterPosition({0.0, 0.0}); +} + +void Crosshair::Manager::SetCrosshairSize(const glm::dvec2& size) noexcept { + if (!IsCrosshairDataValid()) return; if (currentCrosshairData.scale == size) return; if (!Config::GetCurrentConfig()->enableCrosshairSizeManip) return; auto menu = MenuManager::GetSingleton()->GetMenu(&UIStringHolder::GetSingleton()->hudMenu); - if (menu && menu->view) { - GFxValue va; - va.SetNumber(static_cast(size.x)); - menu->view->SetVariable("_root.HUDMovieBaseInstance.Crosshair._width", &va, 0); - va.SetNumber(static_cast(size.y)); - menu->view->SetVariable("_root.HUDMovieBaseInstance.Crosshair._height", &va, 0); + GFxValue instance; + if (menu && menu->view && menu->view->GetVariable(&instance, "_root.HUDMovieBaseInstance.Crosshair")) { + GFxValue member; + if (instance.GetMember("_width", &member)) { + member.SetNumber(size.x); + instance.SetMember("_width", &member); + } + + if (instance.GetMember("_height", &member)) { + member.SetNumber(size.y); + instance.SetMember("_height", &member); + } + menu->view->SetVariable("_root.HUDMovieBaseInstance.Crosshair", &instance, 2); currentCrosshairData.scale = size; + g_crosshairData.scale = size; } } -void Crosshair::Manager::SetDefaultSize() { +void Crosshair::Manager::SetDefaultSize() noexcept { + if (!IsCrosshairDataValid()) return; SetCrosshairSize({ baseCrosshairData.xScale, baseCrosshairData.yScale }); } -void Crosshair::Manager::SetCrosshairEnabled(bool enabled) { +void Crosshair::Manager::SetCrosshairEnabled(bool enabled) noexcept { + if (!IsCrosshairDataValid()) return; if (currentCrosshairData.enabled == enabled && !currentCrosshairData.invalidated) return; auto menu = MenuManager::GetSingleton()->GetMenu(&UIStringHolder::GetSingleton()->hudMenu); @@ -443,23 +605,24 @@ void Crosshair::Manager::SetCrosshairEnabled(bool enabled) { currentCrosshairData.enabled = enabled; currentCrosshairData.invalidated = false; + g_crosshairData.enabled = enabled; } } -void Crosshair::Manager::Set3DCrosshairType(Config::CrosshairType type) { +void Crosshair::Manager::Set3DCrosshairType(Config::CrosshairType type) noexcept { if (!Render::HasContext()) return; if (renderables.crosshairType == type) return; // Crosshair either changed or this is the first time running switch (type) { case Config::CrosshairType::Skyrim: - renderables.curCrosshair = std::make_unique(); + renderables.curCrosshair = eastl::make_unique(); break; case Config::CrosshairType::Dot: - renderables.curCrosshair = std::make_unique(); + renderables.curCrosshair = eastl::make_unique(); break; default: - renderables.curCrosshair = std::make_unique(); + renderables.curCrosshair = eastl::make_unique(); break; } @@ -467,44 +630,100 @@ void Crosshair::Manager::Set3DCrosshairType(Config::CrosshairType type) { renderables.crosshairType = type; } -void Crosshair::Manager::InvalidateEnablementCache() { +void Crosshair::Manager::InvalidateEnablementCache() noexcept { currentCrosshairData.invalidated = true; } +void Crosshair::Manager::Update(PlayerCharacter* player, CorrectedPlayerCamera* camera) noexcept { + if (!IsCrosshairDataValid()) return; + + // Stealth meter offset + const auto config = Config::GetCurrentConfig(); + + if (!config->offsetStealthMeter) { + if (currentCrosshairData.stealthMeterMutated) + CenterStealthMeter(); + return; + } + + if (!GameState::IsSneaking(player)) { + if (currentCrosshairData.stealthMeterMutated) { + CenterStealthMeter(); + currentCrosshairData.stealthMeterMutated = false; + } + return; + } + + const auto usingBow = GameState::IsBowDrawn(player); + const auto usingMagic = GameState::IsMagicDrawn(player); + if ((config->useWorldCrosshair && (usingBow || usingMagic)) || config->alwaysOffsetStealthMeter) { + SetStealthMeterPosition({ + config->stealthMeterXOffset, + config->stealthMeterYOffset + }); + + } else if (currentCrosshairData.stealthMeterMutated) { + CenterStealthMeter(); + currentCrosshairData.stealthMeterMutated = false; + } +} + void Crosshair::Manager::Render(Render::D3DContext& ctx, const glm::vec3& cameraPosition, const glm::vec2& cameraRotation, const NiFrustum& frustum) noexcept { - const auto config = Config::GetCurrentConfig(); + if (!IsCrosshairDataValid()) { + // Try and capture now + ReadInitialCrosshairInfo(); + } - // Update view, projection and common per frame data - const auto matProj = Render::GetProjectionMatrix(frustum); - const auto matView = Render::BuildViewMatrix(cameraPosition, cameraRotation); - renderables.cbufPerFrameStaging.matProjView = matProj * matView; + const auto config = Config::GetCurrentConfig(); - renderables.cbufPerFrameStaging.curTime = static_cast(GameTime::CurTime()); - renderables.cbufPerFrame->Update( - &renderables.cbufPerFrameStaging, 0, - sizeof(decltype(renderables.cbufPerFrameStaging)), ctx - ); + // Only update GPU if we are going to draw +#ifdef DEBUG + if (1) { +#else + if (renderables.arrowTailSegments.size() > 0 || (renderables.drawCrosshair && renderables.curCrosshair)) { +#endif + // Update view, projection and common per frame data + const auto matProj = Render::GetProjectionMatrix(frustum); + const auto matView = Render::BuildViewMatrix(cameraPosition, cameraRotation); + renderables.cbufPerFrameStaging.matProjView = matProj * matView; + + // Color tinting for crosshair alert mode (ex: conjuration) + renderables.cbufPerFrameStaging.tint = g_crosshairData.alertMode ? + glm::vec4{ 1.0f, 0.0f, 0.0f, 1.0f } : + glm::vec4{ 1.0f, 1.0f, 1.0f, 1.0f }; + + renderables.cbufPerFrameStaging.curTime = static_cast(GameTime::CurTime()); + renderables.cbufPerFrame->Update( + &renderables.cbufPerFrameStaging, 0, + sizeof(decltype(renderables.cbufPerFrameStaging)), ctx + ); - // Bind at register b1 - renderables.cbufPerFrame->Bind(Render::PipelineStage::Vertex, 1, ctx); - renderables.cbufPerFrame->Bind(Render::PipelineStage::Fragment, 1, ctx); - - // Setup depth and blending - Render::SetDepthState(ctx, true, true, D3D11_COMPARISON_FUNC::D3D11_COMPARISON_LESS_EQUAL); - Render::SetBlendState( - ctx, true, - D3D11_BLEND_OP::D3D11_BLEND_OP_ADD, D3D11_BLEND_OP::D3D11_BLEND_OP_ADD, - D3D11_BLEND::D3D11_BLEND_SRC_ALPHA, D3D11_BLEND::D3D11_BLEND_INV_SRC_ALPHA, - D3D11_BLEND::D3D11_BLEND_ONE, D3D11_BLEND::D3D11_BLEND_INV_SRC_ALPHA - ); + // Bind at register b1 + renderables.cbufPerFrame->Bind(Render::PipelineStage::Vertex, 1, ctx); + renderables.cbufPerFrame->Bind(Render::PipelineStage::Fragment, 1, ctx); + + // Setup depth and blending + Render::SetDepthState(ctx, true, true, D3D11_COMPARISON_FUNC::D3D11_COMPARISON_LESS_EQUAL); + Render::SetBlendState( + ctx, true, + D3D11_BLEND_OP::D3D11_BLEND_OP_ADD, D3D11_BLEND_OP::D3D11_BLEND_OP_ADD, + D3D11_BLEND::D3D11_BLEND_SRC_ALPHA, D3D11_BLEND::D3D11_BLEND_INV_SRC_ALPHA, + D3D11_BLEND::D3D11_BLEND_ONE, D3D11_BLEND::D3D11_BLEND_INV_SRC_ALPHA + ); + } if (renderables.arrowTailSegments.size() > 0) { renderables.tailDrawer->Submit(renderables.arrowTailSegments); renderables.arrowTailSegments.clear(); } +#ifdef DEBUG + // Draw arrow debug overlay here, render state is already configured + ArrowFixes::Draw(ctx); +#endif + if (renderables.drawCrosshair && renderables.curCrosshair) { // Invert the scaling factor from our perspective projection to give the crosshair an // apparent constant size at any distance @@ -522,8 +741,6 @@ void Crosshair::Manager::Render(Render::D3DContext& ctx, const glm::vec3& camera } } - const auto aspect = ctx.windowSize.x / ctx.windowSize.y; - renderables.curCrosshair->SetScale({ (pScale * 0.001f) * s * 0.0055f, (pScale * 0.001f) * s * 0.0055f, @@ -536,4 +753,23 @@ void Crosshair::Manager::Render(Render::D3DContext& ctx, const glm::vec3& camera renderables.drawCrosshair = false; renderables.hitCharacter = false; } +} + +void Crosshair::Manager::Reset(bool hard) noexcept { + if (!IsCrosshairDataValid()) return; + if (hard) { + Config::GetCurrentConfig()->hideCrosshairMeleeCombat = false; + Config::GetCurrentConfig()->hideNonCombatCrosshair = false; + Config::GetCurrentConfig()->useWorldCrosshair = false; + Config::GetCurrentConfig()->use3DBowAimCrosshair = false; + Config::GetCurrentConfig()->use3DMagicCrosshair = false; + } + + InvalidateEnablementCache(); + SetCrosshairEnabled(true); + SetDefaultSize(); + CenterCrosshair(); + if (currentCrosshairData.stealthMeterMutated || hard) + CenterStealthMeter(); + currentCrosshairData.stealthMeterMutated = false; } \ No newline at end of file diff --git a/SmoothCam/source/crosshair/base.cpp b/SmoothCam/source/crosshair/base.cpp index ee74642..3ee248e 100644 --- a/SmoothCam/source/crosshair/base.cpp +++ b/SmoothCam/source/crosshair/base.cpp @@ -1,21 +1,21 @@ #include "crosshair/base.h" -void Crosshair::Base::SetPosition(const glm::vec3& pos) { +void Crosshair::Base::SetPosition(const glm::vec3& pos) noexcept { position = pos; dirty = true; } -void Crosshair::Base::SetRotation(const glm::vec3& rot) { +void Crosshair::Base::SetRotation(const glm::vec3& rot) noexcept { rotation = rot; dirty = true; } -void Crosshair::Base::SetScale(const glm::vec3& s) { +void Crosshair::Base::SetScale(const glm::vec3& s) noexcept { scale = s; dirty = true; } -void Crosshair::Base::UpdateTransform() { +void Crosshair::Base::UpdateTransform() noexcept { if (!dirty) return; transform = glm::identity(); transform = glm::translate(transform, Render::ToRenderScale(position)); diff --git a/SmoothCam/source/crosshair/dot.cpp b/SmoothCam/source/crosshair/dot.cpp index 31d4d6d..dd69699 100644 --- a/SmoothCam/source/crosshair/dot.cpp +++ b/SmoothCam/source/crosshair/dot.cpp @@ -1,9 +1,10 @@ #include "crosshair/dot.h" #include "render/models/dot.h" -#include "render/shaders/vertex_color.h" +#include "render/shaders/vertex_color_world.h" +#include "render/shader_cache.h" #include "camera.h" -void Crosshair::Dot::Create3D(Render::D3DContext& ctx, std::shared_ptr& perObjectBuf) { +void Crosshair::Dot::Create3D(Render::D3DContext& ctx, eastl::shared_ptr& perObjectBuf) noexcept { Render::Model::Model mdl; if (!Render::Model::Load(dotMesh, mdl)) FatalError(L"SmoothCam: Failed to load 3D asset"); @@ -12,14 +13,14 @@ void Crosshair::Dot::Create3D(Render::D3DContext& ctx, std::shared_ptr(vsCreateInfo, ctx); - meshInfo.ps = std::make_shared(psCreateInfo, ctx); + meshInfo.vs = Render::ShaderCache::Get().Load(vsCreateInfo, ctx); + meshInfo.ps = Render::ShaderCache::Get().Load(psCreateInfo, ctx); // Assert our shaders are good if (!meshInfo.vs->IsValid() || !meshInfo.ps->IsValid()) { @@ -27,13 +28,13 @@ void Crosshair::Dot::Create3D(Render::D3DContext& ctx, std::shared_ptr(meshInfo, perObjectBuffer, ctx); + meshDrawer = eastl::make_unique(meshInfo, perObjectBuffer, ctx); glm::vec3 ourSize{ 10.0f, 10.0f, 1.0f }; SetScale(ourSize); } -void Crosshair::Dot::Render(Render::D3DContext& ctx, float curTime, float deltaTime, bool allowDepthTesting) { +void Crosshair::Dot::Render(Render::D3DContext& ctx, float curTime, float deltaTime, bool allowDepthTesting) noexcept { // Compute our transform UpdateTransform(); diff --git a/SmoothCam/source/crosshair/skyrim.cpp b/SmoothCam/source/crosshair/skyrim.cpp index 08eeb2f..bc8b283 100644 --- a/SmoothCam/source/crosshair/skyrim.cpp +++ b/SmoothCam/source/crosshair/skyrim.cpp @@ -1,9 +1,10 @@ #include "crosshair/skyrim.h" #include "render/models/skyrim_crosshair.h" -#include "render/shaders/vertex_color.h" +#include "render/shaders/vertex_color_world.h" +#include "render/shader_cache.h" #include "camera.h" -void Crosshair::Skyrim::Create3D(Render::D3DContext& ctx, std::shared_ptr& perObjectBuf) { +void Crosshair::Skyrim::Create3D(Render::D3DContext& ctx, eastl::shared_ptr& perObjectBuf) noexcept { Render::Model::Model mdl; if (!Render::Model::Load(skyrimCrosshairMesh, mdl)) FatalError(L"SmoothCam: Failed to load 3D asset"); @@ -12,14 +13,14 @@ void Crosshair::Skyrim::Create3D(Render::D3DContext& ctx, std::shared_ptr(vsCreateInfo, ctx); - meshInfo.ps = std::make_shared(psCreateInfo, ctx); + meshInfo.vs = Render::ShaderCache::Get().Load(vsCreateInfo, ctx); + meshInfo.ps = Render::ShaderCache::Get().Load(psCreateInfo, ctx); // Assert our shaders are good if (!meshInfo.vs->IsValid() || !meshInfo.ps->IsValid()) { @@ -27,13 +28,13 @@ void Crosshair::Skyrim::Create3D(Render::D3DContext& ctx, std::shared_ptr(meshInfo, perObjectBuffer, ctx); + meshDrawer = eastl::make_unique(meshInfo, perObjectBuffer, ctx); glm::vec3 ourSize{ 10.0f, 10.0f, 1.0f }; SetScale(ourSize); } -void Crosshair::Skyrim::Render(Render::D3DContext& ctx, float curTime, float deltaTime, bool allowDepthTesting) { +void Crosshair::Skyrim::Render(Render::D3DContext& ctx, float curTime, float deltaTime, bool allowDepthTesting) noexcept { // Compute our transform UpdateTransform(); diff --git a/SmoothCam/source/debug/ICommand.cpp b/SmoothCam/source/debug/ICommand.cpp new file mode 100644 index 0000000..5a09e87 --- /dev/null +++ b/SmoothCam/source/debug/ICommand.cpp @@ -0,0 +1,6 @@ +#ifdef _DEBUG +#include "debug/ICommand.h" + +Debug::ICommand::~ICommand() {} + +#endif \ No newline at end of file diff --git a/SmoothCam/source/debug/commands/dump_addrlib_db.cpp b/SmoothCam/source/debug/commands/dump_addrlib_db.cpp new file mode 100644 index 0000000..d22393f --- /dev/null +++ b/SmoothCam/source/debug/commands/dump_addrlib_db.cpp @@ -0,0 +1,14 @@ +#ifdef _DEBUG +#include "debug/commands/dump_addrlib_db.h" + +Debug::DumpAddrLibDB::~DumpAddrLibDB() {} + +void Debug::DumpAddrLibDB::Run(const eastl::string& args) noexcept { + Offsets::DumpDatabaseTextFile(); + puts("Dumped offsets database to offsets.txt"); +} + +const eastl::string_view Debug::DumpAddrLibDB::GetHelpString() const noexcept { + return helpMsg; +} +#endif \ No newline at end of file diff --git a/SmoothCam/source/debug/commands/dump_game_ini.cpp b/SmoothCam/source/debug/commands/dump_game_ini.cpp new file mode 100644 index 0000000..e5798dd --- /dev/null +++ b/SmoothCam/source/debug/commands/dump_game_ini.cpp @@ -0,0 +1,48 @@ +#ifdef _DEBUG +#include "debug/commands/dump_game_ini.h" + +Debug::DumpGameINI::~DumpGameINI() {} + +void Debug::DumpGameINI::Run(const eastl::string& args) noexcept { + auto settings = *g_iniSettingCollection; + auto node = &settings->items; + + puts("Current INI values:"); + + while (node) { + switch (node->setting->GetType()) { + case Setting::kType_Bool: { + printf("\t%s :: %s\n", node->setting->name, node->setting->data.u8 ? "true" : "false"); + break; + } + case Setting::kType_Float: { + printf("\t%s :: %f\n", node->setting->name, node->setting->data.f32); + break; + } + case Setting::kType_Integer: { + printf("\t%s :: %d\n", node->setting->name, node->setting->data.s32); + break; + } + case Setting::kType_String: { + printf("\t%s :: %s\n", node->setting->name, node->setting->data.s); + break; + } + case Setting::kType_ID: { + printf("\t%s :: %d\n", node->setting->name, node->setting->data.u32); + break; + } + default: { + printf("\t%s :: unhandled value type\n", node->setting->name); + break; + } + } + node = node->next; + } + + puts(""); +} + +const eastl::string_view Debug::DumpGameINI::GetHelpString() const noexcept { + return helpMsg; +} +#endif \ No newline at end of file diff --git a/SmoothCam/source/debug/commands/dump_game_perfs_ini.cpp b/SmoothCam/source/debug/commands/dump_game_perfs_ini.cpp new file mode 100644 index 0000000..f3a4ede --- /dev/null +++ b/SmoothCam/source/debug/commands/dump_game_perfs_ini.cpp @@ -0,0 +1,48 @@ +#ifdef _DEBUG +#include "debug/commands/dump_game_perfs_ini.h" + +Debug::DumpGamePerfsINI::~DumpGamePerfsINI() {} + +void Debug::DumpGamePerfsINI::Run(const eastl::string& args) noexcept { + auto settings = *g_iniPrefSettingCollection; + auto node = &settings->items; + + puts("Current INI values:"); + + while (node) { + switch (node->setting->GetType()) { + case Setting::kType_Bool: { + printf("\t%s :: %s\n", node->setting->name, node->setting->data.u8 ? "true" : "false"); + break; + } + case Setting::kType_Float: { + printf("\t%s :: %f\n", node->setting->name, node->setting->data.f32); + break; + } + case Setting::kType_Integer: { + printf("\t%s :: %d\n", node->setting->name, node->setting->data.s32); + break; + } + case Setting::kType_String: { + printf("\t%s :: %s\n", node->setting->name, node->setting->data.s); + break; + } + case Setting::kType_ID: { + printf("\t%s :: %d\n", node->setting->name, node->setting->data.u32); + break; + } + default: { + printf("\t%s :: unhandled value type\n", node->setting->name); + break; + } + } + node = node->next; + } + + puts(""); +} + +const eastl::string_view Debug::DumpGamePerfsINI::GetHelpString() const noexcept { + return helpMsg; +} +#endif \ No newline at end of file diff --git a/SmoothCam/source/debug/commands/get_setting.cpp b/SmoothCam/source/debug/commands/get_setting.cpp new file mode 100644 index 0000000..7ba042c --- /dev/null +++ b/SmoothCam/source/debug/commands/get_setting.cpp @@ -0,0 +1,49 @@ +#ifdef _DEBUG +#include "debug/commands/get_setting.h" +#include "debug/registry.h" + +Debug::GetSetting::~GetSetting() {} + +void Debug::GetSetting::Run(const eastl::string& args) noexcept { + auto setting = (*g_iniSettingCollection)->Get(args.c_str()); + + if (!setting) { + setting = (*g_iniPrefSettingCollection)->Get(args.c_str()); + if (!setting) { + puts("Setting not found"); + return; + } + } + + switch (setting->GetType()) { + case Setting::kType_Bool: { + printf("%s :: %s\n", setting->name, setting->data.u8 ? "true" : "false"); + break; + } + case Setting::kType_Float: { + printf("%s :: %f\n", setting->name, setting->data.f32); + break; + } + case Setting::kType_Integer: { + printf("%s :: %d\n", setting->name, setting->data.s32); + break; + } + case Setting::kType_String: { + printf("%s :: %s\n", setting->name, setting->data.s); + break; + } + case Setting::kType_ID: { + printf("%s :: %d\n", setting->name, setting->data.u32); + break; + } + default: { + printf("%s :: unhandled value type\n",setting->name); + break; + } + } +} + +const eastl::string_view Debug::GetSetting::GetHelpString() const noexcept { + return helpMsg; +} +#endif \ No newline at end of file diff --git a/SmoothCam/source/debug/commands/help.cpp b/SmoothCam/source/debug/commands/help.cpp new file mode 100644 index 0000000..cfcd45d --- /dev/null +++ b/SmoothCam/source/debug/commands/help.cpp @@ -0,0 +1,18 @@ +#ifdef _DEBUG +#include "debug/commands/help.h" +#include "debug/registry.h" + +Debug::Help::~Help() {} + +void Debug::Help::Run(const eastl::string& args) noexcept { + puts("All commands:"); + for (const auto& it : Debug::CommandRegistry::Get()->GetCommands()) { + printf("\t%s - %s\n", it.first.data(), it.second->GetHelpString().data()); + } + puts(""); +} + +const eastl::string_view Debug::Help::GetHelpString() const noexcept { + return helpMsg; +} +#endif \ No newline at end of file diff --git a/SmoothCam/source/debug/commands/set_setting.cpp b/SmoothCam/source/debug/commands/set_setting.cpp new file mode 100644 index 0000000..ea5afd7 --- /dev/null +++ b/SmoothCam/source/debug/commands/set_setting.cpp @@ -0,0 +1,49 @@ +#ifdef _DEBUG +#include "debug/commands/set_setting.h" +#include "debug/registry.h" + +Debug::SetSetting::~SetSetting() {} + +void Debug::SetSetting::Run(const eastl::string& args) noexcept { + auto pos = args.find_first_of(' ', 0); + auto name = pos != std::string::npos ? args.substr(0, pos) : args; + auto value = pos != std::string::npos ? args.substr(glm::min(pos+1, args.length())) : ""; + + auto setting = (*g_iniSettingCollection)->Get(name.c_str()); + + if (!setting) { + setting = (*g_iniPrefSettingCollection)->Get(name.c_str()); + if (!setting) { + puts("Setting not found"); + return; + } + } + + switch (setting->GetType()) { + case Setting::kType_Bool: { + setting->data.u8 = value.compare("true") == 0; + break; + } + case Setting::kType_Float: { + setting->data.f32 = std::stof(value.c_str()); + break; + } + case Setting::kType_Integer: { + setting->data.s32 = std::stoi(value.c_str()); + break; + } + case Setting::kType_ID: { + setting->data.u32 = std::stoul(value.c_str()); + break; + } + default: { + printf("%s :: unhandled value type\n",setting->name); + break; + } + } +} + +const eastl::string_view Debug::SetSetting::GetHelpString() const noexcept { + return helpMsg; +} +#endif \ No newline at end of file diff --git a/SmoothCam/source/debug/console.cpp b/SmoothCam/source/debug/console.cpp new file mode 100644 index 0000000..65e3eb6 --- /dev/null +++ b/SmoothCam/source/debug/console.cpp @@ -0,0 +1,68 @@ +#ifdef _DEBUG +#include "debug/console.h" +#include "debug/ICommand.h" +#include "debug/registry.h" + +#include "debug/commands/dump_addrlib_db.h" +#include "debug/commands/dump_game_ini.h" +#include "debug/commands/dump_game_perfs_ini.h" +#include "debug/commands/get_setting.h" +#include "debug/commands/set_setting.h" +#include "debug/commands/help.h" + +static std::mutex consoleLock; +static std::thread repl; + +static std::mutex commandListLock; +static eastl::vector> commandList; + +std::mutex& Debug::GetTerminalLock() noexcept { + return consoleLock; +} + +void Debug::StartREPL(FILE* outStream) noexcept { + CommandRegistry::Get()->Register(eastl::make_unique()); + CommandRegistry::Get()->Register(eastl::make_unique()); + CommandRegistry::Get()->Register(eastl::make_unique()); + CommandRegistry::Get()->Register(eastl::make_unique()); + CommandRegistry::Get()->Register(eastl::make_unique()); + CommandRegistry::Get()->Register(eastl::make_unique()); + + repl = std::thread([](FILE* outStream) { + AllocConsole(); + AttachConsole(GetCurrentProcessId()); + FILE* streamOut; + FILE* streamIn; + freopen_s(&streamIn, "CONIN$", "r", stdin); + freopen_s(&streamOut, "CONOUT$", "w", outStream); + + while (true) { + std::string command; + std::getline(std::cin, command); + + std::lock_guard listLock(commandListLock); + + auto pos = command.find_first_of(' ', 0); + auto cmd = pos != std::string::npos ? command.substr(0, pos) : command; + auto args = pos != std::string::npos ? command.substr(glm::min(pos+1, command.length())) : ""; + commandList.push_back(eastl::tie(eastl::string(cmd.c_str()), eastl::string(args.c_str()))); + } + }, outStream); + repl.detach(); +} + +void Debug::CommandPump() noexcept { + std::lock_guard listLock(commandListLock); + + for (const auto& [cmd, args] : commandList) { + auto command = Debug::CommandRegistry::Get()->Find(cmd); + if (!command) { + puts("Unknown command"); + continue; + } + command->Run(args); + } + + commandList.clear(); +} +#endif \ No newline at end of file diff --git a/SmoothCam/source/debug/eh.cpp b/SmoothCam/source/debug/eh.cpp new file mode 100644 index 0000000..496479e --- /dev/null +++ b/SmoothCam/source/debug/eh.cpp @@ -0,0 +1,170 @@ +#ifdef EMIT_MINIDUMPS +#include "debug/eh.h" +#include +#include + +constexpr auto dmpMsg = LR"(An unrecoverable error occurred during execution of SmoothCam code. Sorry! :( +SmoothCam has created a mini-dump file which the developer can use to fix this problem. + +To report this issue: +1). Locate the mini-dump file in the root of your Skyrim directory (The same folder as SkyrimSE.exe). This file is called "SmoothCam_AppCrash.mdmp". + +2). Visit the Github issues page (https://github.com/mwilsnd/SkyrimSE-SmoothCam/issues). + +3). Create a new issue and try to describe what you were doing when the crash occurred if you can. Upload and link the mini-dump file in your new issue. + +3a). If possible, also provide the version of SkyrimSE you are running, your SKSE version and a list of other mods installed (A Net Script Framework crash log also works).)"; + +constexpr auto dmpFailMsg = LR"(If this issue persist and SmoothCam is unable to write a mini-dump, you can try the following: +1). Run Skyrim as an administrator - Permissions may be preventing the dump file from being written. + +2). Ensure there is no locking handle on the file "SmoothCam_AppCrash.mdmp", if such file exists (It would be next to SkyrimSE.exe). + +3). Install the latest Visual C++ 2019 Runtime Redistributable. + +4). Deactivate other SKSE or .NET mods.)"; + +static bool dumpRunning = false; +static std::atomic vehRC = { 0 }; +Debug::MiniDumpScope::MiniDumpScope() noexcept { + ++vehRC; +} + +Debug::MiniDumpScope::~MiniDumpScope() noexcept { + assert(vehRC > 0); + --vehRC; +} + +void WriteMiniDump(EXCEPTION_POINTERS* exceptionInfo) { + dumpRunning = true; + + constexpr auto flags = MiniDumpNormal | MiniDumpWithDataSegs | + MiniDumpWithIndirectlyReferencedMemory; + + HANDLE hFile = CreateFile(L"SmoothCam_AppCrash.mdmp", GENERIC_READ | GENERIC_WRITE, + 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + + if (hFile != NULL && hFile != INVALID_HANDLE_VALUE) { + MINIDUMP_EXCEPTION_INFORMATION info; + info.ExceptionPointers = exceptionInfo; + info.ThreadId = GetCurrentThreadId(); + info.ClientPointers = FALSE; + + if (MiniDumpWriteDump( + GetCurrentProcess(), + GetCurrentProcessId(), + hFile, + static_cast(flags), + &info, NULL, NULL + )) { + ShowCursor(TRUE); + MessageBox(nullptr, dmpMsg, L"SmoothCam: Skyrim has crashed!", MB_OK); + } else { + ShowCursor(TRUE); + MessageBox(nullptr, dmpFailMsg, L"SmoothCam: Skyrim has crashed!", MB_OK); + } + + CloseHandle(hFile); + } + + dumpRunning = false; +} + +HMODULE GetCurrentModule() { + HMODULE hModule = NULL; + GetModuleHandleEx( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + (LPCTSTR)GetCurrentModule, + &hModule + ); + return hModule; +} + +LONG WINAPI VectorHandler(struct _EXCEPTION_POINTERS* pointers) { + if (vehRC == 0 || dumpRunning || IS_UNWINDING(pointers->ExceptionRecord->ExceptionFlags)) + return EXCEPTION_CONTINUE_SEARCH; + + // Figure out if we are in the stack + // If so, run the mini dump and fastfail + STACKFRAME64 stack; + stack.AddrPC.Offset = pointers->ContextRecord->Rip; + stack.AddrFrame.Offset = pointers->ContextRecord->Rbp; + stack.AddrStack.Offset = pointers->ContextRecord->Rsp; + stack.AddrPC.Mode = AddrModeFlat; + stack.AddrFrame.Mode = AddrModeFlat; + stack.AddrStack.Mode = AddrModeFlat; + + HANDLE hProcess = GetCurrentProcess(); + HMODULE hModule = GetCurrentModule(); + HANDLE hThread = GetCurrentThread(); + + MODULEINFO mi; + GetModuleInformation(hProcess, hModule, &mi, sizeof(mi)); + + // @Note: We could at some point have enough code (in theroy) to generate more than one .text section. + // 4 should be plenty + struct TextRegion { + uintptr_t start = 0; + uintptr_t end = 0; + }; + TextRegion codeSegs[4] = {}; + uint8_t numSegs = 0; + + const auto moduleBaseAddr = (uintptr_t)hModule; + const auto ntHeader = ImageNtHeader(hModule); + auto sec = IMAGE_FIRST_SECTION(ntHeader); + for (auto i = 0; i < ntHeader->FileHeader.NumberOfSections; i++) { + if (memcmp(sec->Name, ".text\x00\x00\x00", 8) == 0) { + codeSegs[numSegs++] = { moduleBaseAddr+sec->VirtualAddress, moduleBaseAddr+sec->Misc.VirtualSize }; + if (numSegs >= 4) break; + } + sec++; + } + + auto ctx = *pointers->ContextRecord; + + do { + if (stack.AddrPC.Offset != 0) { + // If PC is anywhere in the range of our .text section(s), invoke the mini dump + for (auto seg = 0; seg < numSegs; seg++) { + if (stack.AddrPC.Offset >= codeSegs[seg].start && stack.AddrPC.Offset <= codeSegs[seg].end) { + WriteMiniDump(pointers); + __fastfail(FAST_FAIL_FATAL_APP_EXIT); + break; + } + } + } + + } while (StackWalk64( + IMAGE_FILE_MACHINE_AMD64, + hProcess, + hThread, + &stack, + &ctx, + NULL, + SymFunctionTableAccess64, + SymGetModuleBase64, + NULL + )); + + return EXCEPTION_CONTINUE_SEARCH; +} + +static HANDLE vehHandle = nullptr; +void Debug::InstallMiniDumpHandler() noexcept { + assert(!vehHandle); + // @Note: A process that calls SymInitialize should not call it again unless it calls SymCleanup first. + // I don't see a way to query if the symbol handler has already been initialized. + // In testing, calling it twice and then causing an exception seems to still function, and the only other mod + // which uses dbghelp as far as i'm aware is the net script framework's crash log, which seems to function normally. + SymInitialize(GetCurrentProcess(), NULL, TRUE); + SymSetOptions(SYMOPT_LOAD_LINES); + vehHandle = AddVectoredExceptionHandler(1, VectorHandler); +} + +void Debug::RemoveMiniDumpHandler() noexcept { + if (vehHandle) + RemoveVectoredExceptionHandler(vehHandle); + vehHandle = nullptr; +} +#endif \ No newline at end of file diff --git a/SmoothCam/source/debug/registry.cpp b/SmoothCam/source/debug/registry.cpp new file mode 100644 index 0000000..da847d3 --- /dev/null +++ b/SmoothCam/source/debug/registry.cpp @@ -0,0 +1,23 @@ +#ifdef _DEBUG +#include "debug/registry.h" +#include "debug/ICommand.h" + +Debug::CommandRegistry* Debug::CommandRegistry::Get() noexcept { + static CommandRegistry registry; + return ®istry; +} + +void Debug::CommandRegistry::Register(eastl::unique_ptr&& command) noexcept { + registry.insert({ command->GetName(), std::move(command) }); +} + +const Debug::CommandRegistry::CommandTable& Debug::CommandRegistry::GetCommands() const noexcept { + return registry; +} + +Debug::ICommand* Debug::CommandRegistry::Find(const eastl::string& name) const noexcept { + auto it = registry.find(name); + if (it == registry.end()) return nullptr; + return it->second.get(); +} +#endif \ No newline at end of file diff --git a/SmoothCam/source/detours.cpp b/SmoothCam/source/detours.cpp index c6d1f8c..63b9a43 100644 --- a/SmoothCam/source/detours.cpp +++ b/SmoothCam/source/detours.cpp @@ -1,61 +1,53 @@ #include "detours.h" #include "camera.h" #include "arrow_fixes.h" +#include "debug/eh.h" -static PLH::VFuncMap origVFuncs_PlayerInput; -static PLH::VFuncMap origVFuncs_MenuOpenClose; - -static std::unique_ptr playerInputHooks; -static std::unique_ptr menuOpenCloseHooks; - -extern std::shared_ptr g_theCamera; - -#define CAMERA_UPDATE_DETOUR_IMPL(name) \ -static PLH::VFuncMap origVFuncs_##name##; \ -void __fastcall mCameraStateUpdate##name##( \ - TESCameraState* pThis, BSTSmartPointer& nextState) \ -{ \ - GameTime::StepFrameTime(); \ - auto player = *g_thePlayer; \ - auto camera = CorrectedPlayerCamera::GetSingleton(); \ - player->IncRef(); \ - if (g_theCamera != nullptr) { \ - if (g_theCamera->PreGameUpdate(player, camera, nextState)) { \ - player->DecRef(); \ - return; \ - } \ - } \ - reinterpret_cast(origVFuncs_##name##.at(3))(pThis, nextState); \ - if (g_theCamera != nullptr) { \ - g_theCamera->UpdateCamera(player, camera, nextState); \ - } \ - player->DecRef(); \ -} +extern eastl::unique_ptr g_theCamera; + +static eastl::unique_ptr> cameraUpdateHooks; +static eastl::unique_ptr> playerInputHook; +static eastl::unique_ptr>> menuOpenCloseHook; + +// Camera update +typedef void(__thiscall* CameraOnUpdate)(TESCameraState*, BSTSmartPointer&); +void mCameraUpdate(TESCameraState* state, BSTSmartPointer& nextState) { + const auto mdmp = Debug::MiniDumpScope(); + + GameTime::StepFrameTime(); + + auto player = *g_thePlayer; + player->IncRef(); + auto camera = CorrectedPlayerCamera::GetSingleton(); + + if (g_theCamera) { + if (g_theCamera->PreGameUpdate(player, camera, nextState)) { + player->DecRef(); + return; + } + } + + // TPS1&2 share the same vtable + TESCameraState* selector; + if (state == camera->cameraStates[CorrectedPlayerCamera::kCameraState_ThirdPerson1]) + selector = camera->cameraStates[CorrectedPlayerCamera::kCameraState_ThirdPerson2]; + else + selector = state; -#define DO_CAMERA_UPDATE_DETOUR_IMPL(name, state) \ -static auto detour_##name## = Detours::CameraStateDetour( \ - CorrectedPlayerCamera::GetSingleton()->cameraStates[state], \ - static_cast(3), \ - reinterpret_cast(&mCameraStateUpdate##name##), \ - origVFuncs_##name## \ -); - -CAMERA_UPDATE_DETOUR_IMPL(FPS); -CAMERA_UPDATE_DETOUR_IMPL(TPS); -CAMERA_UPDATE_DETOUR_IMPL(Dragon); -CAMERA_UPDATE_DETOUR_IMPL(Horse); -CAMERA_UPDATE_DETOUR_IMPL(Tween); -CAMERA_UPDATE_DETOUR_IMPL(VATS); -CAMERA_UPDATE_DETOUR_IMPL(Free); -CAMERA_UPDATE_DETOUR_IMPL(Vanity); -CAMERA_UPDATE_DETOUR_IMPL(Furniture); -CAMERA_UPDATE_DETOUR_IMPL(Bleedout); -CAMERA_UPDATE_DETOUR_IMPL(Transition); + cameraUpdateHooks->GetBase(selector, 3)(state, nextState); + + if (g_theCamera) + g_theCamera->UpdateCamera(player, camera, nextState); + + player->DecRef(); +} // POV Handler typedef uintptr_t(__thiscall* OnInput)(PlayerInputHandler*, InputEvent*); -uintptr_t __fastcall mOnInput(PlayerInputHandler* pThis, InputEvent* input) { +uintptr_t mOnInput(PlayerInputHandler* pThis, InputEvent* input) { if (input) { + const auto mdmp = Debug::MiniDumpScope(); + switch (input->eventType) { case InputEvent::kEventType_Button: { const auto ev = reinterpret_cast(input); @@ -77,13 +69,19 @@ uintptr_t __fastcall mOnInput(PlayerInputHandler* pThis, InputEvent* input) { break; } } - return reinterpret_cast(origVFuncs_PlayerInput[1])(pThis, input); + return playerInputHook->GetBase(1)(pThis, input); } -typedef EventResult(__thiscall* MenuOpenCloseHandler)(uintptr_t pThis, MenuOpenCloseEvent* ev, EventDispatcher* dispatcher); -EventResult __fastcall mMenuOpenCloseHandler(uintptr_t pThis, MenuOpenCloseEvent* ev, EventDispatcher* dispatcher) { +typedef EventResult(__thiscall* MenuOpenCloseHandler)(uintptr_t pThis, MenuOpenCloseEvent* ev, + EventDispatcher* dispatcher); +EventResult mMenuOpenCloseHandler(uintptr_t pThis, MenuOpenCloseEvent* ev, + EventDispatcher* dispatcher) +{ if (pThis == (uintptr_t)&(*g_thePlayer)->menuOpenCloseEvent) { + const auto mdmp = Debug::MiniDumpScope(); + if (ev->menuName.data && g_theCamera) { + DebugPrint("Menu %s is %s\n", ev->menuName.data, ev->opening ? "opening" : "closing"); auto id = Camera::MenuID::None; if (strcmp(ev->menuName.data, "Dialogue Menu") == 0) @@ -96,59 +94,56 @@ EventResult __fastcall mMenuOpenCloseHandler(uintptr_t pThis, MenuOpenCloseEvent id = Camera::MenuID::FaderMenu; else if (strcmp(ev->menuName.data, "LoadWaitSpinner") == 0) id = Camera::MenuID::LoadWaitSpinner; + else if (strcmp(ev->menuName.data, "MapMenu") == 0) + id = Camera::MenuID::MapMenu; + else if (strcmp(ev->menuName.data, "InventoryMenu") == 0) + id = Camera::MenuID::InventoryMenu; if (id != Camera::MenuID::None) g_theCamera->OnMenuOpenClose(id, ev); } } - return reinterpret_cast(origVFuncs_MenuOpenClose[1])(pThis, ev, dispatcher); + return menuOpenCloseHook->GetBase(1)(pThis, ev, dispatcher); } bool Detours::Attach() { GameTime::Initialize(); - { - playerInputHooks = std::make_unique( - (uint64_t)PlayerControls::GetSingleton()->togglePOVHandler, - PLH::VFuncMap{ - { static_cast(1), reinterpret_cast(&mOnInput) }, - }, - &origVFuncs_PlayerInput - ); - - if (!playerInputHooks->hook()) { - _ERROR("Failed to place detour on target virtual function(togglePOVHandler), this error is fatal."); - FatalError(L"Failed to place detour on target virtual function(togglePOVHandler), this error is fatal."); - } + playerInputHook = eastl::make_unique>(PlayerControls::GetSingleton()->togglePOVHandler); + playerInputHook->Add(1, mOnInput); + if (!playerInputHook->Attach()) { + _ERROR("Failed to place detour on target virtual function(togglePOVHandler), this error is fatal."); + FatalError(L"Failed to place detour on target virtual function(togglePOVHandler), this error is fatal."); } - { - // Intercept menu open/close events - menuOpenCloseHooks = std::make_unique( - (uint64_t)&(*g_thePlayer)->menuOpenCloseEvent, - PLH::VFuncMap{ - { static_cast(1), reinterpret_cast(&mMenuOpenCloseHandler) }, - }, - &origVFuncs_MenuOpenClose - ); - - if (!menuOpenCloseHooks->hook()) { - _ERROR("Failed to place detour on target virtual function(menuOpenCloseEvent), this error is fatal."); - FatalError(L"Failed to place detour on target virtual function(menuOpenCloseEvent), this error is fatal."); - } + menuOpenCloseHook = eastl::make_unique>>(&(*g_thePlayer)->menuOpenCloseEvent); + menuOpenCloseHook->Add(1, mMenuOpenCloseHandler); + if (!menuOpenCloseHook->Attach()) { + _ERROR("Failed to place detour on target virtual function(togglePOVHandler), this error is fatal."); + FatalError(L"Failed to place detour on target virtual function(togglePOVHandler), this error is fatal."); } - DO_CAMERA_UPDATE_DETOUR_IMPL(FPS, CorrectedPlayerCamera::kCameraState_FirstPerson); - DO_CAMERA_UPDATE_DETOUR_IMPL(TPS, CorrectedPlayerCamera::kCameraState_ThirdPerson2); - DO_CAMERA_UPDATE_DETOUR_IMPL(Dragon, CorrectedPlayerCamera::kCameraState_Dragon); - DO_CAMERA_UPDATE_DETOUR_IMPL(Horse, CorrectedPlayerCamera::kCameraState_Horse); - DO_CAMERA_UPDATE_DETOUR_IMPL(Tween, CorrectedPlayerCamera::kCameraState_TweenMenu); - DO_CAMERA_UPDATE_DETOUR_IMPL(VATS, CorrectedPlayerCamera::kCameraState_VATS); - DO_CAMERA_UPDATE_DETOUR_IMPL(Free, CorrectedPlayerCamera::kCameraState_Free); - DO_CAMERA_UPDATE_DETOUR_IMPL(Vanity, CorrectedPlayerCamera::kCameraState_AutoVanity); - DO_CAMERA_UPDATE_DETOUR_IMPL(Furniture, CorrectedPlayerCamera::kCameraState_Furniture); - DO_CAMERA_UPDATE_DETOUR_IMPL(Bleedout, CorrectedPlayerCamera::kCameraState_Bleedout); - DO_CAMERA_UPDATE_DETOUR_IMPL(Transition, CorrectedPlayerCamera::kCameraState_Transition); + auto states = CorrectedPlayerCamera::GetSingleton()->cameraStates; + cameraUpdateHooks = eastl::make_unique>(); + cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_FirstPerson], 3, mCameraUpdate); + //cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_ThirdPerson1], 3, mCameraUpdate); // 1 and 2 share the same vtbl + cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_ThirdPerson2], 3, mCameraUpdate); + cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_Dragon], 3, mCameraUpdate); + cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_Horse], 3, mCameraUpdate); + cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_TweenMenu], 3, mCameraUpdate); + cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_VATS], 3, mCameraUpdate); + cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_Free], 3, mCameraUpdate); + cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_AutoVanity], 3, mCameraUpdate); + cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_Furniture], 3, mCameraUpdate); + cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_Bleedout], 3, mCameraUpdate); + cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_Transition], 3, mCameraUpdate); + cameraUpdateHooks->Add(states[CorrectedPlayerCamera::kCameraState_IronSights], 3, mCameraUpdate); + + DebugPrint("Hooking camera state update methods\n"); + if (!cameraUpdateHooks->Attach()) { + _ERROR("Failed to place detour on target virtual function(TESCameraState Update), this error is fatal."); + FatalError(L"Failed to place detour on target virtual function(TESCameraState Update), this error is fatal."); + } return ArrowFixes::Attach(); } \ No newline at end of file diff --git a/SmoothCam/source/firstperson.cpp b/SmoothCam/source/firstperson.cpp new file mode 100644 index 0000000..f874a9a --- /dev/null +++ b/SmoothCam/source/firstperson.cpp @@ -0,0 +1,175 @@ +#include "firstperson.h" + +//1406a1820:39401 +typedef uintptr_t(*SwitchSkeleton)(Actor*, byte); +static eastl::unique_ptr> detSwitchSkeleton; +static bool tpSkeletonVisible = true; +uintptr_t mSwitchSkeleton(Actor* actor, byte param_2) { + tpSkeletonVisible = !param_2; + return detSwitchSkeleton->GetBase()(actor, param_2); +} + +Camera::Firstperson::Firstperson(Camera* baseCamera) : ICamera(baseCamera, CameraID::Firstperson) { +#ifdef DEVELOPER + config = Config::GetCurrentConfig(); + + // Game is switching player skeleton + detSwitchSkeleton = eastl::make_unique>( + 39401, + mSwitchSkeleton + ); + detSwitchSkeleton->Attach(); +#endif +} + +Camera::Firstperson::~Firstperson() {} + +void Camera::Firstperson::OnBegin(PlayerCharacter* player, CorrectedPlayerCamera* camera, ICamera* lastState) noexcept { +#ifdef DEVELOPER + if (m_camera->currentFocusObject == player) + ToggleThirdpersonSkeleton(true); +#endif +} + +void Camera::Firstperson::OnEnd(PlayerCharacter* player, CorrectedPlayerCamera* camera, ICamera* newState) noexcept { +#ifdef DEVELOPER + if (m_camera->currentFocusObject == player) { + ToggleThirdpersonSkeleton(true); + HidePlayerHead(false); + } +#endif +} + +bool Camera::Firstperson::OnPreGameUpdate(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState) +{ + return false; +} + +void Camera::Firstperson::OnUpdateCamera(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState) +{ +#ifdef DEVELOPER + auto state = reinterpret_cast(camera->cameraState); + if (!state->cameraObj) return; + if (!m_camera->currentFocusObject->loadedState || !m_camera->currentFocusObject->loadedState->node) return; + + // Make sure the thirdperson skeleton is visible + if (!tpSkeletonVisible) + ToggleThirdpersonSkeleton(true); + + // Make sure the player's head remains hidden + HidePlayerHead(true); + + // Position & trackir stuff + auto refPos = glm::vec3{ + state->cameraObj->m_worldTransform.pos.x, + state->cameraObj->m_worldTransform.pos.y, + state->cameraObj->m_worldTransform.pos.z + }; + + // Extract camera rotation + auto rot = mmath::NiMatrixToEuler(m_camera->cameraNi->m_worldTransform.rot); + // Current TrackIR data + const auto& tracking = m_camera->trackIRData; + + auto node = m_camera->currentFocusObject->loadedState->node->GetObjectByName(&Strings.headPositionTarget.data); + if (node) { + glm::vec3 headPos = { + node->m_worldTransform.pos.x, + node->m_worldTransform.pos.y, + node->m_worldTransform.pos.z + }; + + // And player basis vectors + auto obj = m_camera->currentFocusObject; + glm::vec3 f, s, u, coef; + mmath::DecomposeToBasis( + refPos, + { obj->rot.x, obj->rot.y, obj->rot.z }, + f, s, u, coef + ); + + // Position + // @TODO: This is kind of crap, can we get some form of body IK going here? + constexpr auto scale = 0.01f; + glm::vec3 trackPos = tracking.pos * scale; + headPos += (f * -trackPos.x) + (s * -trackPos.z) + (u * trackPos.y); + + m_camera->SetPosition(headPos, camera); + } + + // Rotation + glm::vec3 trackRot = glm::radians(tracking.rot); + glm::vec3 rotation = rot + glm::vec3{ trackRot.x, -trackRot.y, trackRot.z }; + + auto rm = glm::identity(); + rm = glm::rotate(rm, -mmath::half_pi + rotation.z, { 1.0f, 0.0f, 0.0f }); + rm = glm::rotate(rm, -rotation.x -mmath::half_pi, { 0.0f, 1.0f, 0.0f }); + rm = glm::rotate(rm, rotation.y - mmath::half_pi, { 0.0f, 0.0f, 1.0f }); + + NiMatrix33 cameraNiT; + cameraNiT.data[0][0] = rm[0][0]; + cameraNiT.data[0][1] = rm[0][1]; + cameraNiT.data[0][2] = rm[0][2]; + cameraNiT.data[1][0] = rm[1][0]; + cameraNiT.data[1][1] = rm[1][1]; + cameraNiT.data[1][2] = rm[1][2]; + cameraNiT.data[2][0] = rm[2][0]; + cameraNiT.data[2][1] = rm[2][1]; + cameraNiT.data[2][2] = rm[2][2]; + m_camera->cameraNi->m_worldTransform.rot = cameraNiT; + + NiMatrix33 cameraNodeT; + cameraNodeT.arr[1] = cameraNiT.arr[0]; + cameraNodeT.arr[2] = cameraNiT.arr[1]; + cameraNodeT.arr[0] = cameraNiT.arr[2]; + cameraNodeT.arr[4] = cameraNiT.arr[3]; + cameraNodeT.arr[5] = cameraNiT.arr[4]; + cameraNodeT.arr[3] = cameraNiT.arr[5]; + cameraNodeT.arr[7] = cameraNiT.arr[6]; + cameraNodeT.arr[8] = cameraNiT.arr[7]; + cameraNodeT.arr[6] = cameraNiT.arr[8]; + camera->cameraNode->m_worldTransform.rot = cameraNodeT; + camera->cameraNode->m_localTransform.rot = cameraNodeT; + + // Update w2s matrix, required for correct culling + mmath::Rotation camRot; + camRot.SetEuler(rotation.x, rotation.y); + m_camera->UpdateInternalWorldToScreenMatrix(camRot, camera); +#endif +} + +void Camera::Firstperson::Render(Render::D3DContext& ctx) noexcept {} + +void Camera::Firstperson::OnTogglePOV(const ButtonEvent* ev) noexcept {} + +bool Camera::Firstperson::OnKeyPress(const ButtonEvent* ev) noexcept { + return false; +} + +bool Camera::Firstperson::OnMenuOpenClose(MenuID id, const MenuOpenCloseEvent* const ev) noexcept { + return false; +} + +void Camera::Firstperson::OnCameraActionStateTransition(const PlayerCharacter* player, const CameraActionState newState, + const CameraActionState oldState) noexcept +{} + +void Camera::Firstperson::OnCameraStateTransition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, + const GameState::CameraState newState, const GameState::CameraState oldState) noexcept +{} + +void Camera::Firstperson::ToggleThirdpersonSkeleton(bool show) noexcept { + detSwitchSkeleton->GetBase()(m_camera->currentFocusObject, show ? 0 : 1); + tpSkeletonVisible = show; +} + +void Camera::Firstperson::HidePlayerHead(bool hide) noexcept { + if (!m_camera->currentFocusObject) return; + if (m_camera->currentFocusObject->loadedState && m_camera->currentFocusObject->loadedState->node) { + auto faceNode = m_camera->currentFocusObject->loadedState->node->GetObjectByName(&Strings.faceNode.data); + if (faceNode) + reinterpret_cast*>(&faceNode->m_flags)->set(0, hide); + } +} \ No newline at end of file diff --git a/SmoothCam/source/game_state.cpp b/SmoothCam/source/game_state.cpp index 2b1d058..6088d71 100644 --- a/SmoothCam/source/game_state.cpp +++ b/SmoothCam/source/game_state.cpp @@ -1,48 +1,30 @@ #include "game_state.h" +extern HMODULE hImprovedCamera; + // Returns the bits for player->actorState->flags04 which appear to convey movement info -const std::bitset<32> GameState::GetPlayerMovementBits(const Actor* player) noexcept { - const auto bits = std::bitset<32>(player->actorState.flags04); -#ifdef _DEBUG - // Just to see what actions end up setting these unknown bits - for (int i = 0; i < 32; i++) { - if (bits[i]) { - auto it = knownMovementBits.find(i); - if (it == knownMovementBits.end()) { - __debugbreak(); - } - } - } -#endif +const eastl::bitset<32> GameState::GetPlayerMovementBits(const Actor* player) noexcept { + const auto bits = eastl::bitset<32>(player->actorState.flags04); return bits; } // Returns the bits for player->actorState->flags08 which appear to convey action info -const std::bitset<32> GameState::GetPlayerActionBits(const Actor* player) noexcept { - const auto bits = std::bitset<32>(player->actorState.flags08); -#ifdef _DEBUG - // Just to see what actions end up setting these unknown bits - for (int i = 0; i < 32; i++) { - if (bits[i]) { - auto it = knownActionBits.find(i); - if (it == knownActionBits.end()) { - __debugbreak(); - } - } - } -#endif +const eastl::bitset<32> GameState::GetPlayerActionBits(const Actor* player) noexcept { + const auto bits = eastl::bitset<32>(player->actorState.flags08); return bits; } const bool GameState::IC_InFirstPersonState(const TESObjectREFR* ref, const CorrectedPlayerCamera* camera) noexcept { - static BSFixedString faceGen = "BSFaceGenNiNodeSkinned"; - if (!ref->loadedState || !ref->loadedState->node || !faceGen.data) return false; - - const auto npc = ref->loadedState->node->GetObjectByName(&faceGen.data); - if (!npc) return false; + // ImprovedCamera compat is kinda a nightmare - let's choose violence + // IC 1.0.0.4 - > 18004d510 = g_isThirdPerson + if (hImprovedCamera != NULL) { + const auto g_isThirdPerson = reinterpret_cast( + reinterpret_cast(hImprovedCamera) + 0x4d510 + ); + return *g_isThirdPerson; + } - const auto bits = std::bitset<32>(npc->m_flags); - return !bits[26]; + return false; } const bool GameState::IFPV_InFirstPersonState(const TESObjectREFR* ref, const CorrectedPlayerCamera* camera) noexcept { @@ -52,7 +34,7 @@ const bool GameState::IFPV_InFirstPersonState(const TESObjectREFR* ref, const Co const auto npc = ref->loadedState->node->GetObjectByName(&faceGen.data); if (!npc) return false; - const auto bits = std::bitset<32>(npc->m_flags); + const auto bits = eastl::bitset<32>(npc->m_flags); return bits[0]; } @@ -190,7 +172,7 @@ const GameState::CameraState GameState::GetCameraState(const Actor* player, cons const bool GameState::IsWeaponDrawn(const Actor* player) noexcept { const auto bits = GameState::GetPlayerActionBits(player); - return /*bits[5] &&*/ bits[6]; // Looks like bit 5 flips when switching weapons/magic, which we want to ignore. + return bits[6]; } // Get an equipped weapon @@ -207,6 +189,21 @@ const TESObjectWEAP* GameState::GetEquippedWeapon(const Actor* player, bool left return reinterpret_cast(wep); } +const TESAmmo* GameState::GetCurrentAmmo(const Actor* player) noexcept { + typedef TESAmmo*(__thiscall Actor::* GetAmmo)() const; + return (player->*reinterpret_cast(&Actor::Unk_9E))(); +} + +float GameState::GetCurrentBowDrawTimer(const PlayerCharacter* player) noexcept { + // @Note: Read projectile->unk188 + // This is the arrow draw duration/shot power, stored on the player in a small array used as a stack + // This grows, with the top value being the current timer - until an arrow is fired, clearing the stack + const auto arr = reinterpret_cast(&(player->unkBA0)); + float drawTimerValue = arr->size > 0 ? glm::min(arr->top().bowDrawTime, 1.0f) : 1.0f; + if (!mmath::IsValid(drawTimerValue)) drawTimerValue = 1.0f; + return drawTimerValue; +} + bool GameState::IsUsingMagicItem(const Actor* player, bool leftHand) noexcept { if (leftHand && !player->leftHandSpell) return false; if (!leftHand && !player->rightHandSpell) return false; @@ -253,10 +250,8 @@ const bool GameState::IsMeleeWeaponDrawn(const Actor* player) noexcept { // Continue to look for a staff here - consider a non-enchanted staff to be melee if (right) { if (right->gameData.type != TESObjectWEAP::GameData::kType_Bow && - /*right->gameData.type != TESObjectWEAP::GameData::kType_Staff &&*/ right->gameData.type != TESObjectWEAP::GameData::kType_CrossBow && right->gameData.type != TESObjectWEAP::GameData::kType_Bow2 && - /*right->gameData.type != TESObjectWEAP::GameData::kType_Staff2 &&*/ right->gameData.type != TESObjectWEAP::GameData::kType_CBow) { return true; @@ -265,10 +260,8 @@ const bool GameState::IsMeleeWeaponDrawn(const Actor* player) noexcept { if (left) { if (left->gameData.type != TESObjectWEAP::GameData::kType_Bow && - /*left->gameData.type != TESObjectWEAP::GameData::kType_Staff &&*/ left->gameData.type != TESObjectWEAP::GameData::kType_CrossBow && left->gameData.type != TESObjectWEAP::GameData::kType_Bow2 && - /*left->gameData.type != TESObjectWEAP::GameData::kType_Staff2 &&*/ left->gameData.type != TESObjectWEAP::GameData::kType_CBow) { return true; @@ -409,7 +402,7 @@ const bool GameState::IsBowDrawn(const Actor* player) noexcept { static bool drawnLastCall = false; static bool objective = false; - if (!movementBits[31] || movementBits[30]/* || actionBits[3]*/) { + if (!movementBits[31] || movementBits[30]) { drawnLastCall = objective = false; return false; } diff --git a/SmoothCam/source/main.cpp b/SmoothCam/source/main.cpp index dfc6214..efb149f 100644 --- a/SmoothCam/source/main.cpp +++ b/SmoothCam/source/main.cpp @@ -1,4 +1,13 @@ #include "main.h" +#include "trackir/trackir.h" +#include "render/shader_cache.h" +#include "debug/eh.h" +#include +#include + +#ifdef DEBUG +#include "arrow_fixes.h" +#endif PluginHandle g_pluginHandle = kPluginHandle_Invalid; const SKSEMessagingInterface* g_messaging = nullptr; @@ -6,10 +15,162 @@ const SKSEPapyrusInterface* g_papyrus = nullptr; static bool hooked = false; static bool d3dHooked = false; -std::shared_ptr g_theCamera = nullptr; +eastl::unique_ptr g_theCamera = nullptr; +#ifdef WITH_D2D +eastl::unique_ptr g_D2D = nullptr; +#endif + +// Module handle to improved camera, if it is loaded +HMODULE hImprovedCamera = NULL; +uint8_t improvedCameraStatus = 1; + +// Let's be nice and (try to) cleanly release our resources +// Do this here before the game nukes com +typedef uintptr_t(*CalledDuringRenderShutdown)(); +static eastl::unique_ptr> detCalledDuringRenderShutdown; +static uintptr_t mCalledDuringRenderShutdown() { + DebugPrint("Shutting down...\n"); + +#ifdef EMIT_MINIDUMPS + DebugPrint("Removing minidump handler\n"); + Debug::RemoveMiniDumpHandler(); +#endif + + if (hooked) { + DebugPrint("Freeing the camera\n"); + g_theCamera.reset(); + } + +#ifdef WITH_D2D + DebugPrint("Freeing Direct2D\n"); + g_D2D.reset(); +#endif + +#ifdef DEVELOPER + if (TrackIR::IsRunning()) { + DebugPrint("Shutting down TrackIR\n"); + TrackIR::Shutdown(); + } +#endif + +#ifdef DEBUG + ArrowFixes::Shutdown(); +#endif + + DebugPrint("Shutting down the rendering subsystem\n"); + Render::ShaderCache::Get().Release(); + Render::Shutdown(); + + DebugPrint("SmoothCam shutdown, continue with game shutdown...\n"); + return detCalledDuringRenderShutdown->GetBase()(); +} + +void attachD3DHooks() { + if (!d3dHooked) { + // Hook the shutdown function first + { + const auto mdmp = Debug::MiniDumpScope(); + _MESSAGE("Hooking render shutdown method\n"); + detCalledDuringRenderShutdown = eastl::make_unique>( + 75446, + mCalledDuringRenderShutdown + ); + if (!detCalledDuringRenderShutdown->Attach()) { + _ERROR("Failed to place detour on target function(Render Shutdown), this error is fatal."); + FatalError(L"Failed to place detour on target function(Render Shutdown), this error is fatal."); + } + } + + // Wait until now to ensure D3D is loaded and we aren't racing it + _MESSAGE("Hooking D3D11\n"); + Render::InstallHooks(); + if (!Render::HasContext()) { + WarningPopup(L"SmoothCam: Failed to hook DirectX, Rendering features will be disabled. Try running with overlay software disabled if this warning keeps occurring."); + } + d3dHooked = true; + #ifdef WITH_D2D -std::unique_ptr g_D2D = nullptr; + if (Render::HasContext()) { + const auto mdmp = Debug::MiniDumpScope(); + _MESSAGE("Initializing Direct2D\n"); + g_D2D = eastl::make_unique(Render::GetContext()); + } +#endif + +#ifdef DEVELOPER + if (Render::HasContext()) { + const auto mdmp = Debug::MiniDumpScope(); + const auto result = TrackIR::Initialize(Render::GetContext().hWnd); + if (result != TrackIR::NPResult::OK) { + _MESSAGE("Failed to load TrackIR interface"); + } else { + _MESSAGE("TrackIR is running."); + } + } #endif + } +} + +// Try to locate ImprovedCamera in memory and validate the exact file +// We are looking for the beta 4 release version posted on reddit, no +// other version should pass this check. Kind of upset I have to read +// a memory address for full compatibility but whatever. +enum class ICCheckResult { + OK = 0, + NOT_FOUND = 1, + VERSION_MISMATCH = 2 +}; + +namespace ICSignatures { + constexpr const DWORD SizeOfImage = 0x00054000; + constexpr const DWORD Signature = 0x00004550; + constexpr const DWORD AddressOfEntryPoint = 0x0001b0a4; + constexpr const DWORD TimeDateStamp = 0x5d3e15f0; + constexpr const DWORD FileVersion[4] = { 1, 0, 0, 4 }; +}; + +ICCheckResult loadImprovedCameraHandle() { + auto hMod = GetModuleHandle(L"ImprovedCamera.dll"); + if (hMod == NULL) return ICCheckResult::NOT_FOUND; + MODULEINFO mi; + GetModuleInformation(GetCurrentProcess(), hMod, &mi, sizeof(mi)); + if (mi.SizeOfImage != ICSignatures::SizeOfImage) return ICCheckResult::VERSION_MISMATCH; + + const auto ntHeader = ImageNtHeader(hMod); + if (ntHeader->Signature != ICSignatures::Signature || + ntHeader->OptionalHeader.AddressOfEntryPoint != ICSignatures::AddressOfEntryPoint || + ntHeader->FileHeader.TimeDateStamp != ICSignatures::TimeDateStamp) + return ICCheckResult::VERSION_MISMATCH; + + DWORD dwHandle; + auto sz = GetFileVersionInfoSize(L"ImprovedCamera.dll", &dwHandle); + if (sz != 0) { + LPSTR verData = (LPSTR)malloc(sizeof(char) * sz); + + if (GetFileVersionInfo(L"ImprovedCamera.dll", dwHandle, sz, verData)) { + LPBYTE lpBuffer = NULL; + UINT size = 0; + if (VerQueryValue(verData, L"\\", reinterpret_cast(&lpBuffer), &size) && size) { + VS_FIXEDFILEINFO* verInfo = reinterpret_cast(lpBuffer); + if (verInfo->dwSignature == 0xfeef04bd) { + auto v0 = (verInfo->dwFileVersionMS >> 16) & 0xffff; + auto v1 = (verInfo->dwFileVersionMS >> 0) & 0xffff; + auto v2 = (verInfo->dwFileVersionLS >> 16) & 0xffff; + auto v3 = (verInfo->dwFileVersionLS >> 0) & 0xffff; + + // Now check for our match + if (v0 == ICSignatures::FileVersion[0] && v1 == ICSignatures::FileVersion[1] && + v2 == ICSignatures::FileVersion[2] && v3 == ICSignatures::FileVersion[3]) + hImprovedCamera = hMod; + } + } + } + + free(verData); + } + + return hImprovedCamera != NULL ? ICCheckResult::OK : ICCheckResult::VERSION_MISMATCH; +} #pragma warning( push ) #pragma warning( disable : 26461 ) // skse function pointer is not const @@ -17,27 +178,26 @@ void SKSEMessageHandler(SKSEMessagingInterface::Message* message) { switch (message->type) { case SKSEMessagingInterface::kMessage_NewGame: case SKSEMessagingInterface::kMessage_PostLoadGame: { + // @Note: coc from the main menu does not fire post load game + + // @Note: New game does not fire preloadgame + if (message->type == SKSEMessagingInterface::kMessage_NewGame) + attachD3DHooks(); + // The game has loaded, go ahead and hook the camera now if (!hooked) { - g_theCamera = std::make_shared(); + const auto mdmp = Debug::MiniDumpScope(); + + _MESSAGE("Creating the camera\n"); + g_theCamera = eastl::make_unique(); + _MESSAGE("Attaching detours\n"); hooked = Detours::Attach(); } + break; } case SKSEMessagingInterface::kMessage_PreLoadGame: { - if (!d3dHooked) { - // Wait until now to ensure D3D is loaded and we aren't racing it - Render::InstallHooks(); - if (!Render::HasContext()) { - WarningPopup(L"SmoothCam: Failed to hook DirectX, Rendering features will be disabled. Try running with overlay software disabled if this warning keeps occurring."); - } - -#ifdef WITH_D2D - if (Render::HasContext()) - g_D2D = std::make_unique(Render::GetContext()); -#endif - d3dHooked = true; - } + attachD3DHooks(); break; } default: @@ -46,32 +206,13 @@ void SKSEMessageHandler(SKSEMessagingInterface::Message* message) { } #pragma warning( pop ) -// Let's be nice and (try to) cleanly release our resources -// Do this here before the game nukes com -typedef uintptr_t(*CalledDuringRenderShutdown)(); -static CalledDuringRenderShutdown fnCalledDuringRenderShutdown; -static std::unique_ptr detCalledDuringRenderShutdown; -static uintptr_t mCalledDuringRenderShutdown() { - if (hooked) - g_theCamera.reset(); - -#ifdef WITH_D2D - g_D2D.reset(); -#endif - Render::Shutdown(); - - return fnCalledDuringRenderShutdown(); -} - extern "C" { __declspec(dllexport) bool SKSEPlugin_Query(const SKSEInterface* skse, PluginInfo* info) { gLog.OpenRelative(CSIDL_MYDOCUMENTS, "\\My Games\\Skyrim Special Edition\\SKSE\\SmoothCam.log"); gLog.SetPrintLevel(IDebugLog::kLevel_DebugMessage); gLog.SetLogLevel(IDebugLog::kLevel_DebugMessage); -#ifdef _DEBUG - _DMESSAGE("SmoothCam plugin query begin"); -#endif + _MESSAGE("SKSEPlugin_Query begin"); if (!Offsets::Initialize()) { _ERROR("Failed to load game offset database. Visit https://www.nexusmods.com/skyrimspecialedition/mods/32444 to aquire the correct database file."); @@ -79,13 +220,9 @@ extern "C" { return false; } -//#ifdef _DEBUG -// Offsets::DumpDatabaseTextFile(); -//#endif - info->infoVersion = PluginInfo::kInfoVersion; info->name = "SmoothCam"; - info->version = 12; + info->version = 13; g_pluginHandle = skse->GetPluginHandle(); @@ -96,11 +233,39 @@ extern "C" { if (skse->runtimeVersion != RUNTIME_VERSION_1_5_97) { _WARNING("This module was compiled for skse 1.5.97, you are running an unsupported version. You may experience crashes or other strange issues."); } - + + _MESSAGE("Allowing module to load"); return true; } __declspec(dllexport) bool SKSEPlugin_Load(const SKSEInterface* skse) { + _MESSAGE("SKSEPlugin_Load begin"); + + _MESSAGE("Reading config file"); + Config::ReadConfigFile(); + +#ifdef EMIT_MINIDUMPS + if (Config::GetCurrentConfig()->enableCrashDumps) { + _MESSAGE("Installing vectored minidump exception handler"); + Debug::InstallMiniDumpHandler(); + } +#endif + const auto mdmp = Debug::MiniDumpScope(); + +#ifdef _DEBUG + Debug::StartREPL(); +#endif + + _MESSAGE("Caching offset IDs"); + for (const auto id : idsToCache) + Offsets::CacheID(id); + +#ifndef DEBUG + _MESSAGE("Releasing the address library database from memory, desired IDs are cached"); + Offsets::ReleaseDB(); +#endif + + _MESSAGE("Registering plugin interfaces"); g_messaging = reinterpret_cast(skse->QueryInterface(kInterface_Messaging)); if (!g_messaging) { _ERROR("Failed to load messaging interface! This error is fatal, will not load."); @@ -123,19 +288,21 @@ extern "C" { return true; }); - Config::ReadConfigFile(); - - - { - fnCalledDuringRenderShutdown = Offsets::Get(75446); - detCalledDuringRenderShutdown = std::make_unique( - reinterpret_cast(&fnCalledDuringRenderShutdown), - mCalledDuringRenderShutdown - ); - - if (!detCalledDuringRenderShutdown->Attach()) { - _ERROR("Failed to place detour on target function(Render Shutdown), this error is fatal."); - FatalError(L"Failed to place detour on target function(Render Shutdown), this error is fatal."); + switch (loadImprovedCameraHandle()) { + case ICCheckResult::OK: { + _MESSAGE("Found ImprovedCamera.dll beta 1.0.0.4"); + improvedCameraStatus = 0; + break; + } + case ICCheckResult::NOT_FOUND: { + _MESSAGE("ImprovedCamera.dll beta 1.0.0.4 not found, running without compatibility."); + improvedCameraStatus = 1; + break; + } + case ICCheckResult::VERSION_MISMATCH: { + _WARNING("Found ImprovedCamera.dll but unable to validate as beta 1.0.0.4, please install the offical release module for compatibility support."); + improvedCameraStatus = 2; + break; } } diff --git a/SmoothCam/source/mmath.cpp b/SmoothCam/source/mmath.cpp index 52afe04..76a6575 100644 --- a/SmoothCam/source/mmath.cpp +++ b/SmoothCam/source/mmath.cpp @@ -49,20 +49,18 @@ glm::vec3 mmath::GetViewVector(const glm::vec3& forwardRefer, float pitch, float } glm::vec3 mmath::NiMatrixToEuler(const NiMatrix33& m) noexcept { - const float pitch = glm::asin(glm::clamp(-m.data[2][1], -1.0f, 1.0f)); - float yaw = 0.0f; - if (m.data[0][0] <= 0.0000001f || m.data[2][2] <= 0.0000001f) { - const auto ab = glm::atan(m.data[0][2], m.data[1][2]); - yaw = ab - half_pi; - } else { - yaw = glm::atan(m.data[0][0], m.data[1][0]); - } - - return { - pitch, - yaw, - 0.0f - }; + glm::vec3 rot; + const auto a = glm::clamp(-m.data[2][0], -1.0f, 1.0f); + rot.x = glm::clamp(glm::asin(a), -mmath::half_pi, mmath::half_pi) - mmath::half_pi; + + if (m.data[0][0] <= 0.001f || m.data[2][2] <= 0.001f) { + auto ab = glm::atan(m.data[0][2], m.data[1][2]); + rot.y = ab - mmath::half_pi; + } else + rot.y = glm::atan(m.data[0][0], m.data[1][0]); + + rot.z = 0.0f; + return rot; } NiMatrix33 mmath::ToddHowardTransform(const float pitch, const float yaw) noexcept { @@ -127,11 +125,7 @@ void mmath::DecomposeToBasis(const glm::vec3& point, const glm::vec3& rotation, } glm::vec3 mmath::PointToScreen(const glm::vec3& point) noexcept { - auto port = NiRect(); - port.m_left = -1.0f; - port.m_right = 1.0f; - port.m_top = 1.0f; - port.m_bottom = -1.0f; + static NiRect port = { -1.0f, 1.0f, 1.0f, -1.0f }; glm::vec3 screen = {}; auto niPt = NiPoint3(point.x, point.y, point.z); diff --git a/SmoothCam/source/papyrus.cpp b/SmoothCam/source/papyrus.cpp index 6289284..0dbc941 100644 --- a/SmoothCam/source/papyrus.cpp +++ b/SmoothCam/source/papyrus.cpp @@ -1,19 +1,28 @@ #include "papyrus.h" +#include "camera.h" +#include "thirdperson.h" +#include "crosshair.h" + +extern eastl::unique_ptr g_theCamera; +extern uint8_t improvedCameraStatus; using namespace PapyrusBindings; +#define PAPYRUS_MANGLE(VarName) \ + "_^@"##VarName##"_SmoothCamSetting" + #define IMPL_GETTER(VarName, Var) \ - { VarName, []() noexcept { \ + { PAPYRUS_MANGLE(VarName), []() noexcept { \ return Config::GetCurrentConfig()->Var; \ } }, #define IMPL_SETTER(VarName, Var, Type) \ - { VarName, [](Type arg) { \ + { PAPYRUS_MANGLE(VarName), [](Type arg) { \ Config::GetCurrentConfig()->Var = arg; \ Config::SaveCurrentConfig(); \ } }, #define IMPL_SCALAR_METHOD_GETTER(VarName, Var) \ - { VarName, []() { \ + { PAPYRUS_MANGLE(VarName), []() { \ const auto it = Config::scalarMethodRevLookup.find( \ Config::GetCurrentConfig()->Var \ ); \ @@ -24,8 +33,9 @@ using namespace PapyrusBindings; } }, #define IMPL_SCALAR_METHOD_SETTER(VarName, Var) \ - { VarName, [](BSFixedString& str) { \ - const auto it = Config::scalarMethods.find(str.c_str()); \ + { PAPYRUS_MANGLE(VarName), [](BSFixedString& str) { \ + const auto upper = Util::UpperCase(str); \ + const auto it = Config::scalarMethods.find(upper.c_str()); \ if (it != Config::scalarMethods.end()) { \ Config::GetCurrentConfig()->Var = it->second; \ Config::SaveCurrentConfig(); \ @@ -33,7 +43,7 @@ using namespace PapyrusBindings; } }, #define IMPL_GROUP_SETTER(VarName, Var, Type) \ - { VarName, [](Type arg) { \ + { PAPYRUS_MANGLE(VarName), [](Type arg) { \ auto cfg = Config::GetCurrentConfig(); \ cfg->standing.Var = arg; \ cfg->walking.Var = arg; \ @@ -96,7 +106,7 @@ using namespace PapyrusBindings; IMPL_SETTER("Interp"##GroupNamePrefix##"MagicCombat", GroupVarName.interpMagicCombat, bool) \ IMPL_SETTER("Interp"##GroupNamePrefix##"MeleeCombat", GroupVarName.interpMeleeCombat, bool) -const std::unordered_map> stringGetters = { +const eastl::unordered_map> stringGetters = { IMPL_SCALAR_METHOD_GETTER("InterpolationMethod", currentScalar) IMPL_SCALAR_METHOD_GETTER("SeparateZInterpMethod", separateZScalar) IMPL_SCALAR_METHOD_GETTER("SepLocalInterpMethod", separateLocalScalar) @@ -104,7 +114,7 @@ const std::unordered_map> s IMPL_SCALAR_METHOD_GETTER("ZoomTransitionMethod", zoomScalar) IMPL_SCALAR_METHOD_GETTER("FOVTransitionMethod", fovScalar) - { "WorldCrosshairType", []() { + { PAPYRUS_MANGLE("WorldCrosshairType"), []() { const auto it = Config::crosshairTypeRevLookup.find(Config::GetCurrentConfig()->worldCrosshairType); if (it != Config::crosshairTypeRevLookup.end()) { return BSFixedString(it->second.c_str()); @@ -113,12 +123,16 @@ const std::unordered_map> s }}, }; -const std::unordered_map> boolGetters = { +const eastl::unordered_map> boolGetters = { { - "D3DHooked", []() noexcept { - return Render::HasContext(); + PAPYRUS_MANGLE("D3DHooked"), []() noexcept { + return Render::HasContext(); } }, + + // Misc + IMPL_GETTER("ModEnabled", modDisabled) + IMPL_GETTER("EnableCrashDumps", enableCrashDumps) // Comapt IMPL_GETTER("ACCCompat", compatACC) @@ -136,6 +150,8 @@ const std::unordered_map> boolGetter IMPL_GETTER("EnableCrosshairSizeManip", enableCrosshairSizeManip) IMPL_GETTER("HideCrosshairOutOfCombat", hideNonCombatCrosshair) IMPL_GETTER("HideCrosshairMeleeCombat", hideCrosshairMeleeCombat) + IMPL_GETTER("OffsetStealthMeter", offsetStealthMeter) + IMPL_GETTER("AlwaysOffsetStealthMeter", alwaysOffsetStealthMeter) // Primary interpolation IMPL_GETTER("InterpolationEnabled", enableInterp) @@ -181,7 +197,10 @@ const std::unordered_map> boolGetter IMPL_GETTER("InterpBowAimSneaking", bowAim.interpMeleeCombat) }; -const std::unordered_map> floatGetters = { +const eastl::unordered_map> floatGetters = { + // Misc + IMPL_GETTER("CustomZOffsetAmount", customZOffset) + // Primary interpolation IMPL_GETTER("MinFollowDistance", minCameraFollowDistance) IMPL_GETTER("MinCameraFollowRate", minCameraFollowRate) @@ -198,6 +217,8 @@ const std::unordered_map> floatGett IMPL_GETTER("ArrowArcColorB", arrowArcColor.b) IMPL_GETTER("ArrowArcColorA", arrowArcColor.a) IMPL_GETTER("MaxArrowPredictionRange", maxArrowPredictionRange) + IMPL_GETTER("StealthMeterOffsetX", stealthMeterXOffset) + IMPL_GETTER("StealthMeterOffsetY", stealthMeterYOffset) // Separate local interpolation IMPL_GETTER("SepLocalInterpRate", localScalarRate) @@ -252,11 +273,14 @@ const std::unordered_map> floatGett IMPL_GETTER("BowaimSneak:FOVOffset", bowAim.combatMeleeFOVOffset) }; -const std::unordered_map> intGetters = { +const eastl::unordered_map> intGetters = { IMPL_GETTER("ShoulderSwapKeyCode", shoulderSwapKey) + IMPL_GETTER("NextPresetKeyCode", nextPresetKey) + IMPL_GETTER("ModEnabledKeyCode", modToggleKey) + IMPL_GETTER("ToggleCustomZKeyCode", applyZOffsetKey) }; -const std::unordered_map> stringSetters = { +const eastl::unordered_map> stringSetters = { IMPL_SCALAR_METHOD_SETTER("InterpolationMethod", currentScalar) IMPL_SCALAR_METHOD_SETTER("SeparateZInterpMethod", separateZScalar) IMPL_SCALAR_METHOD_SETTER("SepLocalInterpMethod", separateLocalScalar) @@ -264,8 +288,9 @@ const std::unordered_map> s IMPL_SCALAR_METHOD_SETTER("ZoomTransitionMethod", zoomScalar) IMPL_SCALAR_METHOD_SETTER("FOVTransitionMethod", fovScalar) - { "WorldCrosshairType", [](BSFixedString& str) { - const auto it = Config::crosshairTypeLookup.find(str.c_str()); + { PAPYRUS_MANGLE("WorldCrosshairType"), [](BSFixedString& str) { + const auto upper = Util::UpperCase(str); + const auto it = Config::crosshairTypeLookup.find(upper.c_str()); if (it != Config::crosshairTypeLookup.end()) { Config::GetCurrentConfig()->worldCrosshairType = it->second; Config::SaveCurrentConfig(); @@ -273,7 +298,11 @@ const std::unordered_map> s }}, }; -const std::unordered_map> boolSetters = { +const eastl::unordered_map> boolSetters = { + // Misc + IMPL_SETTER("ModEnabled", modDisabled, bool) + IMPL_SETTER("EnableCrashDumps", enableCrashDumps, bool) + // Compat IMPL_SETTER("ACCCompat", compatACC, bool) IMPL_SETTER("ICCompat", compatIC, bool) @@ -290,6 +319,8 @@ const std::unordered_map> boolSetter IMPL_SETTER("EnableCrosshairSizeManip", enableCrosshairSizeManip, bool) IMPL_SETTER("HideCrosshairOutOfCombat", hideNonCombatCrosshair, bool) IMPL_SETTER("HideCrosshairMeleeCombat", hideCrosshairMeleeCombat, bool) + IMPL_SETTER("OffsetStealthMeter", offsetStealthMeter, bool) + IMPL_SETTER("AlwaysOffsetStealthMeter", alwaysOffsetStealthMeter, bool) // Primary interpolation IMPL_SETTER("InterpolationEnabled", enableInterp, bool) @@ -335,7 +366,10 @@ const std::unordered_map> boolSetter IMPL_SETTER("InterpBowAimSneaking", bowAim.interpMeleeCombat, bool) }; -const std::unordered_map> floatSetters = { +const eastl::unordered_map> floatSetters = { + // Misc + IMPL_SETTER("CustomZOffsetAmount", customZOffset, float) + // Primary interpolation IMPL_SETTER("MinFollowDistance", minCameraFollowDistance, float) IMPL_SETTER("MinCameraFollowRate", minCameraFollowRate, float) @@ -352,6 +386,8 @@ const std::unordered_map> floatSett IMPL_SETTER("ArrowArcColorB", arrowArcColor.b, float) IMPL_SETTER("ArrowArcColorA", arrowArcColor.a, float) IMPL_SETTER("MaxArrowPredictionRange", maxArrowPredictionRange, float) + IMPL_SETTER("StealthMeterOffsetX", stealthMeterXOffset, float) + IMPL_SETTER("StealthMeterOffsetY", stealthMeterYOffset, float) // Separate local interpolation IMPL_SETTER("SepLocalInterpRate", localScalarRate, float) @@ -427,8 +463,11 @@ const std::unordered_map> floatSett IMPL_GROUP_SETTER("GroupCombat:Melee:FOVOffset", combatMeleeFOVOffset, float) }; -const std::unordered_map> intSetters = { +const eastl::unordered_map> intSetters = { IMPL_SETTER("ShoulderSwapKeyCode", shoulderSwapKey, int) + IMPL_SETTER("NextPresetKeyCode", nextPresetKey, int) + IMPL_SETTER("ModEnabledKeyCode", modToggleKey, int) + IMPL_SETTER("ToggleCustomZKeyCode", applyZOffsetKey, int) }; void PapyrusBindings::Bind(VMClassRegistry* registry) { @@ -587,4 +626,42 @@ void PapyrusBindings::Bind(VMClassRegistry* registry) { registry ) ); + + registry->RegisterFunction( + new NativeFunction0( + "SmoothCam_ResetCrosshair", + ScriptClassName, + [](StaticFunctionTag* thisInput) { + g_theCamera->GetThirdpersonCamera()->GetCrosshairManager()->Reset(true); + }, + registry + ) + ); + + registry->RegisterFunction( + new NativeFunction0( + "SmoothCam_FixCameraState", + ScriptClassName, + [](StaticFunctionTag* thisInput) { + g_theCamera->SetShouldForceCameraState(true, CorrectedPlayerCamera::kCameraState_ThirdPerson2); + }, + registry + ) + ); + + registry->RegisterFunction( + new NativeFunction0( + "SmoothCam_IsImprovedCameraDetected", + ScriptClassName, + [](StaticFunctionTag* thisInput) { + if (improvedCameraStatus == 0) + return BSFixedString("Detected"); + else if (improvedCameraStatus == 1) + return BSFixedString("Not Detected"); + else + return BSFixedString("Version Mismatch"); + }, + registry + ) + ); } \ No newline at end of file diff --git a/SmoothCam/source/pch.cpp b/SmoothCam/source/pch.cpp index 1730571..ff09a3c 100644 --- a/SmoothCam/source/pch.cpp +++ b/SmoothCam/source/pch.cpp @@ -1 +1,17 @@ -#include "pch.h" \ No newline at end of file +#include "pch.h" + +void* operator new[](size_t size, const char* pName, int flags, unsigned debugFlags, const char* file, int line) { + return new uint8_t[size]; +} + +void* operator new[](size_t size, size_t alignment, size_t alignmentOffset, const char* pName, int flags, + unsigned debugFlags, const char* file, int line) +{ + return new uint8_t[size]; +} + +extern "C" { + int __cdecl Vsnprintf8(char* p, size_t n, const char* pFormat, va_list arguments) { + return vsnprintf_s(p, n, _TRUNCATE, pFormat, arguments); + } +} \ No newline at end of file diff --git a/SmoothCam/source/physics.cpp b/SmoothCam/source/physics.cpp index 8dcd286..f159286 100644 --- a/SmoothCam/source/physics.cpp +++ b/SmoothCam/source/physics.cpp @@ -1,14 +1,29 @@ +#include "physics.h" + namespace { typedef bhkWorld*(__fastcall* bhkWorldGetter)(const TESObjectCELL* cell); constexpr auto hkpBroadphaseOffset = 0x88; } -hkp3AxisSweep* Physics::GetBroadphase(const bhkWorld* physicsWorld) { - auto ptr = reinterpret_cast(physicsWorld->unk40() + hkpBroadphaseOffset); +hkp3AxisSweep* Physics::GetBroadphase(bhkWorld* physicsWorld) { + auto ptr = reinterpret_cast(physicsWorld->GetHavokWorld() + hkpBroadphaseOffset); return *ptr; } bhkWorld* Physics::GetWorld(const TESObjectCELL* parentCell) { static auto getWorld = Offsets::Get(18536); return getWorld(parentCell); // 0x2654c0 +} + +glm::vec3 Physics::GetGravityVector(const TESObjectREFR* ref) { + glm::vec3 gravity = { 0.0f, 0.0f, -9.8f }; + if (ref && ref->parentCell) { + auto bhkWorld = Physics::GetWorld(ref->parentCell); + if (bhkWorld) { + auto hkpWorld = bhkWorld->GetHavokWorld(); + if (hkpWorld) + gravity = static_cast(hkpWorld->gravity); + } + } + return gravity; } \ No newline at end of file diff --git a/SmoothCam/source/raycast.cpp b/SmoothCam/source/raycast.cpp index b854db5..abb4f55 100644 --- a/SmoothCam/source/raycast.cpp +++ b/SmoothCam/source/raycast.cpp @@ -119,7 +119,7 @@ Raycast::RayResult Raycast::hkpCastRay(glm::vec4 start, glm::vec4 end) { result.hit = av != nullptr; if (result.hit) { - auto ref = av->m_owner; // Yay, updates //*reinterpret_cast(&av->unkF8); + auto ref = av->m_owner; if (ref && ref->formType == kFormType_Character) { // This might not *always* be a valid cast, but I've only ever seen valid data // and the dynamic cast _always_ fails. For now, we just check that this isn't null - diff --git a/SmoothCam/source/render/cbuffer.cpp b/SmoothCam/source/render/cbuffer.cpp index bfdb711..68a3796 100644 --- a/SmoothCam/source/render/cbuffer.cpp +++ b/SmoothCam/source/render/cbuffer.cpp @@ -50,4 +50,8 @@ size_t Render::CBuffer::Size() const noexcept { D3D11_USAGE Render::CBuffer::Usage() const noexcept { return usage; +} + +winrt::com_ptr& Render::CBuffer::GetBuffer() noexcept { + return buffer; } \ No newline at end of file diff --git a/SmoothCam/source/render/d2d.cpp b/SmoothCam/source/render/d2d.cpp index 2e971cf..582d9e5 100644 --- a/SmoothCam/source/render/d2d.cpp +++ b/SmoothCam/source/render/d2d.cpp @@ -5,6 +5,7 @@ #include "render/srv.h" #include "render/vertex_buffer.h" #include "render/shaders/draw_fullscreen_texture.h" +#include "render/shader_cache.h" Render::D2D::D2D(D3DContext& ctx) { D2D1_FACTORY_OPTIONS options; @@ -40,9 +41,6 @@ Render::D2D::D2D(D3DContext& ctx) { // And the whole reason we have to make our own device uint32_t flags = D3D11_CREATE_DEVICE_FLAG::D3D11_CREATE_DEVICE_BGRA_SUPPORT; -/*#ifdef _DEBUG - flags |= D3D11_CREATE_DEVICE_FLAG::D3D11_CREATE_DEVICE_DEBUG; -#endif*/ if (!SUCCEEDED(D3D11CreateDevice( gameAdapter.get(), @@ -81,7 +79,7 @@ Render::D2D::D2D(D3DContext& ctx) { ZeroMemory(&props, sizeof(D2D1_BITMAP_PROPERTIES1)); props.dpiX = 96; props.dpiY = 96; - props.pixelFormat.format = DXGI_FORMAT_B8G8R8A8_UNORM; // desc.BufferDesc.Format; + props.pixelFormat.format = DXGI_FORMAT_B8G8R8A8_UNORM; props.pixelFormat.alphaMode = D2D1_ALPHA_MODE_PREMULTIPLIED; props.bitmapOptions = D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW; @@ -99,7 +97,7 @@ Render::D2D::D2D(D3DContext& ctx) { context->SetTarget(bitmap.get()); // Start direct write - dwrite = std::make_unique(this); + dwrite = eastl::make_unique(this); // Make a work query for forcing sync on the shared texture D3D11_QUERY_DESC qd; @@ -114,13 +112,13 @@ Render::D2D::D2D(D3DContext& ctx) { Render::Shaders::DrawFullscreenTextureVS, Render::PipelineStage::Vertex ); - fullScreenVS = std::make_shared(vsCreateInfo, ctx); + fullScreenVS = ShaderCache::Get().Load(vsCreateInfo, ctx); Render::ShaderCreateInfo psCreateInfo( Render::Shaders::DrawFullscreenTexturePS, Render::PipelineStage::Fragment ); - fullScreenPS = std::make_shared(psCreateInfo, ctx); + fullScreenPS = ShaderCache::Get().Load(psCreateInfo, ctx); float verts[] = { -1.0f, 1.0f, 0.0f, 0.0f, 0.0f, @@ -153,12 +151,68 @@ Render::D2D::D2D(D3DContext& ctx) { "UV", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }); - vboFullscreen = std::make_unique(vbInfo, ctx); + vboFullscreen = eastl::make_unique(vbInfo, ctx); + + // Init our different stroke styles + strokeStyles[static_cast(StrokeStyle::Solid)] = GetStrokeStyle<0>( + nullptr, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_FLAT, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_FLAT, + D2D1_LINE_JOIN::D2D1_LINE_JOIN_MITER, + D2D1_DASH_STYLE::D2D1_DASH_STYLE_SOLID + ); + strokeStyles[static_cast(StrokeStyle::RoundedSolid)] = GetStrokeStyle<0>( + nullptr, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_ROUND, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_FLAT, + D2D1_LINE_JOIN::D2D1_LINE_JOIN_MITER, + D2D1_DASH_STYLE::D2D1_DASH_STYLE_SOLID + ); + strokeStyles[static_cast(StrokeStyle::RoundedSolidRounded)] = GetStrokeStyle<0>( + nullptr, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_ROUND, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_FLAT, + D2D1_LINE_JOIN::D2D1_LINE_JOIN_ROUND, + D2D1_DASH_STYLE::D2D1_DASH_STYLE_SOLID + ); + + const float dashes[2] = { 5.0f, 5.0f }; + strokeStyles[static_cast(StrokeStyle::Dashed)] = GetStrokeStyle<2>( + dashes, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_FLAT, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_FLAT, + D2D1_LINE_JOIN::D2D1_LINE_JOIN_MITER, + D2D1_DASH_STYLE::D2D1_DASH_STYLE_CUSTOM + ); + strokeStyles[static_cast(StrokeStyle::RoundedDashed)] = GetStrokeStyle<2>( + dashes, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_ROUND, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_FLAT, + D2D1_LINE_JOIN::D2D1_LINE_JOIN_MITER, + D2D1_DASH_STYLE::D2D1_DASH_STYLE_CUSTOM + ); + strokeStyles[static_cast(StrokeStyle::RoundedDashedRounded)] = GetStrokeStyle<2>( + dashes, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_ROUND, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_FLAT, + D2D1_LINE_JOIN::D2D1_LINE_JOIN_ROUND, + D2D1_DASH_STYLE::D2D1_DASH_STYLE_CUSTOM + ); + strokeStyles[static_cast(StrokeStyle::RoundedDashedRoundedSmooth)] = GetStrokeStyle<2>( + dashes, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_ROUND, + D2D1_CAP_STYLE::D2D1_CAP_STYLE_ROUND, + D2D1_LINE_JOIN::D2D1_LINE_JOIN_ROUND, + D2D1_DASH_STYLE::D2D1_DASH_STYLE_CUSTOM + ); } Render::D2D::~D2D() { colorBrushes.clear(); + for (auto& ptr : strokeStyles) + ptr = nullptr; + dwrite.reset(); bitmap = nullptr; @@ -221,7 +275,7 @@ void Render::D2D::WriteToBackbuffer(D3DContext& ctx) noexcept { vboFullscreen->Draw(); } -std::unique_ptr& Render::D2D::GetDWrite() noexcept { +eastl::unique_ptr& Render::D2D::GetDWrite() noexcept { return dwrite; } @@ -243,6 +297,16 @@ void Render::D2D::DrawLine(const glm::vec2& p1, const glm::vec2& p2, const glm:: context->DrawLine({ p1.x, p1.y }, { p2.x, p2.y }, GetColorBrush(color).get(), thickness); } +void Render::D2D::DrawEllipse(const glm::vec2& center, const glm::vec2& extents, const glm::vec4& color, + StrokeStyle style, const float stroke) noexcept +{ + D2D1_ELLIPSE el; + el.point = { center.x, center.y }; + el.radiusX = extents.x; + el.radiusY = extents.y; + context->DrawEllipse(el, GetColorBrush(color).get(), stroke, strokeStyles[static_cast(style)].get()); +} + void Render::D2D::CreateRenderTarget(D3DContext& ctx, D3DContext& renderingCtx) { Texture2DCreateInfo texInfo; texInfo.width = renderingCtx.windowSize.x; @@ -252,7 +316,7 @@ void Render::D2D::CreateRenderTarget(D3DContext& ctx, D3DContext& renderingCtx) texInfo.bindFlags = D3D11_BIND_FLAG::D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_FLAG::D3D11_BIND_RENDER_TARGET; texInfo.miscFlags = D3D11_RESOURCE_MISC_SHARED; - colorBuffer = std::make_shared(ctx, texInfo); + colorBuffer = eastl::make_shared(ctx, texInfo); winrt::com_ptr res; auto code = colorBuffer->GetResource()->QueryInterface(__uuidof(IDXGIResource), res.put_void()); @@ -283,7 +347,7 @@ void Render::D2D::CreateRenderTarget(D3DContext& ctx, D3DContext& renderingCtx) } texInfo.createSampler = true; - sharedColorBuffer = std::make_shared(renderingCtx, sharedTex, texInfo); + sharedColorBuffer = eastl::make_shared(renderingCtx, sharedTex, texInfo); SRVCreateInfo srv; srv.dimensions = D3D11_SRV_DIMENSION::D3D11_SRV_DIMENSION_TEXTURE2D; @@ -291,6 +355,6 @@ void Render::D2D::CreateRenderTarget(D3DContext& ctx, D3DContext& renderingCtx) srv.texture = sharedColorBuffer; srv.texture2D.MipLevels = 1; srv.texture2D.MostDetailedMip = 0; - colorSRV = std::make_unique(renderingCtx, srv); + colorSRV = eastl::make_unique(renderingCtx, srv); } #endif \ No newline at end of file diff --git a/SmoothCam/source/render/d3d_context.cpp b/SmoothCam/source/render/d3d_context.cpp index 13ecc95..a3f1bd0 100644 --- a/SmoothCam/source/render/d3d_context.cpp +++ b/SmoothCam/source/render/d3d_context.cpp @@ -6,30 +6,33 @@ #include "render/srv.h" #include "render/texture2d.h" #include "render/vertex_buffer.h" +#include "util.h" +#include "debug/eh.h" static Render::D3DContext gameContext; -static std::vector presentCallbacks; +static eastl::vector presentCallbacks; +static eastl::unique_ptr> dxgiHook; static bool initialized = false; #ifdef WITH_D2D -extern std::unique_ptr g_D2D; +extern eastl::unique_ptr g_D2D; #endif struct D3DObjectsStore { winrt::com_ptr depthStencilView; winrt::com_ptr gameRTV; - std::unordered_map< + eastl::unordered_map< Render::DSStateKey, Render::DSState, Render::DSHasher, Render::DSCompare > loadedDepthStates; - std::unordered_map< + eastl::unordered_map< Render::BlendStateKey, Render::BlendState, Render::BlendStateHasher, Render::BlendStateCompare > loadedBlendStates; - std::unordered_map< + eastl::unordered_map< Render::RasterStateKey, Render::RasterState, Render::RasterStateHasher, Render::RasterStateCompare > loadedRasterStates; @@ -44,10 +47,10 @@ struct D3DObjectsStore { }; static D3DObjectsStore d3dObjects; -static std::unique_ptr d3dHook; -static PLH::VFuncMap origVFuncs_D3D; -static Render::D3D11Present fnPresentOrig; + HRESULT Render::Present(IDXGISwapChain* swapChain, UINT syncInterval, UINT flags) { + const auto mdmp = Debug::MiniDumpScope(); + // Save some context state to restore later winrt::com_ptr gameDSState; uint32_t gameStencilRef; @@ -98,8 +101,6 @@ HRESULT Render::Present(IDXGISwapChain* swapChain, UINT syncInterval, UINT flags } // Put things back the way we found it - auto ptrRTV = d3dObjects.gameRTV.get(); - gameContext.context->OMSetRenderTargets(1, &ptrRTV, d3dObjects.depthStencilView.get()); gameContext.context->RSSetState(rasterState.get()); gameContext.context->RSSetViewports(1, &gamePort); gameContext.context->OMSetBlendState(gameBlendState.get(), gameBlendFactors, gameSampleMask); @@ -108,32 +109,29 @@ HRESULT Render::Present(IDXGISwapChain* swapChain, UINT syncInterval, UINT flags d3dObjects.depthStencilView = nullptr; d3dObjects.gameRTV = nullptr; - return reinterpret_cast(origVFuncs_D3D[8])(swapChain, syncInterval, flags); + return dxgiHook->GetBase(8)(swapChain, syncInterval, flags); } static bool ReadSwapChain() { // Hopefully SEH will shield us from unexpected crashes with other mods/programs + // @Note: no MiniDumpScope here, we allow for this to fail and simply disable D3D features __try { - struct UnkCreationD3D { - uintptr_t unk0; - uintptr_t unk1; - uintptr_t unk2; - IDXGISwapChain* swapChain; - }; - auto data = **Offsets::Get(524730); + auto data = *Offsets::Get(524728); // Naked pointer to the swap chain - gameContext.swapChain = data.swapChain; + gameContext.swapChain = data->swapChain; + // Device + gameContext.device.copy_from(data->device); + // Context + gameContext.context.copy_from(data->ctx); // Try and read the desc as a simple test DXGI_SWAP_CHAIN_DESC desc; - if (!SUCCEEDED(data.swapChain->GetDesc(&desc))) + if (!SUCCEEDED(data->swapChain->GetDesc(&desc))) return false; - // Read our screen size - RECT r; - GetClientRect(desc.OutputWindow, &r); - gameContext.windowSize.x = static_cast(r.right); - gameContext.windowSize.y = static_cast(r.bottom); + gameContext.windowSize.x = static_cast(data->windowW); + gameContext.windowSize.y = static_cast(data->windowH); + gameContext.hWnd = data->window; } __except (1) { return false; } @@ -146,25 +144,15 @@ void Render::InstallHooks() { return; } - if (!SUCCEEDED(gameContext.swapChain->GetDevice(__uuidof(ID3D11Device), gameContext.device.put_void()))) { - _ERROR("SmoothCam: Failed to get rendering device."); - return; - } - - d3dHook = std::make_unique( - (uint64_t)gameContext.swapChain, - PLH::VFuncMap{ - { static_cast(8), reinterpret_cast(&Render::Present) }, - }, - &origVFuncs_D3D - ); - - if (!d3dHook->hook()) { + const auto mdmp = Debug::MiniDumpScope(); + + dxgiHook = eastl::make_unique>(gameContext.swapChain); + dxgiHook->Add(8, Render::Present); + if (!dxgiHook->Attach()) { _ERROR("SmoothCam: Failed to place detour on virtual IDXGISwapChain->Present."); return; } - gameContext.device->GetImmediateContext(gameContext.context.put()); initialized = true; } @@ -176,7 +164,7 @@ void Render::Shutdown() { d3dObjects.release(); // Free our present hook - d3dHook->unHook(); + dxgiHook->Detach(); // Explicit release gameContext.context = nullptr; @@ -269,4 +257,18 @@ void Render::SetRasterState(D3DContext& ctx, D3D11_FILL_MODE fillMode, D3D11_CUL auto state = RasterState{ ctx, key }; ctx.context->RSSetState(state.state.get()); d3dObjects.loadedRasterStates.emplace(key, std::move(state)); +} + +Render::GBuffer::WrappedCameraData* Render::GBuffer::CameraSwap(NiCamera* inCamera, byte flags) { + // FUN_140d7d7b0 + typedef WrappedCameraData*(*ty)(GBuffer* param_1, NiCamera* param_2, byte param_3); + static auto fn = Offsets::Get(75713); + return fn(this, inCamera, flags); +} + +void Render::GBuffer::UpdateGPUCameraData(NiCamera* inCamera, byte flags) { + // FUN_140d7bab0 + typedef void(*ty)(GBuffer* param_1, NiCamera* param_2, byte param_3); + static auto fn = Offsets::Get(75694); + fn(this, inCamera, flags); } \ No newline at end of file diff --git a/SmoothCam/source/render/dwrite.cpp b/SmoothCam/source/render/dwrite.cpp index 8324c04..79447cc 100644 --- a/SmoothCam/source/render/dwrite.cpp +++ b/SmoothCam/source/render/dwrite.cpp @@ -41,7 +41,7 @@ void Render::DWrite::CreateFont() { } // Generate and return a text layout -winrt::com_ptr& Render::DWrite::GetLayout(const std::wstring_view& text, float maxWidth, float maxHeight) { +winrt::com_ptr& Render::DWrite::GetLayout(const eastl::wstring_view& text, float maxWidth, float maxHeight) { textLayout = nullptr; if (!SUCCEEDED(factory->CreateTextLayout( text.data(), @@ -57,7 +57,7 @@ winrt::com_ptr& Render::DWrite::GetLayout(const std::wstring_ return textLayout; } -void Render::DWrite::Write(const std::wstring_view& text, float maxWidth, float maxHeight, const glm::vec2& pos, +void Render::DWrite::Write(const eastl::wstring_view& text, float maxWidth, float maxHeight, const glm::vec2& pos, const glm::vec4& color) noexcept { textLayout = nullptr; @@ -93,7 +93,7 @@ void Render::DWrite::Write(winrt::com_ptr& layout, const glm: ); } -glm::vec2 Render::DWrite::GetTextSize(const std::wstring_view& text, float maxWidth, float maxHeight) noexcept { +glm::vec2 Render::DWrite::GetTextSize(const eastl::wstring_view& text, float maxWidth, float maxHeight) noexcept { DWRITE_TEXT_METRICS metrics; if (!SUCCEEDED(GetLayout(text, maxWidth, maxHeight)->GetMetrics(&metrics))) { FatalError(L"SmoothCam: Failed to get text metrics"); diff --git a/SmoothCam/source/render/gradbox.cpp b/SmoothCam/source/render/gradbox.cpp index d3e69af..41780f8 100644 --- a/SmoothCam/source/render/gradbox.cpp +++ b/SmoothCam/source/render/gradbox.cpp @@ -1,23 +1,25 @@ +#ifdef WITH_D2D #include "render/gradbox.h" #include "render/vertex_buffer.h" #include "render/shader.h" -#include "render/shaders/shader_vertex_color.h" +#include "render/shaders/vertex_color_screen.h" +#include "render/shader_cache.h" Render::GradBox::GradBox(Render::D3DContext& ctx, uint32_t width, uint32_t height) : bgSize(width, height) { backgroundDirty = true; Render::ShaderCreateInfo vsCreateInfo( - Render::Shaders::VertexColorPassThruVS, + Render::Shaders::VertexColorScreenVS, Render::PipelineStage::Vertex ); - vsBackground = std::make_shared(vsCreateInfo, ctx); + vsBackground = ShaderCache::Get().Load(vsCreateInfo, ctx); Render::ShaderCreateInfo psCreateInfo( - Render::Shaders::VertexColorPassThruPS, + Render::Shaders::VertexColorScreenPS, Render::PipelineStage::Fragment ); - psBackground = std::make_shared(psCreateInfo, ctx); + psBackground = ShaderCache::Get().Load(psCreateInfo, ctx); } Render::GradBox::~GradBox() { @@ -37,6 +39,8 @@ void Render::GradBox::SetBackgroundPosition(const glm::vec2& pos) noexcept { } void Render::GradBox::SetBackgroundColors(const glm::vec4& color1, const glm::vec4& color2) noexcept { + bgColor1 = color1; + bgColor2 = color2; backgroundDirty = true; } @@ -88,7 +92,7 @@ void Render::GradBox::MakeBackgroundVerts(D3DContext& ctx) noexcept { D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }); - vboBackground = std::make_unique(vbInfo, ctx); + vboBackground = eastl::make_unique(vbInfo, ctx); } else { auto data = vboBackground->Map(D3D11_MAP::D3D11_MAP_WRITE_DISCARD); memcpy(data.pData, backgroundVerts.data(), backgroundVerts.size() * sizeof(float)); @@ -96,4 +100,5 @@ void Render::GradBox::MakeBackgroundVerts(D3DContext& ctx) noexcept { } backgroundDirty = false; -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/SmoothCam/source/render/line_drawer.cpp b/SmoothCam/source/render/line_drawer.cpp index c5e6e53..d6d9b42 100644 --- a/SmoothCam/source/render/line_drawer.cpp +++ b/SmoothCam/source/render/line_drawer.cpp @@ -1,5 +1,6 @@ #include "render/line_drawer.h" -#include "render/shaders/shader_vertex_color.h" +#include "render/shaders/vertex_color_screen.h" +#include "render/shader_cache.h" Render::LineDrawer::LineDrawer(D3DContext& ctx) { CreateObjects(ctx); @@ -15,16 +16,16 @@ Render::LineDrawer::~LineDrawer() { void Render::LineDrawer::CreateObjects(D3DContext& ctx) { Render::ShaderCreateInfo vsCreateInfo( - Render::Shaders::VertexColorPassThruVS, + Render::Shaders::VertexColorScreenVS, Render::PipelineStage::Vertex ); - vs = std::make_shared(vsCreateInfo, ctx); + vs = ShaderCache::Get().Load(vsCreateInfo, ctx); Render::ShaderCreateInfo psCreateInfo( - Render::Shaders::VertexColorPassThruPS, + Render::Shaders::VertexColorScreenPS, Render::PipelineStage::Fragment ); - ps = std::make_shared(psCreateInfo, ctx); + ps = ShaderCache::Get().Load(psCreateInfo, ctx); Render::VertexBufferCreateInfo vbInfo; vbInfo.elementSize = sizeof(Point); @@ -37,7 +38,7 @@ void Render::LineDrawer::CreateObjects(D3DContext& ctx) { vbInfo.iaLayout.emplace_back(D3D11_INPUT_ELEMENT_DESC{ "COL", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }); for (auto i = 0; i < NumBuffers; i++) - vbo[i] = std::move(std::make_unique(vbInfo, ctx)); + vbo[i] = eastl::move(eastl::make_unique(vbInfo, ctx)); } void Render::LineDrawer::Submit(const LineList& lines) noexcept { diff --git a/SmoothCam/source/render/line_graph.cpp b/SmoothCam/source/render/line_graph.cpp index 6e07424..1e0c68e 100644 --- a/SmoothCam/source/render/line_graph.cpp +++ b/SmoothCam/source/render/line_graph.cpp @@ -4,9 +4,8 @@ #include "render/d2d.h" #include "render/dwrite.h" #include "render/shader.h" -#include "render/shaders/shader_vertex_color.h" -extern std::unique_ptr g_D2D; +extern eastl::unique_ptr g_D2D; Render::LineGraph::LineGraph(uint8_t numPlots, uint32_t maxPoints, uint32_t width, uint32_t height, D3DContext& ctx) : numPlots(numPlots), maxPoints(maxPoints), width(width), height(height), GradBox(ctx, width, height) @@ -44,7 +43,7 @@ void Render::LineGraph::SetLineThickness(float amount) noexcept { lineThickness = amount; } -void Render::LineGraph::SetName(const std::wstring& n) { +void Render::LineGraph::SetName(const eastl::wstring& n) { name = n; } @@ -65,7 +64,7 @@ void Render::LineGraph::Draw(D3DContext& ctx) noexcept { DrawBackground(ctx); // Draw line(s) - std::vector plotMetrics; + eastl::vector plotMetrics; const auto xAdd = static_cast(width) / static_cast(maxPoints); for (auto i = 0; i < plots.size(); i++) { const auto& points = plots[i]; @@ -73,11 +72,11 @@ void Render::LineGraph::Draw(D3DContext& ctx) noexcept { const auto& plotRange = plotRanges[i]; float x = 0.0f; - float minVal = points.size() == 0 ? 0.0f : std::numeric_limits::max(); - float maxVal = points.size() == 0 ? 0.0f : std::numeric_limits::min(); + float minVal = points.size() == 0 ? 0.0f : eastl::numeric_limits::max(); + float maxVal = points.size() == 0 ? 0.0f : eastl::numeric_limits::min(); float avg = 0.0f; - std::vector plotLocations; + eastl::vector plotLocations; uint32_t j = 0; for (const auto& value : points) { if (value > maxVal) maxVal = value; @@ -135,8 +134,8 @@ void Render::LineGraph::Draw(D3DContext& ctx) noexcept { float yoff = nameSize.y; float longestLine = 0.0f; - std::wstring minStr = L"min: "; - minStr.append(std::to_wstring(metrics.min)); + eastl::wstring minStr = L"min: "; + minStr.append(std::to_wstring(metrics.min).c_str()); g_D2D->GetDWrite()->Write( minStr, ctx.windowSize.x, ctx.windowSize.y, @@ -145,9 +144,9 @@ void Render::LineGraph::Draw(D3DContext& ctx) noexcept { ); auto sz = g_D2D->GetDWrite()->GetTextSize(minStr, ctx.windowSize.x, ctx.windowSize.y); - longestLine = std::max(sz.x, longestLine); - std::wstring maxStr = L"max: "; - maxStr.append(std::to_wstring(metrics.max)); + longestLine = eastl::max(sz.x, longestLine); + eastl::wstring maxStr = L"max: "; + maxStr.append(std::to_wstring(metrics.max).c_str()); g_D2D->GetDWrite()->Write( maxStr, ctx.windowSize.x, ctx.windowSize.y, @@ -157,9 +156,9 @@ void Render::LineGraph::Draw(D3DContext& ctx) noexcept { yoff += sz.y; sz = g_D2D->GetDWrite()->GetTextSize(maxStr, ctx.windowSize.x, ctx.windowSize.y); - longestLine = std::max(sz.x, longestLine); - std::wstring avgStr = L"avg: "; - avgStr.append(std::to_wstring(metrics.avg)); + longestLine = eastl::max(sz.x, longestLine); + eastl::wstring avgStr = L"avg: "; + avgStr.append(std::to_wstring(metrics.avg).c_str()); g_D2D->GetDWrite()->Write( avgStr, ctx.windowSize.x, ctx.windowSize.y, @@ -169,7 +168,7 @@ void Render::LineGraph::Draw(D3DContext& ctx) noexcept { yoff += sz.y; sz = g_D2D->GetDWrite()->GetTextSize(avgStr, ctx.windowSize.x, ctx.windowSize.y); - xoff += std::max(sz.x, longestLine) + xpad; + xoff += eastl::max(sz.x, longestLine) + xpad; } } #endif \ No newline at end of file diff --git a/SmoothCam/source/render/mesh_drawer.cpp b/SmoothCam/source/render/mesh_drawer.cpp index 75ece1d..9fe8766 100644 --- a/SmoothCam/source/render/mesh_drawer.cpp +++ b/SmoothCam/source/render/mesh_drawer.cpp @@ -1,6 +1,6 @@ #include "render/mesh_drawer.h" -Render::MeshDrawer::MeshDrawer(MeshCreateInfo& info, std::shared_ptr& perObjectBuffer, D3DContext& ctx) : +Render::MeshDrawer::MeshDrawer(MeshCreateInfo& info, eastl::shared_ptr& perObjectBuffer, D3DContext& ctx) : vs(info.vs), ps(info.ps) { assert(perObjectBuffer->Size() == sizeof(glm::mat4)); @@ -17,7 +17,7 @@ Render::MeshDrawer::~MeshDrawer() { cbufPerObject.reset(); } -void Render::MeshDrawer::CreateObjects(std::vector& vertices, D3DContext& ctx) { +void Render::MeshDrawer::CreateObjects(eastl::vector& vertices, D3DContext& ctx) { D3D11_SUBRESOURCE_DATA data; data.pSysMem = vertices.data(); data.SysMemPitch = 0; @@ -37,7 +37,7 @@ void Render::MeshDrawer::CreateObjects(std::vector& vertices, D3D vbInfo.iaLayout.emplace_back(D3D11_INPUT_ELEMENT_DESC{ "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }); vbInfo.iaLayout.emplace_back(D3D11_INPUT_ELEMENT_DESC{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }); - vbo = std::make_unique(vbInfo, ctx); + vbo = eastl::make_unique(vbInfo, ctx); context = ctx; } @@ -50,7 +50,7 @@ void Render::MeshDrawer::Submit(glm::mat4& modelMatrix) noexcept { vbo->Draw(); } -void Render::MeshDrawer::SetShaders(std::shared_ptr& nvs, std::shared_ptr& nps) { +void Render::MeshDrawer::SetShaders(eastl::shared_ptr& nvs, eastl::shared_ptr& nps) { IALayout iaLayout; iaLayout.emplace_back(D3D11_INPUT_ELEMENT_DESC{ "POS", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }); iaLayout.emplace_back(D3D11_INPUT_ELEMENT_DESC{ "UV", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }); diff --git a/SmoothCam/source/render/ninode_tree_display.cpp b/SmoothCam/source/render/ninode_tree_display.cpp index 7634caa..7d542d6 100644 --- a/SmoothCam/source/render/ninode_tree_display.cpp +++ b/SmoothCam/source/render/ninode_tree_display.cpp @@ -4,15 +4,15 @@ #include "render/dwrite.h" #include -extern std::unique_ptr g_D2D; +extern eastl::unique_ptr g_D2D; template< typename T > -std::wstring int_to_hex(T i) { +eastl::wstring int_to_hex(T i) { std::wstringstream stream; stream << "0x" << std::setfill (L'0') << std::setw(sizeof(T)*2) << std::hex << i; - return stream.str(); + return stream.str().c_str(); } Render::NiNodeTreeDisplay::NiNodeTreeDisplay(uint32_t width, uint32_t height, D3DContext& ctx) : @@ -37,11 +37,11 @@ void Render::NiNodeTreeDisplay::Draw(D3DContext& ctx, NiNode* node) noexcept { DrawBackground(ctx); builder.clear(); - std::wstring str; + eastl::wstring str; const auto maxSize = glm::vec2{ width, height }; - std::function walkFun; - walkFun = [&ctx, &maxSize, &walkFun, &str, this](NiNode* n, float& x, float& y, uint32_t level) { + eastl::function walkFun; + walkFun = [&ctx, &maxSize, &walkFun, &str, this](NiAVObject* n, float& x, float& y, uint32_t level) { constexpr auto lineHeight = 14.0f; if (!n->m_name) return; @@ -77,10 +77,12 @@ void Render::NiNodeTreeDisplay::Draw(D3DContext& ctx, NiNode* node) noexcept { builder.append(L"\n"); y += lineHeight; - for (auto i = 0; i < n->m_children.m_size; i++) { - auto no = DYNAMIC_CAST(n->m_children.m_data[i], NiAVObject, NiNode); - if (!no) continue; - walkFun(no, x, y, level + 1); + auto no = DYNAMIC_CAST(n, NiAVObject, NiNode); + if (no) { + for (auto i = 0; i < no->m_children.m_arrayBufLen; i++) { + if (no->m_children.m_data[i]) + walkFun(no->m_children.m_data[i], x, y, level + 1); + } } }; diff --git a/SmoothCam/source/render/render_target.cpp b/SmoothCam/source/render/render_target.cpp index 976a6ca..70a75d5 100644 --- a/SmoothCam/source/render/render_target.cpp +++ b/SmoothCam/source/render/render_target.cpp @@ -20,7 +20,7 @@ Render::RenderTarget::RenderTarget(Render::D3DContext& ctx, const RenderTargetCr srvInfo.texture2D.MipLevels = 1; srvInfo.texture2D.MostDetailedMip = 0; srvInfo.texture = texture; - srvColor = std::make_unique(ctx, srvInfo); + srvColor = eastl::make_unique(ctx, srvInfo); } Render::RenderTarget::~RenderTarget() { @@ -44,6 +44,6 @@ void Render::RenderTarget::Clear(Render::D3DContext& ctx, glm::vec4& color) noex ctx.context->ClearRenderTargetView(rtv.get(), col); } -std::unique_ptr& Render::RenderTarget::GetColorSRV() noexcept { +eastl::unique_ptr& Render::RenderTarget::GetColorSRV() noexcept { return srvColor; } \ No newline at end of file diff --git a/SmoothCam/source/render/shader.cpp b/SmoothCam/source/render/shader.cpp index 5ac8c2a..6bad09d 100644 --- a/SmoothCam/source/render/shader.cpp +++ b/SmoothCam/source/render/shader.cpp @@ -1,7 +1,7 @@ #include "render/shader.h" Render::Shader::Shader(const ShaderCreateInfo& createInfo, D3DContext& ctx) noexcept : stage(createInfo.stage), context(ctx) { - validBinary = Compile(createInfo.source, createInfo.entryName, createInfo.version); + validBinary = Compile(createInfo.source.source, createInfo.entryName, createInfo.version); if (!validBinary) return; if (stage == PipelineStage::Vertex) { @@ -45,13 +45,13 @@ bool Render::Shader::IsValid() const noexcept { return validProgram && validBinary; } -bool Render::Shader::Compile(const std::string& source, const std::string& entryName, const std::string& version) noexcept { +bool Render::Shader::Compile(const eastl::string& source, const eastl::string& entryName, const eastl::string& version) noexcept { UINT compileFlags = D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_PACK_MATRIX_COLUMN_MAJOR; winrt::com_ptr errorBlob; auto versionStr = stage == PipelineStage::Vertex ? - std::string("vs_").append(version) : - std::string("ps_").append(version); + eastl::string("vs_").append(version) : + eastl::string("ps_").append(version); auto result = D3DCompile( source.c_str(), source.length(), nullptr, nullptr, nullptr, diff --git a/SmoothCam/source/render/shader_cache.cpp b/SmoothCam/source/render/shader_cache.cpp new file mode 100644 index 0000000..738a515 --- /dev/null +++ b/SmoothCam/source/render/shader_cache.cpp @@ -0,0 +1,25 @@ +#include "render/shader_cache.h" + +Render::ShaderCache::ShaderCache() noexcept {} + +void Render::ShaderCache::Release() noexcept { + shaders.clear(); +} + +eastl::shared_ptr Render::ShaderCache::Load(const ShaderCreateInfo& info, Render::D3DContext& ctx) + noexcept +{ + auto it = shaders.find(info); + if (it != shaders.end()) { + if (auto ptr = it->second.lock(); ptr != nullptr) { + return ptr; + } else { + shaders.erase(it); + } + } + + auto ptr = eastl::make_shared(info, ctx); + if (ptr->IsValid()) + shaders.insert({ info, ptr }); + return ptr; +} \ No newline at end of file diff --git a/SmoothCam/source/render/state_overlay.cpp b/SmoothCam/source/render/state_overlay.cpp index 9a0622f..bbd4174 100644 --- a/SmoothCam/source/render/state_overlay.cpp +++ b/SmoothCam/source/render/state_overlay.cpp @@ -3,8 +3,9 @@ #include "render/d2d.h" #include "render/dwrite.h" #include "camera.h" +#include "thirdperson.h" -extern std::unique_ptr g_D2D; +extern eastl::unique_ptr g_D2D; static const wchar_t* ActionStateEnumNames[] = { L"Standing", @@ -67,7 +68,7 @@ static const wchar_t* OffsetGroupNames[]{ constexpr auto ofsNameLen = sizeof(OffsetGroupNames) / sizeof(OffsetGroupNames[0]); static_assert(ofsNameLen == static_cast(Config::OffsetGroupID::MAX_OFS)); -Render::StateOverlay::StateOverlay(uint32_t width, uint32_t height, Camera::SmoothCamera* camera, +Render::StateOverlay::StateOverlay(uint32_t width, uint32_t height, Camera::Thirdperson* camera, D3DContext& ctx) : width(width), height(height), camera(camera), GradBox(ctx, width, height) {} @@ -85,12 +86,11 @@ void Render::StateOverlay::SetSize(uint32_t w, uint32_t h) noexcept { SetBackgroundSize({ w, h }); } -void Render::StateOverlay::Draw(const Config::OffsetGroup* curGroup, D3DContext& ctx) noexcept { +void Render::StateOverlay::Draw(const Actor* focus, const Config::OffsetGroup* curGroup, D3DContext& ctx) noexcept { DrawBackground(ctx); constexpr auto lineHeight = 14.0f; glm::vec2 curPos = { 5.0f + xPos, 0.0f + yPos }; - auto ply = *g_thePlayer; g_D2D->GetDWrite()->Write( L"Player State Bits", @@ -100,14 +100,14 @@ void Render::StateOverlay::Draw(const Config::OffsetGroup* curGroup, D3DContext& ); curPos.y += lineHeight; - DrawBitset32(L"Movement Bits: ", GameState::GetPlayerMovementBits(ply), curPos, ctx); + DrawBitset32(L"Movement Bits: ", GameState::GetPlayerMovementBits(focus), curPos, ctx); curPos.y += lineHeight; - DrawBitset32(L"Action Bits: ", GameState::GetPlayerActionBits(ply), curPos, ctx); + DrawBitset32(L"Action Bits: ", GameState::GetPlayerActionBits(focus), curPos, ctx); curPos.y += lineHeight; - std::wstring cameraState = L"Camera State: "; - cameraState.append(CameraStateEnumNames[static_cast(camera->GetCurrentCameraState())]); + eastl::wstring cameraState = L"Camera State: "; + cameraState.append(CameraStateEnumNames[static_cast(camera->m_camera->GetCurrentCameraState())]); g_D2D->GetDWrite()->Write( cameraState.c_str(), ctx.windowSize.x, ctx.windowSize.y, @@ -116,8 +116,8 @@ void Render::StateOverlay::Draw(const Config::OffsetGroup* curGroup, D3DContext& ); curPos.y += lineHeight; - std::wstring actionState = L"Action State: "; - actionState.append(ActionStateEnumNames[static_cast(camera->GetCurrentCameraActionState())]); + eastl::wstring actionState = L"Action State: "; + actionState.append(ActionStateEnumNames[static_cast(camera->m_camera->GetCurrentCameraActionState())]); g_D2D->GetDWrite()->Write( actionState.c_str(), ctx.windowSize.x, ctx.windowSize.y, @@ -126,52 +126,55 @@ void Render::StateOverlay::Draw(const Config::OffsetGroup* curGroup, D3DContext& ); curPos.y += lineHeight; - DrawBool(L"Weapon Drawn", GameState::IsWeaponDrawn(ply), curPos, ctx); + DrawBool(L"Weapon Drawn", GameState::IsWeaponDrawn(focus), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Using Left Magic Item", GameState::IsUsingMagicItem(ply, true), curPos, ctx); + DrawBool(L"Using Left Magic Item", GameState::IsUsingMagicItem(focus, true), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Using Right Magic Item", GameState::IsUsingMagicItem(ply), curPos, ctx); + DrawBool(L"Using Right Magic Item", GameState::IsUsingMagicItem(focus), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Left Combat Magic", GameState::IsCombatMagic(ply->leftHandSpell), curPos, ctx); + DrawBool(L"Left Combat Magic", GameState::IsCombatMagic(focus->leftHandSpell), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Right Combat Magic", GameState::IsCombatMagic(ply->rightHandSpell), curPos, ctx); + DrawBool(L"Right Combat Magic", GameState::IsCombatMagic(focus->rightHandSpell), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Melee Drawn", GameState::IsMeleeWeaponDrawn(ply), curPos, ctx); + DrawBool(L"Melee Drawn", GameState::IsMeleeWeaponDrawn(focus), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Magic Drawn", GameState::IsMagicDrawn(ply), curPos, ctx); + DrawBool(L"Magic Drawn", GameState::IsMagicDrawn(focus), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Ranged Drawn", GameState::IsRangedWeaponDrawn(ply), curPos, ctx); + DrawBool(L"Ranged Drawn", GameState::IsRangedWeaponDrawn(focus), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Using Crossbow", GameState::IsUsingCrossbow(ply), curPos, ctx); + DrawBool(L"Using Crossbow", GameState::IsUsingCrossbow(focus), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Using Bow", GameState::IsUsingBow(ply), curPos, ctx); + DrawBool(L"Using Bow", GameState::IsUsingBow(focus), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Aiming With Bow", GameState::IsBowDrawn(ply), curPos, ctx); + DrawBool(L"Aiming With Bow", GameState::IsBowDrawn(focus), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Sitting", GameState::IsSitting(ply), curPos, ctx); + DrawBool(L"Sitting", GameState::IsSitting(focus), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Sleeping", GameState::IsSleeping(ply), curPos, ctx); + DrawBool(L"Sleeping", GameState::IsSleeping(focus), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Swimming", GameState::IsSwimming(ply), curPos, ctx); + DrawBool(L"Swimming", GameState::IsSwimming(focus), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Mounting", GameState::IsMountingHorse(ply), curPos, ctx); + DrawBool(L"Mounting", GameState::IsMountingHorse(focus), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"Dismounting", GameState::IsDisMountingHorse(ply), curPos, ctx); + DrawBool(L"Dismounting", GameState::IsDisMountingHorse(focus), curPos, ctx); curPos.y += lineHeight; DrawBool(L"POV Slide Mode", GameState::InPOVSlideMode(), curPos, ctx); curPos.y += lineHeight; + DrawBool(L"IsVampire", GameState::IsVampireLord(focus), curPos, ctx); + curPos.y += lineHeight; + DrawBool(L"IsWerewolf", GameState::IsWerewolf(focus), curPos, ctx); + curPos.y += lineHeight; curPos.x = xPos + 270; curPos.y = yPos + lineHeight * 4; const auto cam = CorrectedPlayerCamera::GetSingleton(); - const auto focus = camera->GetCurrentCameraTarget(cam); if (focus) { DrawBool(L"IsFirstPerson", GameState::IsFirstPerson(focus, cam), curPos, ctx); curPos.y += lineHeight; DrawBool(L"IsThirdPerson", GameState::IsThirdPerson(focus, cam), curPos, ctx); curPos.y += lineHeight; - DrawBool(L"IsThirdPersonCombat", GameState::IsThirdPersonCombat(ply, cam), curPos, ctx); + DrawBool(L"IsThirdPersonCombat", GameState::IsThirdPersonCombat(focus, cam), curPos, ctx); curPos.y += lineHeight; DrawBool(L"IsInKillMove", GameState::IsInKillMove(cam), curPos, ctx); curPos.y += lineHeight; @@ -196,6 +199,8 @@ void Render::StateOverlay::Draw(const Config::OffsetGroup* curGroup, D3DContext& DrawBool(L"IsInDragonCamera", GameState::IsInDragonCamera(cam), curPos, ctx); curPos.y += lineHeight; } + DrawBool(L"ShoulderSwapped", camera->shoulderSwap <= 0, curPos, ctx); + curPos.y += lineHeight; curPos.x = xPos + 160; curPos.y = yPos; @@ -208,7 +213,7 @@ void Render::StateOverlay::Draw(const Config::OffsetGroup* curGroup, D3DContext& { 1.0f, 0.33f, 0.33f, 1.0f } ); } else { - std::wstring ofs = L"Offset Group: "; + eastl::wstring ofs = L"Offset Group: "; ofs.append(OffsetGroupNames[static_cast(curGroup->id)]); g_D2D->GetDWrite()->Write( ofs, @@ -219,14 +224,14 @@ void Render::StateOverlay::Draw(const Config::OffsetGroup* curGroup, D3DContext& } } -void Render::StateOverlay::DrawBitset32(const std::wstring& name, const std::bitset<32>& bits, +void Render::StateOverlay::DrawBitset32(const eastl::wstring& name, const eastl::bitset<32>& bits, const glm::vec2& pos, D3DContext& ctx) noexcept { - std::wstring str; + eastl::wstring str; str.reserve(32); for (auto i = 0; i < 32; i++) { - str.append(std::to_wstring(bits[i])); + str.append(std::to_wstring(bits[i]).c_str()); } g_D2D->GetDWrite()->Write( @@ -245,10 +250,10 @@ void Render::StateOverlay::DrawBitset32(const std::wstring& name, const std::bit ); } -void Render::StateOverlay::DrawBool(const std::wstring& name, bool value, const glm::vec2& pos, +void Render::StateOverlay::DrawBool(const eastl::wstring& name, bool value, const glm::vec2& pos, D3DContext& ctx) noexcept { - std::wstring str; + eastl::wstring str; str.reserve(name.length() + 10); str.append(name); str.append(L" = "); diff --git a/SmoothCam/source/render/vertex_buffer.cpp b/SmoothCam/source/render/vertex_buffer.cpp index 7181f1b..90a2f9c 100644 --- a/SmoothCam/source/render/vertex_buffer.cpp +++ b/SmoothCam/source/render/vertex_buffer.cpp @@ -13,9 +13,6 @@ Render::VertexBuffer::VertexBuffer(const VertexBufferCreateInfo& createInfo, D3D } Render::VertexBuffer::~VertexBuffer() noexcept { - context.context->IASetInputLayout(nullptr); - context.context->IASetVertexBuffers(0, 0, nullptr, nullptr, nullptr); - if (buffer) buffer = nullptr; @@ -23,7 +20,7 @@ Render::VertexBuffer::~VertexBuffer() noexcept { inputLayout = nullptr; } -void Render::VertexBuffer::CreateIALayout(const IALayout& layout, const std::shared_ptr& vertexProgram) noexcept { +void Render::VertexBuffer::CreateIALayout(const IALayout& layout, const eastl::shared_ptr& vertexProgram) noexcept { if (inputLayout) inputLayout = nullptr; diff --git a/SmoothCam/source/thirdperson.cpp b/SmoothCam/source/thirdperson.cpp new file mode 100644 index 0000000..9d32729 --- /dev/null +++ b/SmoothCam/source/thirdperson.cpp @@ -0,0 +1,1160 @@ +#include "thirdperson.h" +#include "debug/eh.h" + +Camera::Thirdperson::Thirdperson(Camera* baseCamera) : ICamera(baseCamera, CameraID::Thirdperson) { + config = Config::GetCurrentConfig(); + crosshair = eastl::make_unique(); + + cameraStates[static_cast(GameState::CameraState::ThirdPerson)] = + eastl::move(eastl::make_unique(this)); + cameraStates[static_cast(GameState::CameraState::ThirdPersonCombat)] = + eastl::move(eastl::make_unique(this)); + cameraStates[static_cast(GameState::CameraState::Horseback)] = + eastl::move(eastl::make_unique(this)); + +#ifdef WITH_CHARTS + if (Render::HasContext()) { + focusTargetNodeTree = eastl::make_unique(600, 1080, Render::GetContext()); + stateOverlay = eastl::make_unique(600, 128, this, Render::GetContext()); + + graph_worldPosTarget = eastl::make_unique(3, 128, 600, 128, Render::GetContext()); + graph_offsetPos = eastl::make_unique(3, 128, 600, 128, Render::GetContext()); + graph_targetOffsetPos = eastl::make_unique(3, 128, 600, 128, Render::GetContext()); + graph_localSpace = eastl::make_unique(3, 128, 600, 128, Render::GetContext()); + graph_rotation = eastl::make_unique(2, 128, 600, 128, Render::GetContext()); + graph_tpsRotation = eastl::make_unique(4, 128, 600, 128, Render::GetContext()); + graph_computeTime = eastl::make_unique(2, 128, 600, 128, Render::GetContext()); + + const auto xColor = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); + const auto yColor = glm::vec4(0.0f, 1.0f, 0.0f, 1.0f); + const auto zColor = glm::vec4(0.0f, 0.0f, 1.0f, 1.0f); + graph_worldPosTarget->SetLineColor(0, xColor); + graph_worldPosTarget->SetLineColor(1, yColor); + graph_worldPosTarget->SetLineColor(2, zColor); + graph_worldPosTarget->SetName(L"World Pos"); + + graph_localSpace->SetLineColor(0, xColor); + graph_localSpace->SetLineColor(1, yColor); + graph_localSpace->SetLineColor(2, zColor); + graph_localSpace->SetPosition(0, 128); + graph_localSpace->SetName(L"Local Space"); + + graph_offsetPos->SetLineColor(0, xColor); + graph_offsetPos->SetLineColor(1, yColor); + graph_offsetPos->SetLineColor(2, zColor); + graph_offsetPos->SetPosition(0, 256); + graph_offsetPos->SetName(L"Offset Pos"); + + graph_targetOffsetPos->SetLineColor(0, xColor); + graph_targetOffsetPos->SetLineColor(1, yColor); + graph_targetOffsetPos->SetLineColor(2, zColor); + graph_targetOffsetPos->SetPosition(0, 384); + graph_targetOffsetPos->SetName(L"Target Offset Pos"); + + graph_rotation->SetLineColor(0, xColor); + graph_rotation->SetLineColor(1, yColor); + graph_rotation->SetPosition(0, 512); + graph_rotation->SetName(L"Current Rotation (Pitch, Yaw)"); + + graph_tpsRotation->SetLineColor(0, xColor); + graph_tpsRotation->SetLineColor(1, yColor); + graph_tpsRotation->SetLineColor(2, zColor); + graph_tpsRotation->SetLineColor(3, { 1.0f, 1.0f, 0.0f, 1.0f }); + graph_tpsRotation->SetPosition(0, 640); + graph_tpsRotation->SetName(L"TPS Rotation (Yaw1, Yaw2, Yaw), camera->lookYaw"); + + graph_computeTime->SetLineColor(0, { 1.0f, 0.266f, 0.984f, 1.0f }); + graph_computeTime->SetLineColor(1, { 1.0f, 0.541f, 0.218f, 1.0f }); + graph_computeTime->SetName(L"Compute Time (Camera::Update()), Frame Time , seconds"); + graph_computeTime->SetPosition(0, 768); + + Render::CBufferCreateInfo cbuf; + cbuf.bufferUsage = D3D11_USAGE::D3D11_USAGE_DYNAMIC; + cbuf.cpuAccessFlags = D3D11_CPU_ACCESS_WRITE; + cbuf.size = sizeof(glm::mat4); + cbuf.initialData = &orthoMatrix; + perFrameBuffer = eastl::make_unique(cbuf, Render::GetContext()); + } +#endif +} + +Camera::Thirdperson::~Thirdperson() {} + +void Camera::Thirdperson::OnBegin(PlayerCharacter* player, CorrectedPlayerCamera* camera, ICamera* lastState) noexcept { + currentFocusObject = m_camera->GetCurrentCameraTarget(camera); + if (!currentFocusObject) + currentFocusObject = player; + + if (lastState && lastState->m_id == CameraID::Firstperson) { + // We want to invalidate any interpolation state when exiting first-person + MoveToGoalPosition(player, camera); + } +} + +void Camera::Thirdperson::OnEnd(PlayerCharacter* player, CorrectedPlayerCamera* camera, ICamera* newState) noexcept { + crosshair->Reset(); +} + +bool Camera::Thirdperson::OnPreGameUpdate(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState) +{ + const auto state = GameState::GetCameraState(player, camera); + if (state != GameState::CameraState::Transitioning) return false; + auto cstate = reinterpret_cast( + camera->cameraStates[CorrectedPlayerCamera::kCameraState_Transition] + ); + + auto horse = camera->cameraStates[CorrectedPlayerCamera::kCameraState_Horse]; + auto tps = camera->cameraStates[CorrectedPlayerCamera::kCameraState_ThirdPerson2]; + if (cstate->fromState == horse && cstate->toState == tps) { + if (!(config->compatIFPV && GameState::IFPV_InFirstPersonState(player, camera)) && + !(config->compatIC && GameState::IC_InFirstPersonState(player, camera))) + { + // Getting off a horse, MURDER THIS STATE + // @TODO: Write our own transition for this state - It still looks nicer with it disabled anyways though + if (nextState.ptr) + InterlockedDecrement(&nextState.ptr->refCount.m_refCount); + InterlockedIncrement(&cstate->toState->refCount.m_refCount); + nextState.ptr = cstate->toState; + + auto tpsState = reinterpret_cast( + camera->cameraStates[CorrectedPlayerCamera::kCameraState_ThirdPerson2] + ); + + auto horseState = reinterpret_cast( + camera->cameraStates[CorrectedPlayerCamera::kCameraState_Horse] + ); + + // Just copy a whole wack ton of stuff + tpsState->offsetVector = horseState->offsetVector; + tpsState->translation = horseState->translation; + tpsState->zoom = horseState->zoom; + tpsState->cameraZoom = horseState->cameraZoom; + tpsState->cameraLastZoom = horseState->cameraLastZoom; + tpsState->fOverShoulderPosX = horseState->fOverShoulderPosX; + tpsState->fOverShoulderCombatAddY = horseState->fOverShoulderCombatAddY; + tpsState->fOverShoulderPosZ = horseState->fOverShoulderPosZ; + tpsState->controllerNode->m_worldTransform = tpsState->controllerNode->m_oldWorldTransform; + MoveToGoalPosition(player, camera); + return true; + } + } + + return false; +} + +void Camera::Thirdperson::OnUpdateCamera(PlayerCharacter* player, CorrectedPlayerCamera* camera, + BSTSmartPointer& nextState) +{ +#ifdef WITH_CHARTS + Profiler prof; +#endif + + // Update POV switch smoothing + switch (m_camera->GetCurrentCameraState()) { + case GameState::CameraState::ThirdPerson: + case GameState::CameraState::ThirdPersonCombat: { + if (UpdatePOVSwitchState(camera, PlayerCamera::kCameraState_ThirdPerson2)) return; + break; + } + case GameState::CameraState::Horseback: { + if (UpdatePOVSwitchState(camera, PlayerCamera::kCameraState_Horse)) return; + break; + } + case GameState::CameraState::Dragon: { + if (UpdatePOVSwitchState(camera, PlayerCamera::kCameraState_Dragon)) return; + break; + } + case GameState::CameraState::Bleedout: { + if (UpdatePOVSwitchState(camera, PlayerCamera::kCameraState_Bleedout)) return; + break; + } + } + + // Invalidate while we are in a loading screen state + // Also if we were in the dialog menu - I'm not able to replicate this particular issue but it doesn't hurt + // to do it in this case also. + if (m_camera->InLoadingScreen() || wasDialogMenuOpen) + crosshair->InvalidateEnablementCache(); + + // Update our current crosshair selection if required + if (Render::HasContext() && config->useWorldCrosshair) + crosshair->Set3DCrosshairType(config->worldCrosshairType); + + currentFocusObject = m_camera->GetCurrentCameraTarget(camera); + if (!currentFocusObject) + currentFocusObject = player; + + offsetState.currentGroup = GetOffsetForState(m_camera->GetCurrentCameraActionState()); + const auto currentOffset = GetCurrentCameraOffset(player, camera); + const auto curTime = GameTime::CurTime(); + + // Don't continue to update transition states if we aren't running update code + bool shouldRun = false; + switch (m_camera->GetCurrentCameraState()) { + case GameState::CameraState::ThirdPerson: + case GameState::CameraState::ThirdPersonCombat: + case GameState::CameraState::Horseback: + shouldRun = true; + break; + default: break; + } + + // And set our current reference position + if (m_camera->wasLoading || (wasDialogMenuOpen && config->compatACC)) { + // Exiting a loading screen or dialog with ACC, set the goal positon + MoveToGoalPosition(player, camera); + } + + // Save our last position + lastPosition = currentPosition; + + if (!shouldRun || (config->compatACC && dialogMenuOpen)) { + // + + } else { + // Update ref position + currentPosition.SetRef(currentFocusObject->pos, currentFocusObject->rot); + + // Update transition states + mmath::UpdateTransitionState( + curTime, + config->enableOffsetInterpolation, + config->offsetInterpDurationSecs, + config->offsetScalar, + offsetTransitionState, + { currentOffset.x, currentOffset.z } + ); + + if (!povWasPressed && !povTransitionState.running) { + mmath::UpdateTransitionState( + curTime, + config->enableZoomInterpolation, + config->zoomInterpDurationSecs, + config->zoomScalar, + zoomTransitionState, + currentOffset.y + ); + } else { + zoomTransitionState.lastPosition = zoomTransitionState.currentPosition = + zoomTransitionState.targetPosition = currentOffset.y; + } + + mmath::UpdateTransitionState( + curTime, + config->enableFOVInterpolation, + config->fovInterpDurationSecs, + config->fovScalar, + fovTransitionState, + currentOffset.w + ); + + offsetState.position = { + offsetTransitionState.currentPosition.x, + zoomTransitionState.currentPosition, + offsetTransitionState.currentPosition.y + }; + offsetState.fov = fovTransitionState.currentPosition; + SetFOVOffset(offsetState.fov); + + // Now run the active camera state + switch (m_camera->GetCurrentCameraState()) { + case GameState::CameraState::ThirdPerson: { + UpdateInternalRotation(camera); + cameraStates.at(static_cast(GameState::CameraState::ThirdPerson))->Update(player, currentFocusObject, camera); + break; + } + case GameState::CameraState::ThirdPersonCombat: { + UpdateInternalRotation(camera); + cameraStates.at(static_cast(GameState::CameraState::ThirdPersonCombat))->Update(player, currentFocusObject, camera); + break; + } + case GameState::CameraState::Horseback: { + UpdateInternalRotation(camera); + cameraStates.at(static_cast(GameState::CameraState::Horseback))->Update(player, currentFocusObject, camera); + break; + } + default: break; + } + } + + povWasPressed = false; + wasDialogMenuOpen = false; + + if (!m_camera->InLoadingScreen()) + crosshair->Update(player, camera); + +#ifdef WITH_CHARTS + lastProfSnap = prof.Snap(); +#endif +} + +void Camera::Thirdperson::Render(Render::D3DContext& ctx) noexcept { + if (m_camera->InLoadingScreen()) return; + crosshair->Render(ctx, currentPosition.world, rotation.euler, m_camera->GetFrustum()); + +#ifdef WITH_CHARTS + if (!currentFocusObject) return; + if (GetAsyncKeyState(VK_UP)) { + if (!dbgKeyDown) { + switch (curDebugMode) { + case DisplayMode::None: + curDebugMode = DisplayMode::Graphs; + break; + case DisplayMode::Graphs: + curDebugMode = DisplayMode::NodeTree; + break; + case DisplayMode::NodeTree: + curDebugMode = DisplayMode::StateOverlay; + break; + case DisplayMode::StateOverlay: + curDebugMode = DisplayMode::None; + break; + } + dbgKeyDown = true; + } + } else { + dbgKeyDown = false; + } + + const auto& size = ctx.windowSize; + orthoMatrix = glm::ortho(0.0f, size.x, size.y, 0.0f); + perFrameBuffer->Update(&orthoMatrix, 0, sizeof(glm::mat4), ctx); + perFrameBuffer->Bind(Render::PipelineStage::Vertex, 1, ctx); + + switch (curDebugMode) { + case DisplayMode::None: + break; + + case DisplayMode::Graphs: { + const auto worldPos = GetCurrentCameraTargetWorldPosition(currentFocusObject, CorrectedPlayerCamera::GetSingleton()); + graph_worldPosTarget->AddPoint(0, worldPos.x); + graph_worldPosTarget->AddPoint(1, worldPos.y); + graph_worldPosTarget->AddPoint(2, worldPos.z); + + graph_localSpace->AddPoint(0, lastPosition.local.x); + graph_localSpace->AddPoint(1, lastPosition.local.y); + graph_localSpace->AddPoint(2, lastPosition.local.z); + + graph_offsetPos->AddPoint(0, offsetState.position.x); + graph_offsetPos->AddPoint(1, offsetState.position.y); + graph_offsetPos->AddPoint(2, offsetState.position.z); + + const auto ofsPos = GetCurrentCameraOffset(*g_thePlayer, CorrectedPlayerCamera::GetSingleton()); + graph_targetOffsetPos->AddPoint(0, ofsPos.x); + graph_targetOffsetPos->AddPoint(1, ofsPos.y); + graph_targetOffsetPos->AddPoint(2, ofsPos.z); + + graph_rotation->AddPoint(0, rotation.euler.x); + graph_rotation->AddPoint(1, rotation.euler.y); + + if (GameState::IsThirdPerson(currentFocusObject, CorrectedPlayerCamera::GetSingleton())) { + auto tps = reinterpret_cast(CorrectedPlayerCamera::GetSingleton()->cameraState); + graph_tpsRotation->AddPoint(0, tps->yaw1); + graph_tpsRotation->AddPoint(1, tps->yaw2); + graph_tpsRotation->AddPoint(2, tps->yaw); + graph_tpsRotation->AddPoint(3, CorrectedPlayerCamera::GetSingleton()->lookYaw); + } + + graph_computeTime->AddPoint(0, lastProfSnap); + graph_computeTime->AddPoint(1, static_cast(GameTime::GetFrameDelta())); + + graph_worldPosTarget->Draw(ctx); + graph_localSpace->Draw(ctx); + graph_offsetPos->Draw(ctx); + graph_targetOffsetPos->Draw(ctx); + graph_rotation->Draw(ctx); + graph_tpsRotation->Draw(ctx); + graph_computeTime->Draw(ctx); + + break; + } + case DisplayMode::NodeTree: { + focusTargetNodeTree->SetPosition(0, 0); + focusTargetNodeTree->SetSize(size.x, size.y); + if (currentFocusObject->loadedState->node) + focusTargetNodeTree->Draw(ctx, currentFocusObject->loadedState->node); + break; + } + case DisplayMode::StateOverlay: { + stateOverlay->SetPosition((size.x / 2) - 300, size.y - 600); + stateOverlay->SetSize(600, 400); + stateOverlay->Draw(currentFocusObject, offsetState.currentGroup, ctx); + break; + } + } +#endif +} + +void Camera::Thirdperson::OnTogglePOV(const ButtonEvent* ev) noexcept { + povWasPressed = true; +} + +bool Camera::Thirdperson::OnKeyPress(const ButtonEvent* ev) noexcept { + auto code = ev->keyMask; + if (code <= 0x6 && ev->deviceType == kDeviceType_Mouse) + code += 0x100; + + if (config->shoulderSwapKey >= 0 && config->shoulderSwapKey == code && ev->timer <= 0.000001f) { + shoulderSwap = shoulderSwap == 1 ? -1 : 1; + return true; + } else if (config->applyZOffsetKey >= 0 && config->applyZOffsetKey == code && ev->timer <= 0.000001f) { + config->zOffsetActive = !config->zOffsetActive; + } + + return false; +} + +bool Camera::Thirdperson::OnMenuOpenClose(MenuID id, const MenuOpenCloseEvent* const ev) noexcept { + switch (id) { + case MenuID::DialogMenu: { + if (dialogMenuOpen && !ev->opening) wasDialogMenuOpen = true; + dialogMenuOpen = ev->opening; + + if (dialogMenuOpen && config->compatACC) { + stateCopyData.accPitch = (*g_thePlayer)->rot.x; + } + return true; + } + case MenuID::MapMenu: { + SetFOVOffset(0.0f, true); + return true; + } + } + return false; +} + +void Camera::Thirdperson::OnCameraActionStateTransition(const PlayerCharacter* player, const CameraActionState newState, + const CameraActionState oldState) noexcept +{ + +} + +void Camera::Thirdperson::OnCameraStateTransition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera, + const GameState::CameraState newState, const GameState::CameraState oldState) noexcept +{ + auto& oldCameraState = cameraStates.at(static_cast(oldState)); + auto& newCameraState = cameraStates.at(static_cast(newState)); + + switch (oldState) { + case GameState::CameraState::UsingObject: { + MoveToGoalPosition(player, camera); + break; + } + case GameState::CameraState::FirstPerson: { + break; + } + case GameState::CameraState::ThirdPerson: { + oldCameraState->OnEnd(player, currentFocusObject, camera, newCameraState.get()); + break; + } + case GameState::CameraState::ThirdPersonCombat: { + oldCameraState->OnEnd(player, currentFocusObject, camera, newCameraState.get()); + break; + } + case GameState::CameraState::Horseback: { + oldCameraState->OnEnd(player, currentFocusObject, camera, newCameraState.get()); + + if (newState == GameState::CameraState::Tweening) { + // The game clears horseYaw to 0 when going from tween back to horseback, which results in a rotation jump + // once the horse starts moving again. We can store the horse yaw value here to restore later. + stateCopyData.horseYaw = reinterpret_cast( + camera->cameraStates[CorrectedPlayerCamera::kCameraState_Horse] + )->horseYaw; + + } else if (newState == GameState::CameraState::ThirdPerson) { + MoveToGoalPosition(player, camera); + } + + break; + } + case GameState::CameraState::Tweening: + case GameState::CameraState::Transitioning: { + // Don't copy positions from these states + break; + } + default: + break; + } + + switch (newState) { + case GameState::CameraState::ThirdPerson: { + newCameraState->OnBegin(player, currentFocusObject, camera, oldCameraState.get()); + break; + } + case GameState::CameraState::ThirdPersonCombat: { + newCameraState->OnBegin(player, currentFocusObject, camera, oldCameraState.get()); + break; + } + case GameState::CameraState::Horseback: { + newCameraState->OnBegin(player, currentFocusObject, camera, oldCameraState.get()); + + if (oldState == GameState::CameraState::Tweening) { + // Fix annoying horse rotation reset + reinterpret_cast( + camera->cameraStates[CorrectedPlayerCamera::kCameraState_Horse] + )->horseYaw = stateCopyData.horseYaw; + } + + break; + } + case GameState::CameraState::Free: { + // Forward our position to the free cam state + auto state = reinterpret_cast(camera->cameraState); + state->unk30[0] = lastPosition.world.x; + state->unk30[1] = lastPosition.world.y; + state->unk30[2] = lastPosition.world.z; + + const auto quat = rotation.ToNiQuat(); + + //FUN_140848880((longlong)ppuVar3,local_18); + typedef void(__fastcall* UpdateFreeCamTransform)(FreeCameraState*, const NiQuaternion&); + Offsets::Get(49816)(state, quat); + } + [[fallthrough]]; + default: { + // Do this once here on transition to a new state we don't run in + crosshair->Reset(); + break; + } + } +} + +glm::vec3 Camera::Thirdperson::GetCurrentCameraTargetWorldPosition(const TESObjectREFR* ref, + const CorrectedPlayerCamera* camera) const +{ + if (ref->loadedState && ref->loadedState->node && Strings.spine1.data) { + NiAVObject* node; + if (m_camera->currentState == GameState::CameraState::Horseback) + node = ref->loadedState->node->GetObjectByName(&Strings.spine1.data); + else + node = FindFollowBone(ref); + + if (node) + return glm::vec3( + ref->pos.x, + ref->pos.y, + node->m_worldTransform.pos.z + ); + } + + return { + ref->pos.x, + ref->pos.y, + ref->pos.z + }; +} + +void Camera::Thirdperson::GetCameraGoalPosition(const CorrectedPlayerCamera* camera, glm::vec3& world, glm::vec3& local) { + const auto cameraLocal = offsetState.position; + auto translated = rotation.ToRotationMatrix() * glm::vec4( + cameraLocal.x, + cameraLocal.y, + 0.0f, + 1.0f + ); + translated.z += cameraLocal.z; + + local = static_cast(translated); + world = + GetCurrentCameraTargetWorldPosition(currentFocusObject, camera) + + local; +} + +glm::vec2 Camera::Thirdperson::GetAimRotation(const TESObjectREFR* ref, const CorrectedPlayerCamera* camera) const { + if (GameState::IsInHorseCamera(ref, camera)) { + // In horse camera, we need to clamp our local y axis + const auto yaw = ref->rot.z + glm::pi(); // convert to 0-tau + const auto yawLocal = glm::mod(rotation.euler.y - yaw, glm::pi() * 2.0f); // confine to tau + const auto clampedLocal = glm::clamp(yawLocal, mmath::half_pi, mmath::half_pi*3.0f); // clamp it to faceforward zone + + return { + rotation.euler.x, + yaw + clampedLocal + }; + } else { + const auto tps = reinterpret_cast(camera->cameraStates[CorrectedPlayerCamera::kCameraState_ThirdPerson2]); + if ((tps && tps->freeRotationEnabled) || GameState::InPOVSlideMode()) { + // In free rotation mode aim yaw is locked to player rotation + // POVSlideMode - The character stays still while the camera orbits + + // @Note: PR #48: Hopefully this still works correctly as both cases are checked, + // with POVSlideMode checked last + return { + rotation.euler.x, + ref->rot.z + }; + } + } + + return rotation.euler; +} + +const mmath::Rotation& Camera::Thirdperson::GetCameraRotation() const noexcept { + return rotation; +} + +void Camera::Thirdperson::SetPosition(const glm::vec3& pos, const CorrectedPlayerCamera* camera) noexcept { + m_camera->SetPosition(pos, camera); + m_camera->UpdateInternalWorldToScreenMatrix(rotation, camera); +} + +Crosshair::Manager* Camera::Thirdperson::GetCrosshairManager() noexcept { + return crosshair.get(); +} + +void Camera::Thirdperson::MoveToGoalPosition(const PlayerCharacter* player, const CorrectedPlayerCamera* camera) noexcept { + const auto pov = m_camera->UpdateCameraPOVState(player, camera); + const auto actionState = m_camera->UpdateCurrentCameraActionState(player, camera); + offsetState.currentGroup = GetOffsetForState(actionState); + + auto ofs = GetCurrentCameraOffset(player, camera); + offsetTransitionState.currentPosition = offsetTransitionState.lastPosition = { + ofs.x, ofs.z + }; + zoomTransitionState.currentPosition = zoomTransitionState.lastPosition = ofs.y; + fovTransitionState.currentPosition = fovTransitionState.lastPosition = ofs.w; + + GetCameraGoalPosition(camera, currentPosition.world, currentPosition.local); + lastPosition = currentPosition; +} + +void Camera::Thirdperson::UpdateInternalRotation(CorrectedPlayerCamera* camera) noexcept { + const auto tps = reinterpret_cast(camera->cameraState); + if (!tps) return; + + if (config->compatACC && wasDialogMenuOpen) { + auto pl = *g_thePlayer; + if (pl) { + tps->rotation = rotation.ToNiQuat(); + tps->yaw1 = tps->yaw2 = rotation.euler.y; + camera->lookYaw = rotation.euler.y; + pl->rot.x = stateCopyData.accPitch; + } + + } else { + // Alright, just lie and force the game to compute yaw for us + // We get some jitter when things like magic change our character, not sure why yet @TODO + auto last = tps->freeRotationEnabled; + auto pl = *g_thePlayer; + tps->freeRotationEnabled = true; + tps->UpdateRotation(); + rotation.SetQuaternion(tps->rotation); + tps->freeRotationEnabled = last; + tps->UpdateRotation(); + } +} + +NiAVObject* Camera::Thirdperson::FindFollowBone(const TESObjectREFR* ref) const noexcept { + if (!ref->loadedState || !ref->loadedState->node) return nullptr; + auto& boneList = Config::GetBonePriorities(); + + for (auto it = boneList.begin(); it != boneList.end(); it++) { + auto node = ref->loadedState->node->GetObjectByName(&it->data); + if (node) return node; + } + + return nullptr; +} + +float Camera::Thirdperson::GetCurrentCameraZoom(const CorrectedPlayerCamera* camera, + const GameState::CameraState currentState) const noexcept +{ + switch (currentState) { + case GameState::CameraState::ThirdPerson: + case GameState::CameraState::ThirdPersonCombat: { + return GetCameraZoomScalar(camera, PlayerCamera::kCameraState_ThirdPerson2); + } + case GameState::CameraState::Horseback: { + return GetCameraZoomScalar(camera, PlayerCamera::kCameraState_Horse); + } + case GameState::CameraState::Dragon: { + return GetCameraZoomScalar(camera, PlayerCamera::kCameraState_Dragon); + } + case GameState::CameraState::Bleedout: { + return GetCameraZoomScalar(camera, PlayerCamera::kCameraState_Bleedout); + } + default: + return 0.0f; + } +} + +float Camera::Thirdperson::GetCameraZoomScalar(const CorrectedPlayerCamera* camera, uint16_t cameraState) const noexcept { + const auto state = reinterpret_cast(camera->cameraStates[cameraState]); + if (!state) return 0.0f; + + auto minZoom = 0.2f; + static auto minZoomConf = (*g_iniSettingCollection)->Get("fMinCurrentZoom:Camera"); + if (minZoomConf) minZoom = minZoomConf->data.f32; + + return state->cameraZoom + (minZoom *-1); +} + +bool Camera::Thirdperson::IsInterpAllowed(const PlayerCharacter* player) const noexcept { + auto ofs = offsetState.currentGroup; + if (m_camera->currentState == GameState::CameraState::Horseback) { + if (GameState::IsBowDrawn(player)) { + return config->bowAim.interpHorseback; + } else { + ofs = &config->horseback; + } + } + + if (GameState::IsSneaking(player)) { + if (GameState::IsBowDrawn(player)) { + return config->bowAim.interpMeleeCombat; + } + } + + if (!GameState::IsWeaponDrawn(player)) return ofs->interp; + if (GameState::IsRangedWeaponDrawn(player)) { + return ofs->interpRangedCombat; + } + if (GameState::IsMagicDrawn(player)) { + return ofs->interpMagicCombat; + } + return ofs->interpMeleeCombat; +} + +const Config::OffsetGroup* Camera::Thirdperson::GetOffsetForState(const CameraActionState state) const noexcept { + switch (state) { + case CameraActionState::VampireLord: { + return &config->vampireLord; + } + case CameraActionState::Werewolf: { + return &config->werewolf; + } + case CameraActionState::DisMounting: { + return &config->standing; // Better when dismounting + } + case CameraActionState::Sleeping: { + return &config->sitting; + } + case CameraActionState::Sitting: { + return &config->sitting; + } + case CameraActionState::Sneaking: { + return &config->sneaking; + } + case CameraActionState::Aiming: { + return &config->bowAim; + } + case CameraActionState::Swimming: { + return &config->swimming; + } + case CameraActionState::Sprinting: { + return &config->sprinting; + } + case CameraActionState::Walking: { + return &config->walking; + } + case CameraActionState::Running: { + return &config->running; + } + case CameraActionState::Standing: { + return &config->standing; + } + default: { + return &config->standing; + } + } +} + +float Camera::Thirdperson::GetActiveWeaponStateZoomOffset(const PlayerCharacter* player, + const Config::OffsetGroup* group) const noexcept +{ + if (!GameState::IsWeaponDrawn(player)) return group->zoomOffset; + if (GameState::IsRangedWeaponDrawn(player)) { + return group->combatRangedZoomOffset; + } + if (GameState::IsMagicDrawn(player)) { + return group->combatMagicZoomOffset; + } + return group->combatMeleeZoomOffset; +} + +float Camera::Thirdperson::GetActiveWeaponStateUpOffset(const PlayerCharacter* player, + const Config::OffsetGroup* group) const noexcept +{ + if (!GameState::IsWeaponDrawn(player)) return group->upOffset; + if (GameState::IsRangedWeaponDrawn(player)) { + return group->combatRangedUpOffset; + } + if (GameState::IsMagicDrawn(player)) { + return group->combatMagicUpOffset; + } + return group->combatMeleeUpOffset; +} + +float Camera::Thirdperson::GetActiveWeaponStateSideOffset(const PlayerCharacter* player, + const Config::OffsetGroup* group) const noexcept +{ + if (!GameState::IsWeaponDrawn(player)) return group->sideOffset; + if (GameState::IsRangedWeaponDrawn(player)) { + return group->combatRangedSideOffset; + } + if (GameState::IsMagicDrawn(player)) { + return group->combatMagicSideOffset; + } + return group->combatMeleeSideOffset; +} + +float Camera::Thirdperson::GetActiveWeaponStateFOVOffset(const PlayerCharacter* player, + const Config::OffsetGroup* group) const noexcept +{ + if (!GameState::IsWeaponDrawn(player)) return group->fovOffset; + if (GameState::IsRangedWeaponDrawn(player)) { + return group->combatRangedFOVOffset; + } + if (GameState::IsMagicDrawn(player)) { + return group->combatMagicFOVOffset; + } + return group->combatMeleeFOVOffset; +} + +float Camera::Thirdperson::GetCurrentCameraZoomOffset(const PlayerCharacter* player) const noexcept { + switch (m_camera->currentState) { + case GameState::CameraState::Horseback: { + if (GameState::IsBowDrawn(player)) { + return config->bowAim.horseZoomOffset; + } else { + return GetActiveWeaponStateZoomOffset(player, &config->horseback); + } + } + default: + break; + } + + switch (m_camera->currentActionState) { + case CameraActionState::DisMounting: + case CameraActionState::Sleeping: + case CameraActionState::Sitting: + case CameraActionState::Swimming: { + return offsetState.currentGroup->zoomOffset; + } + case CameraActionState::Aiming: { + if (GameState::IsSneaking(player)) + return config->bowAim.combatMeleeZoomOffset; + return offsetState.currentGroup->zoomOffset; + } + case CameraActionState::Sneaking: + case CameraActionState::VampireLord: + case CameraActionState::Werewolf: + case CameraActionState::Sprinting: + case CameraActionState::Walking: + case CameraActionState::Running: + case CameraActionState::Standing: { + return GetActiveWeaponStateZoomOffset(player, offsetState.currentGroup); + } + default: { + break; + } + } + return 0.0f; +} + +float Camera::Thirdperson::GetCurrentCameraHeight(const PlayerCharacter* player) const noexcept { + switch (m_camera->currentState) { + case GameState::CameraState::Horseback: { + if (GameState::IsBowDrawn(player)) { + return config->bowAim.horseUpOffset; + } else { + return GetActiveWeaponStateUpOffset(player, &config->horseback); + } + } + default: + break; + } + + switch (m_camera->currentActionState) { + case CameraActionState::DisMounting: + case CameraActionState::Sleeping: + case CameraActionState::Sitting: + case CameraActionState::Swimming: { + return offsetState.currentGroup->upOffset; + } + case CameraActionState::Aiming: { + if (GameState::IsSneaking(player)) + return config->bowAim.combatMeleeUpOffset; + return offsetState.currentGroup->upOffset; + } + case CameraActionState::Sneaking: + case CameraActionState::VampireLord: + case CameraActionState::Werewolf: + case CameraActionState::Sprinting: + case CameraActionState::Walking: + case CameraActionState::Running: + case CameraActionState::Standing: { + return GetActiveWeaponStateUpOffset(player, offsetState.currentGroup); + } + default: { + break; + } + } + return 0.0f; +} + +float Camera::Thirdperson::GetCurrentCameraSideOffset(const PlayerCharacter* player, + const CorrectedPlayerCamera* camera) const noexcept +{ + switch (m_camera->currentState) { + case GameState::CameraState::Horseback: { + if (GameState::IsBowDrawn(player)) { + return config->bowAim.horseSideOffset * shoulderSwap; + } else { + return GetActiveWeaponStateSideOffset(player, &config->horseback) * shoulderSwap; + } + } + default: + break; + } + + switch (m_camera->currentActionState) { + case CameraActionState::DisMounting: + case CameraActionState::Sleeping: + case CameraActionState::Sitting: + case CameraActionState::Swimming: { + return offsetState.currentGroup->sideOffset * shoulderSwap; + } + case CameraActionState::Aiming: { + if (GameState::IsSneaking(player)) + return config->bowAim.combatMeleeSideOffset * shoulderSwap; + return offsetState.currentGroup->sideOffset * shoulderSwap; + } + case CameraActionState::Sneaking: + case CameraActionState::VampireLord: + case CameraActionState::Werewolf: + case CameraActionState::Sprinting: + case CameraActionState::Walking: + case CameraActionState::Running: + case CameraActionState::Standing: { + return GetActiveWeaponStateSideOffset(player, offsetState.currentGroup) * shoulderSwap; + } + default: { + break; + } + } + return 0.0f; +} + +float Camera::Thirdperson::GetCurrentCameraFOVOffset(const PlayerCharacter* player) const noexcept { + switch (m_camera->currentState) { + case GameState::CameraState::Horseback: { + if (GameState::IsBowDrawn(player)) { + return config->bowAim.horseFOVOffset; + } else { + return GetActiveWeaponStateFOVOffset(player, &config->horseback); + } + } + default: + break; + } + + switch (m_camera->currentActionState) { + case CameraActionState::DisMounting: + case CameraActionState::Sleeping: + case CameraActionState::Sitting: + case CameraActionState::Swimming: { + return offsetState.currentGroup->fovOffset; + } + case CameraActionState::Aiming:{ + if (GameState::IsSneaking(player)) + return config->bowAim.combatMeleeFOVOffset; + return offsetState.currentGroup->fovOffset; + } + case CameraActionState::Sneaking: + case CameraActionState::VampireLord: + case CameraActionState::Werewolf: + case CameraActionState::Sprinting: + case CameraActionState::Walking: + case CameraActionState::Running: + case CameraActionState::Standing: { + return GetActiveWeaponStateFOVOffset(player, offsetState.currentGroup); + } + default: { + break; + } + } + return 0.0f; +} + +float Camera::Thirdperson::GetCurrentCameraDistance(const CorrectedPlayerCamera* camera) const noexcept { + return -(config->minCameraFollowDistance + (GetCurrentCameraZoom(camera, m_camera->currentState) * config->zoomMul)); +} + +glm::vec4 Camera::Thirdperson::GetCurrentCameraOffset(const PlayerCharacter* player, + const CorrectedPlayerCamera* camera) const noexcept +{ + return { + GetCurrentCameraSideOffset(player, camera), + GetCurrentCameraDistance(camera) + GetCurrentCameraZoomOffset(player), + GetCurrentCameraHeight(player) + (config->zOffsetActive ? config->customZOffset : 0.0f), + GetCurrentCameraFOVOffset(player) + }; +} + +double Camera::Thirdperson::GetCurrentSmoothingScalar(const float distance, ScalarSelector method) const { + Config::ScalarMethods scalarMethod; + + // Work in FP64 here to eek out some more precision + // Avoid a divide-by-zero error by clamping to this lower bound + constexpr const double minZero = 0.000000000001; + + double scalar = 1.0; + double interpValue = 1.0; + double remapped = 1.0; + + if (method == ScalarSelector::SepZ) { + const auto max = static_cast(config->separateZMaxSmoothingDistance); + scalar = glm::clamp(glm::max(1.0 - (max - distance), minZero) / max, 0.0, 1.0); + remapped = mmath::Remap( + scalar, 0.0, 1.0, static_cast(config->separateZMinFollowRate), static_cast(config->separateZMaxFollowRate) + ); + scalarMethod = config->separateZScalar; + } else if (method == ScalarSelector::LocalSpace) { + remapped = distance; + scalarMethod = config->separateLocalScalar; + } else { + const auto max = static_cast(config->zoomMaxSmoothingDistance); + scalar = glm::clamp(glm::max(1.0 - (max - distance), minZero) / max, 0.0, 1.0); + remapped = mmath::Remap( + scalar, 0.0, 1.0, static_cast(config->minCameraFollowRate), static_cast(config->maxCameraFollowRate) + ); + scalarMethod = config->currentScalar; + } + + if (!config->disableDeltaTime) { + const double delta = glm::max(GameTime::GetFrameDelta(), minZero); + const double fps = 1.0 / delta; + const double mul = -fps * glm::log2(1.0 - remapped); + interpValue = glm::clamp(1.0 - glm::exp2(-mul * delta), 0.0, 1.0); + } else { + interpValue = remapped; + } + + return mmath::RunScalarFunction(scalarMethod, interpValue); +} + +std::tuple Camera::Thirdperson::GetDistanceClamping() const noexcept { + float minsX = config->cameraDistanceClampXMin; + float maxsX = config->cameraDistanceClampXMax; + + if (config->swapXClamping && shoulderSwap < 1) { + std::swap(minsX, maxsX); + maxsX *= -1.0f; + minsX *= -1.0f; + } + + return std::tie( + glm::vec3{ minsX, config->cameraDistanceClampYMin, config->cameraDistanceClampZMin }, + glm::vec3{ maxsX, config->cameraDistanceClampYMax, config->cameraDistanceClampZMax } + ); +} + +void Camera::Thirdperson::SetFOVOffset(float fov, bool force) noexcept { + if (force) { + // When in the map menu, this won't be reset, thus we have to force it. + // Opening the map menu when zoomed in with a bow does the same thing. + *Offsets::Get(527997) = fov; + + // This is caused by the reset code being the job of thirdperson cameras (Third, Horse, Dragon) + // The map menu is a different camera + + } else { + // FOV reset each frame relies on the controller node being valid + // If this isn't the case, we should do it manually here + // Controller node becomes null in certain cases - Undeath lich form, transformation spells, so on + auto cam = CorrectedPlayerCamera::GetSingleton(); + auto curState = cam->cameraState; + if (curState == cam->cameraStates[CorrectedPlayerCamera::kCameraState_ThirdPerson2]) { + if (reinterpret_cast(curState)->controllerNode == nullptr) + *Offsets::Get(527997) = 0.0f; + + } else if (curState == cam->cameraStates[CorrectedPlayerCamera::kCameraState_Horse]) { + if (reinterpret_cast(curState)->controllerNode == nullptr) + *Offsets::Get(527997) = 0.0f; + + } else if (curState == cam->cameraStates[CorrectedPlayerCamera::kCameraState_Dragon]) { + if (reinterpret_cast(curState)->controllerNode == nullptr) + *Offsets::Get(527997) = 0.0f; + } + } + + // This is an offset value applied to any call to SetFOV. + // Used internally in the game by things like zooming with the bow. + // Appears to be set by the game every frame so we can just add our offset on top like this. + const auto baseValue = CorrectedPlayerCamera::GetSingleton()->worldFOV; + const auto currentOffset = *Offsets::Get(527997); + + // We never want the FOV to go negative or past 180, so clamp it to a sane-ish range + // (10-170) + const auto actualFov = baseValue + currentOffset; + constexpr auto fovMax = 170.0f; + constexpr auto fovMin = 10.0f; + + // Break early if we are already past our limit + if (actualFov >= fovMax) return; + if (actualFov <= fovMin) return; + + // Compute the maximal range we can offset before hitting the limit + const auto fullOffset = currentOffset + fov; + const auto f = baseValue + fullOffset; + auto finalOffset = fov; + + if (f > fovMax) { + auto delta = f - fovMax; + finalOffset -= delta; + } else if (f < fovMin) { + auto delta = fovMin - f; + finalOffset += delta; + } + + *Offsets::Get(527997) += finalOffset; +} + +bool Camera::Thirdperson::UpdatePOVSwitchState(CorrectedPlayerCamera* camera, uint16_t cameraState) noexcept { + auto state = reinterpret_cast(camera->cameraStates[cameraState]); + if (state->stateNotActive) { + auto minZoom = 0.2f; + static auto minZoomConf = (*g_iniSettingCollection)->Get("fMinCurrentZoom:Camera"); + if (minZoomConf) minZoom = minZoomConf->data.f32; + const auto curTime = GameTime::CurTime(); + constexpr auto duration = 0.5f; + + // We are switching over to first person + if (povWasPressed) { + // We just started the switch + povTransitionState.running = true; + povTransitionState.startTime = curTime; + povTransitionState.lastValue = state->cameraZoom = lastZoomValue; + } + + // Switch running + const auto scalar = glm::clamp( + static_cast(curTime - povTransitionState.startTime) / glm::max(duration, 0.01f), + 0.0f, 1.0f + ); + povTransitionState.lastValue = mmath::Interpolate( + povTransitionState.lastValue, + minZoom, + mmath::RunScalarFunction(Config::ScalarMethods::LINEAR, scalar) + ); + + lastZoomValue = state->cameraZoom = state->cameraLastZoom = povTransitionState.lastValue; + + // Switch mid-zoom + if (scalar >= 0.5f) { + povTransitionState.running = false; + typedef void(*SwitchToFirstPerson)(CorrectedPlayerCamera*); + Offsets::Get(49858)(camera); + return true; + } + + } else { + // Record the current camera zoom value for use during switches + lastZoomValue = state->cameraZoom; + povTransitionState.running = false; + } + + return false; +} \ No newline at end of file diff --git a/SmoothCam/source/timer.cpp b/SmoothCam/source/timer.cpp index 5d1d7b8..fbdd412 100644 --- a/SmoothCam/source/timer.cpp +++ b/SmoothCam/source/timer.cpp @@ -28,6 +28,11 @@ double GameTime::GetQPC() noexcept { } void GameTime::StepFrameTime() noexcept { +#ifdef _DEBUG + // @TODO: This is an awful place for this + Debug::CommandPump(); +#endif + lastFrame = curFrame; curFrame = GetTime(); diff --git a/SmoothCam/source/trackir/trackir.cpp b/SmoothCam/source/trackir/trackir.cpp new file mode 100644 index 0000000..2230290 --- /dev/null +++ b/SmoothCam/source/trackir/trackir.cpp @@ -0,0 +1,288 @@ +#ifdef DEVELOPER +#include "trackir/trackir.h" + +static bool running = false; + +TrackIR::PF_NP_REGISTERWINDOWHANDLE gpfNP_RegisterWindowHandle = NULL; +TrackIR::PF_NP_UNREGISTERWINDOWHANDLE gpfNP_UnregisterWindowHandle = NULL; +TrackIR::PF_NP_REGISTERPROGRAMPROFILEID gpfNP_RegisterProgramProfileID = NULL; +TrackIR::PF_NP_QUERYVERSION gpfNP_QueryVersion = NULL; +TrackIR::PF_NP_REQUESTDATA gpfNP_RequestData = NULL; +TrackIR::PF_NP_GETSIGNATURE gpfNP_GetSignature = NULL; +TrackIR::PF_NP_GETDATA gpfNP_GetData = NULL; +TrackIR::PF_NP_STARTCURSOR gpfNP_StartCursor = NULL; +TrackIR::PF_NP_STOPCURSOR gpfNP_StopCursor = NULL; +TrackIR::PF_NP_RECENTER gpfNP_ReCenter = NULL; +TrackIR::PF_NP_STARTDATATRANSMISSION gpfNP_StartDataTransmission = NULL; +TrackIR::PF_NP_STOPDATATRANSMISSION gpfNP_StopDataTransmission = NULL; + +HMODULE ghNPClientDLL = NULL; + +TrackIR::NPResult __stdcall TrackIR::RegisterWindowHandle(HWND hWnd) { + auto result = NPResult::DLL_NOT_FOUND; + if (gpfNP_RegisterWindowHandle) + result = (*gpfNP_RegisterWindowHandle)(hWnd); + return result; +} + +TrackIR::NPResult __stdcall TrackIR::UnregisterWindowHandle() { + auto result = NPResult::DLL_NOT_FOUND; + + if (gpfNP_UnregisterWindowHandle) + result = (*gpfNP_UnregisterWindowHandle)(); + return result; +} + +TrackIR::NPResult __stdcall TrackIR::RegisterProgramProfileID(uint16_t wPPID) { + auto result = NPResult::DLL_NOT_FOUND; + + if (gpfNP_RegisterProgramProfileID) + result = (*gpfNP_RegisterProgramProfileID)(wPPID); + return result; +} + +TrackIR::NPResult __stdcall TrackIR::QueryVersion(uint16_t* pwVersion) { + auto result = NPResult::DLL_NOT_FOUND; + if (gpfNP_QueryVersion) + result = (*gpfNP_QueryVersion)(pwVersion); + return result; +} + +TrackIR::NPResult __stdcall TrackIR::RequestData(uint16_t wDataReq) { + auto result = NPResult::DLL_NOT_FOUND; + if (gpfNP_RequestData) + result = (*gpfNP_RequestData)(wDataReq); + return result; +} + +TrackIR::NPResult __stdcall TrackIR::GetSignature(LPTRACKIRSIGNATURE pSignature) { + auto result = NPResult::DLL_NOT_FOUND; + if (gpfNP_GetSignature) + result = (*gpfNP_GetSignature)(pSignature); + return result; +} + +TrackIR::NPResult __stdcall TrackIR::GetData(LPTRACKIRDATA pTID) { + auto result = NPResult::DLL_NOT_FOUND; + if (gpfNP_GetData) + result = (*gpfNP_GetData)(pTID); + return result; +} + +TrackIR::NPResult __stdcall TrackIR::StartCursor() { + auto result = NPResult::DLL_NOT_FOUND; + if (gpfNP_StartCursor) + result = (*gpfNP_StartCursor)(); + return result; +} + +TrackIR::NPResult __stdcall TrackIR::StopCursor() { + auto result = NPResult::DLL_NOT_FOUND; + if (gpfNP_StopCursor) + result = (*gpfNP_StopCursor)(); + return result; +} + +TrackIR::NPResult __stdcall TrackIR::ReCenter() { + auto result = NPResult::DLL_NOT_FOUND; + if (gpfNP_ReCenter) + result = (*gpfNP_ReCenter)(); + return result; +} + +TrackIR::NPResult __stdcall TrackIR::StartDataTransmission() { + auto result = NPResult::DLL_NOT_FOUND; + if (gpfNP_StartDataTransmission) + result = (*gpfNP_StartDataTransmission)(); + return result; +} + +TrackIR::NPResult __stdcall TrackIR::StopDataTransmission() { + auto result = NPResult::DLL_NOT_FOUND; + if (gpfNP_StopDataTransmission) + result = (*gpfNP_StopDataTransmission)(); + return result; +} + +TrackIR::NPResult TrackIR::Init(LPTSTR pszDLLPath) { + NPResult result = NPResult::OK; + WCHAR szFullPath[MAX_PATH * 2]; + + if (pszDLLPath == NULL) + return NPResult::DLL_NOT_FOUND; + + lstrcpy(szFullPath, pszDLLPath); + + if (lstrlen(szFullPath) > 0) { + lstrcat(szFullPath, L"\\"); + } + +#if defined(_WIN64) || defined(__amd64__) + lstrcat(szFullPath, L"NPClient64.dll"); +#else + strcat(szFullPath, "NPClient.dll"); +#endif + + ghNPClientDLL = ::LoadLibrary(szFullPath); + if (NULL != ghNPClientDLL) { + // verify the dll signature + gpfNP_GetSignature = (PF_NP_GETSIGNATURE)::GetProcAddress(ghNPClientDLL, "NP_GetSignature"); + + SIGNATUREDATA pSignature; + SIGNATUREDATA verifySignature; + // init the signatures + strcpy_s(verifySignature.DllSignature, "precise head tracking\n put your head into the game\n now go look around\n\n Copyright EyeControl Technologies"); + strcpy_s(verifySignature.AppSignature, "hardware camera\n software processing data\n track user movement\n\n Copyright EyeControl Technologies"); + // query the dll and compare the results + auto vresult = TrackIR::GetSignature(&pSignature); + if (vresult == NPResult::OK) { + if ((strcmp(verifySignature.DllSignature,pSignature.DllSignature)==0) + && (strcmp(verifySignature.AppSignature,pSignature.AppSignature)==0)) + { + result = NPResult::OK; + + // Get addresses of all exported functions + gpfNP_RegisterWindowHandle = (PF_NP_REGISTERWINDOWHANDLE)::GetProcAddress(ghNPClientDLL, "NP_RegisterWindowHandle"); + gpfNP_UnregisterWindowHandle = (PF_NP_UNREGISTERWINDOWHANDLE)::GetProcAddress(ghNPClientDLL, "NP_UnregisterWindowHandle"); + gpfNP_RegisterProgramProfileID = (PF_NP_REGISTERPROGRAMPROFILEID)::GetProcAddress(ghNPClientDLL, "NP_RegisterProgramProfileID"); + gpfNP_QueryVersion = (PF_NP_QUERYVERSION)::GetProcAddress(ghNPClientDLL, "NP_QueryVersion"); + gpfNP_RequestData = (PF_NP_REQUESTDATA)::GetProcAddress(ghNPClientDLL, "NP_RequestData"); + gpfNP_GetData = (PF_NP_GETDATA)::GetProcAddress(ghNPClientDLL, "NP_GetData"); + gpfNP_StartCursor = (PF_NP_STARTCURSOR)::GetProcAddress(ghNPClientDLL, "NP_StartCursor"); + gpfNP_StopCursor = (PF_NP_STOPCURSOR)::GetProcAddress(ghNPClientDLL, "NP_StopCursor"); + gpfNP_ReCenter = (PF_NP_RECENTER)::GetProcAddress(ghNPClientDLL, "NP_ReCenter"); + gpfNP_StartDataTransmission = (PF_NP_STARTDATATRANSMISSION)::GetProcAddress(ghNPClientDLL, "NP_StartDataTransmission"); + gpfNP_StopDataTransmission = (PF_NP_STOPDATATRANSMISSION)::GetProcAddress(ghNPClientDLL, "NP_StopDataTransmission"); + } else { + result = NPResult::DLL_NOT_FOUND; + } + } else { + result = NPResult::DLL_NOT_FOUND; + } + } else + result = NPResult::DLL_NOT_FOUND; + + return result; +} + +TrackIR::NPResult TrackIR::GetDLLLocation(LPTSTR pszPath) { + if (pszPath == NULL) + return NPResult::INVALID_ARG; + + //find path to NPClient.dll + HKEY pKey = NULL; + //open the registry key + if (::RegOpenKeyEx(HKEY_CURRENT_USER, + L"Software\\NaturalPoint\\NATURALPOINT\\NPClient Location", + 0, + KEY_READ, + &pKey) != ERROR_SUCCESS) + { + return NPResult::DLL_NOT_FOUND; + } + + //get the value from the key + LPTSTR pszValue; + DWORD dwSize; + //first discover the size of the value + if (RegQueryValueEx(pKey, + L"Path", + NULL, + NULL, + NULL, + &dwSize) == ERROR_SUCCESS) + { + //allocate memory for the buffer for the value + pszValue = (LPTSTR)malloc(dwSize); + if (!pszValue) + return NPResult::DLL_NOT_FOUND; + + //now get the value + if (RegQueryValueEx(pKey, + L"Path", + NULL, + NULL, + (LPBYTE)pszValue, + &dwSize) == ERROR_SUCCESS) + { + //everything worked + ::RegCloseKey(pKey); + lstrcpy(pszPath, pszValue); + free(pszValue); + + return NPResult::OK; + + } else { + free(pszValue); + return NPResult::DLL_NOT_FOUND; + } + + } + + ::RegCloseKey(pKey); + lstrcpy(pszPath, L"Error"); + return NPResult::DLL_NOT_FOUND; +} + +TrackIR::NPResult TrackIR::Initialize(HWND hWnd) { + auto path = reinterpret_cast(malloc(sizeof(wchar_t) * 512)); + if (GetDLLLocation(path) != NPResult::OK) { + free(path); + return NPResult::DLL_NOT_FOUND; + } + + if (Init(path) != NPResult::OK) { + free(path); + return NPResult::DLL_NOT_FOUND; + } + + free(path); + if (RegisterWindowHandle(hWnd) != NPResult::OK) + return NPResult::INVALID_ARG; + + // Register with the developer ID + if (RegisterProgramProfileID(1000) != NPResult::OK) + return NPResult::INVALID_ARG; + + if (StartDataTransmission() != NPResult::OK) + return NPResult::NO_DATA; + + running = true; + return NPResult::OK; +} + +void TrackIR::Shutdown() { + StopDataTransmission(); + UnregisterWindowHandle(); + running = false; +} + +bool TrackIR::IsRunning() noexcept { + return running; +} + +TrackIR::TrackingSnapshot TrackIR::GetTrackingData() noexcept { + static TrackingSnapshot snap; + + TRACKIRDATA tid; + if (GetData(&tid) != NPResult::OK) + return snap; + + if (tid.wNPStatus != NPSTATUS_REMOTEACTIVE) + return snap; + + static uint32_t frameSig = 0; + if (frameSig == tid.wPFrameSignature) + return snap; + frameSig = tid.wPFrameSignature; + + snap.pos = { tid.fNPX, tid.fNPY, tid.fNPZ }; + snap.rot = { + tid.fNPPitch * (MaxRotation / NPMaxValue), + tid.fNPYaw * (MaxRotation / NPMaxValue), + tid.fNPRoll * (MaxRotation / NPMaxValue) + }; + + return snap; +} +#endif \ No newline at end of file diff --git a/acknowledgements.md b/acknowledgements.md index afef81a..d6bed49 100644 --- a/acknowledgements.md +++ b/acknowledgements.md @@ -157,4 +157,276 @@ 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. +``` + +## EABAse +### https://github.com/electronicarts/EABase + +``` +/* +Copyright (C) 2017 Electronic Arts Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of Electronic Arts, Inc. ("EA") nor the names of + its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY ELECTRONIC ARTS AND ITS CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ELECTRONIC ARTS OR ITS CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +``` + +## EASTL +### https://github.com/electronicarts/EASTL + +``` +BSD 3-Clause License + +Copyright (c) 2019, Electronic Arts +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + + +# ModelBaker + +## Open Asset Import Library +### https://github.com/assimp/assimp + +``` +Open Asset Import Library (assimp) + +Copyright (c) 2006-2021, assimp team +All rights reserved. + +Redistribution and use of this software in source and binary forms, +with or without modification, are permitted provided that the +following conditions are met: + +* Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + +* Neither the name of the assimp team, nor the names of its + contributors may be used to endorse or promote products + derived from this software without specific prior + written permission of the assimp team. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +****************************************************************************** + +AN EXCEPTION applies to all files in the ./test/models-nonbsd folder. +These are 3d models for testing purposes, from various free sources +on the internet. They are - unless otherwise stated - copyright of +their respective creators, which may impose additional requirements +on the use of their work. For any of these models, see +.source.txt for more legal information. Contact us if you +are a copyright holder and believe that we credited you inproperly or +if you don't want your files to appear in the repository. + + +****************************************************************************** + +Poly2Tri Copyright (c) 2009-2010, Poly2Tri Contributors +http://code.google.com/p/poly2tri/ + +All rights reserved. +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of Poly2Tri nor the names of its contributors may be + used to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +## bindbc-assimp +### https://github.com/Sobaya007/bindbc-assimp + +``` +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +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, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +``` + +## bindbc-loader +### https://github.com/BindBC/bindbc-loader + +``` +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +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, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +``` + +## GFM +### https://github.com/d-gamedev-team/gfm + +``` +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to +``` + +## intel-intrinsics +### https://github.com/AuburnSounds/intel-intrinsics + +``` +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +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, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. ``` \ No newline at end of file diff --git a/premake5.lua b/premake5.lua index ee5f659..4ecd0e9 100644 --- a/premake5.lua +++ b/premake5.lua @@ -162,6 +162,7 @@ dofile( "BuildScripts/project_common_skse64.lua" ) dofile( "BuildScripts/project_skse64_common.lua" ) dofile( "BuildScripts/project_skse64.lua" ) dofile( "BuildScripts/project_polyhook.lua" ) +dofile( "BuildScripts/project_eastl.lua" )(os.getcwd(), _OPTIONS["VS_PLATFORM"]) local outputDir = "%{cfg.buildcfg}-%{cfg.system}-%{cfg.architecture}" project "SmoothCam" @@ -174,7 +175,7 @@ project "SmoothCam" targetdir( "SmoothCam/bin/".. outputDir.. "/%{prj.name}" ) objdir( "SmoothCam/bin-obj/".. outputDir.. "/%{prj.name}" ) - dependson { "common_skse64", "skse64_common", "skse64", "PolyHook2" } + dependson { "common_skse64", "skse64_common", "skse64", "PolyHook2", "eastl" } files { "SmoothCam/**.h", "SmoothCam/**.cpp", "SmoothCam/**.rc", "SmoothCam/**.def", "SmoothCam/**.ini" } pchheader "pch.h" @@ -192,6 +193,8 @@ project "SmoothCam" "Deps/PolyHook_2_0", "Deps/eternal/include", "Deps/json/single_include", + "Deps/EASTL/include", + "Deps/EABase/include/Common", } filter "system:windows" @@ -220,6 +223,7 @@ project "SmoothCam" "Deps/Detours/lib.X64", "Deps/PolyHook_2_0/bin/Debug-windows-x86_64/PolyHook2", + "Deps/EASTL/build/Debug" } links { @@ -229,6 +233,7 @@ project "SmoothCam" "detours.lib", "PolyHook2.lib", + "EASTL.lib" } filter "configurations:Release" @@ -253,6 +258,7 @@ project "SmoothCam" "Deps/Detours/lib.X64", "Deps/PolyHook_2_0/bin/Release-windows-x86_64/PolyHook2", + "Deps/EASTL/build/Release" } links { @@ -262,4 +268,5 @@ project "SmoothCam" "detours.lib", "PolyHook2.lib", + "EASTL.lib" } \ No newline at end of file