diff --git a/CBPSSE/ActorEntry.h b/CBPSSE/ActorEntry.h new file mode 100644 index 0000000..3bc159b --- /dev/null +++ b/CBPSSE/ActorEntry.h @@ -0,0 +1,9 @@ +#pragma once +#include "f4se/GameReferences.h" + +class ActorEntry +{ +public: + UInt32 id; + Actor* actor; +}; \ No newline at end of file diff --git a/CBPSSE/ActorUtils.cpp b/CBPSSE/ActorUtils.cpp new file mode 100644 index 0000000..6ad9fcd --- /dev/null +++ b/CBPSSE/ActorUtils.cpp @@ -0,0 +1,406 @@ +#include +#include + +#include "ActorUtils.h" +#include "log.h" +#include "unordered_dense.h" + +#include "f4se/GameExtraData.h" +#include "f4se/GameObjects.h" +#include +#include "f4se/GameRTTI.h" + +#include +#include +#include + +std::string actorUtils::GetActorRaceEID(Actor* actor) +{ + if (!IsActorValid(actor)) + { + logger.Info("GetActorRaceEID: no actor!\n"); + return ""; + } + + return std::string(actor->race->editorId.c_str()); +} + +NiAVObject* actorUtils::GetBaseSkeleton(Actor* actor) +{ + BSFixedString skeletonNif_name("skeleton.nif"); + + if (!actorUtils::IsActorValid(actor)) + { + logger.Error("%s: No valid actor\n", __func__); + return NULL; + } + auto loadedState = actor->unkF0; + if (!loadedState || !loadedState->rootNode) + { + logger.Error("%s:No loaded state for actor %08x\n", __func__, actor->formID); + return NULL; + } + auto obj = loadedState->rootNode->GetObjectByName(&skeletonNif_name); + + if (!obj) + { + logger.Error("%s: Couldn't get name for loaded state for actor %08x\n", __func__, actor->formID); + return NULL; + } + + return obj; +} + +bool actorUtils::IsActorFilteredActor(Actor* actor, UInt32 priority) +{ + auto& actorOverrideConfigEntry = configActorOverrideMap[priority]; + auto& overrideActors = actorOverrideConfigEntry.actors; + auto isFilterInverted = actorOverrideConfigEntry.isFilterInverted; + bool result; + + if (isFilterInverted) + { + logger.Info("%s: actor %08x is actor whitelisted\n", __func__, actor->formID); + // Result is in the whitelist + result = !(overrideActors.count(actor->formID) > 0); + } + else + { + logger.Info("%s: actor %08x is actor blacklisted\n", __func__, actor->formID); + // Result is in the blacklist + result = (overrideActors.count(actor->formID) > 0); + } + + // Simplified to xor + //result = isFilterInverted ^ (overrideActors.count(actor->formID) == 0); + + return result; +} + +bool actorUtils::IsActorMale(Actor* actor) +{ + if (!IsActorValid(actor)) + { + logger.Info("IsActorMale: no actor!\n"); + return false; + } + + TESNPC* actorNPC = DYNAMIC_CAST(actor->baseForm, TESForm, TESNPC); + + auto npcSex = actorNPC ? CALL_MEMBER_FN(actorNPC, GetSex)() : 1; + + if (npcSex == 0) //Actor is male + return true; + else + return false; +} + +bool actorUtils::IsActorInPowerArmor(Actor* actor) +{ + bool isInPowerArmor = false; + if (!IsActorValid(actor)) + { + logger.Info("IsActorInPowerArmor: no actor!\n"); + return true; + } + if (!actor->extraDataList) + { + logger.Info("IsActorInPowerArmor: no extraDataList!\n"); + return true; + } + + isInPowerArmor = actor->extraDataList->HasType(kExtraData_PowerArmor); + //logger.Info("is in power armor: %d\n", isInPowerArmor); + return isInPowerArmor; +} + +bool actorUtils::IsActorTrackable(Actor* actor) +{ + if (!IsActorValid(actor)) + { + logger.Info("%s: actor %x is not trackable.\n", __func__, actor->formID); + return false; + } + + bool inRaceWhitelist = find(raceWhitelist.begin(), raceWhitelist.end(), actorUtils::GetActorRaceEID(actor)) != raceWhitelist.end(); + return (!playerOnly || (actor->formID == 0x14 && playerOnly)) && + (!maleOnly || (IsActorMale(actor) && maleOnly)) && + (!femaleOnly || (!IsActorMale(actor) && femaleOnly)) && + (!npcOnly || (actor->formID != 0x14 && npcOnly)) && + (!useWhitelist || (inRaceWhitelist && useWhitelist)); +} + +bool actorUtils::IsActorValid(Actor* actor) +{ + if (!actor) + { + logger.Info("%s: actor %x is null\n", __func__, actor->formID); + return false; + } + if (actor->flags & TESForm::kFlag_IsDeleted) + { + logger.Info("%s: actor %x has deleted flag\n", __func__, actor->formID); + return false; + } + if (actor->unkF0 && actor->unkF0->rootNode) + { + return true; + } + logger.Info("%s: actor %x is not in a valid state\n", __func__, actor->formID); + return false; +} + +bool actorUtils::IsBoneInWhitelist(Actor* actor, std::string boneName) +{ + if (!IsActorValid(actor)) + { + logger.Info("IsBoneInWhitelist: actor is not valid.\n"); + return false; + } + bool result; + auto raceEID = actorUtils::GetActorRaceEID(actor); + auto whitelist_bone = whitelist.find(boneName); + if (whitelist_bone != whitelist.end()) + { + auto & racesMap = whitelist_bone->second; + + if (racesMap.find(raceEID) != racesMap.end()) + { + if (IsActorMale(actor)) + { + result = racesMap.at(raceEID).male; + //logger.Info("%s: %s male is %d for actor %x.\n", __func__, boneName.c_str(), result, actor->formID); + return result; + } + else + { + result = racesMap.at(raceEID).female; + //logger.Info("%s: %s female is %d for actor %x.\n", __func__, boneName.c_str(), result, actor->formID); + return result; + } + } + } + return false; +} + +const actorUtils::EquippedArmor actorUtils::GetActorEquippedArmor(Actor* actor, UInt32 slot) +{ + bool isEquipped = false; + bool isArmorIgnored = false; + + if (!actorUtils::IsActorValid(actor)) + { + logger.Error("Actor is not valid"); + return actorUtils::EquippedArmor{ nullptr, nullptr }; + } + if (!actor->equipData || !actor->equipData->slots) + { + logger.Error("Actor has no equipData"); + return actorUtils::EquippedArmor{ nullptr, nullptr }; + } + + isEquipped = actor->equipData->slots[slot].item; + + // Check if armor is ignored + if (isEquipped) + { + if (!actor->equipData->slots[slot].item) + { + logger.Error("slot %d item check failed.", slot); + // redundant check but just in case + return actorUtils::EquippedArmor{ nullptr, nullptr }; + } + return actorUtils::EquippedArmor{ actor->equipData->slots[slot].item, actor->equipData->slots[slot].model }; + } + + return actorUtils::EquippedArmor{ nullptr, nullptr }; +} + +UInt64 actorUtils::BuildActorKey(Actor* actor) +{ + std::unordered_map key; + ankerl::unordered_dense::hash hash; + UInt64 hashKey = 0; + UInt64 actorFormID = (UInt64)actor->formID; + + // key always starts with actors formID (really the refID) + key[actorFormID] = 1; + hashKey = hash((UInt64)actor->formID); + + for (auto slot : usedSlots) + { + // Check if actor has config's slot equipped + UInt64 data = 0; + auto equipped = actorUtils::GetActorEquippedArmor(actor, slot); + + // If there were any slots found, store it + if (equipped.armor) + { + data |= equipped.armor->formID; + data = data << 32; + } + if (equipped.model) + { + data |= equipped.model->formID; + } + if (data) + { + auto valIter = key.find(data); + if (valIter != key.end()) + { + key[data] = key[data] + 1; + hashKey += hash(data) * key[data]; + } + else + { + key[data] = 1; + hashKey += hash(data) * key[data]; + } + } + } + + //logger.Info("%s: actor %08x has hash key 0x%x\n", __func__, actor->formID, hashKey); + + return hashKey; +} + +config_t actorUtils::BuildConfigForActor(Actor* actor, UInt64 hashKey) +{ + // Search cached configs for already existing config + auto found = cachedConfigs.find(hashKey); + if (found != cachedConfigs.end()) + { + logger.Info("%s: Cached config found for actor %08x: %x\n", __func__, actor->formID, hashKey); + return found->second; + } + + // Otherwise, build the actor's config + config_t baseConfig = config; + + for (auto priIter = priorities.rbegin(); priIter != priorities.rend(); ++priIter) + { + UInt32 priority = *priIter; + + // Check if there is no armor slot override entry + if (configArmorOverrideMap.count(priority) == 0 + || (configArmorOverrideMap[priority].armors.empty() + && configArmorOverrideMap[priority].slots.empty())) + { + auto & overrideConfig = configActorOverrideMap[priority].config; + + if (IsActorFilteredActor(actor, priority)) + { + for (auto & val : overrideConfig) + { + // for each bone, if it is empty, we need to disable it, + // otherwise the configEntry is good. + if (overrideConfig[val.first].empty()) + { + // This is ok because we're doing this to a premade copy sequentially + baseConfig.unsafe_erase(val.first); + } + else + { + baseConfig[val.first] = val.second; + } + } + } + else + { + logger.Info("%s: actor %08x is not filtered for priority %d\n", __func__, actor->formID, priority); + } + } + else // There is an armor slot override entry + { + // If priority level has an entry in actor override map AND + // actor is not whitelisted or is blacklisted, continue on + if (configActorOverrideMap.count(priority) > 0) + { + if (IsActorFilteredActor(actor, priority)) + { + logger.Info("%s: actor %08x is filtered for priority %d\n", __func__, actor->formID, priority); + continue; + } + } + + auto & orData = configArmorOverrideMap[priority]; + + std::vector equippedList; + + // Make a list of actor's slots that are config's slots + for (auto slot : orData.slots) + { + EquippedArmor equipped = actorUtils::GetActorEquippedArmor(actor, slot); + if (equipped.armor && equipped.model) + { + equippedList.push_back(equipped); + } + } + + // whitelist filter + if (orData.isFilterInverted) + { + logger.Info("%s: actor %08x is armor whitelisted\n", __func__, actor->formID); + + // Check config's filter IDs against found slot's IDs + for (auto & equipped : equippedList) + { + // Filter all armors listed + if (orData.armors.count(equipped.armor->formID) || + orData.armors.count(equipped.model->formID)) + { + for (auto & val : orData.config) + { + // for each bone, if it is empty, we need to disable it, + // otherwise the configEntry is good. + if (orData.config[val.first].empty()) + { + // This is ok because we're doing this to a premade copy sequentially + baseConfig.unsafe_erase(val.first); + } + else + { + baseConfig[val.first] = val.second; + } + } + } + } + } + + // blacklist filter + if (!orData.isFilterInverted && !equippedList.empty()) + { + logger.Info("%s: actor %08x is armor blacklisted\n", __func__, actor->formID); + + // Check config's filter IDs against found slot's IDs + for (auto& equipped : equippedList) + { + // Filter all armors not listed + if (orData.armors.count(equipped.armor->formID) == 0 && + orData.armors.count(equipped.model->formID) == 0) + { + for (auto& val : orData.config) + { + // for each bone, if it is empty, we need to disable it, + // otherwise the configEntry is good. + if (orData.config[val.first].empty()) + { + // This is ok because we're doing this to a premade copy sequentially + baseConfig.unsafe_erase(val.first); + } + else + { + baseConfig[val.first] = val.second; + } + } + } + } + } + } + } + + logger.Info("%s: Inserting cached config for actor %08x: %x\n", __func__, actor->formID, hashKey); + cachedConfigs.insert(std::make_pair(hashKey, baseConfig)); + //logger.Info("%s: exiting\n", __func__); + return baseConfig; +} \ No newline at end of file diff --git a/CBPSSE/ActorUtils.h b/CBPSSE/ActorUtils.h new file mode 100644 index 0000000..1f0e7ab --- /dev/null +++ b/CBPSSE/ActorUtils.h @@ -0,0 +1,26 @@ +#pragma once +#include "f4se/GameReferences.h" +#include "config.h" + +namespace actorUtils +{ + struct EquippedArmor + { + const TESForm* armor; + const TESForm* model; + }; + + std::string GetActorRaceEID(Actor* actor); + NiAVObject* GetBaseSkeleton(Actor* actor); + bool IsActorInPowerArmor(Actor* actor); + bool IsActorFilteredActor(Actor* actor, UInt32 priority); + bool IsActorMale(Actor* actor); + bool IsActorTrackable(Actor* actor); + bool IsActorValid(Actor* actor); + bool IsActorInPowerArmor(Actor* actor); + bool IsBoneInWhitelist(Actor* actor, std::string boneName); + + const EquippedArmor GetActorEquippedArmor(Actor* actor, UInt32 slot); + UInt64 BuildActorKey(Actor* actor); + config_t BuildConfigForActor(Actor* actor, UInt64 hashKey); +} \ No newline at end of file diff --git a/CBPSSE/CBPSSE.vcxproj b/CBPSSE/CBPSSE.vcxproj index 65f3d98..ce5af52 100644 --- a/CBPSSE/CBPSSE.vcxproj +++ b/CBPSSE/CBPSSE.vcxproj @@ -22,8 +22,8 @@ 15.0 {49438A48-1C92-4D07-97C0-BDF0744386F9} CBBPSSE - 10.0.15063.0 - CBPSSE + 10.0 + OpenCBP_FO4 @@ -48,7 +48,7 @@ DynamicLibrary false - v141 + v142 true MultiByte @@ -72,14 +72,14 @@ .dll - D:\SteamLibrary\steamapps\common\Skyrim Special Edition\Data\SKSE\Plugins cbp .dll - D:\SteamLibrary\steamapps\common\Skyrim Special Edition\Data\SKSE\Plugins cbp false + $(ProjectDir)x64_v142\Release\;$(OutDir);$(LibraryPath) + $(IncludePath) @@ -97,8 +97,8 @@ Disabled - $(SolutionDir)\skse64\src\;$(SolutionDir)\skse64\src\skse64;%(AdditionalIncludeDirectories) - _USRDLL;PLUGIN_EXAMPLE_EXPORTS;RUNTIME;RUNTIME_VERSION=0x01050610;%(PreprocessorDefinitions) + $(SolutionDir)f4se;$(SolutionDir);%(AdditionalIncludeDirectories) + _USRDLL;PLUGIN_EXAMPLE_EXPORTS;RUNTIME;RUNTIME_VERSION=0x010A0A30;%(PreprocessorDefinitions) common/IPrefix.h;%(ForcedIncludeFiles) MultiThreadedDebug ProgramDatabase @@ -131,43 +131,73 @@ true - $(SolutionDir)\skse64\src\;$(SolutionDir)\skse64\src\skse64;%(AdditionalIncludeDirectories) - _USRDLL;PLUGIN_EXAMPLE_EXPORTS;RUNTIME;RUNTIME_VERSION=0x01050610;%(PreprocessorDefinitions) + $(SolutionDir)f4se;$(SolutionDir);%(AdditionalIncludeDirectories) + _USRDLL;PLUGIN_EXAMPLE_EXPORTS;RUNTIME;RUNTIME_VERSION=0x010A3D80;%(PreprocessorDefinitions) MultiThreaded common/IPrefix.h;%(ForcedIncludeFiles) + stdcpp17 true true exports.def - %(AdditionalDependencies) Windows true + %(AdditionalLibraryDirectories) + $(OutDir)f4se_1_10_163.lib;$(OutDir)f4se_common.lib;$(SolutionDir)x64_v142\Release\common_vc14.lib;%(AdditionalDependencies) + + copy "$(TargetPath)" "$(Fallout4Path)\$(TargetFileName)" + + + Installing DLL to FO4 Plugins... + + - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + @@ -177,12 +207,21 @@ - - - {5fd1c08d-db80-480c-a1c6-f0920005cd13} + + {472e19ab-def0-42df-819b-18722e8dc822} + false + true + + + {a236f69d-8ff9-4491-ac5f-45bf49448bbe} + false + true + + + {20c6411c-596f-4b85-be4e-8bc91f59d8a6} diff --git a/CBPSSE/CBPSSE.vcxproj.filters b/CBPSSE/CBPSSE.vcxproj.filters index b32ea4b..bd9ce33 100644 --- a/CBPSSE/CBPSSE.vcxproj.filters +++ b/CBPSSE/CBPSSE.vcxproj.filters @@ -13,12 +13,15 @@ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - {dd544c35-f8aa-4e6c-9504-0565cc379822} - {850dfb75-eff3-4420-adde-cfae8d537337} + + {644f2ce2-9716-41b9-99ab-5ca319adf7be} + + + {15ad9f91-fd32-4588-bc5c-830673dc512a} + @@ -36,26 +39,62 @@ Header Files - - Source Files\api + + Header Files\api - - Source Files\api + + Header Files\api - - Source Files\api + + Header Files\api - - Source Files\api + + Header Files\api - - Source Files\api + + Header Files\api - - Source Files\api + + Header Files\api - - Source Files\api + + Header Files\api + + + Header Files\api + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files\api + + + Header Files\api + + + Header Files\api + + + Header Files\api + + + Header Files\api + + + Header Files\api + + + Header Files @@ -83,25 +122,52 @@ Source Files - + + Source Files\api + + + Source Files\api + + + Source Files\api + + + Source Files\api + + Source Files\api - + Source Files\api - + Source Files\api - + Source Files\api - + + Source Files + + + Source Files + + + Source Files\api + + Source Files\api - + Source Files\api - + + Source Files\api + + + Source Files\api + + Source Files\api @@ -109,9 +175,6 @@ Source Files - - Source Files\api - diff --git a/CBPSSE/INIReader.h b/CBPSSE/INIReader.h new file mode 100644 index 0000000..c5c8547 --- /dev/null +++ b/CBPSSE/INIReader.h @@ -0,0 +1,461 @@ +// Read an INI file into easy-to-access name/value pairs. + +// inih and INIReader are released under the New BSD license (see LICENSE.txt). +// Go to the project home page for more info: +// +// https://github.com/benhoyt/inih +/* inih -- simple .INI file parser + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#ifndef __INI_H__ +#define __INI_H__ + +/* Make this header file easier to include in C++ code */ +#ifdef __cplusplus +extern "C" { +#endif + +#include +/* Typedef for prototype of handler function. */ +typedef int (*ini_handler)(void* user, const char* section, + const char* name, const char* value); + +/* Typedef for prototype of fgets-style reader function. */ +typedef char* (*ini_reader)(char* str, int num, void* stream); + +/* Parse given INI-style file. May have [section]s, name=value pairs + (whitespace stripped), and comments starting with ';' (semicolon). Section + is "" if name=value pair parsed before any section heading. name:value + pairs are also supported as a concession to Python's configparser. + + For each name=value pair parsed, call handler function with given user + pointer as well as section, name, and value (data only valid for duration + of handler call). Handler should return nonzero on success, zero on error. + + Returns 0 on success, line number of first error on parse error (doesn't + stop on first error), -1 on file open error, or -2 on memory allocation + error (only when INI_USE_STACK is zero). +*/ +int ini_parse(const char* filename, ini_handler handler, void* user); + +/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't + close the file when it's finished -- the caller must do that. */ +int ini_parse_file(FILE* file, ini_handler handler, void* user); + +/* Same as ini_parse(), but takes an ini_reader function pointer instead of + filename. Used for implementing custom or string-based I/O. */ +int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user); + +/* Nonzero to allow multi-line value parsing, in the style of Python's + configparser. If allowed, ini_parse() will call the handler with the same + name for each subsequent line parsed. */ +#ifndef INI_ALLOW_MULTILINE +#define INI_ALLOW_MULTILINE 1 +#endif + +/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of + the file. See http://code.google.com/p/inih/issues/detail?id=21 */ +#ifndef INI_ALLOW_BOM +#define INI_ALLOW_BOM 1 +#endif + +/* Nonzero to allow inline comments (with valid inline comment characters + specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match + Python 3.2+ configparser behaviour. */ +#ifndef INI_ALLOW_INLINE_COMMENTS +#define INI_ALLOW_INLINE_COMMENTS 1 +#endif +#ifndef INI_INLINE_COMMENT_PREFIXES +#define INI_INLINE_COMMENT_PREFIXES ";" +#endif + +/* Nonzero to use stack, zero to use heap (malloc/free). */ +#ifndef INI_USE_STACK +#define INI_USE_STACK 1 +#endif + +/* Stop parsing on first error (default is to keep parsing). */ +#ifndef INI_STOP_ON_FIRST_ERROR +#define INI_STOP_ON_FIRST_ERROR 0 +#endif + +/* Maximum line length for any line in INI file. */ +#ifndef INI_MAX_LINE +#define INI_MAX_LINE 200 +#endif + +#ifdef __cplusplus +} +#endif + +/* inih -- simple .INI file parser + +inih is released under the New BSD license (see LICENSE.txt). Go to the project +home page for more info: + +https://github.com/benhoyt/inih + +*/ + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include +#include +#include + +#if !INI_USE_STACK +#include +#endif + +#define MAX_SECTION 80 +#define MAX_NAME 80 + +/* Strip whitespace chars off end of given string, in place. Return s. */ +inline static char* rstrip(char* s) +{ + char* p = s + strlen(s); + while (p > s && isspace((unsigned char)(*--p))) + *p = '\0'; + return s; +} + +/* Return pointer to first non-whitespace char in given string. */ +inline static char* lskip(const char* s) +{ + while (*s && isspace((unsigned char)(*s))) + s++; + return (char*)s; +} + +/* Return pointer to first char (of chars) or inline comment in given string, + or pointer to null at end of string if neither found. Inline comment must + be prefixed by a whitespace character to register as a comment. */ +inline static char* find_chars_or_comment(const char* s, const char* chars) +{ +#if INI_ALLOW_INLINE_COMMENTS + int was_space = 0; + while (*s && (!chars || !strchr(chars, *s)) && + !(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) { + was_space = isspace((unsigned char)(*s)); + s++; + } +#else + while (*s && (!chars || !strchr(chars, *s))) { + s++; + } +#endif + return (char*)s; +} + +/* Version of strncpy that ensures dest (size bytes) is null-terminated. */ +inline static char* strncpy0(char* dest, const char* src, size_t size) +{ + strncpy_s(dest, size, src, size); + dest[size - 1] = '\0'; + return dest; +} + +/* See documentation in header file. */ +inline int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler, + void* user) +{ + /* Uses a fair bit of stack (use heap instead if you need to) */ +#if INI_USE_STACK + char line[INI_MAX_LINE]; +#else + char* line; +#endif + char section[MAX_SECTION] = ""; + char prev_name[MAX_NAME] = ""; + + char* start; + char* end; + char* name; + char* value; + int lineno = 0; + int error = 0; + +#if !INI_USE_STACK + line = (char*)malloc(INI_MAX_LINE); + if (!line) { + return -2; + } +#endif + + /* Scan through stream line by line */ + while (reader(line, INI_MAX_LINE, stream) != NULL) { + lineno++; + + start = line; +#if INI_ALLOW_BOM + if (lineno == 1 && (unsigned char)start[0] == 0xEF && + (unsigned char)start[1] == 0xBB && + (unsigned char)start[2] == 0xBF) { + start += 3; + } +#endif + start = lskip(rstrip(start)); + + if (*start == ';' || *start == '#') { + /* Per Python configparser, allow both ; and # comments at the + start of a line */ + } +#if INI_ALLOW_MULTILINE + else if (*prev_name && *start && start > line) { + +#if INI_ALLOW_INLINE_COMMENTS + end = find_chars_or_comment(start, NULL); + if (*end) + *end = '\0'; + rstrip(start); +#endif + + /* Non-blank line with leading whitespace, treat as continuation + of previous name's value (as per Python configparser). */ + if (!handler(user, section, prev_name, start) && !error) + error = lineno; + } +#endif + else if (*start == '[') { + /* A "[section]" line */ + end = find_chars_or_comment(start + 1, "]"); + if (*end == ']') { + *end = '\0'; + strncpy0(section, start + 1, sizeof(section)); + *prev_name = '\0'; + } + else if (!error) { + /* No ']' found on section line */ + error = lineno; + } + } + else if (*start) { + /* Not a comment, must be a name[=:]value pair */ + end = find_chars_or_comment(start, "=:"); + if (*end == '=' || *end == ':') { + *end = '\0'; + name = rstrip(start); + value = lskip(end + 1); +#if INI_ALLOW_INLINE_COMMENTS + end = find_chars_or_comment(value, NULL); + if (*end) + *end = '\0'; +#endif + rstrip(value); + + /* Valid name[=:]value pair found, call handler */ + strncpy0(prev_name, name, sizeof(prev_name)); + + if (!handler(user, section, name, value) && !error) + error = lineno; + } + else if (!error) { + /* No '=' or ':' found on name[=:]value line */ + error = lineno; + } + } + +#if INI_STOP_ON_FIRST_ERROR + if (error) + break; +#endif + } + +#if !INI_USE_STACK + free(line); +#endif + + return error; +} + +/* See documentation in header file. */ +inline int ini_parse_file(FILE* file, ini_handler handler, void* user) +{ + return ini_parse_stream((ini_reader)fgets, file, handler, user); +} + +/* See documentation in header file. */ +inline int ini_parse(const char* filename, ini_handler handler, void* user) +{ + FILE* file; + int error; + + fopen_s(&file, filename, "r"); + if (!file) + return -1; + error = ini_parse_file(file, handler, user); + fclose(file); + return error; +} + +#endif /* __INI_H__ */ + + +#ifndef __INIREADER_H__ +#define __INIREADER_H__ + +#include +#include +#include + +// Read an INI file into easy-to-access name/value pairs. (Note that I've gone +// for simplicity here rather than speed, but it should be pretty decent.) +class INIReader +{ +public: + // Empty Constructor + INIReader() {}; + + // Construct INIReader and parse given filename. See ini.h for more info + // about the parsing. + INIReader(std::string filename); + + // Construct INIReader and parse given file. See ini.h for more info + // about the parsing. + INIReader(FILE *file); + + // Return the result of ini_parse(), i.e., 0 on success, line number of + // first error on parse error, or -1 on file open error. + int ParseError() const; + + // Return the list of sections found in ini file + const std::set& Sections() const; + + const std::unordered_map & Section(std::string section) const; + + // Get a string value from INI file, returning default_value if not found. + std::string Get(std::string section, std::string name, + std::string default_value) const; + + // Get an integer (long) value from INI file, returning default_value if + // not found or not a valid integer (decimal "1234", "-1234", or hex "0x4d2"). + long GetInteger(std::string section, std::string name, long default_value) const; + + // Get a real (floating point double) value from INI file, returning + // default_value if not found or not a valid floating point value + // according to strtod(). + double GetReal(std::string section, std::string name, double default_value) const; + + // Get a single precision floating point number value from INI file, returning + // default_value if not found or not a valid floating point value + // according to strtof(). + float GetFloat(std::string section, std::string name, float default_value) const; + + // Get a boolean value from INI file, returning default_value if not found or if + // not a valid true/false value. Valid true values are "true", "yes", "on", "1", + // and valid false values are "false", "no", "off", "0" (not case sensitive). + bool GetBoolean(std::string section, std::string name, bool default_value) const; + +protected: + int _error; + std::unordered_map> _values; + std::set _sections; + static int ValueHandler(void* user, const char* section, const char* name, + const char* value); +}; + +#endif // __INIREADER_H__ + + +#ifndef __INIREADER__ +#define __INIREADER__ + +#include +#include +#include + +inline INIReader::INIReader(std::string filename) +{ + _error = ini_parse(filename.c_str(), ValueHandler, this); +} + +inline INIReader::INIReader(FILE *file) +{ + _error = ini_parse_file(file, ValueHandler, this); +} + +inline int INIReader::ParseError() const +{ + return _error; +} + +inline const std::set& INIReader::Sections() const +{ + return _sections; +} + +inline const std::unordered_map& INIReader::Section(std::string section) const +{ + return _values.at(section); +} + +inline std::string INIReader::Get(std::string section, std::string name, std::string default_value) const +{ + try { + return _values.at(section).count(name) ? _values.at(section).at(name) : default_value; + } + catch (...) { + return default_value; + } +} + +inline long INIReader::GetInteger(std::string section, std::string name, long default_value) const +{ + std::string valstr = Get(section, name, ""); + const char* value = valstr.c_str(); + char* end; + // This parses "1234" (decimal) and also "0x4D2" (hex) + long n = strtol(value, &end, 0); + return end > value ? n : default_value; +} + +inline double INIReader::GetReal(std::string section, std::string name, double default_value) const +{ + std::string valstr = Get(section, name, ""); + const char* value = valstr.c_str(); + char* end; + double n = strtod(value, &end); + return end > value ? n : default_value; +} + +inline float INIReader::GetFloat(std::string section, std::string name, float default_value) const +{ + std::string valstr = Get(section, name, ""); + const char* value = valstr.c_str(); + char* end; + float n = strtof(value, &end); + return end > value ? n : default_value; +} + +inline bool INIReader::GetBoolean(std::string section, std::string name, bool default_value) const +{ + std::string valstr = Get(section, name, ""); + // Convert to lower case to make string comparisons case-insensitive + std::transform(valstr.begin(), valstr.end(), valstr.begin(), ::tolower); + if (valstr == "true" || valstr == "yes" || valstr == "on" || valstr == "1") + return true; + else if (valstr == "false" || valstr == "no" || valstr == "off" || valstr == "0") + return false; + else + return default_value; +} + +inline int INIReader::ValueHandler(void* user, const char* section, const char* name, + const char* value) +{ + INIReader* reader = (INIReader*)user; + reader->_values[section][name] = value; + reader->_sections.insert(section); + return 1; +} + +#endif // __INIREADER__ diff --git a/CBPSSE/PapyrusOCBP.cpp b/CBPSSE/PapyrusOCBP.cpp new file mode 100644 index 0000000..a7327c7 --- /dev/null +++ b/CBPSSE/PapyrusOCBP.cpp @@ -0,0 +1,63 @@ +#pragma warning(disable : 5040) + +#include "PapyrusOCBP.h" + +//#include "f4se/PapyrusVM.h" +#include "f4se/PapyrusNativeFunctions.h" + +#include "f4se/GameReferences.h" +#include "f4se/GameRTTI.h" + +#include +#include + +#include "f4se/NiNodes.h" +#include "f4se/NiExtraData.h" +#include "f4se/BSGeometry.h" + +#include "f4se/GameObjects.h" + +#include "SimObj.h" + + +namespace papyrusOCBP +{ + concurrency::concurrent_unordered_map> boneIgnores; + + void SetBoneToggle(StaticFunctionTag*, Actor* actor, bool toggle, BSFixedString boneName) + { + boneIgnores[actor->formID][std::string(boneName.c_str())] = toggle; + } + + bool GetBoneToggle(StaticFunctionTag*, Actor* actor, BSFixedString boneName) + { + if (boneIgnores.find(actor->formID) != boneIgnores.end()) + { + auto actorsBoneIgns = boneIgnores.at(actor->formID); + if (actorsBoneIgns.find(std::string(boneName.c_str())) != actorsBoneIgns.end()) + { + return actorsBoneIgns.at(std::string(boneName.c_str())); + } + } + + return false; + } + + void ClearBoneToggles(StaticFunctionTag*) + { + boneIgnores.clear(); + } + +} + +void papyrusOCBP::RegisterFuncs(VirtualMachine* vm) +{ + vm->RegisterFunction( + new NativeFunction3("SetBoneToggle", "OCBP_API", papyrusOCBP::SetBoneToggle, vm)); + + vm->RegisterFunction( + new NativeFunction2("GetBoneToggle", "OCBP_API", papyrusOCBP::GetBoneToggle, vm)); + + vm->RegisterFunction( + new NativeFunction0("ClearBoneToggles", "OCBP_API", papyrusOCBP::ClearBoneToggles, vm)); +} \ No newline at end of file diff --git a/CBPSSE/PapyrusOCBP.h b/CBPSSE/PapyrusOCBP.h new file mode 100644 index 0000000..1eac986 --- /dev/null +++ b/CBPSSE/PapyrusOCBP.h @@ -0,0 +1,16 @@ +#pragma once + +#include "f4se/GameTypes.h" +#include "f4se/PapyrusVM.h" +#include + +#include + +class VirtualMachine; +struct StaticFunctionTag; + +namespace papyrusOCBP +{ + extern concurrency::concurrent_unordered_map> boneIgnores; + void RegisterFuncs(VirtualMachine* vm); +}; \ No newline at end of file diff --git a/CBPSSE/SimObj.cpp b/CBPSSE/SimObj.cpp index 009be0d..16bd71a 100644 --- a/CBPSSE/SimObj.cpp +++ b/CBPSSE/SimObj.cpp @@ -1,89 +1,180 @@ -#include "SimObj.h" -#include "skse64/NiNodes.h" -#include "skse64/GameForms.h" -#include "skse64/GameRTTI.h" -#include "log.h" - +#pragma warning(disable : 5040) -// Note we don't ref count the nodes becasue it's ignored when the Actor is deleted, and calling Release after that can corrupt memory +#include "f4se/NiNodes.h" +#include "f4se/GameForms.h" +#include "f4se/GameRTTI.h" -const char *leftBreastName = "NPC L Breast"; -const char *rightBreastName = "NPC R Breast"; -const char *leftButtName = "NPC L Butt"; -const char *rightButtName = "NPC R Butt"; -const char *bellyName = "HDT Belly"; +#include "ActorUtils.h" +#include "config.h" +#include "log.h" +#include "PapyrusOCBP.h" +#include "SimObj.h" -const char *scrotumName = "NPC GenitalsScrotum [GenScrot]"; -const char *leftScrotumName = "NPC L GenitalsScrotum [LGenScrot]"; -const char *rightScrotumName = "NPC R GenitalsScrotum [RGenScrot]"; +using actorUtils::GetBaseSkeleton; +using actorUtils::IsBoneInWhitelist; +using actorUtils::IsActorInPowerArmor; +using papyrusOCBP::boneIgnores; -std::unordered_map configMap = { - {leftBreastName, "Breast"}, {rightBreastName, "Breast"}, - {leftButtName, "Butt"}, {rightButtName, "Butt"}, - {bellyName, "Belly"} }; +// Note we don't ref count the nodes becasue it's ignored when the Actor is deleted, and calling Release after that can corrupt memory +std::vector boneNames; +SimObj::SimObj(Actor* actor) + : things(4) +{ + id = actor->formID; + gender = Gender::Unassigned; + raceEid = std::string(""); +} -std::vector femaleBones = { leftBreastName, rightBreastName, leftButtName, rightButtName, bellyName }; +SimObj::~SimObj() +{ +} -SimObj::SimObj(Actor *actor, config_t &config) - : things(5){ - id = actor->formID; +bool SimObj::AddBonesToThings(Actor* actor, std::vector& boneNames) +{ + //logger.Error("%s\n", __func__); + if (!actor) + { + return false; + } + auto loadedData = actor->unkF0; + + if (loadedData && loadedData->rootNode) + { + for (std::string & b : boneNames) + { + if (!useWhitelist || (IsBoneInWhitelist(actor, b) && useWhitelist)) + { + //logger.Error("%s: adding Bone %s for actor %08x\n", __func__, b.c_str(), actor->formID); + BSFixedString cs(b.c_str()); + auto bone = loadedData->rootNode->GetObjectByName(&cs); + auto findBone = things.find(b); + if (!bone) + { + logger.Info("%s: Failed to find Bone %s for actor %08x\n", __func__, b.c_str(), actor->formID); + } + else if (findBone == things.end()) + { + //logger.info("Doing Bone %s for actor %08x\n", b, actor->formID); + things.insert(std::make_pair(b, Thing(bone, cs, actor))); + } + } + } + } + return true; } -SimObj::~SimObj() { +bool SimObj::Bind(Actor* actor, std::vector& boneNames, config_t& config) +{ + if (!actor) + { + return false; + } + auto loadedData = actor->unkF0; + if (loadedData && loadedData->rootNode) + { + bound = true; + + things.clear(); + + if (actorUtils::IsActorMale(actor)) + { + gender = Gender::Male; + } + else + { + gender = Gender::Female; + } + + raceEid = actorUtils::GetActorRaceEID(actor); + + AddBonesToThings(actor, boneNames); + UpdateConfigs(config); + return true; + } + return false; } +UInt64 SimObj::GetActorKey() +{ + return currentActorKey; +} -bool SimObj::bind(Actor *actor, std::vector& boneNames, config_t &config) +SimObj::Gender SimObj::GetGender() { - //logger.error("bind\n"); - - - auto loadedState = actor->loadedState; - if (loadedState && loadedState->node) { - bound = true; - - things.clear(); - for (const char * &b : boneNames) { - BSFixedString cs(b); - auto bone = loadedState->node->GetObjectByName(&cs.data); - if (!bone) { - logger.info("Failed to find Bone %s for actor %d\n", b, actor->formID); - } else { - things.emplace(b, Thing(bone, cs)); - } - } - updateConfig(config); - return true; - } - return false; + return gender; } -bool SimObj::actorValid(Actor *actor) { - if (actor->flags & TESForm::kFlagIsDeleted) - return false; - if (actor && actor->loadedState && actor->loadedState->node) - return true; - return false; +std::string SimObj::GetRaceEID() +{ + return raceEid; } +void SimObj::Reset() +{ + bound = false; + things.clear(); +} -void SimObj::update(Actor *actor) { - if (!bound) - return; - //logger.error("update\n"); - for (auto &t : things) { - t.second.update(actor); - } - //logger.error("end SimObj update\n"); +void SimObj::SetActorKey(UInt64 key) +{ + currentActorKey = key; } -bool SimObj::updateConfig(config_t & config) { - for (auto &t : things) { - std::string §ion = configMap[t.first]; - auto ¢ry = config[section]; - t.second.updateConfig(centry); - } - return true; +void SimObj::Update(Actor* actor) +{ + if (!bound || + IsActorInPowerArmor(actor) || + NULL == GetBaseSkeleton(actor)) + { + return; + } + + for (auto& t : things) + { + //concurrency::parallel_for_each(things.begin(), things.end(), [&](auto& thing) + // { + // Might be a better way to do this + auto actorBoneMapIter = boneIgnores.find(actor->formID); + if (actorBoneMapIter != boneIgnores.end()) + { + auto & actorBoneMap = actorBoneMapIter->second; + auto boneDisabledIter = actorBoneMap.find(t.first); + if (boneDisabledIter != actorBoneMap.end()) + { + if (true == boneDisabledIter->second) + { + continue; + } + } + } + + if (t.second.isEnabled) + { + t.second.UpdateThing(actor); + } + //}); + } } +bool SimObj::UpdateConfigs(config_t& config) +{ + for (auto & thing : things) + { + //concurrency::parallel_for_each(things.begin(), things.end(), [&](auto& thing) + // { + //logger.Info("%s: Updating config for Thing %s\n", __func__, thing.first.c_str()); + + if (config.count(thing.first) > 0) + { + thing.second.UpdateConfig(config[thing.first]); + thing.second.isEnabled = true; + } + else + { + thing.second.isEnabled = false; + } + //}); + } + return true; +} \ No newline at end of file diff --git a/CBPSSE/SimObj.h b/CBPSSE/SimObj.h index 1d98c0a..ef9a5dc 100644 --- a/CBPSSE/SimObj.h +++ b/CBPSSE/SimObj.h @@ -1,31 +1,44 @@ #pragma once -#include #include -#include "skse64/GameReferences.h" +#include "amp.h" +#include + +#include "f4se/GameReferences.h" #include "Thing.h" #include "config.h" -class SimObj { - UInt32 id = 0; - bool bound = false; +class SimObj +{ + public: - std::unordered_map things; - SimObj(Actor *actor, config_t &config); - SimObj() {} - ~SimObj(); - bool bind(Actor *actor, std::vector &boneNames, config_t &config); - bool actorValid(Actor *actor); - void update(Actor *actor); - bool updateConfig(config_t &config); - bool isBound() { return bound; } + enum class Gender + { + Male, + Female, + Unassigned + }; + concurrency::concurrent_unordered_map things; + SimObj(Actor* actor); + SimObj() {} + ~SimObj(); + bool AddBonesToThings(Actor* actor, std::vector& boneNames); + bool Bind(Actor* actor, std::vector& boneNames, config_t& config); + UInt64 GetActorKey(); + Gender GetGender(); + std::string GetRaceEID(); + void Reset(); + void SetActorKey(UInt64 key); + void Update(Actor* actor); + bool UpdateConfigs(config_t& config); + bool IsBound() { return bound; } +private: + UInt32 id = 0; + bool bound = false; + Gender gender; + std::string raceEid; + UInt64 currentActorKey; }; - -extern std::vector femaleBones; -extern const char *leftBreastName; -extern const char *rightBreastName; -extern const char *leftButtName; -extern const char *rightButtName; -extern const char *bellyName; \ No newline at end of file +extern std::vector boneNames; \ No newline at end of file diff --git a/CBPSSE/Thing.cpp b/CBPSSE/Thing.cpp index a333a02..13594a6 100644 --- a/CBPSSE/Thing.cpp +++ b/CBPSSE/Thing.cpp @@ -1,169 +1,633 @@ +#include "ActorUtils.h" #include "Thing.h" #include "log.h" -#include "skse64\NiNodes.h" +#include "f4se\NiNodes.h" #include -Thing::Thing(NiAVObject *obj, BSFixedString &name) - : boneName(name) - , velocity(NiPoint3(0, 0, 0)) -{ - oldWorldPos = obj->m_worldTransform.pos; - time = clock(); -} +constexpr float DEG_TO_RAD = 3.14159265 / 180; +const char* skeletonNif_boneName = "skeleton.nif"; +const float DIFF_LIMIT = 100.0; -Thing::~Thing() { -} +// TODO Make these logger macros +//#define DEBUG 1 +//#define TRANSFORM_DEBUG 1 -void showPos(NiPoint3 &p) { - logger.info("%8.2f %8.2f %8.2f\n", p.x, p.y, p.z); -} +// > +pos_map Thing::origLocalPos; +rot_map Thing::origLocalRot; +rot_map Thing::origChestWorldRot; -void showRot(NiMatrix33 &r) { - logger.info("%8.2f %8.2f %8.2f\n", r.data[0][0], r.data[0][1], r.data[0][2]); - logger.info("%8.2f %8.2f %8.2f\n", r.data[1][0], r.data[1][1], r.data[1][2]); - logger.info("%8.2f %8.2f %8.2f\n", r.data[2][0], r.data[2][1], r.data[2][2]); -} +//shared_mutex Thing::thing_map_lock; - -float solveQuad(float a, float b, float c) { - float k1 = (-b + sqrtf(b*b - 4*a*c)) / (2 * a); - //float k2 = (-b - sqrtf(b*b - 4*a*c)) / (2 * a); - //logger.error("k2 = %f\n", k2); - return k1; +void Thing::ShowPos(NiPoint3& p) +{ + logger.Info("%8.4f %8.4f %8.4f\n", p.x, p.y, p.z); } - - - -void Thing::updateConfig(configEntry_t & centry) { - stiffness = centry["stiffness"]; - stiffness2 = centry["stiffness2"]; - damping = centry["damping"]; - maxOffset = centry["maxoffset"]; - timeTick = centry["timetick"]; - linearX = centry["linearX"]; - linearY = centry["linearY"]; - linearZ = centry["linearZ"]; - rotational = centry["rotational"]; - // Optional entries for backwards compatability - if (centry.find("timeStep") != centry.end()) - timeStep = centry["timeStep"]; - else - timeStep = 1.0f; - gravityBias = centry["gravityBias"]; - gravityCorrection = centry["gravityCorrection"]; - cogOffset = centry["cogOffset"]; - if (timeTick <= 1) - timeTick = 1; - - //zOffset = solveQuad(stiffness2, stiffness, -gravityBias); - - //logger.error("z offset = %f\n", solveQuad(stiffness2, stiffness, -gravityBias)); +void Thing::ShowRot(NiMatrix43& r) +{ + logger.Info("%8.4f %8.4f %8.4f %8.4f\n", r.data[0][0], r.data[0][1], r.data[0][2], r.data[0][3]); + logger.Info("%8.4f %8.4f %8.4f %8.4f\n", r.data[1][0], r.data[1][1], r.data[1][2], r.data[1][3]); + logger.Info("%8.4f %8.4f %8.4f %8.4f\n", r.data[2][0], r.data[2][1], r.data[2][2], r.data[2][3]); } -void Thing::dump() { - //showPos(obj->m_worldTransform.pos); - //showPos(obj->m_localTransform.pos); -} +Thing::Thing(NiAVObject* obj, BSFixedString& name, Actor* actor) + : thingObj(obj) + , boneName(name) + , velocity(NiPoint3(0, 0, 0)) + , m_actor(actor) +{ + isEnabled = true; -static float clamp(float val, float min, float max) { - if (val < min) return min; - else if (val > max) return max; - return val; -} + auto skeletonObj = actorUtils::GetBaseSkeleton(actor); + if (skeletonObj == NULL) + { + logger.Error("%s: Didn't find thing %s's base skeleton.nif for actor %08x \n", __func__, boneName.c_str(), actor->formID); + return; + } -void Thing::reset() { - // TODO -} + //->m_worldTransform.rot * skeletonObj->m_worldTransform.pos + auto firstWorldPos = skeletonObj->m_worldTransform.rot * obj->m_worldTransform.pos; + auto firstSkeletonPos = skeletonObj->m_worldTransform.rot * skeletonObj->m_worldTransform.pos; -template int sgn(T val) { - return (T(0) < val) - (val < T(0)); -} + rightSide = (firstWorldPos.x - firstSkeletonPos.x) >= 0.0; -void Thing::update(Actor *actor) { - auto newTime = clock(); - auto deltaT = newTime - time; + // Set initial positions + oldWorldPos = obj->m_worldTransform.pos; - time = newTime; - if (deltaT > 64) deltaT = 64; - if (deltaT < 8) deltaT = 8; + //logger.Error("obj->m_worldTransform.rot.Transpose():"); + //ShowRot(obj->m_worldTransform.rot.Transpose()); + //logger.Error("obj->m_localTransform.rot:"); + //ShowRot(obj->m_localTransform.rot); + origWorldRot = obj->m_localTransform.rot.Transpose() * obj->m_worldTransform.rot; - auto loadedState = actor->loadedState; - if (!loadedState || !loadedState->node) { - logger.error("No loaded state for actor %08x\n", actor->formID); - return; - } - auto obj = loadedState->node->GetObjectByName(&boneName.data); - if (!obj) - return; + time = clock(); - //Offset to move Center of Mass make rotaional motion more significant - NiPoint3 target = obj->m_parent->m_worldTransform * NiPoint3(0, cogOffset, 0); - //logger.error("Target: "); - //showPos(target); - NiPoint3 diff = target - oldWorldPos; - diff += obj->m_parent->m_worldTransform.rot * NiPoint3(0, 0, gravityCorrection); + IsBreastBone = ContainsNoCase(std::string(boneName.c_str()), "Breast"); +} - if (fabs(diff.x) > 100 || fabs(diff.y) > 100 || fabs(diff.z) > 100) { - //logger.error("transform reset\n"); - obj->m_localTransform.pos = NiPoint3(0, 0, 0); - oldWorldPos = target; - velocity = NiPoint3(0, 0, 0); - time = clock(); - } else { +Thing::~Thing() +{ +} - diff *= timeTick / (float)deltaT; - NiPoint3 posDelta = NiPoint3(0, 0, 0); +NiPoint3 Thing::CalculateGravitySupine(Actor* actor) +{ + NiPoint3 varGravitySupine = NiPoint3(0.0, 0.0, 0.0); - // Compute the "Spring" Force - NiPoint3 diff2(diff.x * diff.x * sgn(diff.x), diff.y * diff.y * sgn(diff.y), diff.z * diff.z * sgn(diff.z)); - NiPoint3 force = (diff * stiffness) + (diff2 * stiffness2) - NiPoint3(0, 0, gravityBias); - //showPos(diff); - //showPos(force); + if (!IsBreastBone) //other bones don't need to edited gravity by SPINE2 obj + { + //other nodes are based on parent obj + varGravitySupine = NiPoint3(0.0, 0.0, 0.0); + return varGravitySupine; + } - do { - // Assume mass is 1, so Accelleration is Force, can vary mass by changinf force - //velocity = (velocity + (force * timeStep)) * (1 - (damping * timeStep)); - velocity = (velocity + (force * timeStep)) - (velocity * (damping * timeStep)); + //Get the reference bone to know which way the breasts are orientated + //thing_ReadNode_lock.lock(); + BSFixedString chest_str("Chest"); - // New position accounting for time - posDelta += (velocity * timeStep); - deltaT -= timeTick; - } while (deltaT >= timeTick); + NiAVObject* breastGravityReferenceBone = actor->unkF0->rootNode->GetObjectByName(&chest_str); + //thing_ReadNode_lock.unlock(); - NiPoint3 newPos = oldWorldPos + posDelta; - // clamp the difference to stop the breast severely lagging at low framerates - auto diff = newPos - target; - diff.x = clamp(diff.x, -maxOffset, maxOffset); - diff.y = clamp(diff.y, -maxOffset, maxOffset); - diff.z = clamp(diff.z-gravityCorrection, -maxOffset, maxOffset) + gravityCorrection; + float gravityRatio = 0.0f; + if (breastGravityReferenceBone != nullptr) + { + //auto breastRot = breastGravityReferenceBone->m_worldTransform.rot; + //Get the orientation (here the Z element of the rotation matrix (approx 1.0 when standing up, approx -1.0 when upside down)) + auto chestOrientation = breastGravityReferenceBone->m_worldTransform.rot.data[1][2]; + gravityRatio = chestOrientation >= 0.0 ? chestOrientation : 0.0; + } - oldWorldPos = diff + target; + varGravitySupine = NiPoint3(gravitySupineX, gravitySupineY, gravitySupineZ) * gravityRatio; - //logger.error("set positions\n"); - // move the bones based on the supplied weightings - // Convert the world translations into local coordinates - auto invRot = obj->m_parent->m_worldTransform.rot.Transpose(); - auto ldiff = invRot * diff; + return varGravitySupine; +} - // remove component along bone - might want something closer to worldY - //ldiff.y = 0; +void Thing::StoreOriginalTransforms(Actor* actor) +{ + // Save the bones' original local values, per actor, if they already haven't + auto origLocalPos_iter = origLocalPos.find(boneName.c_str()); + auto origLocalRot_iter = origLocalRot.find(boneName.c_str()); + + auto obj = thingObj; + + //thing_map_lock.lock(); + if (IsBreastBone) + { + BSFixedString chest_name("Chest"); + NiAVObject* chestObj = actor->unkF0->rootNode->GetObjectByName(&chest_name); + + if (chestObj && chestObj->m_parent) + { + if (origLocalRot_iter == origChestWorldRot.end()) + { + origChestWorldRot[boneName.c_str()][actor->formID] = chestObj->m_parent->m_worldTransform.rot * chestObj->m_localTransform.rot; + } + else + { + auto actorRotMap = origChestWorldRot.at(boneName.c_str()); + auto actor_iter = actorRotMap.find(actor->formID); + if (actor_iter == actorRotMap.end()) + { + origChestWorldRot[boneName.c_str()][actor->formID] = chestObj->m_parent->m_worldTransform.rot * chestObj->m_localTransform.rot; + } + } + } + } + + // Original Bone Position + if (origLocalPos_iter == origLocalPos.end()) + { + origLocalPos[boneName.c_str()][actor->formID] = obj->m_localTransform.pos; +#ifdef DEBUG + logger.Error("for bone %s, actor %08x: \n", boneName.c_str(), actor->formID); + logger.Error("firstRun pos Set: \n"); + ShowPos(obj->m_localTransform.pos); +#endif + } + else + { + auto actorPosMap = origLocalPos.at(boneName.c_str()); + auto actor_iter = actorPosMap.find(actor->formID); + if (actor_iter == actorPosMap.end()) + { + origLocalPos[boneName.c_str()][actor->formID] = obj->m_localTransform.pos; +#ifdef DEBUG + logger.Error("for bone %s, actor %08x: \n", boneName.c_str(), actor->formID); + logger.Error("firstRun pos Set: \n"); + ShowPos(obj->m_localTransform.pos); +#endif + } + } + + // Original Bone Rotation + if (origLocalRot_iter == origLocalRot.end()) + { + origLocalRot[boneName.c_str()][actor->formID] = obj->m_localTransform.rot; +#ifdef DEBUG + logger.Error("for bone %s, actor %08x: \n", boneName.c_str(), actor->formID); + logger.Error("firstRun rot Set:\n"); + ShowRot(obj->m_localTransform.rot); +#endif + } + else + { + auto actorRotMap = origLocalRot.at(boneName.c_str()); + auto actor_iter = actorRotMap.find(actor->formID); + if (actor_iter == actorRotMap.end()) + { + origLocalRot[boneName.c_str()][actor->formID] = obj->m_localTransform.rot; +#ifdef DEBUG + logger.Error("for bone %s, actor %08x: \n", boneName.c_str(), actor->formID); + logger.Error("firstRun rot Set: \n"); + ShowRot(obj->m_localTransform.rot); +#endif + } + } + //thing_map_lock.unlock(); +} - oldWorldPos = (obj->m_parent->m_worldTransform.rot * ldiff) + target; +void Thing::UpdateConfig(configEntry_t& centry) +{ + stiffness = centry["stiffness"]; + stiffness2 = centry["stiffness2"]; + damping = centry["damping"]; + + maxOffsetX = centry["maxoffsetX"]; + maxOffsetY = centry["maxoffsetY"]; + maxOffsetZ = centry["maxoffsetZ"]; + + linearX = centry["linearX"]; + linearY = centry["linearY"]; + linearZ = centry["linearZ"]; + + rotationalX = centry["rotationalX"]; + rotationalY = centry["rotationalY"]; + rotationalZ = centry["rotationalZ"]; + + rotateLinearX = centry["rotateLinearX"]; + rotateLinearY = centry["rotateLinearY"]; + rotateLinearZ = centry["rotateLinearZ"]; + + rotateRotationX = centry["rotateRotationX"]; + rotateRotationY = centry["rotateRotationY"]; + rotateRotationZ = centry["rotateRotationZ"]; + + scaleMultiplierX = 1.0f;// centry["scaleMultiplierX"]; + scaleMultiplierY = 1.0f;// centry["scaleMultiplierY"]; + scaleMultiplierZ = 1.0f;// centry["scaleMultiplierZ"]; + + posOffsetX = centry["posOffsetX"]; + posOffsetY = centry["posOffsetY"]; + + timeTick = centry["timetick"]; + + if (centry.find("timeStep") != centry.end()) + timeStep = centry["timeStep"]; + else + timeStep = 0.016f; + + gravityBias = centry["gravityBias"]; + gravityCorrection = centry["gravityCorrection"]; + cogOffsetY = centry["cogOffsetY"]; + cogOffsetX = centry["cogOffsetX"]; + cogOffsetZ = centry["cogOffsetZ"]; + if (timeTick <= 1) + timeTick = 1; + + absRotX = centry["absRotX"] != 0.0; + + linearSpreadforceXtoY = centry["linearSpreadforceXtoY"]; + linearSpreadforceXtoZ = centry["linearSpreadforceXtoZ"]; + linearSpreadforceYtoX = centry["linearSpreadforceYtoX"]; + linearSpreadforceYtoZ = centry["linearSpreadforceYtoZ"]; + linearSpreadforceZtoX = centry["linearSpreadforceZtoX"]; + linearSpreadforceZtoY = centry["linearSpreadforceZtoY"]; + + gravitySupineX = centry["gravitySupineX"]; + gravitySupineY = centry["gravitySupineY"]; + gravitySupineZ = centry["gravitySupineZ"]; +} - obj->m_localTransform.pos.x = ldiff.x * linearX; - obj->m_localTransform.pos.y = ldiff.y * linearY; - obj->m_localTransform.pos.z = ldiff.z * linearZ; - auto rdiff = ldiff * rotational; - obj->m_localTransform.rot.SetEulerAngles(0, 0, rdiff.z); +static float clamp(float val, float min, float max) +{ + if (val < min) return min; + else if (val > max) return max; + return val; +} +static float remap(float value, float start1, float stop1, float start2, float stop2) +{ + return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1)); +} - } - //logger.error("end update()\n"); +void Thing::Reset(Actor* actor) +{ + auto loadedState = actor->unkF0; + if (!loadedState || !loadedState->rootNode) + { + logger.Error("No loaded state for actor %08x\n", actor->formID); + return; + } + auto obj = loadedState->rootNode->GetObjectByName(&boneName); + + if (!obj) + { + logger.Error("Couldn't get name for loaded state for actor %08x\n", actor->formID); + return; + } + + obj->m_localTransform.pos = origLocalPos[boneName.c_str()][actor->formID]; + obj->m_localTransform.rot = origLocalRot[boneName.c_str()][actor->formID]; +} +// Returns +template int sgn(T val) +{ + return (T(0) < val) - (val < T(0)); +} +NiAVObject* Thing::IsThingActorValid(Actor* actor) +{ + if (!actorUtils::IsActorValid(actor)) + { + //logger.Error("%s: No valid actor in Thing::Update\n", __func__); + return NULL; + } + auto loadedState = actor->unkF0; + if (!loadedState || !loadedState->rootNode) + { + //logger.Error("%s: No loaded state for actor %08x\n", __func__, actor->formID); + return NULL; + } + auto obj = loadedState->rootNode->GetObjectByName(&boneName); + + if (!obj) + { + //logger.Error("%s: Couldn't get name for loaded state for actor %08x\n", __func__, actor->formID); + return NULL; + } + + if (!obj->m_parent) + { + //logger.Error("%s: Couldn't get bone %s parent for actor %08x\n", __func__, boneName.c_str(), actor->formID); + return NULL; + } + + return obj; +} +void Thing::UpdateThing(Actor* actor) +{ + auto forceMultiplier = 1.0; + + thingObj = IsThingActorValid(actor); + auto obj = thingObj; + + if (!obj) + { + return; + } + + auto newTime = clock(); + auto deltaT = newTime - time; + + time = newTime; + if (deltaT > 64) deltaT = 64; + if (deltaT < 8) deltaT = 8; + +#if TRANSFORM_DEBUG + auto sceneObj = obj; + while (sceneObj->m_parent && sceneObj->m_name != "skeleton.nif") + { + logger.Info(sceneObj->m_name); + logger.Info("\n---\n"); + logger.Error("Actual m_localTransform.pos: "); + ShowPos(sceneObj->m_localTransform.pos); + logger.Error("Actual m_worldTransform.pos: "); + ShowPos(sceneObj->m_worldTransform.pos); + logger.Info("---\n"); + //logger.Error("Actual m_localTransform.rot Matrix:\n"); + ShowRot(sceneObj->m_localTransform.rot); + //logger.Error("Actual m_worldTransform.rot Matrix:\n"); + ShowRot(sceneObj->m_worldTransform.rot); + logger.Info("---\n"); + //if (sceneObj->m_parent) { + // logger.Error("Calculated m_worldTransform.pos: "); + // ShowPos((sceneObj->m_parent->m_worldTransform.rot.Transpose() * sceneObj->m_localTransform.pos) + sceneObj->m_parent->m_worldTransform.pos); + // logger.Error("Calculated m_worldTransform.rot Matrix:\n"); + // ShowRot(sceneObj->m_localTransform.rot * sceneObj->m_parent->m_worldTransform.rot); + //} + sceneObj = sceneObj->m_parent; + } +#endif + + StoreOriginalTransforms(actor); + + auto skeletonObj = actorUtils::GetBaseSkeleton(actor); + if (skeletonObj == NULL) + { + logger.Error("%s: Didn't find thing %s's base skeleton.nif for actor %08x \n", __func__, boneName.c_str(), actor->formID); + return; + } + +#if DEBUG + logger.Error("bone %s for actor %08x with parent %s\n", boneName.c_str(), actor->formID, skeletonObj->m_name.c_str()); + ShowRot(skeletonObj->m_worldTransform.rot); + //ShowPos(obj->m_parent->m_worldTransform.rot.Transpose() * obj->m_localTransform.pos); +#endif + + NiMatrix43 skelSpaceInvTransform = skeletonObj->m_localTransform.rot.Transpose(); + NiPoint3 origWorldPos = (obj->m_parent->m_worldTransform.rot.Transpose() * origLocalPos[boneName.c_str()][actor->formID]) + obj->m_parent->m_worldTransform.pos; + NiPoint3 origWorldPosRot = (obj->m_parent->m_worldTransform.rot.Transpose() * origLocalPos[boneName.c_str()][actor->formID]) + obj->m_parent->m_worldTransform.pos; + //float nodeParentInvScale = 1.0f / obj->m_parent->m_worldTransform.scale; + + // Cog offset is offset to move Center of Mass make rotational motion more significant + // First transform the cog offset (which is in skeleton space) to world space + // target is in world space + NiPoint3 target = (skelSpaceInvTransform * NiPoint3(cogOffsetX, cogOffsetY, cogOffsetZ)) + origWorldPos; + +#if DEBUG + logger.Error("World Position: "); + ShowPos(obj->m_worldTransform.pos); + //logger.Error("Parent World Position difference: "); + //ShowPos(obj->m_worldTransform.pos - obj->m_parent->m_worldTransform.pos); + ShowPos(skelSpaceInvTransform * NiPoint3(cogOffsetX, cogOffsetY, cogOffsetZ)); + //logger.Error("Target Rotation:\n"); + //ShowRot(skelSpaceInvTransform); + logger.Error("cogOffset x Transformation:"); + ShowPos(skelSpaceInvTransform * NiPoint3(cogOffsetX, 0, 0)); + logger.Error("cogOffset y Transformation:"); + ShowPos(skelSpaceInvTransform * NiPoint3(0, cogOffsetY, 0)); + logger.Error("cogOffset z Transformation:"); + ShowPos(skelSpaceInvTransform * NiPoint3(0, 0, cogOffsetZ)); +#endif + + // diff is Difference in position between old and new world position + // diff is world space + NiPoint3 diff = (target - oldWorldPos) * forceMultiplier; + +#if DEBUG + logger.Error("Diff after gravity correction %f: ", varGravityCorrection); + ShowPos(diff); +#endif + + if (fabs(diff.x) > DIFF_LIMIT || fabs(diff.y) > DIFF_LIMIT || fabs(diff.z) > DIFF_LIMIT) + { + logger.Error("%s: bone %s transform reset for actor %x\n", __func__, boneName.c_str(), actor->formID); + obj->m_localTransform.pos = origLocalPos[boneName.c_str()][actor->formID]; + obj->m_localTransform.rot = origLocalRot[boneName.c_str()][actor->formID]; + + oldWorldPos = target; + oldWorldPosRot = origWorldPos; + + velocity = NiPoint3(0, 0, 0); + time = clock(); + return; + } + + // Rotation for transforming gravityBias back to world coordinates + auto newRotation = obj->m_parent->m_worldTransform.rot * origWorldRot.Transpose(); + + // move the bones based on the supplied weightings + // Convert the world translations into local coordinates + + NiMatrix43 rotateLinear; + rotateLinear.SetEulerAngles(rotateLinearX * DEG_TO_RAD, rotateLinearY * DEG_TO_RAD, rotateLinearZ * DEG_TO_RAD); + + auto timeMultiplier = timeTick / (float)deltaT; + + // Transform to local space + //diff = obj->m_parent->m_worldTransform.rot * (diff * timeMultiplier); + diff = (diff * timeMultiplier); + + NiPoint3 posDelta = NiPoint3(0, 0, 0); + + // Compute the "Spring" Force + + NiPoint3 diff2(diff.x * diff.x * sgn(diff.x), + diff.y * diff.y * sgn(diff.y), + diff.z * diff.z * sgn(diff.z)); + + NiPoint3 force = (diff * stiffness) + (diff2 * stiffness2) - (newRotation * skelSpaceInvTransform * NiPoint3(0, 0, gravityBias)); + +#if DEBUG + logger.Error("Diff2: "); + ShowPos(diff2); + logger.Error("Force with stiffness %f, stiffness2 %f, gravity bias %f: ", stiffness, stiffness2, gravityBias); + ShowPos(force); +#endif + + do + { + // Assume mass is 1, so Accelleration is Force, can vary mass by changinf force + //velocity = (velocity + (force * timeStep)) * (1 - (damping * timeStep)); + velocity = (velocity + (force * timeStep)) - (velocity * (damping * timeStep)); + + // New position accounting for time + posDelta += (velocity * timeStep); + deltaT -= timeTick; + } while (deltaT >= timeTick); + + velocity = /*obj->m_parent->m_worldTransform.rot.Transpose() * */velocity; + + NiPoint3 newPos = oldWorldPos + /*obj->m_parent->m_worldTransform.rot.Transpose() **/ posDelta; + NiPoint3 newPosRot = origWorldPosRot; + + oldWorldPos = diff + target; + +#if DEBUG + //logger.Error("posDelta: "); + //ShowPos(posDelta); + logger.Error("newPos: "); + ShowPos(newPos); +#endif + // clamp the difference to stop the breast severely lagging at low framerates + diff = newPos - target; + + diff.x = clamp(diff.x, -maxOffsetX, maxOffsetX); + diff.y = clamp(diff.y, -maxOffsetY, maxOffsetY); + diff.z = clamp(diff.z, -maxOffsetZ, maxOffsetZ); + +#if DEBUG + logger.Error("diff from newPos: "); + ShowPos(diff); + //logger.Error("oldWorldPos: "); + //ShowPos(oldWorldPos); +#endif + + auto localDiff = diff; + + // Transform localDiff (which is in world space) to skeleton space + localDiff = skeletonObj->m_localTransform.rot * localDiff; + + auto scaleMultiplier = 1.0; + localDiff.x *= scaleMultiplier; + localDiff.y *= scaleMultiplier; + localDiff.z *= scaleMultiplier; + + auto beforeLocalDiff = localDiff; + + // Clamp against settings (which are in skeleton space) + localDiff.x = clamp(localDiff.x, -maxOffsetX, maxOffsetX); + localDiff.y = clamp(localDiff.y, -maxOffsetY, maxOffsetY); + localDiff.z = clamp(localDiff.z, -maxOffsetZ, maxOffsetZ); + + beforeLocalDiff = beforeLocalDiff - localDiff; + + localDiff.x += (beforeLocalDiff.y * linearSpreadforceXtoY) + (beforeLocalDiff.z * linearSpreadforceXtoZ); + localDiff.y += (beforeLocalDiff.x * linearSpreadforceYtoX) + (beforeLocalDiff.z * linearSpreadforceYtoZ); + localDiff.z += (beforeLocalDiff.x * linearSpreadforceZtoX) + (beforeLocalDiff.y * linearSpreadforceZtoY); + + // Clamp against settings (which are in skeleton space) + localDiff.x = clamp(localDiff.x, -maxOffsetX, maxOffsetX); + localDiff.y = clamp(localDiff.y, -maxOffsetY, maxOffsetY); + localDiff.z = clamp(localDiff.z, -maxOffsetZ, maxOffsetZ); + + localDiff.x *= linearX; + localDiff.y *= linearY; + localDiff.z *= linearZ; + + // Store a copy of localDiff for later for transforming rotation motions + auto rotDiff = localDiff; + + auto varGravitySupine = CalculateGravitySupine(actor); + + // Transform localDiff to world coordinates + localDiff = skeletonObj->m_localTransform.rot.Transpose() * localDiff; + + auto newWorldPos = localDiff; + + newWorldPos.x += varGravitySupine.x * linearX; + newWorldPos.y += varGravitySupine.y * linearY; + newWorldPos.z += varGravitySupine.z * linearZ; + + oldWorldPos = diff + target; + + // Create the rotated world space transformation matrix + NiMatrix43 rotatedInvWorldTrans = rotateLinear * newRotation.Transpose() * obj->m_parent->m_worldTransform.rot; + + // Transform localDiff to a settings-rotated local space + //newWorldPos = rotatedInvWorldTrans * newWorldPos; + + auto newLocalPos = origLocalPos[boneName.c_str()][actor->formID] + (rotatedInvWorldTrans * newWorldPos); + + // Apply gravityCorrection, which will always point downward + newLocalPos += rotateLinear * obj->m_parent->m_worldTransform.rot * skeletonObj->m_localTransform.rot.Transpose() * NiPoint3(0, 0, gravityCorrection * linearZ); + + //if (ContainsNoCase(std::string(boneName.c_str()), "Breast_CBP_R_02") || + // ContainsNoCase(std::string(boneName.c_str()), "Breast_CBP_L_02") + // ) + //{ + // //logger.Error("skeletonObj->m_localTransform.rot.Transpose()\n"); + // //ShowRot(skeletonObj->m_localTransform.rot.Transpose()); + // //logger.Error("rotatedInvWorldTrans\n"); + // //ShowRot(rotatedInvWorldTrans); + // logger.Error("newWorldPos: "); + // ShowPos(newWorldPos); + // logger.Error("newLocalPos: "); + // ShowPos(newLocalPos); + //} + +#if DEBUG + logger.Error("rotatedInvWorldTrans x=10 Transformation:"); + ShowPos(rotatedInvWorldTrans * NiPoint3(10, 0, 0)); + logger.Error("rotatedInvWorldTrans y=10 Transformation:"); + ShowPos(rotatedInvWorldTrans * NiPoint3(0, 10, 0)); + logger.Error("rotatedInvWorldTrans z=10 Transformation:"); + ShowPos(rotatedInvWorldTrans * NiPoint3(0, 0, 10)); + logger.Error("oldWorldPos: "); + ShowPos(oldWorldPos); + logger.Error("localTransform.pos: "); + ShowPos(obj->m_localTransform.pos); + logger.Error("rotDiff: "); + ShowPos(rotDiff); + +#endif + + obj->m_localTransform.pos = newLocalPos; + + // Calculate rotational motion + if (absRotX) rotDiff.x = fabs(rotDiff.x); + + // Rotational motion scale + rotDiff.x *= rotationalX; + rotDiff.y *= rotationalY; + rotDiff.z *= rotationalZ; + + +#if DEBUG + logger.Error("localTransform.pos after: "); + ShowPos(obj->m_localTransform.pos); + logger.Error("origLocalPos:"); + ShowPos(origLocalPos[boneName.c_str()][actor->formID]); + logger.Error("origLocalRot:"); + ShowRot(origLocalRot[boneName.c_str()][actor->formID]); + +#endif + // Rotate rotation according to settings. + NiMatrix43 rotateRotation; + rotateRotation.SetEulerAngles(rotateRotationX * DEG_TO_RAD, + rotateRotationY * DEG_TO_RAD, + rotateRotationZ * DEG_TO_RAD); + + NiMatrix43 standardRot; + + rotDiff = rotateRotation * rotDiff; + standardRot.SetEulerAngles(rotDiff.x, rotDiff.y, rotDiff.z); + // Calculate the new local rot as an offset from the original local rot + obj->m_localTransform.rot = standardRot * origLocalRot[boneName.c_str()][actor->formID]; + +#if DEBUG + logger.Error("end update()\n"); +#endif + + //logger.Error("end update()\n"); + /*QueryPerformanceCounter(&endingTime); + elapsedMicroseconds.QuadPart = endingTime.QuadPart - startingTime.QuadPart; + elapsedMicroseconds.QuadPart *= 1000000000LL; + elapsedMicroseconds.QuadPart /= frequency.QuadPart; + _MESSAGE("Thing.update() Update Time = %lld ns\n", elapsedMicroseconds.QuadPart);*/ } \ No newline at end of file diff --git a/CBPSSE/Thing.h b/CBPSSE/Thing.h index a3faf14..36af14a 100644 --- a/CBPSSE/Thing.h +++ b/CBPSSE/Thing.h @@ -1,39 +1,134 @@ #pragma once -#include -#include -#include +#include +#include +#include #include #include "config.h" +#include -class Thing { - BSFixedString boneName; - NiPoint3 oldWorldPos; - NiPoint3 velocity; - clock_t time; +//using std::shared_mutex; + +typedef concurrency::concurrent_unordered_map> pos_map; +typedef concurrency::concurrent_unordered_map> rot_map; + +inline void RefreshNode(NiAVObject* node) +{ + if (node == nullptr || node->m_name == nullptr) + return; + + NiAVObject::NiUpdateData npd; + npd.unk00 = nullptr; + npd.pCamera = nullptr; + npd.flags = 0; + npd.unk14 = 0; + npd.unk18 = 0; + npd.unk20 = 0; + npd.unk28 = 0; + npd.unk30 = 0; + npd.unk38 = 0; + node->UpdateWorldData(&npd); +} + +class Thing +{ + BSFixedString boneName; + NiPoint3 oldWorldPos; + NiPoint3 oldWorldPosRot; + //NiPoint3 oldLocalDiff; + float oldRotZ; + NiPoint3 velocity; + clock_t time; + NiAVObject* thingObj; + bool IsBreastBone; + NiMatrix43 firstWorldRot; + NiMatrix43 origWorldRot; + bool rightSide; + Actor* m_actor; public: - float stiffness = 0.5f; - float stiffness2 = 0.0f; - float damping = 0.2f; - float maxOffset = 5.0f; - float cogOffset = 0.0f; - float gravityBias = 0.0f; - float gravityCorrection = 0.0f; - //float zOffset = 0.0f; // Computed based on GravityBias value - float timeTick = 4.0f; - float linearX = 0; - float linearY = 0; - float linearZ = 0; - float rotational = 0.1; - float timeStep = 1.0f; - - Thing(NiAVObject *obj, BSFixedString &name); - ~Thing(); - - void updateConfig(configEntry_t ¢ry); - void dump(); - - void update(Actor *actor); - void reset(); + bool isEnabled; + + float stiffness = 0.5f; + float stiffness2 = 0.0f; + float damping = 0.2f; + float maxOffsetX = 5.0f; + float maxOffsetY = 5.0f; + float maxOffsetZ = 5.0f; + float cogOffsetX = 0.0f; + float cogOffsetY = 0.0f; + float cogOffsetZ = 0.0f; + + float gravityBias = 0.0f; + float gravityCorrection = 0.0f; + float timeTick = 4.0f; + float linearX = 0.0f; + float linearY = 0.0f; + float linearZ = 0.0f; + float rotationalX = 0.0f; + float rotationalY = 0.0f; + float rotationalZ = 0.0f; + + float rotateLinearX = 0.0f; + float rotateLinearY = 0.0f; + float rotateLinearZ = 0.0f; + + float rotateRotationX = 0.0f; + float rotateRotationY = 0.0f; + float rotateRotationZ = 0.0f; + + float scaleMultiplierX = 1.0f; + float scaleMultiplierY = 1.0f; + float scaleMultiplierZ = 1.0f; + + float posOffsetX = 0.0f; + float posOffsetY = 0.0f; + + float timeStep = 0.016f; + + bool absRotX = 0; + + float linearSpreadforceXtoY = 0.0f; + float linearSpreadforceXtoZ = 0.0f; + float linearSpreadforceYtoX = 0.0f; + float linearSpreadforceYtoZ = 0.0f; + float linearSpreadforceZtoX = 0.0f; + float linearSpreadforceZtoY = 0.0f; + + float gravitySupineX = 0.0f; + float gravitySupineY = 0.0f; + float gravitySupineZ = 0.0f; + + static pos_map origLocalPos; + static rot_map origLocalRot; + static rot_map origChestWorldRot; + + // Maps are sorted every edit time, so if it is parallel processing then a high probability of overloading + //static shared_mutex thing_map_lock; + + Thing(NiAVObject* obj, BSFixedString& name, Actor* actor); + ~Thing(); + + NiAVObject* IsThingActorValid(Actor* actor); + void Reset(Actor* actor); + NiPoint3 CalculateGravitySupine(Actor* actor); + void StoreOriginalTransforms(Actor* actor); + void UpdateThing(Actor* actor); + void UpdateConfig(configEntry_t& centry); + + void ShowPos(NiPoint3& p); + void ShowRot(NiMatrix43& r); + + static inline bool ContainsNoCase(std::string str, std::string ministr) + { + std::transform(str.begin(), str.end(), str.begin(), ::tolower); + std::transform(ministr.begin(), ministr.end(), ministr.begin(), ::tolower); + + if (str.find(ministr) != std::string::npos) + { + return true; + } + else + return false; + } }; \ No newline at end of file diff --git a/CBPSSE/config.cpp b/CBPSSE/config.cpp index 4bfd55c..7f07565 100644 --- a/CBPSSE/config.cpp +++ b/CBPSSE/config.cpp @@ -1,48 +1,648 @@ -#include "Thing.h" +#pragma warning(disable : 5040) + +#include "config.h" +#include "INIReader.h" #include "log.h" #include "SimObj.h" -#include +#include "Thing.h" + +#include +#include +#include +#include +#include #include -#include "config.h" +#include + +#include "f4se/GameObjects.h" +#include "f4se/GameRTTI.h" +#include "f4se_common/Utilities.h" +#include "f4se/GameData.h" +#define DEBUG 0 #pragma warning(disable : 4996) int configReloadCount = 60; - +bool playerOnly = false; +bool femaleOnly = false; +bool maleOnly = false; +bool npcOnly = false; +bool useWhitelist = false; config_t config; +concurrency::concurrent_unordered_map configArmorOverrideMap; +concurrency::concurrent_unordered_map configActorOverrideMap; +std::unordered_map priorityNameMappings; +concurrency::concurrent_unordered_set usedSlots; +concurrency::concurrent_unordered_map cachedConfigs; +std::set priorities; + +// TODO data structure these +whitelist_t whitelist; +std::vector raceWhitelist; +void DumpConfigToLog(); +void DumpUsedSlotsToLog(); +UInt32 GetFormIDFromString(std::string const& configString) +{ + size_t colonPos = configString.find_first_of(":"); + auto pluginFormID = configString.substr(0, colonPos); + std::string pluginName = ""; + if (colonPos != std::string::npos && colonPos < configString.length() - 1) + { + pluginName = configString.substr(colonPos + 1); + } + for (auto digit : pluginFormID) + { + if (!std::isxdigit(digit)) + { + logger.Error("Invalid FormID %s, invalid hex character %c\n", pluginFormID.c_str(), digit); + return -1; + } + } -void loadConfig() { - char buffer[1024]; - //logger.info("loadConfig\n"); - FILE *fh = fopen("Data\\SKSE\\Plugins\\CBPConfig.txt", "r"); - if (!fh) { - logger.error("Failed to open config file CBPConfig.txt\n"); - //Console_Print("Failed to open config file CBPConfig.txt"); - configReloadCount = 0; - return; - } - - //Console_Print("Reading CBP Config"); - config.clear(); - do { - auto str = fgets(buffer, 1023, fh); - //logger.error("str %s\n", str); - if (str && strlen(str) > 1) { - if (str[0] != '#') { - char *tok0 = strtok(str, "."); - char *tok1 = strtok(NULL, " "); - char *tok2 = strtok(NULL, " "); - - if (tok0 && tok1 && tok2) { - config[std::string(tok0)][std::string(tok1)] = atof(tok2); - } - } - } - } while (!feof(fh)); - fclose(fh); - - configReloadCount = config["Tuning"]["rate"]; + UInt32 formID = std::stoul(pluginFormID, nullptr, 16); + + if (!pluginName.empty()) + { + auto dataHandler = *g_dataHandler; + auto modInfo = dataHandler->LookupLoadedModByName(pluginName.c_str()); + + if (!modInfo) + { + logger.Error("Plugin with name %s does not exist\n", pluginName.c_str()); + return -1; + } + + bool isLightPlugin = modInfo->recordFlags & modInfo->kRecordFlags_ESL; + size_t maxPartialIdLength = isLightPlugin ? 3 : 6; + + if (pluginFormID.length() > maxPartialIdLength) + { + logger.Error("Invalid FormID %s, too many characters when%s plugin name specified\n", pluginFormID.c_str(), isLightPlugin ? " light" : ""); + return -1; + } + + UInt32 loadIndex = isLightPlugin ? dataHandler->GetLoadedLightModIndex(pluginName.c_str()) : dataHandler->GetLoadedModIndex(pluginName.c_str()); + formID |= loadIndex << 24; + } + else + { + if (pluginFormID.length() > 8) + { + logger.Error("Invalid FormID %s, too many characters\n", pluginFormID.c_str()); + return -1; + } + } + + return formID; } + +bool LoadConfig() +{ + logger.Info("loadConfig\n"); + + config_t configOverrides; + std::map configArmorBoneOverrides; + std::set bonesSet; + + bool reloadActors = false; + auto playerOnlyOld = playerOnly; + auto femaleOnlyOld = femaleOnly; + auto maleOnlyOld = maleOnly; + auto npcOnlyOld = npcOnly; + auto useWhitelistOld = useWhitelist; + + boneNames.clear(); + config.clear(); + configArmorOverrideMap.clear(); + usedSlots.clear(); + cachedConfigs.clear(); + + // Note: Using INIReader results in a slight double read + INIReader configReader("Data\\F4SE\\Plugins\\ocbp.ini"); + if (configReader.ParseError() < 0) + { + logger.Error("Can't load 'ocbp.ini'\n"); + } + logger.Error("Reading CBP Config\n"); + + // Read general settings + playerOnly = configReader.GetBoolean("General", "playerOnly", false); + npcOnly = configReader.GetBoolean("General", "npcOnly", false); + useWhitelist = configReader.GetBoolean("General", "useWhitelist", false); + + if (useWhitelist) + { + maleOnly = false; + femaleOnly = false; + } + else + { + femaleOnly = configReader.GetBoolean("General", "femaleOnly", false); + maleOnly = configReader.GetBoolean("General", "maleOnly", false); + } + + reloadActors = (playerOnly ^ playerOnlyOld) || + (femaleOnly ^ femaleOnlyOld) || + (maleOnly ^ maleOnlyOld) || + (npcOnly ^ npcOnlyOld) || + (useWhitelist ^ useWhitelistOld); + + configReloadCount = configReader.GetInteger("Tuning", "rate", 0); + + // Read sections + auto & sections = configReader.Sections(); + auto prioritySection = sections.find("Priority"); + bool detectArmorCompat = configReader.GetBoolean("General", "detectArmor", false) && (prioritySection == sections.end() || configReader.Section(*prioritySection).empty()); + + // Backwards compatibility for detectArmor style method + if (detectArmorCompat) + { + priorityNameMappings["A"] = 0; + configArmorOverrideMap[0].slots.insert(11); + usedSlots.insert(11); + configArmorOverrideMap[0].isFilterInverted = false; + + //Read armorIgnore + auto armorIgnoreStr = configReader.Get("General", "armorIgnore", ""); + { + size_t commaPos = 0; + do + { + commaPos = armorIgnoreStr.find_first_of(","); + auto token = armorIgnoreStr.substr(0, commaPos); + + try + { + UInt32 formID = std::stoul(token); + configArmorOverrideMap[0].armors.insert(formID); + } + catch (const std::exception&) {} + + armorIgnoreStr = armorIgnoreStr.substr(commaPos + 1); + } while (commaPos != -1); + } + } + + for (auto sectionsIter = sections.begin(); sectionsIter != sections.end(); ++sectionsIter) + { + + // Split for override section check + auto overrideStr = std::string("Override:"); + auto splitOverrideStr = std::mismatch(overrideStr.begin(), overrideStr.end(), sectionsIter->begin()); + + auto subOverrideStr = std::string("Override."); + auto splitSubOverrideStr = std::mismatch(subOverrideStr.begin(), subOverrideStr.end(), sectionsIter->begin()); + + auto subAttachStr = std::string("Attach."); + auto splitSubAttachStr = std::mismatch(subAttachStr.begin(), subAttachStr.end(), sectionsIter->begin()); + + auto armorStr = std::string("Armor."); + auto splitArmorStr = std::mismatch(armorStr.begin(), armorStr.end(), sectionsIter->begin()); + + auto actorStr = std::string("Actor."); + auto splitActorStr = std::mismatch(actorStr.begin(), actorStr.end(), sectionsIter->begin()); + + if (*sectionsIter == std::string("Attach")) + { + // Get section contents + auto & sectionMap = configReader.Section(*sectionsIter); + for (auto& valuesIter : sectionMap) + { + auto& boneName = valuesIter.first; + auto& attachName = valuesIter.second; + boneNames.push_back(boneName); + // Find specified bone section and insert map values into config + if (sections.find(attachName) != sections.end()) + { + auto & attachMapSection = configReader.Section(attachName); + for (auto& attachIter : attachMapSection) + { + auto& keyName = attachIter.first; + config[boneName][keyName] = configReader.GetFloat(attachName, keyName, 0.0); + } + } + } + } + else if (splitSubAttachStr.first == subAttachStr.end()) + { + // Get "XYZ" from Attach.XYZ + auto attachSubname = std::string(splitSubAttachStr.second, sectionsIter->end()); + + UInt32 attachPriority; + auto mapEntry = priorityNameMappings.find(attachSubname); + if (mapEntry != priorityNameMappings.end()) + { + attachPriority = mapEntry->second; + } + else + { + std::string priorityMapping = configReader.Get("Priority", attachSubname, ""); + try + { + attachPriority = std::stoul(priorityMapping); + } + catch (const std::exception&) + { + continue; + } + + priorityNameMappings[attachSubname] = attachPriority; + } + + // Finally read the "Attach." section + auto & sectionMap = configReader.Section(*sectionsIter); + for (auto& valuesIter : sectionMap) + { + auto& boneName = valuesIter.first; + auto& attachName = valuesIter.second; + boneNames.push_back(boneName); + // Find specified bone section and insert map values into config_t in configArmorOverrideMap at attachPriority + if (attachName.empty()) + { + // "Touch" the map to add empty entry for bone in config_t to signal deletion later when building config from overrides + // This allows for disabling specific disabling attach configs + configArmorOverrideMap[attachPriority].config[boneName]; + configActorOverrideMap[attachPriority].config[boneName]; + } + else if (sections.find(attachName) != sections.end()) + { + auto & attachMapSection = configReader.Section(attachName); + // Find the bone's settings and add them to configArmorOverrideMap + for (auto& attachIter : attachMapSection) + { + auto& keyName = attachIter.first; + configArmorOverrideMap[attachPriority].config[boneName][keyName] = configReader.GetFloat(attachName, keyName, 0.0); + configActorOverrideMap[attachPriority].config[boneName][keyName] = configReader.GetFloat(attachName, keyName, 0.0); + } + } + } + } + else if (splitArmorStr.first == armorStr.end()) + { + auto armorSubname = std::string(splitArmorStr.second, sectionsIter->end()); + + UInt32 armorPriority; + auto mapEntry = priorityNameMappings.find(armorSubname); + if (mapEntry != priorityNameMappings.end()) + { + armorPriority = mapEntry->second; + } + else + { + std::string priorityMapping = configReader.Get("Priority", armorSubname, ""); + try + { + armorPriority = std::stoul(priorityMapping); + } + catch (const std::exception&) + { + continue; + } + + priorityNameMappings[armorSubname] = armorPriority; + } + + std::string slots = configReader.Get(*sectionsIter, "slots", ""); + if (slots.empty()) + { + continue; + } + + size_t commaPos = 0; + do + { + commaPos = slots.find_first_of(","); + auto token = slots.substr(0, commaPos); + slots = slots.substr(commaPos + 1); + + UInt32 slot; + try + { + slot = std::stoul(token); + } + catch (const std::exception&) + { + continue; + } + + configArmorOverrideMap[armorPriority].slots.insert(slot - 30); + usedSlots.insert(slot - 30); + } while (commaPos != std::string::npos); + + configArmorOverrideMap[armorPriority].isFilterInverted = configReader.GetBoolean(*sectionsIter, "invertFilter", false); + + // Get section contents + auto & sectionMap = configReader.Section(*sectionsIter); + for (auto& valuesIter : sectionMap) + { + auto& key = valuesIter.first; + if (key == "invertFilter" || key == "slots") + { + continue; + } + + auto formID = GetFormIDFromString(valuesIter.second); + if (formID == -1) + { + continue; + } + + configArmorOverrideMap[armorPriority].armors.insert(formID); + } + } + else if (splitActorStr.first == actorStr.end()) + { + auto actorSubname = std::string(splitActorStr.second, sectionsIter->end()); + + UInt32 actorPriority; + auto mapEntry = priorityNameMappings.find(actorSubname); + if (mapEntry != priorityNameMappings.end()) + { + actorPriority = mapEntry->second; + } + else + { + std::string priorityMapping = configReader.Get("Priority", actorSubname, ""); + try + { + actorPriority = std::stoul(priorityMapping); + } + catch (const std::exception&) + { + continue; + } + + priorityNameMappings[actorSubname] = actorPriority; + } + + configActorOverrideMap[actorPriority].isFilterInverted = configReader.GetBoolean(*sectionsIter, "invertFilter", false); + + // Get section contents + auto & sectionMap = configReader.Section(*sectionsIter); + for (auto& valuesIter : sectionMap) + { + auto& key = valuesIter.first; + if (key == "invertFilter") + { + continue; + } + + auto refID = GetFormIDFromString(valuesIter.second); + + if (refID == -1) + { + continue; + } + + configActorOverrideMap[actorPriority].actors.insert(refID); + } + } + else if (*sectionsIter == std::string("Whitelist") && useWhitelist) + { + whitelist.clear(); + raceWhitelist.clear(); + + // Get section contents + auto & sectionMap = configReader.Section(*sectionsIter); + for (auto& valuesIter : sectionMap) + { + auto& boneName = valuesIter.first; + std::string whitelistName = valuesIter.second; + + size_t commaPos = 0; + do + { + commaPos = whitelistName.find_first_of(","); + auto token = whitelistName.substr(0, commaPos); + size_t colonPos = token.find_last_of(":"); + auto raceName = token.substr(0, colonPos); + auto genderStr = token.substr(colonPos + 1); + + if (colonPos == -1) + { + whitelist[boneName][token].male = true; + whitelist[boneName][token].female = true; + raceWhitelist.push_back(token); + } + else if (genderStr == "male") + { + whitelist[boneName][raceName].male = true; + raceWhitelist.push_back(raceName); + } + else if (genderStr == "female") + { + whitelist[boneName][raceName].female = true; + raceWhitelist.push_back(raceName); + } + whitelistName = whitelistName.substr(commaPos + 1); + + //logger.Info(" %s, %s, %d, %d\n", token.c_str(), whitelistName.c_str(), commaPos >= 0, colonPos < 0); + } while (commaPos != -1); + } + } + else if (splitOverrideStr.first == overrideStr.end()) + { + // If section name is prefixed with "Override:", grab other half of name for bone + auto boneName = std::string(splitOverrideStr.second, sectionsIter->end()); + + // Get section contents + auto & sectionMap = configReader.Section(*sectionsIter); + for (auto& valuesIt : sectionMap) + { + configOverrides[boneName][valuesIt.first] = configReader.GetFloat(*sectionsIter, valuesIt.first, 0.0); + } + } + else if (splitSubOverrideStr.first == subOverrideStr.end()) + { + // If section name is prefixed with "Override.", grab other half for priority and name of bone + auto overrideSection = std::string(splitSubOverrideStr.second, sectionsIter->end()); + + size_t colonPos = overrideSection.find_first_of(":"); + auto overrideSubname = overrideSection.substr(0, colonPos); + auto boneName = overrideSection.substr(colonPos + 1); + + UInt32 overridePriority; + auto mapEntry = priorityNameMappings.find(overrideSubname); + if (mapEntry != priorityNameMappings.end()) + { + overridePriority = mapEntry->second; + } + else + { + std::string priorityMapping = configReader.Get("Priority", overrideSubname, ""); + try + { + overridePriority = std::stoul(priorityMapping); + } + catch (const std::exception&) + { + continue; + } + + priorityNameMappings[overrideSubname] = overridePriority; + } + + // Get section contents + auto & sectionMap = configReader.Section(*sectionsIter); + for (auto& valuesIt : sectionMap) + { + configArmorBoneOverrides[overridePriority][boneName][valuesIt.first] = configReader.GetFloat(*sectionsIter, valuesIt.first, 0.0); + } + } + } + + // replace configs with override settings (if any) + for (auto& boneIter : configOverrides) + { + if (config.count(boneIter.first) > 0) + { + for (auto & settingIter : boneIter.second) + { + config[boneIter.first][settingIter.first] = settingIter.second; + } + } + } + + // replace armor configs with override settings (if any) + for (auto & conf : configArmorBoneOverrides) + { + for (auto& boneIter : conf.second) + { + if (configArmorOverrideMap[conf.first].config.count(boneIter.first) > 0) + { + for (auto & settingIter : boneIter.second) + { + configArmorOverrideMap[conf.first].config[boneIter.first][settingIter.first] = settingIter.second; + } + } + + if (configActorOverrideMap[conf.first].config.count(boneIter.first) > 0) + { + for (auto & settingIter : boneIter.second) + { + configActorOverrideMap[conf.first].config[boneIter.first][settingIter.first] = settingIter.second; + } + } + } + } + + // Remove duplicate entries + bonesSet = std::set(boneNames.begin(), boneNames.end()); + boneNames.assign(bonesSet.begin(), bonesSet.end()); + + // "Delete" bones specified in [Attach] but not [Attach.A] from the latter for compatibility with presets + if (detectArmorCompat) + { + for (auto & boneName : boneNames) + { + if (configArmorOverrideMap[0].config.find(boneName) == configArmorOverrideMap[0].config.end()) + { + configArmorOverrideMap[0].config[boneName]; + } + + if (configActorOverrideMap[0].config.find(boneName) == configActorOverrideMap[0].config.end()) + { + configActorOverrideMap[0].config[boneName]; + } + } + } + + for (auto & map : priorityNameMappings) + { + priorities.insert(map.second); + } + +#if DEBUG + DumpConfigToLog(); +#endif + + DumpUsedSlotsToLog(); + DumpConfigToLog(); + + logger.Error("Finished CBP Config\n"); + return reloadActors; +} + +void DumpConfigToLog() +{ + // Log contents of config + //logger.Info("***** Config Dump *****\n"); + //for (auto & section : config) + //{ + // logger.Info("[%s]\n", section.first.c_str()); + // for (auto & setting : section.second) + // { + // logger.Info("%s=%f\n", setting.first.c_str(), setting.second); + // } + //} + + logger.Info("***** ConfigArmorOverride Dump *****\n"); + for (auto & conf : configArmorOverrideMap) + { + logger.Info("** Slot-Armor Map priority %d **\n", conf.first); + logger.Info("[Slots]\n"); + for (auto slot : conf.second.slots) + { + logger.Info("%ul\n", slot); + } + logger.Info("[Armors]\n"); + for (auto formID : conf.second.armors) + { + logger.Info("%ul\n", formID); + } + logger.Info("** Config priority %d **\n", conf.first); + for (auto & section : conf.second.config) + { + logger.Info("[%s]\n", section.first.c_str()); + for (auto & setting : section.second) + { + logger.Info("%s=%f\n", setting.first.c_str(), setting.second); + } + } + } + + logger.Info("***** ConfigActorOverride Dump *****\n"); + for (auto & conf : configActorOverrideMap) + { + logger.Info("** Slot-Actor Map priority %d **\n", conf.first); + logger.Info("[Actor]\n"); + for (auto & formID : conf.second.actors) + { + logger.Info("%d\n", formID); + } + logger.Info("** Config priority %d **\n", conf.first); + for (auto & section : conf.second.config) + { + logger.Info("[%s]\n", section.first.c_str()); + for (auto & setting : section.second) + { + logger.Info("%s=%f\n", setting.first.c_str(), setting.second); + } + } + } +} + +void DumpWhitelistToLog() +{ + logger.Info("***** Whitelist Dump *****\n"); + for (auto & section : whitelist) + { + logger.Info("[%s]\n", section.first.c_str()); + for (auto & setting : section.second) + { + logger.Info("%s= female: %d, male: %d\n", setting.first.c_str(), setting.second.female, setting.second.male); + } + } +} + +void DumpUsedSlotsToLog() +{ + logger.Info("***** UsedSlots Dump *****\n"); + for (auto& v : usedSlots) + { + logger.Info("used slot : %d\n", v); + } +} \ No newline at end of file diff --git a/CBPSSE/config.h b/CBPSSE/config.h index 71b7cf8..d18b9cc 100644 --- a/CBPSSE/config.h +++ b/CBPSSE/config.h @@ -1,9 +1,63 @@ #pragma once -#include +#include +#include +#include +#include + +#include +#include +#include + +#include "f4se/GameReferences.h" +#include "unordered_dense.h" + +#pragma warning(disable : 4996) + +class Configuration +{ +}; + +struct whitelistSex +{ + bool male; + bool female; +}; + +typedef concurrency::concurrent_unordered_map configEntry_t; // Map settings for a particular bone +typedef concurrency::concurrent_unordered_map config_t; // Settings for a set of bones +typedef concurrency::concurrent_unordered_map> whitelist_t; + + +struct armorOverrideData +{ + bool isFilterInverted; + concurrency::concurrent_unordered_set slots; + concurrency::concurrent_unordered_set armors; + config_t config; +}; + +struct actorOverrideData +{ + bool isFilterInverted; + concurrency::concurrent_unordered_set actors; + config_t config; +}; + +extern bool playerOnly; +extern bool femaleOnly; +extern bool maleOnly; +extern bool npcOnly; +extern bool useWhitelist; -typedef std::unordered_map configEntry_t; -typedef std::unordered_map config_t; extern int configReloadCount; extern config_t config; +extern concurrency::concurrent_unordered_map configArmorOverrideMap; +extern concurrency::concurrent_unordered_map configActorOverrideMap; +extern whitelist_t whitelist; +extern std::vector raceWhitelist; +extern concurrency::concurrent_unordered_set usedSlots; +extern concurrency::concurrent_unordered_map cachedConfigs; +extern std::set priorities; -void loadConfig(); \ No newline at end of file +bool LoadConfig(); +void DumpWhitelistToLog(); \ No newline at end of file diff --git a/CBPSSE/exports.def b/CBPSSE/exports.def index 8ac5543..33dd38d 100644 --- a/CBPSSE/exports.def +++ b/CBPSSE/exports.def @@ -1,4 +1,3 @@ LIBRARY "cbp" EXPORTS -SKSEPlugin_Query -SKSEPlugin_Load +F4SEPlugin_Load diff --git a/CBPSSE/hookD3D.cpp b/CBPSSE/hookD3D.cpp index 031a928..6bcbd97 100644 --- a/CBPSSE/hookD3D.cpp +++ b/CBPSSE/hookD3D.cpp @@ -5,7 +5,8 @@ #include "log.h" #include "../detourxs-master/detourxs.h" -#include "skse64_common/Utilities.h" +#include "f4se_common/f4se_version.h" +#include "f4se_common/Utilities.h" //#define SAFE_RELEASE(x) if((x)) {x->Release(); x = nullptr;} // @@ -140,6 +141,9 @@ // + +/*//OLD METHOD + typedef UINT64 (__cdecl *renderHook) (void* This, UINT64 arg); renderHook orender = nullptr; @@ -147,34 +151,56 @@ renderHook orender = nullptr; void scaleTest(); UINT64 __cdecl Render(void *This, UINT64 arg) { - //logger.error("This is called\n"); - //logger.error("orender = %016llx\n", orender); - scaleTest(); - return orender(This, arg); - //return 0; +//logger.error("This is called\n"); +//logger.error("orender = %016llx\n", orender); +scaleTest(); +return orender(This, arg); +//return 0; } //RelocPtr render(0xD69720); //RelocPtr main(0x640BC0); // Address copied out of SKSE -//RelocPtr ProcessTasks_HookTarget_Enter(0x005B2EF0); -//RelocPtr ProcessTasks_HookTarget_Enter(0x005B34A0); -//RelocPtr ProcessTasks_HookTarget_Enter(0x005B31E0); -//RelocPtr ProcessTasks_HookTarget_Enter(0x005B31E0); -RelocPtr ProcessTasks_HookTarget_Enter(0x005B2FF0); +//RelocPtr ProcessTasks_HookTarget_Enter(0x005B2EF0); //For SSE 1.5.23 +//RelocPtr ProcessTasks_HookTarget_Enter(0x005B34A0); //For SSE 1.5.39 +//RelocPtr ProcessTasks_HookTarget_Enter(0x005B31E0); //For SSE 1.5.53 +RelocPtr ProcessTasks_HookTarget_Enter(0x005BAB10); //For VR 1.4.15 + +DetourXS renderDetour; +void DoHook() { +logger.info("Attempting Game Hook\n"); +// Useful for finding the addresses +//CreateThread(NULL, 0, HookCreateFn, NULL, 0, NULL); + +//renderDetour.Create(render.GetPtr(), Render, &(LPVOID)orender); +renderDetour.Create(ProcessTasks_HookTarget_Enter.GetPtr(), Render, &(LPVOID)orender); +//orender = (renderHook)renderDetour.GetTrampoline(); +} +*/ +typedef void(*_ProcessEventQueue_Internal) (void * thisPtr); +_ProcessEventQueue_Internal orig_ProcessEventQueue_Internal = nullptr; + +void UpdateActors(); + +void hk_ProcessEventQueue_Internal(void *thisPtr) +{ + orig_ProcessEventQueue_Internal(thisPtr); + UpdateActors(); +} + +// See f4se/Hooks_Threads.cpp +// TODO someday address library. someday. +RelocPtr ProcessEventQueue_Internal(0x01A09CB0); DetourXS renderDetour; + void DoHook() { - logger.info("Attempting Game Hook\n"); - // Useful for finding the addresses - //CreateThread(NULL, 0, HookCreateFn, NULL, 0, NULL); + logger.Info("Attempting Game Hook\n"); - //renderDetour.Create(render.GetPtr(), Render, &(LPVOID)orender); - renderDetour.Create(ProcessTasks_HookTarget_Enter.GetPtr(), Render, &(LPVOID)orender); - //orender = (renderHook)renderDetour.GetTrampoline(); + renderDetour.Create((LPVOID)ProcessEventQueue_Internal.GetPtr(), hk_ProcessEventQueue_Internal, (LPVOID*)(&orig_ProcessEventQueue_Internal)); } \ No newline at end of file diff --git a/CBPSSE/log.cpp b/CBPSSE/log.cpp index 88da45b..56a2331 100644 --- a/CBPSSE/log.cpp +++ b/CBPSSE/log.cpp @@ -3,31 +3,46 @@ #pragma warning(disable : 4996) -Logger::Logger(const char *fname) { - handle = fopen(fname, "a"); - if (handle) { - fprintf(handle, "CBP Log initialized\n"); - } +// TODO make better macro +//#define LOG_ON + +CbpLogger::CbpLogger(const char* fname) +{ +#ifdef LOG_ON + handle = fopen(fname, "a"); + if (handle) + { + fprintf(handle, "CBP Log initialized\n"); + } +#endif } -void Logger::info(const char *fmt...) { - if (handle) { - va_list argptr; - va_start(argptr, fmt); - vfprintf(handle, fmt, argptr); - va_end(argptr); - fflush(handle); - } +void CbpLogger::Info(const char* fmt...) +{ +#ifdef LOG_ON + if (handle) + { + va_list argptr; + va_start(argptr, fmt); + vfprintf(handle, fmt, argptr); + va_end(argptr); + fflush(handle); + } +#endif } -void Logger::error(const char *fmt...) { - if (handle) { - va_list argptr; - va_start(argptr, fmt); - vfprintf(handle, fmt, argptr); - va_end(argptr); - fflush(handle); - } +void CbpLogger::Error(const char* fmt...) +{ +#ifdef LOG_ON + if (handle) + { + va_list argptr; + va_start(argptr, fmt); + vfprintf(handle, fmt, argptr); + va_end(argptr); + fflush(handle); + } +#endif } -Logger logger("Data\\SKSE\\Plugins\\cbp.log"); +CbpLogger logger("Data\\F4SE\\Plugins\\cbp.log"); diff --git a/CBPSSE/log.h b/CBPSSE/log.h index e09f433..cd53611 100644 --- a/CBPSSE/log.h +++ b/CBPSSE/log.h @@ -1,13 +1,17 @@ #pragma once #include +#include -class Logger { +class CbpLogger +{ public: - Logger(const char* fname); - void info(const char* args...); - void error(const char* args...); + CbpLogger(const char* fname); + void Info(const char* args...); + void Error(const char* args...); - FILE *handle; + FILE* handle; + + std::shared_mutex log_lock; }; -extern Logger logger; \ No newline at end of file +extern CbpLogger logger; \ No newline at end of file diff --git a/CBPSSE/main.cpp b/CBPSSE/main.cpp index 38c3078..632ecef 100644 --- a/CBPSSE/main.cpp +++ b/CBPSSE/main.cpp @@ -1,145 +1,182 @@ #include "common/ITypes.h" #include -#include "skse64/PluginAPI.h" -#include "skse64_common/skse_version.h" -#include "skse64_common/SafeWrite.h" -#include "skse64/GameAPI.h" -#include "skse64/GameEvents.h" +#include "f4se/PluginAPI.h" +#include "f4se_common/f4se_version.h" +#include "f4se_common/SafeWrite.h" +#include "f4se/GameAPI.h" +#include "f4se/GameEvents.h" #include "log.h" #include "config.h" +#include "PapyrusOCBP.h" +bool RegisterFuncs(VirtualMachine* vm); +PluginHandle g_pluginHandle = kPluginHandle_Invalid; +F4SEMessagingInterface* g_messagingInterface = NULL; -PluginHandle g_pluginHandle = kPluginHandle_Invalid; -//SKSEMessagingInterface * g_messagingInterface = NULL; - -//SKSEScaleformInterface * g_scaleform = NULL; -//SKSESerializationInterface * g_serialization = NULL; -SKSETaskInterface * g_task = nullptr; -//IDebugLog gLog("Data\\SKSE\\Plugins\\hook.log"); +//F4SEScaleformInterface * g_scaleform = NULL; +//F4SESerializationInterface * g_serialization = NULL; +F4SETaskInterface* g_task = nullptr; +F4SEPapyrusInterface* g_papyrus = nullptr; +//IDebugLog gLog("Data\\F4SE\\Plugins\\hook.log"); void DoHook(); -void MessageHandler(SKSEMessagingInterface::Message * msg) +void MessageHandler(F4SEMessagingInterface::Message* msg) { - switch (msg->type) - { - case SKSEMessagingInterface::kMessage_DataLoaded: - { - logger.info("kMessage_DataLoaded\n"); - } - break; - case SKSEMessagingInterface::kMessage_NewGame: - { - logger.info("kMessage_NewGame\n"); - } - break; - case SKSEMessagingInterface::kMessage_PreLoadGame: - { - logger.info("kMessage_PreLoadGame\n"); - } - break; - case SKSEMessagingInterface::kMessage_PostLoad: - { - logger.info("kMessage_PostLoad\n"); - } - break; - case SKSEMessagingInterface::kMessage_PostPostLoad: - { - logger.info("kMessage_PostPostLoad\n"); - } - break; - case SKSEMessagingInterface::kMessage_PostLoadGame: - { - logger.info("kMessage_PostLoadGame\n"); - } - break; - case SKSEMessagingInterface::kMessage_SaveGame: - { - logger.info("kMessage_SaveGame\n"); - } - break; - case SKSEMessagingInterface::kMessage_DeleteGame: - { - logger.info("kMessage_DeleteGame\n"); - } - break; - case SKSEMessagingInterface::kMessage_InputLoaded: - { - logger.info("kMessage_InputLoaded\n"); - } - break; - - } + switch (msg->type) + { + case F4SEMessagingInterface::kMessage_GameDataReady: + { + logger.Info("kMessage_GameDataReady\n"); + // Load initial config + logger.Error("Loading Config"); + LoadConfig(); + logger.Error("Hooking Game"); + DoHook(); + logger.Error("CBP Load Complete\n"); + } + break; + case F4SEMessagingInterface::kMessage_GameLoaded: + { + logger.Info("kMessage_GameLoaded\n"); + } + break; + case F4SEMessagingInterface::kMessage_NewGame: + { + logger.Info("kMessage_NewGame\n"); + } + break; + case F4SEMessagingInterface::kMessage_PreLoadGame: + { + logger.Info("kMessage_PreLoadGame\n"); + } + break; + case F4SEMessagingInterface::kMessage_PostLoad: + { + logger.Info("kMessage_PostLoad\n"); + } + break; + case F4SEMessagingInterface::kMessage_PostPostLoad: + { + logger.Info("kMessage_PostPostLoad\n"); + } + break; + case F4SEMessagingInterface::kMessage_PostLoadGame: + { + logger.Info("kMessage_PostLoadGame\n"); + } + break; + case F4SEMessagingInterface::kMessage_PreSaveGame: + { + logger.Info("kMessage_PreSaveGame\n"); + } + break; + case F4SEMessagingInterface::kMessage_PostSaveGame: + { + logger.Info("kMessage_PostSaveGame\n"); + } + break; + case F4SEMessagingInterface::kMessage_DeleteGame: + { + logger.Info("kMessage_DeleteGame\n"); + } + break; + case F4SEMessagingInterface::kMessage_InputLoaded: + { + logger.Info("kMessage_InputLoaded\n"); + } + break; + + } } extern "C" { + __declspec(dllexport) F4SEPluginVersionData F4SEPlugin_Version = + { + F4SEPluginVersionData::kVersion, - bool SKSEPlugin_Query(const SKSEInterface * skse, PluginInfo * info) - { - logger.info("CBP Physics SKSE Plugin\n"); - logger.error("Query called\n"); - - - // populate info structure - info->infoVersion = PluginInfo::kInfoVersion; - info->name = "CBP plugin"; - info->version = 24; - - // store plugin handle so we can identify ourselves later - g_pluginHandle = skse->GetPluginHandle(); - - if (skse->isEditor) - { - logger.error("loaded in editor, marking as incompatible\n"); - return false; - } - else if (skse->runtimeVersion != RUNTIME_VERSION) - { - logger.error("unsupported runtime version %08X", skse->runtimeVersion); - return false; - } - // supported runtime version - - logger.error("Query complete\n"); - return true; - } - - bool SKSEPlugin_Load(const SKSEInterface * skse) - { - logger.error("CBP Loading\n"); - - g_task = (SKSETaskInterface *)skse->QueryInterface(kInterface_Task); - if (!g_task) - { - logger.error("Couldn't get Task interface\n"); - return false; - } - - // Load initial config before the hook. - logger.error("Loading Config\n"); - loadConfig(); - //g_messagingInterface->RegisterListener(0, "SKSE", MessageHandler); - logger.error("Hooking Game\n"); - DoHook(); - logger.error("CBP Load Complete\n"); - return true; - } + 25, + "OCBP plugin", + "takosako", + 0, // not version independent + 0, // not version independent (extended field) + { RUNTIME_VERSION_1_10_984, 0 }, // compatible with 1.10.984 + 0, // works with any version of the script extender. you probably do not need to put anything here + }; +}; +extern "C" +{ + bool F4SEPlugin_Load(const F4SEInterface* f4se) + { + logger.Info("OCBP Physics F4SE Plugin\n"); + logger.Error("CBP Loading\n"); + + // store plugin handle so we can identify ourselves later + g_pluginHandle = f4se->GetPluginHandle(); + + if (f4se->isEditor) + { + logger.Error("loaded in editor, marking as incompatible\n"); + return false; + } + else if (f4se->runtimeVersion != RUNTIME_VERSION_1_10_984) + { + logger.Error("unsupported runtime version %08X", f4se->runtimeVersion); + return false; + } + // supported runtime version + + g_papyrus = (F4SEPapyrusInterface*)f4se->QueryInterface(kInterface_Papyrus); + if (!g_papyrus) + { + _WARNING("couldn't get papyrus interface"); + } + + g_task = (F4SETaskInterface*)f4se->QueryInterface(kInterface_Task); + if (!g_task) + { + logger.Error("Couldn't get Task interface\n"); + return false; + } + + if (g_papyrus) + g_papyrus->Register(RegisterFuncs); + + g_messagingInterface = (F4SEMessagingInterface*)f4se->QueryInterface(kInterface_Messaging); + if (!g_messagingInterface) + { + logger.Error("Couldn't get messaging interface"); + return false; + } + + g_messagingInterface->RegisterListener(g_pluginHandle, "F4SE", MessageHandler); + + logger.Error("CBP Load complete\n"); + return true; + } }; +bool RegisterFuncs(VirtualMachine* vm) +{ + papyrusOCBP::RegisterFuncs(vm); + return true; +} + BOOL WINAPI DllMain( - _In_ HINSTANCE hinstDLL, - _In_ DWORD fdwReason, - _In_ LPVOID lpvReserved -) { - return true; + _In_ HINSTANCE hinstDLL, + _In_ DWORD fdwReason, + _In_ LPVOID lpvReserved +) +{ + return true; } \ No newline at end of file diff --git a/CBPSSE/scan.cpp b/CBPSSE/scan.cpp index 216f02d..5b46a0a 100644 --- a/CBPSSE/scan.cpp +++ b/CBPSSE/scan.cpp @@ -1,5 +1,9 @@ #pragma once +#pragma warning(disable : 5040) +#define DEBUG + +//#define SIMPLE_BENCHMARK #include #include @@ -20,210 +24,343 @@ #include #include -#include +#include +#include +#include + +#include "ActorEntry.h" +#include "ActorUtils.h" #include "log.h" #include "Thing.h" #include "config.h" +#include "PapyrusOCBP.h" #include "SimObj.h" -#include "skse64/GameRTTI.h" -#include "skse64/GameForms.h" -#include "skse64/GameReferences.h" -#include "skse64/NiTypes.h" -#include "skse64/NiNodes.h" -#include "skse64/NiGeometry.h" -#include "skse64/GameThreads.h" -#include "skse64/PluginAPI.h" -#include "skse64/GameStreams.h" - +#include "f4se/GameRTTI.h" +#include "f4se/GameForms.h" +#include "f4se/GameReferences.h" +#include "f4se/NiTypes.h" +#include "f4se/NiNodes.h" +#include "f4se/BSGeometry.h" +#include "f4se/GameThreads.h" +#include "f4se/PluginAPI.h" +#include "f4se/GameStreams.h" +#include "f4se/GameExtraData.h" #pragma warning(disable : 4996) +using actorUtils::IsActorMale; +using actorUtils::IsActorTrackable; +using actorUtils::IsActorValid; +using actorUtils::BuildActorKey; +using actorUtils::BuildConfigForActor; +using actorUtils::GetActorRaceEID; -extern SKSETaskInterface *g_task; +std::atomic currCell = nullptr; +extern F4SETaskInterface* g_task; -//void UpdateWorldDataToChild(NiAVObject) -void dumpTransform(NiTransform t) { - Console_Print("%8.2f %8.2f %8.2f", t.rot.data[0][0], t.rot.data[0][1], t.rot.data[0][2]); - Console_Print("%8.2f %8.2f %8.2f", t.rot.data[1][0], t.rot.data[1][1], t.rot.data[1][2]); - Console_Print("%8.2f %8.2f %8.2f", t.rot.data[2][0], t.rot.data[2][1], t.rot.data[2][2]); +#ifdef SIMPLE_BENCHMARK +bool firsttimeloginit = true; +LARGE_INTEGER startingTime, endingTime, elapsedMicroseconds; +LARGE_INTEGER frequency; +LARGE_INTEGER totaltime; +int debugtimelog_framecount = 1; +#endif - Console_Print("%8.2f %8.2f %8.2f", t.pos.x, t.pos.y, t.pos.z); - Console_Print("%8.2f", t.scale); +//void UpdateWorldDataToChild(NiAVObject) +void DumpTransform(NiTransform t) +{ + Console_Print("%8.2f %8.2f %8.2f", t.rot.data[0][0], t.rot.data[0][1], t.rot.data[0][2]); + Console_Print("%8.2f %8.2f %8.2f", t.rot.data[1][0], t.rot.data[1][1], t.rot.data[1][2]); + Console_Print("%8.2f %8.2f %8.2f", t.rot.data[2][0], t.rot.data[2][1], t.rot.data[2][2]); + + Console_Print("%8.2f %8.2f %8.2f", t.pos.x, t.pos.y, t.pos.z); + Console_Print("%8.2f", t.scale); } -bool visitObjects(NiAVObject *parent, std::function functor, int depth = 0) { - if (!parent) return false; - NiNode * node = parent->GetAsNiNode(); - if (node) { - if (functor(parent, depth)) - return true; - - for (UInt32 i = 0; i < node->m_children.m_emptyRunStart; i++) { - NiAVObject * object = node->m_children.m_data[i]; - if (object) { - if (visitObjects(object, functor, depth+1)) - return true; - } - } - } - else if (functor(parent, depth)) - return true; - - return false; +bool visitObjects(NiAVObject* parent, std::function functor, int depth = 0) +{ + if (!parent) return false; + NiNode* node = parent->GetAsNiNode(); + if (node) + { + if (functor(parent, depth)) + return true; + + for (UInt32 i = 0; i < node->m_children.m_emptyRunStart; i++) + { + NiAVObject* object = node->m_children.m_data[i]; + if (object) + { + if (visitObjects(object, functor, depth + 1)) + return true; + } + } + } + else if (functor(parent, depth)) + return true; + + return false; } -std::string spaces(int n) { - auto s = std::string(n , ' '); - return s; +std::string spaces(int n) +{ + auto s = std::string(n, ' '); + return s; } -bool printStuff(NiAVObject *avObj, int depth) { - std::string sss = spaces(depth); - const char *ss = sss.c_str(); - logger.info("%savObj Name = %s, RTTI = %s\n", ss, avObj->m_name, avObj->GetRTTI()->name); - - NiNode *node = avObj->GetAsNiNode(); - if (node) { - logger.info("%snode %s, RTTI %s\n", ss, node->m_name, node->GetRTTI()->name); - } - return false; +bool printStuff(NiAVObject* avObj, int depth) +{ + std::string sss = spaces(depth); + const char* ss = sss.c_str(); + //logger.info("%savObj Name = %s, RTTI = %s\n", ss, avObj->m_name, avObj->GetRTTI()->name); + + //NiNode *node = avObj->GetAsNiNode(); + //if (node) { + // logger.info("%snode %s, RTTI %s\n", ss, node->m_name, node->GetRTTI()->name); + //} + //return false; } - -void dumpVec(NiPoint3 p) { - logger.info("%8.2f %8.2f %8.2f\n", p.x, p.y, p.z); -} - - - - template -inline void safe_delete(T*& in) { - if (in) { - delete in; - in = NULL; - } +inline void safe_delete(T*& in) +{ + if (in) + { + delete in; + in = NULL; + } } -std::unordered_map actors; - -TESObjectCELL *curCell = nullptr; - - -struct ActorEntry { - UInt32 id; - Actor *actor; -}; - -void updateActors() { - //LARGE_INTEGER startingTime, endingTime, elapsedMicroseconds; - //LARGE_INTEGER frequency; - - //QueryPerformanceFrequency(&frequency); - //QueryPerformanceCounter(&startingTime); - - // We scan the cell and build the list every time - only look up things by ID once - // we retain all state by actor ID, in a map - it's cleared on cell change - std::vector actorEntries; - - //logger.error("scan Cell\n"); - auto player = DYNAMIC_CAST(LookupFormByID(0x14), TESForm, Actor); - if (!player || !player->loadedState) goto FAILED; - - auto cell = player->parentCell; - if (!cell) goto FAILED; - - if (cell != curCell) { - logger.error("cell change %d\n", cell); - curCell = cell; - actors.clear(); - } else { - for (int i = 0; i < cell->refData.maxSize; i++) { - auto ref = cell->refData.refArray[i]; - if (ref.unk08 != NULL && ref.ref) { - auto actor = DYNAMIC_CAST(ref.ref, TESObjectREFR, Actor); - if (actor && actor->loadedState) { - auto soIt = actors.find(actor->formID); - if (soIt == actors.end()) { - //logger.info("Tracking Actor with form ID %08x in cell %ld\n", actor->formID, actor->parentCell); - auto obj = SimObj(actor, config); - if (obj.actorValid(actor)) { - actors.emplace(actor->formID, obj); - actorEntries.emplace_back(ActorEntry{ actor->formID, actor }); - } - } else if (soIt->second.actorValid(actor)) { - actorEntries.emplace_back(ActorEntry{ actor->formID, actor }); - } - } - } - } - } - - //static bool done = false; - //if (!done && player->loadedState->node) { - // visitObjects(player->loadedState->node, printStuff); - // BSFixedString cs("UUNP"); - // auto bodyAV = player->loadedState->node->GetObjectByName(&cs.data); - // BSTriShape *body = bodyAV->GetAsBSTriShape(); - // logger.info("GetAsBSTriShape returned %lld\n", body); - // auto geometryData = body->geometryData; - // //logger.info("Num verts = %d\n", geometryData->m_usVertices); - - - // done = true; - //} - - static int count = 0; - if (configReloadCount && count++ > configReloadCount) { - count = 0; - loadConfig(); - for (auto &a : actors) { - a.second.updateConfig(config); - } - } - //logger.error("Updating %d entites\n", actorEntries.size()); - for (auto &a : actorEntries) { - auto objIt = actors.find(a.id); - if (objIt == actors.end()) { - logger.error("missing Sim Object\n"); - } else { - auto &obj = objIt->second; - if (obj.isBound()) { - obj.update(a.actor); - } else { - obj.bind(a.actor, femaleBones, config); - } - } - } +concurrency::concurrent_unordered_map actors; // Map of Actor (stored as form ID) to its Simulation Object +TESObjectCELL* curCell = nullptr; + + +void UpdateActors() +{ +#ifdef SIMPLE_BENCHMARK + if (firsttimeloginit) + { + firsttimeloginit = false; + totaltime.QuadPart = 0; + + } + + QueryPerformanceFrequency(&frequency); + QueryPerformanceCounter(&startingTime); +#endif + + // We scan the cell and build the list every time - only look up things by ID once + // we retain all state by actor ID, in a map - it's cleared on cell change + concurrency::concurrent_vector actorEntries; + + //logger.error("scan Cell\n"); + auto player = DYNAMIC_CAST(LookupFormByID(0x14), TESForm, Actor); + if (!player || !player->unkF0) goto FAILED; + + auto cell = player->parentCell; + if (!cell) goto FAILED; + + if (cell != curCell) + { + logger.Error("cell change %d\n", cell->formID); + curCell = cell; + actors.clear(); + actorEntries.clear(); + } + else + { + // Attempt to get cell's objects + // TODO check concurrency + for (UInt32 i = 0; i < cell->objectList.count; i++) + { + auto ref = cell->objectList[i]; + if (ref) + { + // Attempt to get actors + auto actor = DYNAMIC_CAST(ref, TESObjectREFR, Actor); + if (actor && actor->unkF0) + { + // If actor is not being tracked yet + if (actors.count(actor->formID) == 0) + { + // If actor should not be tracked, don't add it. + if (IsActorTrackable(actor)) + { + //logger.Info("Tracking Actor with form ID %08x in cell %ld, race is %s, gender is %d\n", + // actor->formID, actor->parentCell->formID, + // actor->race->editorId.c_str(), + // IsActorMale(actor)); + // Make SimObj and place new element in Things + auto obj = SimObj(actor); + actors.insert(std::make_pair(actor->formID, obj)); + actorEntries.push_back(ActorEntry{ actor->formID, actor }); + } + } + else if (IsActorValid(actor)) + { + // If already tracked then add to entry list for this frame + actorEntries.push_back(ActorEntry{ actor->formID, actor }); + } + } + } + } + } + + + //static bool done = false; + //if (!done && player->loadedState->node) { + // visitObjects(player->loadedState->node, printStuff); + // BSFixedString cs("UUNP"); + // auto bodyAV = player->loadedState->node->GetObjectByName(&cs.data); + // BSTriShape *body = bodyAV->GetAsBSTriShape(); + // logger.info("GetAsBSTriShape returned %lld\n", body); + // auto geometryData = body->geometryData; + // //logger.info("Num verts = %d\n", geometryData->m_usVertices); + + + // done = true; + //} + + // Reload config + static int count = 0; + if (configReloadCount && count++ > configReloadCount) + { + count = 0; + auto reloadActors = LoadConfig(); + for (auto& a : actorEntries) + { + auto actorsIterator = actors.find(a.id); + if (actorsIterator == actors.end()) + { + //logger.error("Sim Object not found in tracked actors\n"); + } + else + { + auto& simObj = actorsIterator->second; + auto key = BuildActorKey(a.actor); + auto& composedConfig = BuildConfigForActor(a.actor, key); + + simObj.SetActorKey(key); + simObj.AddBonesToThings(a.actor, boneNames); + simObj.UpdateConfigs(composedConfig); + } + } + + // Clear actors + if (reloadActors) + { + actors.clear(); + actorEntries.clear(); + } + } + + concurrency::parallel_for_each(actorEntries.begin(), actorEntries.end(), [&](const auto& a) + { + auto actorsIterator = actors.find(a.id); + if (actorsIterator == actors.end()) + { + //logger.error("Sim Object not found in tracked actors\n"); + } + else + { + auto& simObj = actorsIterator->second; + + SimObj::Gender gender = IsActorMale(a.actor) ? SimObj::Gender::Male : SimObj::Gender::Female; + + if (simObj.IsBound()) + { + // If gender and/or race have changed due to game state changes, + // reset simObj data to clear out outdated bone data. + if (gender != simObj.GetGender() || + GetActorRaceEID(a.actor) != simObj.GetRaceEID()) + { + logger.Info("UpdateActors: Reset sim object\n"); + simObj.Reset(); + } + } + else + { + auto key = BuildActorKey(a.actor); + auto& composedConfig = BuildConfigForActor(a.actor, key); + + //logger.Error("UpdateActors: Setting key for actor %x...\n", a.id); + simObj.SetActorKey(key); + simObj.Bind(a.actor, boneNames, composedConfig); + } + } + }); + + concurrency::parallel_for_each(actorEntries.begin(), actorEntries.end(), [&](const auto& a) + { + auto actorsIterator = actors.find(a.id); + if (actorsIterator == actors.end()) + { + //logger.error("Sim Object not found in tracked actors\n"); + } + else + { + auto& simObj = actorsIterator->second; + + if (simObj.IsBound()) + { + UInt64 key = BuildActorKey(a.actor); + UInt64 simObjKey = simObj.GetActorKey(); + + // Detect changes in actor+slots combination + if (key != simObjKey) + { + logger.Error("UpdateActors: Key change detected for actor %08x.\n", a.actor->formID); + simObj.SetActorKey(key); + auto& composedConfig = BuildConfigForActor(a.actor, key); + simObj.UpdateConfigs(composedConfig); + } + simObj.Update(a.actor); + } + } + }); + + + +#ifdef SIMPLE_BENCHMARK + QueryPerformanceCounter(&endingTime); + elapsedMicroseconds.QuadPart = endingTime.QuadPart - startingTime.QuadPart; + elapsedMicroseconds.QuadPart *= 1000000000LL; + elapsedMicroseconds.QuadPart /= frequency.QuadPart; + //long long avg = elapsedMicroseconds.QuadPart / callCount; + totaltime.QuadPart += elapsedMicroseconds.QuadPart; + //LOG_ERR("Collider Check Call Count: %d - Update Time = %lld ns", callCount, elapsedMicroseconds.QuadPart); + if (debugtimelog_framecount % 1000 == 0) + { + logger.Error("Average Update Time in 1000 frame = %lld ns\n", totaltime.QuadPart / debugtimelog_framecount); + totaltime.QuadPart = 0; + debugtimelog_framecount = 0; + //totalcallcount = 0; + } + debugtimelog_framecount++; +#endif FAILED: - return; - //QueryPerformanceCounter(&endingTime); - //elapsedMicroseconds.QuadPart = endingTime.QuadPart - startingTime.QuadPart; - //elapsedMicroseconds.QuadPart *= 1000000000LL; - //elapsedMicroseconds.QuadPart /= frequency.QuadPart; - //logger.info("Update Time = %lld ns\n", elapsedMicroseconds.QuadPart); + return; } -class ScanDelegate : public TaskDelegate { -public: - virtual void Run() { - updateActors(); - } - virtual void Dispose() { - delete this; - } - -}; - - -void scaleTest() { - g_task->AddTask(new ScanDelegate()); - return; -} \ No newline at end of file +//class ScanDelegate : public ITaskDelegate { +//public: +// virtual void Run() { +// UpdateActors(); +// } +// virtual void Dispose() { +// delete this; +// } +//}; +// +// +//void scaleTest() { +// g_task->AddTask(new ScanDelegate()); +// return; +//} \ No newline at end of file diff --git a/CBPSSE/unordered_dense.h b/CBPSSE/unordered_dense.h new file mode 100644 index 0000000..d04dfaf --- /dev/null +++ b/CBPSSE/unordered_dense.h @@ -0,0 +1,1527 @@ +///////////////////////// ankerl::unordered_dense::{map, set} ///////////////////////// + +// A fast & densely stored hashmap and hashset based on robin-hood backward shift deletion. +// Version 3.0.0 +// https://github.com/martinus/unordered_dense +// +// Licensed under the MIT License . +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 Martin Leitner-Ankerl +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef ANKERL_UNORDERED_DENSE_H +#define ANKERL_UNORDERED_DENSE_H + +#pragma warning(disable : 4003) + +// see https://semver.org/spec/v2.0.0.html +#define ANKERL_UNORDERED_DENSE_VERSION_MAJOR 3 // NOLINT(cppcoreguidelines-macro-usage) incompatible API changes +#define ANKERL_UNORDERED_DENSE_VERSION_MINOR 0 // NOLINT(cppcoreguidelines-macro-usage) backwards compatible functionality +#define ANKERL_UNORDERED_DENSE_VERSION_PATCH 0 // NOLINT(cppcoreguidelines-macro-usage) backwards compatible bug fixes + +// API versioning with inline namespace, see https://www.foonathan.net/2018/11/inline-namespaces/ +#define ANKERL_UNORDERED_DENSE_VERSION_CONCAT1(major, minor, patch) v##major##_##minor##_##patch +#define ANKERL_UNORDERED_DENSE_VERSION_CONCAT(major, minor, patch) ANKERL_UNORDERED_DENSE_VERSION_CONCAT1(major, minor, patch) +#define ANKERL_UNORDERED_DENSE_NAMESPACE \ + ANKERL_UNORDERED_DENSE_VERSION_CONCAT( \ + ANKERL_UNORDERED_DENSE_VERSION_MAJOR, ANKERL_UNORDERED_DENSE_VERSION_MINOR, ANKERL_UNORDERED_DENSE_VERSION_PATCH) + +#if defined(_MSVC_LANG) +# define ANKERL_UNORDERED_DENSE_CPP_VERSION _MSVC_LANG +#else +# define ANKERL_UNORDERED_DENSE_CPP_VERSION __cplusplus +#endif + +#if defined(__GNUC__) +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +# define ANKERL_UNORDERED_DENSE_PACK(decl) decl __attribute__((__packed__)) +#elif defined(_MSC_VER) +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +# define ANKERL_UNORDERED_DENSE_PACK(decl) __pragma(pack(push, 1)) decl __pragma(pack(pop)) +#endif + +#if ANKERL_UNORDERED_DENSE_CPP_VERSION < 201703L +# error ankerl::unordered_dense requires C++17 or higher +#else +# include // for array +# include // for uint64_t, uint32_t, uint8_t, UINT64_C +# include // for size_t, memcpy, memset +# include // for equal_to, hash +# include // for initializer_list +# include // for pair, distance +# include // for numeric_limits +# include // for allocator, allocator_traits, shared_ptr +# include // for out_of_range +# include // for basic_string +# include // for basic_string_view, hash +# include // for forward_as_tuple +# include // for enable_if_t, declval, conditional_t, ena... +# include // for forward, exchange, pair, as_const, piece... +# include // for vector + +# define ANKERL_UNORDERED_DENSE_PMR 0 // NOLINT(cppcoreguidelines-macro-usage) +# if defined(__has_include) +# if __has_include() +# undef ANKERL_UNORDERED_DENSE_PMR +# define ANKERL_UNORDERED_DENSE_PMR 1 // NOLINT(cppcoreguidelines-macro-usage) +# include // for polymorphic_allocator +# endif +# endif + +# if defined(_MSC_VER) && defined(_M_X64) +# include +# pragma intrinsic(_umul128) +# endif + +# if defined(__GNUC__) || defined(__INTEL_COMPILER) || defined(__clang__) +# define ANKERL_UNORDERED_DENSE_LIKELY(x) __builtin_expect(x, 1) // NOLINT(cppcoreguidelines-macro-usage) +# define ANKERL_UNORDERED_DENSE_UNLIKELY(x) __builtin_expect(x, 0) // NOLINT(cppcoreguidelines-macro-usage) +# else +# define ANKERL_UNORDERED_DENSE_LIKELY(x) (x) // NOLINT(cppcoreguidelines-macro-usage) +# define ANKERL_UNORDERED_DENSE_UNLIKELY(x) (x) // NOLINT(cppcoreguidelines-macro-usage) +# endif + +namespace ankerl::unordered_dense { +inline namespace ANKERL_UNORDERED_DENSE_NAMESPACE { + +// hash /////////////////////////////////////////////////////////////////////// + +// This is a stripped-down implementation of wyhash: https://github.com/wangyi-fudan/wyhash +// No big-endian support (because different values on different machines don't matter), +// hardcodes seed and the secret, reformattes the code, and clang-tidy fixes. +namespace detail::wyhash { + +static inline void mum(uint64_t* a, uint64_t* b) { +# if defined(__SIZEOF_INT128__) + __uint128_t r = *a; + r *= *b; + *a = static_cast(r); + *b = static_cast(r >> 64U); +# elif defined(_MSC_VER) && defined(_M_X64) + *a = _umul128(*a, *b, b); +# else + uint64_t ha = *a >> 32U; + uint64_t hb = *b >> 32U; + uint64_t la = static_cast(*a); + uint64_t lb = static_cast(*b); + uint64_t hi{}; + uint64_t lo{}; + uint64_t rh = ha * hb; + uint64_t rm0 = ha * lb; + uint64_t rm1 = hb * la; + uint64_t rl = la * lb; + uint64_t t = rl + (rm0 << 32U); + auto c = static_cast(t < rl); + lo = t + (rm1 << 32U); + c += static_cast(lo < t); + hi = rh + (rm0 >> 32U) + (rm1 >> 32U) + c; + *a = lo; + *b = hi; +# endif +} + +// multiply and xor mix function, aka MUM +[[nodiscard]] static inline auto mix(uint64_t a, uint64_t b) -> uint64_t { + mum(&a, &b); + return a ^ b; +} + +// read functions. WARNING: we don't care about endianness, so results are different on big endian! +[[nodiscard]] static inline auto r8(const uint8_t* p) -> uint64_t { + uint64_t v{}; + std::memcpy(&v, p, 8U); + return v; +} + +[[nodiscard]] static inline auto r4(const uint8_t* p) -> uint64_t { + uint32_t v{}; + std::memcpy(&v, p, 4); + return v; +} + +// reads 1, 2, or 3 bytes +[[nodiscard]] static inline auto r3(const uint8_t* p, size_t k) -> uint64_t { + return (static_cast(p[0]) << 16U) | (static_cast(p[k >> 1U]) << 8U) | p[k - 1]; +} + +[[maybe_unused]] [[nodiscard]] static inline auto hash(void const* key, size_t len) -> uint64_t { + static constexpr auto secret = std::array{UINT64_C(0xa0761d6478bd642f), + UINT64_C(0xe7037ed1a0b428db), + UINT64_C(0x8ebc6af09c88c6e3), + UINT64_C(0x589965cc75374cc3)}; + + auto const* p = static_cast(key); + uint64_t seed = secret[0]; + uint64_t a{}; + uint64_t b{}; + if (ANKERL_UNORDERED_DENSE_LIKELY(len <= 16)) { + if (ANKERL_UNORDERED_DENSE_LIKELY(len >= 4)) { + a = (r4(p) << 32U) | r4(p + ((len >> 3U) << 2U)); + b = (r4(p + len - 4) << 32U) | r4(p + len - 4 - ((len >> 3U) << 2U)); + } else if (ANKERL_UNORDERED_DENSE_LIKELY(len > 0)) { + a = r3(p, len); + b = 0; + } else { + a = 0; + b = 0; + } + } else { + size_t i = len; + if (ANKERL_UNORDERED_DENSE_UNLIKELY(i > 48)) { + uint64_t see1 = seed; + uint64_t see2 = seed; + do { + seed = mix(r8(p) ^ secret[1], r8(p + 8) ^ seed); + see1 = mix(r8(p + 16) ^ secret[2], r8(p + 24) ^ see1); + see2 = mix(r8(p + 32) ^ secret[3], r8(p + 40) ^ see2); + p += 48; + i -= 48; + } while (ANKERL_UNORDERED_DENSE_LIKELY(i > 48)); + seed ^= see1 ^ see2; + } + while (ANKERL_UNORDERED_DENSE_UNLIKELY(i > 16)) { + seed = mix(r8(p) ^ secret[1], r8(p + 8) ^ seed); + i -= 16; + p += 16; + } + a = r8(p + i - 16); + b = r8(p + i - 8); + } + + return mix(secret[1] ^ len, mix(a ^ secret[1], b ^ seed)); +} + +[[nodiscard]] static inline auto hash(uint64_t x) -> uint64_t { + return detail::wyhash::mix(x, UINT64_C(0x9E3779B97F4A7C15)); +} + +} // namespace detail::wyhash + +template +struct hash { + auto operator()(T const& obj) const noexcept(noexcept(std::declval>().operator()(std::declval()))) + -> uint64_t { + return std::hash{}(obj); + } +}; + +template +struct hash> { + using is_avalanching = void; + auto operator()(std::basic_string const& str) const noexcept -> uint64_t { + return detail::wyhash::hash(str.data(), sizeof(CharT) * str.size()); + } +}; + +template +struct hash> { + using is_avalanching = void; + auto operator()(std::basic_string_view const& sv) const noexcept -> uint64_t { + return detail::wyhash::hash(sv.data(), sizeof(CharT) * sv.size()); + } +}; + +template +struct hash { + using is_avalanching = void; + auto operator()(T* ptr) const noexcept -> uint64_t { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return detail::wyhash::hash(reinterpret_cast(ptr)); + } +}; + +template +struct hash> { + using is_avalanching = void; + auto operator()(std::unique_ptr const& ptr) const noexcept -> uint64_t { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return detail::wyhash::hash(reinterpret_cast(ptr.get())); + } +}; + +template +struct hash> { + using is_avalanching = void; + auto operator()(std::shared_ptr const& ptr) const noexcept -> uint64_t { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return detail::wyhash::hash(reinterpret_cast(ptr.get())); + } +}; + +template +struct hash::value>::type> { + using is_avalanching = void; + auto operator()(Enum e) const noexcept -> uint64_t { + using underlying = typename std::underlying_type_t; + return detail::wyhash::hash(static_cast(e)); + } +}; + +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +# define ANKERL_UNORDERED_DENSE_HASH_STATICCAST(T) \ + template <> \ + struct hash { \ + using is_avalanching = void; \ + auto operator()(T const& obj) const noexcept -> uint64_t { \ + return detail::wyhash::hash(static_cast(obj)); \ + } \ + } + +# if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wuseless-cast" +# endif +// see https://en.cppreference.com/w/cpp/utility/hash +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(bool); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(char); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(signed char); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(unsigned char); +# if ANKERL_UNORDERED_DENSE_CPP_VERSION >= 202002L +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(char8_t); +# endif +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(char16_t); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(char32_t); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(wchar_t); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(short); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(unsigned short); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(int); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(unsigned int); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(long); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(long long); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(unsigned long); +ANKERL_UNORDERED_DENSE_HASH_STATICCAST(unsigned long long); + +# if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic pop +# endif + +// bucket_type ////////////////////////////////////////////////////////// + +namespace bucket_type { + +struct standard { + static constexpr uint32_t dist_inc = 1U << 8U; // skip 1 byte fingerprint + static constexpr uint32_t fingerprint_mask = dist_inc - 1; // mask for 1 byte of fingerprint + + uint32_t m_dist_and_fingerprint; // upper 3 byte: distance to original bucket. lower byte: fingerprint from hash + uint32_t m_value_idx; // index into the m_values vector. +}; + +ANKERL_UNORDERED_DENSE_PACK(struct big { + static constexpr uint32_t dist_inc = 1U << 8U; // skip 1 byte fingerprint + static constexpr uint32_t fingerprint_mask = dist_inc - 1; // mask for 1 byte of fingerprint + + uint32_t m_dist_and_fingerprint; // upper 3 byte: distance to original bucket. lower byte: fingerprint from hash + size_t m_value_idx; // index into the m_values vector. +}); + +} // namespace bucket_type + +namespace detail { + +struct nonesuch {}; + +template class Op, class... Args> +struct detector { + using value_t = std::false_type; + using type = Default; +}; + +template class Op, class... Args> +struct detector>, Op, Args...> { + using value_t = std::true_type; + using type = Op; +}; + +template