mirror of
https://github.com/OpenMW/openmw.git
synced 2025-06-02 10:11:33 +00:00
Merge branch 'master' of gitlab.com:openmw/openmw into lua_class_data
This commit is contained in:
commit
10030a55e0
165 changed files with 1373 additions and 921 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -28,6 +28,7 @@ Doxygen
|
|||
.idea
|
||||
cmake-build-*
|
||||
files/windows/*.aps
|
||||
.cache/clangd
|
||||
## qt-creator
|
||||
CMakeLists.txt.user*
|
||||
.vs
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
Bug #4754: Stack of ammunition cannot be equipped partially
|
||||
Bug #4816: GetWeaponDrawn returns 1 before weapon is attached
|
||||
Bug #5057: Weapon swing sound plays at same pitch whether it hits or misses
|
||||
Bug #5062: Root bone rotations for NPC animation don't work the same as for creature animation
|
||||
Bug #5129: Stuttering animation on Centurion Archer
|
||||
Bug #5280: Unskinned shapes in skinned equipment are rendered in the wrong place
|
||||
Bug #5371: Keyframe animation tracks are used for any file that begins with an X
|
||||
|
@ -24,6 +25,7 @@
|
|||
Bug #5977: Fatigueless NPCs' corpse underwater changes animation on game load
|
||||
Bug #6025: Subrecords cannot overlap records
|
||||
Bug #6027: Collisionshape becomes spiderweb-like when the mesh is too complex
|
||||
Bug #6190: Unintuitive sun specularity time of day dependence
|
||||
Bug #6222: global map cell size can crash openmw if set to too high a value
|
||||
Bug #6313: Followers with high Fight can turn hostile
|
||||
Bug #6427: Enemy health bar disappears before damaging effect ends
|
||||
|
@ -68,6 +70,7 @@
|
|||
Bug #7298: Water ripples from projectiles sometimes are not spawned
|
||||
Bug #7307: Alchemy "Magic Effect" search string does not match on tool tip for effects related to attributes
|
||||
Bug #7322: Shadows don't cover groundcover depending on the view angle and perspective with compute scene bounds = primitives
|
||||
Bug #7380: NiZBufferProperty issue
|
||||
Bug #7413: Generated wilderness cells don't spawn fish
|
||||
Bug #7415: Unbreakable lock discrepancies
|
||||
Bug #7428: AutoCalc flag is not used to calculate enchantment costs
|
||||
|
@ -90,11 +93,15 @@
|
|||
Bug #7639: NPCs don't use hand-to-hand if their other melee skills were damaged during combat
|
||||
Bug #7642: Items in repair and recharge menus aren't sorted alphabetically
|
||||
Bug #7647: NPC walk cycle bugs after greeting player
|
||||
Bug #7654: Tooltips for enchantments with invalid effects cause crashes
|
||||
Bug #7660: Some inconsistencies regarding Invisibility breaking
|
||||
Bug #7675: Successful lock spell doesn't produce a sound
|
||||
Bug #7679: Scene luminance value flashes when toggling shaders
|
||||
Feature #3537: Shader-based water ripples
|
||||
Feature #5492: Let rain and snow collide with statics
|
||||
Feature #6149: Dehardcode Lua API_REVISION
|
||||
Feature #6152: Playing music via lua scripts
|
||||
Feature #6188: Specular lighting from point light sources
|
||||
Feature #6447: Add LOD support to Object Paging
|
||||
Feature #6491: Add support for Qt6
|
||||
Feature #6556: Lua API for sounds
|
||||
|
|
|
@ -86,7 +86,7 @@ declare -rA GROUPED_DEPS=(
|
|||
libswresample3
|
||||
libswscale5
|
||||
libtinyxml2.6.2v5
|
||||
libyaml-cpp0.7
|
||||
libyaml-cpp0.8
|
||||
python3-pip
|
||||
xvfb
|
||||
"
|
||||
|
@ -125,3 +125,4 @@ add-apt-repository -y ppa:openmw/openmw
|
|||
add-apt-repository -y ppa:openmw/openmw-daily
|
||||
add-apt-repository -y ppa:openmw/staging
|
||||
apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends "${deps[@]}" >/dev/null
|
||||
apt list --installed
|
||||
|
|
|
@ -1204,7 +1204,8 @@ namespace EsmTool
|
|||
std::array<std::string_view, 10> weathers
|
||||
= { "Clear", "Cloudy", "Fog", "Overcast", "Rain", "Thunder", "Ash", "Blight", "Snow", "Blizzard" };
|
||||
for (size_t i = 0; i < weathers.size(); ++i)
|
||||
std::cout << " " << weathers[i] << ": " << mData.mData.mProbabilities[i] << std::endl;
|
||||
std::cout << " " << weathers[i] << ": " << static_cast<unsigned>(mData.mData.mProbabilities[i])
|
||||
<< std::endl;
|
||||
std::cout << " Map Color: " << mData.mMapColor << std::endl;
|
||||
if (!mData.mSleepList.empty())
|
||||
std::cout << " Sleep List: " << mData.mSleepList << std::endl;
|
||||
|
|
|
@ -9,15 +9,14 @@ namespace ESSImport
|
|||
|
||||
void convertInventory(const Inventory& inventory, ESM::InventoryState& state)
|
||||
{
|
||||
int index = 0;
|
||||
uint32_t index = 0;
|
||||
for (const auto& item : inventory.mItems)
|
||||
{
|
||||
ESM::ObjectState objstate;
|
||||
objstate.blank();
|
||||
objstate.mRef = item;
|
||||
objstate.mRef.mRefID = ESM::RefId::stringRefId(item.mId);
|
||||
objstate.mCount = std::abs(item.mCount); // restocking items have negative count in the savefile
|
||||
// openmw handles them differently, so no need to set any flags
|
||||
objstate.mCount = item.mCount;
|
||||
state.mItems.push_back(objstate);
|
||||
if (item.mRelativeEquipmentSlot != -1)
|
||||
// Note we should really write the absolute slot here, which we do not know about
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
#include <components/files/qtconversion.hpp>
|
||||
#include <components/misc/strings/conversion.hpp>
|
||||
#include <components/navmeshtool/protocol.hpp>
|
||||
#include <components/settings/settings.hpp>
|
||||
#include <components/settings/values.hpp>
|
||||
#include <components/vfs/bsaarchive.hpp>
|
||||
|
||||
#include "utils/profilescombobox.hpp"
|
||||
|
@ -123,7 +123,7 @@ namespace Launcher
|
|||
|
||||
int getMaxNavMeshDbFileSizeMiB()
|
||||
{
|
||||
return Settings::Manager::getUInt64("max navmeshdb file size", "Navigator") / (1024 * 1024);
|
||||
return Settings::navigator().mMaxNavmeshdbFileSize / (1024 * 1024);
|
||||
}
|
||||
|
||||
std::optional<QString> findFirstPath(const QStringList& directories, const QString& fileName)
|
||||
|
@ -359,9 +359,8 @@ void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
|
|||
|
||||
void Launcher::DataFilesPage::saveSettings(const QString& profile)
|
||||
{
|
||||
if (const int value = ui.navMeshMaxSizeSpinBox->value(); value != getMaxNavMeshDbFileSizeMiB())
|
||||
Settings::Manager::setUInt64(
|
||||
"max navmeshdb file size", "Navigator", static_cast<std::uint64_t>(std::max(0, value)) * 1024 * 1024);
|
||||
Settings::navigator().mMaxNavmeshdbFileSize.set(
|
||||
static_cast<std::uint64_t>(std::max(0, ui.navMeshMaxSizeSpinBox->value())) * 1024 * 1024);
|
||||
|
||||
QString profileName = profile;
|
||||
|
||||
|
|
|
@ -158,32 +158,32 @@ bool Launcher::GraphicsPage::loadSettings()
|
|||
lightingMethodComboBox->setCurrentIndex(lightingMethod);
|
||||
|
||||
// Shadows
|
||||
if (Settings::Manager::getBool("actor shadows", "Shadows"))
|
||||
if (Settings::shadows().mActorShadows)
|
||||
actorShadowsCheckBox->setCheckState(Qt::Checked);
|
||||
if (Settings::Manager::getBool("player shadows", "Shadows"))
|
||||
if (Settings::shadows().mPlayerShadows)
|
||||
playerShadowsCheckBox->setCheckState(Qt::Checked);
|
||||
if (Settings::Manager::getBool("terrain shadows", "Shadows"))
|
||||
if (Settings::shadows().mTerrainShadows)
|
||||
terrainShadowsCheckBox->setCheckState(Qt::Checked);
|
||||
if (Settings::Manager::getBool("object shadows", "Shadows"))
|
||||
if (Settings::shadows().mObjectShadows)
|
||||
objectShadowsCheckBox->setCheckState(Qt::Checked);
|
||||
if (Settings::Manager::getBool("enable indoor shadows", "Shadows"))
|
||||
if (Settings::shadows().mEnableIndoorShadows)
|
||||
indoorShadowsCheckBox->setCheckState(Qt::Checked);
|
||||
|
||||
shadowComputeSceneBoundsComboBox->setCurrentIndex(shadowComputeSceneBoundsComboBox->findText(
|
||||
QString(tr(Settings::Manager::getString("compute scene bounds", "Shadows").c_str()))));
|
||||
shadowComputeSceneBoundsComboBox->setCurrentIndex(
|
||||
shadowComputeSceneBoundsComboBox->findText(QString(tr(Settings::shadows().mComputeSceneBounds.get().c_str()))));
|
||||
|
||||
int shadowDistLimit = Settings::Manager::getInt("maximum shadow map distance", "Shadows");
|
||||
const int shadowDistLimit = Settings::shadows().mMaximumShadowMapDistance;
|
||||
if (shadowDistLimit > 0)
|
||||
{
|
||||
shadowDistanceCheckBox->setCheckState(Qt::Checked);
|
||||
shadowDistanceSpinBox->setValue(shadowDistLimit);
|
||||
}
|
||||
|
||||
float shadowFadeStart = Settings::Manager::getFloat("shadow fade start", "Shadows");
|
||||
const float shadowFadeStart = Settings::shadows().mShadowFadeStart;
|
||||
if (shadowFadeStart != 0)
|
||||
fadeStartSpinBox->setValue(shadowFadeStart);
|
||||
|
||||
int shadowRes = Settings::Manager::getInt("shadow map resolution", "Shadows");
|
||||
const int shadowRes = Settings::shadows().mShadowMapResolution;
|
||||
int shadowResIndex = shadowResolutionComboBox->findText(QString::number(shadowRes));
|
||||
if (shadowResIndex != -1)
|
||||
shadowResolutionComboBox->setCurrentIndex(shadowResIndex);
|
||||
|
@ -240,55 +240,36 @@ void Launcher::GraphicsPage::saveSettings()
|
|||
Settings::shaders().mLightingMethod.set(lightingMethodMap[lightingMethodComboBox->currentIndex()]);
|
||||
|
||||
// Shadows
|
||||
int cShadowDist = shadowDistanceCheckBox->checkState() != Qt::Unchecked ? shadowDistanceSpinBox->value() : 0;
|
||||
if (Settings::Manager::getInt("maximum shadow map distance", "Shadows") != cShadowDist)
|
||||
Settings::Manager::setInt("maximum shadow map distance", "Shadows", cShadowDist);
|
||||
float cFadeStart = fadeStartSpinBox->value();
|
||||
if (cShadowDist > 0 && Settings::Manager::getFloat("shadow fade start", "Shadows") != cFadeStart)
|
||||
Settings::Manager::setFloat("shadow fade start", "Shadows", cFadeStart);
|
||||
const int cShadowDist = shadowDistanceCheckBox->checkState() != Qt::Unchecked ? shadowDistanceSpinBox->value() : 0;
|
||||
Settings::shadows().mMaximumShadowMapDistance.set(cShadowDist);
|
||||
const float cFadeStart = fadeStartSpinBox->value();
|
||||
if (cShadowDist > 0)
|
||||
Settings::shadows().mShadowFadeStart.set(cFadeStart);
|
||||
|
||||
bool cActorShadows = actorShadowsCheckBox->checkState();
|
||||
bool cObjectShadows = objectShadowsCheckBox->checkState();
|
||||
bool cTerrainShadows = terrainShadowsCheckBox->checkState();
|
||||
bool cPlayerShadows = playerShadowsCheckBox->checkState();
|
||||
const bool cActorShadows = actorShadowsCheckBox->checkState() != Qt::Unchecked;
|
||||
const bool cObjectShadows = objectShadowsCheckBox->checkState() != Qt::Unchecked;
|
||||
const bool cTerrainShadows = terrainShadowsCheckBox->checkState() != Qt::Unchecked;
|
||||
const bool cPlayerShadows = playerShadowsCheckBox->checkState() != Qt::Unchecked;
|
||||
if (cActorShadows || cObjectShadows || cTerrainShadows || cPlayerShadows)
|
||||
{
|
||||
if (!Settings::Manager::getBool("enable shadows", "Shadows"))
|
||||
Settings::Manager::setBool("enable shadows", "Shadows", true);
|
||||
if (Settings::Manager::getBool("actor shadows", "Shadows") != cActorShadows)
|
||||
Settings::Manager::setBool("actor shadows", "Shadows", cActorShadows);
|
||||
if (Settings::Manager::getBool("player shadows", "Shadows") != cPlayerShadows)
|
||||
Settings::Manager::setBool("player shadows", "Shadows", cPlayerShadows);
|
||||
if (Settings::Manager::getBool("object shadows", "Shadows") != cObjectShadows)
|
||||
Settings::Manager::setBool("object shadows", "Shadows", cObjectShadows);
|
||||
if (Settings::Manager::getBool("terrain shadows", "Shadows") != cTerrainShadows)
|
||||
Settings::Manager::setBool("terrain shadows", "Shadows", cTerrainShadows);
|
||||
Settings::shadows().mEnableShadows.set(true);
|
||||
Settings::shadows().mActorShadows.set(cActorShadows);
|
||||
Settings::shadows().mPlayerShadows.set(cPlayerShadows);
|
||||
Settings::shadows().mObjectShadows.set(cObjectShadows);
|
||||
Settings::shadows().mTerrainShadows.set(cTerrainShadows);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Settings::Manager::getBool("enable shadows", "Shadows"))
|
||||
Settings::Manager::setBool("enable shadows", "Shadows", false);
|
||||
if (Settings::Manager::getBool("actor shadows", "Shadows"))
|
||||
Settings::Manager::setBool("actor shadows", "Shadows", false);
|
||||
if (Settings::Manager::getBool("player shadows", "Shadows"))
|
||||
Settings::Manager::setBool("player shadows", "Shadows", false);
|
||||
if (Settings::Manager::getBool("object shadows", "Shadows"))
|
||||
Settings::Manager::setBool("object shadows", "Shadows", false);
|
||||
if (Settings::Manager::getBool("terrain shadows", "Shadows"))
|
||||
Settings::Manager::setBool("terrain shadows", "Shadows", false);
|
||||
Settings::shadows().mEnableShadows.set(false);
|
||||
Settings::shadows().mActorShadows.set(false);
|
||||
Settings::shadows().mPlayerShadows.set(false);
|
||||
Settings::shadows().mObjectShadows.set(false);
|
||||
Settings::shadows().mTerrainShadows.set(false);
|
||||
}
|
||||
|
||||
bool cIndoorShadows = indoorShadowsCheckBox->checkState();
|
||||
if (Settings::Manager::getBool("enable indoor shadows", "Shadows") != cIndoorShadows)
|
||||
Settings::Manager::setBool("enable indoor shadows", "Shadows", cIndoorShadows);
|
||||
|
||||
int cShadowRes = shadowResolutionComboBox->currentText().toInt();
|
||||
if (cShadowRes != Settings::Manager::getInt("shadow map resolution", "Shadows"))
|
||||
Settings::Manager::setInt("shadow map resolution", "Shadows", cShadowRes);
|
||||
|
||||
auto cComputeSceneBounds = shadowComputeSceneBoundsComboBox->currentText().toStdString();
|
||||
if (cComputeSceneBounds != Settings::Manager::getString("compute scene bounds", "Shadows"))
|
||||
Settings::Manager::setString("compute scene bounds", "Shadows", cComputeSceneBounds);
|
||||
Settings::shadows().mEnableIndoorShadows.set(indoorShadowsCheckBox->checkState() != Qt::Unchecked);
|
||||
Settings::shadows().mShadowMapResolution.set(shadowResolutionComboBox->currentText().toInt());
|
||||
Settings::shadows().mComputeSceneBounds.set(shadowComputeSceneBoundsComboBox->currentText().toStdString());
|
||||
}
|
||||
|
||||
QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen)
|
||||
|
|
|
@ -191,6 +191,7 @@ bool Launcher::SettingsPage::loadSettings()
|
|||
}
|
||||
loadSettingBool(Settings::game().mTurnToMovementDirection, *turnToMovementDirectionCheckBox);
|
||||
loadSettingBool(Settings::game().mSmoothMovement, *smoothMovementCheckBox);
|
||||
loadSettingBool(Settings::game().mPlayerMovementIgnoresAnimation, *playerMovementIgnoresAnimationCheckBox);
|
||||
|
||||
distantLandCheckBox->setCheckState(
|
||||
Settings::terrain().mDistantTerrain && Settings::terrain().mObjectPaging ? Qt::Checked : Qt::Unchecked);
|
||||
|
@ -338,6 +339,7 @@ void Launcher::SettingsPage::saveSettings()
|
|||
saveSettingBool(*shieldSheathingCheckBox, Settings::game().mShieldSheathing);
|
||||
saveSettingBool(*turnToMovementDirectionCheckBox, Settings::game().mTurnToMovementDirection);
|
||||
saveSettingBool(*smoothMovementCheckBox, Settings::game().mSmoothMovement);
|
||||
saveSettingBool(*playerMovementIgnoresAnimationCheckBox, Settings::game().mPlayerMovementIgnoresAnimation);
|
||||
|
||||
const bool wantDistantLand = distantLandCheckBox->checkState() == Qt::Checked;
|
||||
if (wantDistantLand != (Settings::terrain().mDistantTerrain && Settings::terrain().mObjectPaging))
|
||||
|
|
|
@ -318,9 +318,14 @@ bool OMW::Engine::frame(float frametime)
|
|||
mViewer->eventTraversal();
|
||||
mViewer->updateTraversal();
|
||||
|
||||
// update GUI by world data
|
||||
{
|
||||
ScopedProfile<UserStatsType::WindowManager> profile(frameStart, frameNumber, *timer, *stats);
|
||||
mWorld->updateWindowManager();
|
||||
|
||||
if (mStateManager->getState() != MWBase::StateManager::State_NoGame)
|
||||
{
|
||||
mWorld->updateWindowManager();
|
||||
}
|
||||
}
|
||||
|
||||
mLuaWorker->allowUpdate(); // if there is a separate Lua thread, it starts the update now
|
||||
|
|
|
@ -34,11 +34,26 @@ namespace MWClass
|
|||
|
||||
static const ESM4::Npc* chooseTemplate(const std::vector<const ESM4::Npc*>& recs, uint16_t flag)
|
||||
{
|
||||
// In case of FO3 the function may return nullptr that will lead to "ESM4 NPC traits not found"
|
||||
// exception and the NPC will not be added to the scene. But in any way it shouldn't cause a crash.
|
||||
for (const auto* rec : recs)
|
||||
if (rec->mIsTES4 || rec->mIsFONV || !(rec->mBaseConfig.tes5.templateFlags & flag))
|
||||
{
|
||||
if (rec->mIsTES4)
|
||||
return rec;
|
||||
else if (rec->mIsFONV)
|
||||
{
|
||||
// TODO: FO3 should use this branch as well. But it is not clear how to distinguish FO3 from
|
||||
// TES5. Currently FO3 uses wrong template flags that can lead to "ESM4 NPC traits not found"
|
||||
// exception the NPC will not be added to the scene. But in any way it shouldn't cause a crash.
|
||||
if (!(rec->mBaseConfig.fo3.templateFlags & flag))
|
||||
return rec;
|
||||
}
|
||||
else if (rec->mIsFO4)
|
||||
{
|
||||
if (!(rec->mBaseConfig.fo4.templateFlags & flag))
|
||||
return rec;
|
||||
}
|
||||
else if (!(rec->mBaseConfig.tes5.templateFlags & flag))
|
||||
return rec;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
@ -75,8 +90,8 @@ namespace MWClass
|
|||
const MWWorld::ESMStore* store = MWBase::Environment::get().getESMStore();
|
||||
auto npcRecs = withBaseTemplates<ESM4::LevelledNpc, ESM4::Npc>(ptr.get<ESM4::Npc>()->mBase);
|
||||
|
||||
data->mTraits = chooseTemplate(npcRecs, ESM4::Npc::TES5_UseTraits);
|
||||
data->mBaseData = chooseTemplate(npcRecs, ESM4::Npc::TES5_UseBaseData);
|
||||
data->mTraits = chooseTemplate(npcRecs, ESM4::Npc::Template_UseTraits);
|
||||
data->mBaseData = chooseTemplate(npcRecs, ESM4::Npc::Template_UseBaseData);
|
||||
|
||||
if (!data->mTraits)
|
||||
throw std::runtime_error("ESM4 NPC traits not found");
|
||||
|
@ -88,10 +103,13 @@ namespace MWClass
|
|||
data->mIsFemale = data->mTraits->mBaseConfig.tes4.flags & ESM4::Npc::TES4_Female;
|
||||
else if (data->mTraits->mIsFONV)
|
||||
data->mIsFemale = data->mTraits->mBaseConfig.fo3.flags & ESM4::Npc::FO3_Female;
|
||||
else if (data->mTraits->mIsFO4)
|
||||
data->mIsFemale
|
||||
= data->mTraits->mBaseConfig.fo4.flags & ESM4::Npc::TES5_Female; // FO4 flags are the same as TES5
|
||||
else
|
||||
data->mIsFemale = data->mTraits->mBaseConfig.tes5.flags & ESM4::Npc::TES5_Female;
|
||||
|
||||
if (auto inv = chooseTemplate(npcRecs, ESM4::Npc::TES5_UseInventory))
|
||||
if (auto inv = chooseTemplate(npcRecs, ESM4::Npc::Template_UseInventory))
|
||||
{
|
||||
for (const ESM4::InventoryItem& item : inv->mInventory)
|
||||
{
|
||||
|
|
|
@ -556,6 +556,20 @@ namespace MWGui
|
|||
std::unique_ptr<MWWorld::Action> action = ptr.getClass().use(ptr, force);
|
||||
action->execute(player);
|
||||
|
||||
// Handles partial equipping (final part)
|
||||
if (mEquippedStackableCount.has_value())
|
||||
{
|
||||
// the count to unequip
|
||||
int count = ptr.getRefData().getCount() - mDragAndDrop->mDraggedCount - mEquippedStackableCount.value();
|
||||
if (count > 0)
|
||||
{
|
||||
MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr);
|
||||
invStore.unequipItemQuantity(ptr, count);
|
||||
updateItemView();
|
||||
}
|
||||
mEquippedStackableCount.reset();
|
||||
}
|
||||
|
||||
if (isVisible())
|
||||
{
|
||||
mItemView->update();
|
||||
|
@ -581,27 +595,21 @@ namespace MWGui
|
|||
}
|
||||
|
||||
// Handles partial equipping
|
||||
const std::pair<std::vector<int>, bool> slots = ptr.getClass().getEquipmentSlots(ptr);
|
||||
mEquippedStackableCount.reset();
|
||||
const auto slots = ptr.getClass().getEquipmentSlots(ptr);
|
||||
if (!slots.first.empty() && slots.second)
|
||||
{
|
||||
int equippedStackableCount = 0;
|
||||
MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr);
|
||||
MWWorld::ConstContainerStoreIterator slotIt = invStore.getSlot(slots.first.front());
|
||||
|
||||
// Get the count before useItem()
|
||||
// Save the currently equipped count before useItem()
|
||||
if (slotIt != invStore.end() && slotIt->getCellRef().getRefId() == ptr.getCellRef().getRefId())
|
||||
equippedStackableCount = slotIt->getRefData().getCount();
|
||||
|
||||
useItem(ptr);
|
||||
int unequipCount = ptr.getRefData().getCount() - mDragAndDrop->mDraggedCount - equippedStackableCount;
|
||||
if (unequipCount > 0)
|
||||
{
|
||||
invStore.unequipItemQuantity(ptr, unequipCount);
|
||||
updateItemView();
|
||||
}
|
||||
mEquippedStackableCount = slotIt->getRefData().getCount();
|
||||
else
|
||||
mEquippedStackableCount = 0;
|
||||
}
|
||||
else
|
||||
MWBase::Environment::get().getLuaManager()->useItem(ptr, MWMechanics::getPlayer(), false);
|
||||
|
||||
MWBase::Environment::get().getLuaManager()->useItem(ptr, MWMechanics::getPlayer(), false);
|
||||
|
||||
// If item is ingredient or potion don't stop drag and drop to simplify action of taking more than one 1
|
||||
// item
|
||||
|
|
|
@ -74,6 +74,7 @@ namespace MWGui
|
|||
DragAndDrop* mDragAndDrop;
|
||||
|
||||
int mSelectedItem;
|
||||
std::optional<int> mEquippedStackableCount;
|
||||
|
||||
MWWorld::Ptr mPtr;
|
||||
|
||||
|
|
|
@ -183,6 +183,10 @@ namespace MWGui
|
|||
return switchFocus(D_Down, false);
|
||||
case MyGUI::KeyCode::Tab:
|
||||
return switchFocus(MyGUI::InputManager::getInstance().isShiftPressed() ? D_Prev : D_Next, true);
|
||||
case MyGUI::KeyCode::Period:
|
||||
return switchFocus(D_Prev, true);
|
||||
case MyGUI::KeyCode::Slash:
|
||||
return switchFocus(D_Next, true);
|
||||
case MyGUI::KeyCode::Return:
|
||||
case MyGUI::KeyCode::NumpadEnter:
|
||||
case MyGUI::KeyCode::Space:
|
||||
|
|
|
@ -413,8 +413,8 @@ namespace MWGui
|
|||
text << Misc::fileTimeToString(mCurrentSlot->mTimeStamp, "%Y.%m.%d %T") << "\n";
|
||||
|
||||
if (mCurrentSlot->mProfile.mMaximumHealth > 0)
|
||||
text << std::fixed << std::setprecision(0) << "#{sHealth} " << mCurrentSlot->mProfile.mCurrentHealth << "/"
|
||||
<< mCurrentSlot->mProfile.mMaximumHealth << "\n";
|
||||
text << "#{sHealth} " << static_cast<int>(mCurrentSlot->mProfile.mCurrentHealth) << "/"
|
||||
<< static_cast<int>(mCurrentSlot->mProfile.mMaximumHealth) << "\n";
|
||||
|
||||
text << "#{sLevel} " << mCurrentSlot->mProfile.mPlayerLevel << "\n";
|
||||
text << "#{sCell=" << mCurrentSlot->mProfile.mPlayerCellName << "}\n";
|
||||
|
|
|
@ -355,12 +355,10 @@ namespace MWGui::Widgets
|
|||
|
||||
const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore();
|
||||
|
||||
const ESM::MagicEffect* magicEffect = store.get<ESM::MagicEffect>().search(mEffectParams.mEffectID);
|
||||
const ESM::MagicEffect* magicEffect = store.get<ESM::MagicEffect>().find(mEffectParams.mEffectID);
|
||||
const ESM::Attribute* attribute = store.get<ESM::Attribute>().search(mEffectParams.mAttribute);
|
||||
const ESM::Skill* skill = store.get<ESM::Skill>().search(mEffectParams.mSkill);
|
||||
|
||||
assert(magicEffect);
|
||||
|
||||
auto windowManager = MWBase::Environment::get().getWindowManager();
|
||||
|
||||
std::string_view pt = windowManager->getGameSettingString("spoint", {});
|
||||
|
|
|
@ -1100,7 +1100,7 @@ namespace MWGui
|
|||
std::string_view settingSection = tag.substr(0, comma_pos);
|
||||
std::string_view settingTag = tag.substr(comma_pos + 1, tag.length());
|
||||
|
||||
_result = Settings::Manager::getString(settingTag, settingSection);
|
||||
_result = Settings::get<MyGUI::Colour>(settingSection, settingTag).get().print();
|
||||
}
|
||||
else if (tag.starts_with(tokenToFind))
|
||||
{
|
||||
|
@ -1115,7 +1115,7 @@ namespace MWGui
|
|||
else
|
||||
{
|
||||
std::vector<std::string> split;
|
||||
Misc::StringUtils::split(std::string{ tag }, split, ":");
|
||||
Misc::StringUtils::split(tag, split, ":");
|
||||
|
||||
l10n::Manager& l10nManager = *MWBase::Environment::get().getL10nManager();
|
||||
|
||||
|
|
|
@ -344,6 +344,12 @@ namespace MWLua
|
|||
playerScripts->uiModeChanged(argId, false);
|
||||
}
|
||||
|
||||
void LuaManager::useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force)
|
||||
{
|
||||
MWBase::Environment::get().getWorldModel()->registerPtr(object);
|
||||
mEngineEvents.addToQueue(EngineEvents::OnUseItem{ getId(actor), getId(object), force });
|
||||
}
|
||||
|
||||
void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
mObjectLists.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet.
|
||||
|
|
|
@ -77,10 +77,7 @@ namespace MWLua
|
|||
{
|
||||
mEngineEvents.addToQueue(EngineEvents::OnActivate{ getId(actor), getId(object) });
|
||||
}
|
||||
void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) override
|
||||
{
|
||||
mEngineEvents.addToQueue(EngineEvents::OnUseItem{ getId(actor), getId(object), force });
|
||||
}
|
||||
void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) override;
|
||||
void exteriorCreated(MWWorld::CellStore& cell) override
|
||||
{
|
||||
mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell });
|
||||
|
|
|
@ -140,7 +140,7 @@ namespace MWLua
|
|||
= sol::overload([](const GlobalStore& store, std::string_view globalId, float val) {
|
||||
auto g = store.search(ESM::RefId::deserializeText(globalId));
|
||||
if (g == nullptr)
|
||||
return;
|
||||
throw std::runtime_error("No variable \"" + std::string(globalId) + "\" in GlobalStore");
|
||||
char varType = MWBase::Environment::get().getWorld()->getGlobalVariableType(globalId);
|
||||
if (varType == 's' || varType == 'l')
|
||||
{
|
||||
|
|
|
@ -457,6 +457,9 @@ namespace MWLua
|
|||
return nullptr;
|
||||
return &*rec.mSchool;
|
||||
});
|
||||
skillT["attribute"] = sol::readonly_property([](const ESM::Skill& rec) -> std::string {
|
||||
return ESM::Attribute::indexToRefId(rec.mData.mAttribute).serializeText();
|
||||
});
|
||||
|
||||
auto schoolT = context.mLua->sol().new_usertype<ESM::MagicSchool>("MagicSchool");
|
||||
schoolT[sol::meta_function::to_string]
|
||||
|
|
|
@ -395,6 +395,11 @@ namespace MWLua
|
|||
return dist <= actorsProcessingRange;
|
||||
};
|
||||
|
||||
actor["isDead"] = [](const Object& o) {
|
||||
const auto& target = o.ptr();
|
||||
return target.getClass().getCreatureStats(target).isDead();
|
||||
};
|
||||
|
||||
actor["getEncumbrance"] = [](const Object& actor) -> float {
|
||||
const MWWorld::Ptr ptr = actor.ptr();
|
||||
return ptr.getClass().getEncumbrance(ptr);
|
||||
|
|
|
@ -39,6 +39,6 @@ namespace MWMechanics
|
|||
{
|
||||
const MagicEffects& magicEffects = actor.getClass().getCreatureStats(actor).getMagicEffects();
|
||||
return (magicEffects.getOrDefault(ESM::MagicEffect::Invisibility).getMagnitude() > 0)
|
||||
|| (magicEffects.getOrDefault(ESM::MagicEffect::Chameleon).getMagnitude() > 75);
|
||||
|| (magicEffects.getOrDefault(ESM::MagicEffect::Chameleon).getMagnitude() >= 75);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2386,49 +2386,55 @@ namespace MWMechanics
|
|||
}
|
||||
}
|
||||
|
||||
osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim && !isScriptedAnimPlaying() ? 0.f : duration);
|
||||
if (duration > 0.0f)
|
||||
moved /= duration;
|
||||
else
|
||||
moved = osg::Vec3f(0.f, 0.f, 0.f);
|
||||
osg::Vec3f movementFromAnimation
|
||||
= mAnimation->runAnimation(mSkipAnim && !isScriptedAnimPlaying() ? 0.f : duration);
|
||||
|
||||
moved.x() *= scale;
|
||||
moved.y() *= scale;
|
||||
|
||||
// Ensure we're moving in generally the right direction...
|
||||
if (speed > 0.f && moved != osg::Vec3f())
|
||||
if (mPtr.getClass().isActor() && isMovementAnimationControlled() && !isScriptedAnimPlaying())
|
||||
{
|
||||
float l = moved.length();
|
||||
if (std::abs(movement.x() - moved.x()) > std::abs(moved.x()) / 2
|
||||
|| std::abs(movement.y() - moved.y()) > std::abs(moved.y()) / 2
|
||||
|| std::abs(movement.z() - moved.z()) > std::abs(moved.z()) / 2)
|
||||
{
|
||||
moved = movement;
|
||||
// For some creatures getSpeed doesn't work, so we adjust speed to the animation.
|
||||
// TODO: Fix Creature::getSpeed.
|
||||
float newLength = moved.length();
|
||||
if (newLength > 0 && !cls.isNpc())
|
||||
moved *= (l / newLength);
|
||||
}
|
||||
}
|
||||
if (duration > 0.0f)
|
||||
movementFromAnimation /= duration;
|
||||
else
|
||||
movementFromAnimation = osg::Vec3f(0.f, 0.f, 0.f);
|
||||
|
||||
if (mFloatToSurface && cls.isActor())
|
||||
{
|
||||
if (cls.getCreatureStats(mPtr).isDead()
|
||||
|| (!godmode
|
||||
&& cls.getCreatureStats(mPtr)
|
||||
.getMagicEffects()
|
||||
.getOrDefault(ESM::MagicEffect::Paralyze)
|
||||
.getModifier()
|
||||
> 0))
|
||||
{
|
||||
moved.z() = 1.0;
|
||||
}
|
||||
}
|
||||
movementFromAnimation.x() *= scale;
|
||||
movementFromAnimation.y() *= scale;
|
||||
|
||||
// Update movement
|
||||
if (isMovementAnimationControlled() && mPtr.getClass().isActor() && !isScriptedAnimPlaying())
|
||||
world->queueMovement(mPtr, moved);
|
||||
if (speed > 0.f && movementFromAnimation != osg::Vec3f())
|
||||
{
|
||||
// Ensure we're moving in the right general direction. In vanilla, all horizontal movement is taken from
|
||||
// animations, even when moving diagonally (which doesn't have a corresponding animation). So to acheive
|
||||
// diagonal movement, we have to rotate the movement taken from the animation to the intended
|
||||
// direction.
|
||||
//
|
||||
// Note that while a complete movement animation cycle will have a well defined direction, no individual
|
||||
// frame will, and therefore we have to determine the direction based on the currently playing cycle
|
||||
// instead.
|
||||
float animMovementAngle = getAnimationMovementDirection();
|
||||
float targetMovementAngle = std::atan2(-movement.x(), movement.y());
|
||||
float diff = targetMovementAngle - animMovementAngle;
|
||||
movementFromAnimation = osg::Quat(diff, osg::Vec3f(0, 0, 1)) * movementFromAnimation;
|
||||
}
|
||||
|
||||
if (!(isPlayer && Settings::game().mPlayerMovementIgnoresAnimation))
|
||||
movement = movementFromAnimation;
|
||||
|
||||
if (mFloatToSurface)
|
||||
{
|
||||
if (cls.getCreatureStats(mPtr).isDead()
|
||||
|| (!godmode
|
||||
&& cls.getCreatureStats(mPtr)
|
||||
.getMagicEffects()
|
||||
.getOrDefault(ESM::MagicEffect::Paralyze)
|
||||
.getModifier()
|
||||
> 0))
|
||||
{
|
||||
movement.z() = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Update movement
|
||||
world->queueMovement(mPtr, movement);
|
||||
}
|
||||
|
||||
mSkipAnim = false;
|
||||
|
||||
|
@ -2909,6 +2915,39 @@ namespace MWMechanics
|
|||
MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, *soundId, volume, pitch);
|
||||
}
|
||||
|
||||
float CharacterController::getAnimationMovementDirection() const
|
||||
{
|
||||
switch (mMovementState)
|
||||
{
|
||||
case CharState_RunLeft:
|
||||
case CharState_SneakLeft:
|
||||
case CharState_SwimWalkLeft:
|
||||
case CharState_SwimRunLeft:
|
||||
case CharState_WalkLeft:
|
||||
return osg::PI_2f;
|
||||
case CharState_RunRight:
|
||||
case CharState_SneakRight:
|
||||
case CharState_SwimWalkRight:
|
||||
case CharState_SwimRunRight:
|
||||
case CharState_WalkRight:
|
||||
return -osg::PI_2f;
|
||||
case CharState_RunForward:
|
||||
case CharState_SneakForward:
|
||||
case CharState_SwimRunForward:
|
||||
case CharState_SwimWalkForward:
|
||||
case CharState_WalkForward:
|
||||
return mAnimation->getLegsYawRadians();
|
||||
case CharState_RunBack:
|
||||
case CharState_SneakBack:
|
||||
case CharState_SwimWalkBack:
|
||||
case CharState_SwimRunBack:
|
||||
case CharState_WalkBack:
|
||||
return mAnimation->getLegsYawRadians() - osg::PIf;
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterController::updateHeadTracking(float duration)
|
||||
{
|
||||
const osg::Node* head = mAnimation->getNode("Bip01 Head");
|
||||
|
|
|
@ -319,6 +319,8 @@ namespace MWMechanics
|
|||
|
||||
void playSwishSound() const;
|
||||
|
||||
float getAnimationMovementDirection() const;
|
||||
|
||||
MWWorld::MovementDirectionFlags getSupportedMovementDirections() const;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -932,6 +932,9 @@ namespace MWMechanics
|
|||
if (target.getCellRef().getLockLevel()
|
||||
< magnitude) // If the door is not already locked to a higher value, lock it to spell magnitude
|
||||
{
|
||||
MWBase::Environment::get().getSoundManager()->playSound3D(
|
||||
target, ESM::RefId::stringRefId("Open Lock"), 1.f, 1.f);
|
||||
|
||||
if (caster == getPlayer())
|
||||
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicLockSuccess}");
|
||||
target.getCellRef().lock(magnitude);
|
||||
|
|
|
@ -1235,9 +1235,11 @@ namespace MWRender
|
|||
mRootController->setEnabled(enable);
|
||||
if (enable)
|
||||
{
|
||||
mRootController->setRotate(osg::Quat(mLegsYawRadians, osg::Vec3f(0, 0, 1))
|
||||
* osg::Quat(mBodyPitchRadians, osg::Vec3f(1, 0, 0)));
|
||||
osg::Quat legYaw = osg::Quat(mLegsYawRadians, osg::Vec3f(0, 0, 1));
|
||||
mRootController->setRotate(legYaw * osg::Quat(mBodyPitchRadians, osg::Vec3f(1, 0, 0)));
|
||||
yawOffset = mLegsYawRadians;
|
||||
// When yawing the root, also update the accumulated movement.
|
||||
movement = legYaw * movement;
|
||||
}
|
||||
}
|
||||
if (mSpineController)
|
||||
|
|
|
@ -247,7 +247,7 @@ namespace MWRender
|
|||
defaultMat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f));
|
||||
stateset->setAttribute(defaultMat);
|
||||
|
||||
SceneUtil::ShadowManager::disableShadowsForStateSet(stateset);
|
||||
SceneUtil::ShadowManager::disableShadowsForStateSet(Settings::shadows(), *stateset);
|
||||
|
||||
// assign large value to effectively turn off fog
|
||||
// shaders don't respect glDisable(GL_FOG)
|
||||
|
|
|
@ -124,6 +124,8 @@ namespace MWRender
|
|||
auto findArmorAddons = [&](const ESM4::Armor* armor) {
|
||||
for (ESM::FormId armaId : armor->mAddOns)
|
||||
{
|
||||
if (armaId.isZeroOrUnset())
|
||||
continue;
|
||||
const ESM4::ArmorAddon* arma = store->get<ESM4::ArmorAddon>().search(armaId);
|
||||
if (!arma)
|
||||
{
|
||||
|
|
|
@ -763,7 +763,7 @@ namespace MWRender
|
|||
|
||||
lightSource->setStateSetModes(*stateset, osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE);
|
||||
|
||||
SceneUtil::ShadowManager::disableShadowsForStateSet(stateset);
|
||||
SceneUtil::ShadowManager::disableShadowsForStateSet(Settings::shadows(), *stateset);
|
||||
|
||||
// override sun for local map
|
||||
SceneUtil::configureStateSetSunOverride(static_cast<SceneUtil::LightManager*>(mSceneRoot), light, stateset);
|
||||
|
|
|
@ -20,11 +20,6 @@ namespace MWRender
|
|||
|
||||
mResolveProgram = shaderManager.getProgram(vertex, std::move(resolveFragment));
|
||||
mLuminanceProgram = shaderManager.getProgram(vertex, std::move(luminanceFragment));
|
||||
}
|
||||
|
||||
void LuminanceCalculator::compile()
|
||||
{
|
||||
int mipmapLevels = osg::Image::computeNumberOfMipmapLevels(mWidth, mHeight);
|
||||
|
||||
for (auto& buffer : mBuffers)
|
||||
{
|
||||
|
@ -38,7 +33,6 @@ namespace MWRender
|
|||
osg::Texture2D::MIN_FILTER, osg::Texture2D::LINEAR_MIPMAP_NEAREST);
|
||||
buffer.mipmappedSceneLuminanceTex->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::LINEAR);
|
||||
buffer.mipmappedSceneLuminanceTex->setTextureSize(mWidth, mHeight);
|
||||
buffer.mipmappedSceneLuminanceTex->setNumMipmapLevels(mipmapLevels);
|
||||
|
||||
buffer.luminanceTex = new osg::Texture2D;
|
||||
buffer.luminanceTex->setInternalFormat(GL_R16F);
|
||||
|
@ -62,14 +56,6 @@ namespace MWRender
|
|||
buffer.luminanceProxyFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0,
|
||||
osg::FrameBufferAttachment(buffer.luminanceProxyTex));
|
||||
|
||||
buffer.resolveSceneLumFbo = new osg::FrameBufferObject;
|
||||
buffer.resolveSceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0,
|
||||
osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex, mipmapLevels - 1));
|
||||
|
||||
buffer.sceneLumFbo = new osg::FrameBufferObject;
|
||||
buffer.sceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0,
|
||||
osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex));
|
||||
|
||||
buffer.sceneLumSS = new osg::StateSet;
|
||||
buffer.sceneLumSS->setAttributeAndModes(mLuminanceProgram);
|
||||
buffer.sceneLumSS->addUniform(new osg::Uniform("sceneTex", 0));
|
||||
|
@ -84,6 +70,26 @@ namespace MWRender
|
|||
|
||||
mBuffers[0].resolveSS->setTextureAttributeAndModes(1, mBuffers[1].luminanceTex);
|
||||
mBuffers[1].resolveSS->setTextureAttributeAndModes(1, mBuffers[0].luminanceTex);
|
||||
}
|
||||
|
||||
void LuminanceCalculator::compile()
|
||||
{
|
||||
int mipmapLevels = osg::Image::computeNumberOfMipmapLevels(mWidth, mHeight);
|
||||
|
||||
for (auto& buffer : mBuffers)
|
||||
{
|
||||
buffer.mipmappedSceneLuminanceTex->setTextureSize(mWidth, mHeight);
|
||||
buffer.mipmappedSceneLuminanceTex->setNumMipmapLevels(mipmapLevels);
|
||||
buffer.mipmappedSceneLuminanceTex->dirtyTextureObject();
|
||||
|
||||
buffer.resolveSceneLumFbo = new osg::FrameBufferObject;
|
||||
buffer.resolveSceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0,
|
||||
osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex, mipmapLevels - 1));
|
||||
|
||||
buffer.sceneLumFbo = new osg::FrameBufferObject;
|
||||
buffer.sceneLumFbo->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0,
|
||||
osg::FrameBufferAttachment(buffer.mipmappedSceneLuminanceTex));
|
||||
}
|
||||
|
||||
mCompiled = true;
|
||||
}
|
||||
|
@ -114,13 +120,14 @@ namespace MWRender
|
|||
buffer.luminanceProxyFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER);
|
||||
ext->glBlitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
|
||||
if (dirty)
|
||||
if (mIsBlank)
|
||||
{
|
||||
// Use current frame data for previous frame to warm up calculations and prevent popin
|
||||
mBuffers[(frameId + 1) % 2].resolveFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER);
|
||||
ext->glBlitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
|
||||
buffer.luminanceProxyFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER);
|
||||
mIsBlank = false;
|
||||
}
|
||||
|
||||
buffer.resolveFbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER);
|
||||
|
|
|
@ -58,6 +58,7 @@ namespace MWRender
|
|||
|
||||
bool mCompiled = false;
|
||||
bool mEnabled = false;
|
||||
bool mIsBlank = true;
|
||||
|
||||
int mWidth = 1;
|
||||
int mHeight = 1;
|
||||
|
|
|
@ -10,9 +10,11 @@
|
|||
|
||||
namespace MWRender
|
||||
{
|
||||
PingPongCanvas::PingPongCanvas(Shader::ShaderManager& shaderManager)
|
||||
PingPongCanvas::PingPongCanvas(
|
||||
Shader::ShaderManager& shaderManager, const std::shared_ptr<LuminanceCalculator>& luminanceCalculator)
|
||||
: mFallbackStateSet(new osg::StateSet)
|
||||
, mMultiviewResolveStateSet(new osg::StateSet)
|
||||
, mLuminanceCalculator(luminanceCalculator)
|
||||
{
|
||||
setUseDisplayList(false);
|
||||
setUseVertexBufferObjects(true);
|
||||
|
@ -26,8 +28,7 @@ namespace MWRender
|
|||
|
||||
addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES, 0, 3));
|
||||
|
||||
mLuminanceCalculator = LuminanceCalculator(shaderManager);
|
||||
mLuminanceCalculator.disable();
|
||||
mLuminanceCalculator->disable();
|
||||
|
||||
Shader::ShaderManager::DefineMap defines;
|
||||
Stereo::shaderStereoDefines(defines);
|
||||
|
@ -142,7 +143,7 @@ namespace MWRender
|
|||
.getTexture());
|
||||
}
|
||||
|
||||
mLuminanceCalculator.dirty(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight());
|
||||
mLuminanceCalculator->dirty(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight());
|
||||
|
||||
if (Stereo::getStereo())
|
||||
mRenderViewport
|
||||
|
@ -158,11 +159,11 @@ namespace MWRender
|
|||
{ GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT2_EXT },
|
||||
{ GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT } } };
|
||||
|
||||
(mAvgLum) ? mLuminanceCalculator.enable() : mLuminanceCalculator.disable();
|
||||
(mAvgLum) ? mLuminanceCalculator->enable() : mLuminanceCalculator->disable();
|
||||
|
||||
// A histogram based approach is superior way to calculate scene luminance. Using mipmaps is more broadly
|
||||
// supported, so that's what we use for now.
|
||||
mLuminanceCalculator.draw(*this, renderInfo, state, ext, frameId);
|
||||
mLuminanceCalculator->draw(*this, renderInfo, state, ext, frameId);
|
||||
|
||||
auto buffer = buffers[0];
|
||||
|
||||
|
@ -195,6 +196,39 @@ namespace MWRender
|
|||
}
|
||||
};
|
||||
|
||||
// When textures are created (or resized) we need to either dirty them and/or clear them.
|
||||
// Otherwise, there will be undefined behavior when reading from a texture that has yet to be written to in a
|
||||
// later pass.
|
||||
for (const auto& attachment : mDirtyAttachments)
|
||||
{
|
||||
const auto [w, h]
|
||||
= attachment.mSize.get(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight());
|
||||
|
||||
attachment.mTarget->setTextureSize(w, h);
|
||||
if (attachment.mMipMap)
|
||||
attachment.mTarget->setNumMipmapLevels(osg::Image::computeNumberOfMipmapLevels(w, h));
|
||||
attachment.mTarget->dirtyTextureObject();
|
||||
|
||||
osg::ref_ptr<osg::FrameBufferObject> fbo = new osg::FrameBufferObject;
|
||||
|
||||
fbo->setAttachment(
|
||||
osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, osg::FrameBufferAttachment(attachment.mTarget));
|
||||
fbo->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER);
|
||||
|
||||
glViewport(0, 0, attachment.mTarget->getTextureWidth(), attachment.mTarget->getTextureHeight());
|
||||
state.haveAppliedAttribute(osg::StateAttribute::VIEWPORT);
|
||||
glClearColor(attachment.mClearColor.r(), attachment.mClearColor.g(), attachment.mClearColor.b(),
|
||||
attachment.mClearColor.a());
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
if (attachment.mTarget->getNumMipmapLevels() > 0)
|
||||
{
|
||||
state.setActiveTextureUnit(0);
|
||||
state.applyTextureAttribute(0, attachment.mTarget);
|
||||
ext->glGenerateMipmap(GL_TEXTURE_2D);
|
||||
}
|
||||
}
|
||||
|
||||
for (const size_t& index : filtered)
|
||||
{
|
||||
const auto& node = mPasses[index];
|
||||
|
@ -202,8 +236,8 @@ namespace MWRender
|
|||
node.mRootStateSet->setTextureAttribute(PostProcessor::Unit_Depth, mTextureDepth);
|
||||
|
||||
if (mAvgLum)
|
||||
node.mRootStateSet->setTextureAttribute(
|
||||
PostProcessor::TextureUnits::Unit_EyeAdaptation, mLuminanceCalculator.getLuminanceTexture(frameId));
|
||||
node.mRootStateSet->setTextureAttribute(PostProcessor::TextureUnits::Unit_EyeAdaptation,
|
||||
mLuminanceCalculator->getLuminanceTexture(frameId));
|
||||
|
||||
if (mTextureNormals)
|
||||
node.mRootStateSet->setTextureAttribute(PostProcessor::TextureUnits::Unit_Normals, mTextureNormals);
|
||||
|
@ -238,6 +272,23 @@ namespace MWRender
|
|||
|
||||
if (pass.mRenderTarget)
|
||||
{
|
||||
if (mDirtyAttachments.size() > 0)
|
||||
{
|
||||
const auto [w, h]
|
||||
= pass.mSize.get(mTextureScene->getTextureWidth(), mTextureScene->getTextureHeight());
|
||||
|
||||
// Custom render targets must be shared between frame ids, so it's impossible to double buffer
|
||||
// without expensive copies. That means the only thread-safe place to resize is in the draw
|
||||
// thread.
|
||||
osg::Texture2D* texture = const_cast<osg::Texture2D*>(dynamic_cast<const osg::Texture2D*>(
|
||||
pass.mRenderTarget->getAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0)
|
||||
.getTexture()));
|
||||
|
||||
texture->setTextureSize(w, h);
|
||||
texture->setNumMipmapLevels(pass.mRenderTexture->getNumMipmapLevels());
|
||||
texture->dirtyTextureObject();
|
||||
}
|
||||
|
||||
pass.mRenderTarget->apply(state, osg::FrameBufferObject::DRAW_FRAMEBUFFER);
|
||||
|
||||
if (pass.mRenderTexture->getNumMipmapLevels() > 0)
|
||||
|
@ -311,5 +362,7 @@ namespace MWRender
|
|||
{
|
||||
bindDestinationFbo();
|
||||
}
|
||||
|
||||
mDirtyAttachments.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,8 @@ namespace MWRender
|
|||
class PingPongCanvas : public osg::Geometry
|
||||
{
|
||||
public:
|
||||
PingPongCanvas(Shader::ShaderManager& shaderManager);
|
||||
PingPongCanvas(
|
||||
Shader::ShaderManager& shaderManager, const std::shared_ptr<LuminanceCalculator>& luminanceCalculator);
|
||||
|
||||
void drawGeometry(osg::RenderInfo& renderInfo) const;
|
||||
|
||||
|
@ -30,6 +31,11 @@ namespace MWRender
|
|||
|
||||
void dirty() { mDirty = true; }
|
||||
|
||||
void setDirtyAttachments(const std::vector<fx::Types::RenderTarget>& attachments)
|
||||
{
|
||||
mDirtyAttachments = attachments;
|
||||
}
|
||||
|
||||
const fx::DispatchArray& getPasses() { return mPasses; }
|
||||
|
||||
void setPasses(fx::DispatchArray&& passes);
|
||||
|
@ -65,11 +71,12 @@ namespace MWRender
|
|||
osg::ref_ptr<osg::Texture> mTextureNormals;
|
||||
|
||||
mutable bool mDirty = false;
|
||||
mutable std::vector<fx::Types::RenderTarget> mDirtyAttachments;
|
||||
mutable osg::ref_ptr<osg::Viewport> mRenderViewport;
|
||||
mutable osg::ref_ptr<osg::FrameBufferObject> mMultiviewResolveFramebuffer;
|
||||
mutable osg::ref_ptr<osg::FrameBufferObject> mDestinationFBO;
|
||||
mutable std::array<osg::ref_ptr<osg::FrameBufferObject>, 3> mFbos;
|
||||
mutable LuminanceCalculator mLuminanceCalculator;
|
||||
mutable std::shared_ptr<LuminanceCalculator> mLuminanceCalculator;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -118,9 +118,14 @@ namespace MWRender
|
|||
, mUsePostProcessing(Settings::postProcessing().mEnabled)
|
||||
, mSamples(Settings::video().mAntialiasing)
|
||||
, mPingPongCull(new PingPongCull(this))
|
||||
, mCanvases({ new PingPongCanvas(mRendering.getResourceSystem()->getSceneManager()->getShaderManager()),
|
||||
new PingPongCanvas(mRendering.getResourceSystem()->getSceneManager()->getShaderManager()) })
|
||||
{
|
||||
auto& shaderManager = mRendering.getResourceSystem()->getSceneManager()->getShaderManager();
|
||||
|
||||
std::shared_ptr<LuminanceCalculator> luminanceCalculator = std::make_shared<LuminanceCalculator>(shaderManager);
|
||||
|
||||
for (auto& canvas : mCanvases)
|
||||
canvas = new PingPongCanvas(shaderManager, luminanceCalculator);
|
||||
|
||||
mHUDCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF);
|
||||
mHUDCamera->setRenderOrder(osg::Camera::POST_RENDER);
|
||||
mHUDCamera->setClearColor(osg::Vec4(0.45, 0.45, 0.14, 1.0));
|
||||
|
@ -139,8 +144,7 @@ namespace MWRender
|
|||
if (Settings::shaders().mSoftParticles || Settings::postProcessing().mTransparentPostpass)
|
||||
{
|
||||
mTransparentDepthPostPass
|
||||
= new TransparentDepthBinCallback(mRendering.getResourceSystem()->getSceneManager()->getShaderManager(),
|
||||
Settings::postProcessing().mTransparentPostpass);
|
||||
= new TransparentDepthBinCallback(shaderManager, Settings::postProcessing().mTransparentPostpass);
|
||||
osgUtil::RenderBin::getRenderBinPrototype("DepthSortedBin")->setDrawCallback(mTransparentDepthPostPass);
|
||||
}
|
||||
|
||||
|
@ -211,25 +215,14 @@ namespace MWRender
|
|||
if (Stereo::getStereo())
|
||||
Stereo::Manager::instance().screenResolutionChanged();
|
||||
|
||||
auto width = renderWidth();
|
||||
auto height = renderHeight();
|
||||
for (auto& technique : mTechniques)
|
||||
{
|
||||
for (auto& [name, rt] : technique->getRenderTargetsMap())
|
||||
{
|
||||
const auto [w, h] = rt.mSize.get(width, height);
|
||||
rt.mTarget->setTextureSize(w, h);
|
||||
}
|
||||
}
|
||||
|
||||
size_t frameId = frame() % 2;
|
||||
|
||||
createObjectsForFrame(frameId);
|
||||
|
||||
mRendering.updateProjectionMatrix();
|
||||
mRendering.setScreenRes(width, height);
|
||||
mRendering.setScreenRes(renderWidth(), renderHeight());
|
||||
|
||||
dirtyTechniques();
|
||||
dirtyTechniques(true);
|
||||
|
||||
mDirty = true;
|
||||
mDirtyFrameId = !frameId;
|
||||
|
@ -534,7 +527,7 @@ namespace MWRender
|
|||
mCanvases[frameId]->dirty();
|
||||
}
|
||||
|
||||
void PostProcessor::dirtyTechniques()
|
||||
void PostProcessor::dirtyTechniques(bool dirtyAttachments)
|
||||
{
|
||||
size_t frameId = frame() % 2;
|
||||
|
||||
|
@ -548,6 +541,8 @@ namespace MWRender
|
|||
mNormals = false;
|
||||
mPassLights = false;
|
||||
|
||||
std::vector<fx::Types::RenderTarget> attachmentsToDirty;
|
||||
|
||||
for (const auto& technique : mTechniques)
|
||||
{
|
||||
if (!technique->isValid())
|
||||
|
@ -613,8 +608,6 @@ namespace MWRender
|
|||
uniform->mName.c_str(), *type, uniform->getNumElements()));
|
||||
}
|
||||
|
||||
std::unordered_map<osg::Texture2D*, osg::Texture2D*> renderTargetCache;
|
||||
|
||||
for (const auto& pass : technique->getPasses())
|
||||
{
|
||||
int subTexUnit = texUnit;
|
||||
|
@ -626,32 +619,39 @@ namespace MWRender
|
|||
|
||||
if (!pass->getTarget().empty())
|
||||
{
|
||||
const auto& rt = technique->getRenderTargetsMap()[pass->getTarget()];
|
||||
|
||||
const auto [w, h] = rt.mSize.get(renderWidth(), renderHeight());
|
||||
|
||||
subPass.mRenderTexture = new osg::Texture2D(*rt.mTarget);
|
||||
renderTargetCache[rt.mTarget] = subPass.mRenderTexture;
|
||||
subPass.mRenderTexture->setTextureSize(w, h);
|
||||
subPass.mRenderTexture->setName(std::string(pass->getTarget()));
|
||||
|
||||
if (rt.mMipMap)
|
||||
subPass.mRenderTexture->setNumMipmapLevels(osg::Image::computeNumberOfMipmapLevels(w, h));
|
||||
auto& renderTarget = technique->getRenderTargetsMap()[pass->getTarget()];
|
||||
subPass.mSize = renderTarget.mSize;
|
||||
subPass.mRenderTexture = renderTarget.mTarget;
|
||||
subPass.mMipMap = renderTarget.mMipMap;
|
||||
|
||||
subPass.mRenderTarget = new osg::FrameBufferObject;
|
||||
subPass.mRenderTarget->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0,
|
||||
osg::FrameBufferAttachment(subPass.mRenderTexture));
|
||||
|
||||
const auto [w, h] = renderTarget.mSize.get(renderWidth(), renderHeight());
|
||||
subPass.mStateSet->setAttributeAndModes(new osg::Viewport(0, 0, w, h));
|
||||
|
||||
if (std::find_if(attachmentsToDirty.cbegin(), attachmentsToDirty.cend(),
|
||||
[renderTarget](const auto& rt) { return renderTarget.mTarget == rt.mTarget; })
|
||||
== attachmentsToDirty.cend())
|
||||
{
|
||||
attachmentsToDirty.push_back(fx::Types::RenderTarget(renderTarget));
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& whitelist : pass->getRenderTargets())
|
||||
for (const auto& name : pass->getRenderTargets())
|
||||
{
|
||||
auto it = technique->getRenderTargetsMap().find(whitelist);
|
||||
if (it != technique->getRenderTargetsMap().end() && renderTargetCache[it->second.mTarget])
|
||||
auto& renderTarget = technique->getRenderTargetsMap()[name];
|
||||
subPass.mStateSet->setTextureAttribute(subTexUnit, renderTarget.mTarget);
|
||||
subPass.mStateSet->addUniform(new osg::Uniform(name.c_str(), subTexUnit));
|
||||
|
||||
if (std::find_if(attachmentsToDirty.cbegin(), attachmentsToDirty.cend(),
|
||||
[renderTarget](const auto& rt) { return renderTarget.mTarget == rt.mTarget; })
|
||||
== attachmentsToDirty.cend())
|
||||
{
|
||||
subPass.mStateSet->setTextureAttribute(subTexUnit, renderTargetCache[it->second.mTarget]);
|
||||
subPass.mStateSet->addUniform(new osg::Uniform(std::string(it->first).c_str(), subTexUnit++));
|
||||
attachmentsToDirty.push_back(fx::Types::RenderTarget(renderTarget));
|
||||
}
|
||||
subTexUnit++;
|
||||
}
|
||||
|
||||
node.mPasses.emplace_back(std::move(subPass));
|
||||
|
@ -668,6 +668,9 @@ namespace MWRender
|
|||
hud->updateTechniques();
|
||||
|
||||
mRendering.getSkyManager()->setSunglare(sunglare);
|
||||
|
||||
if (dirtyAttachments)
|
||||
mCanvases[frameId]->setDirtyAttachments(attachmentsToDirty);
|
||||
}
|
||||
|
||||
PostProcessor::Status PostProcessor::enableTechnique(
|
||||
|
@ -681,7 +684,7 @@ namespace MWRender
|
|||
int pos = std::min<int>(location.value_or(mTechniques.size()), mTechniques.size());
|
||||
|
||||
mTechniques.insert(mTechniques.begin() + pos, technique);
|
||||
dirtyTechniques();
|
||||
dirtyTechniques(Settings::ShaderManager::get().getMode() == Settings::ShaderManager::Mode::Debug);
|
||||
|
||||
return Status_Toggled;
|
||||
}
|
||||
|
@ -774,7 +777,7 @@ namespace MWRender
|
|||
for (auto& technique : mTemplates)
|
||||
technique->compile();
|
||||
|
||||
dirtyTechniques();
|
||||
dirtyTechniques(true);
|
||||
}
|
||||
|
||||
void PostProcessor::disableDynamicShaders()
|
||||
|
|
|
@ -204,7 +204,7 @@ namespace MWRender
|
|||
|
||||
void createObjectsForFrame(size_t frameId);
|
||||
|
||||
void dirtyTechniques();
|
||||
void dirtyTechniques(bool dirtyAttachments = false);
|
||||
|
||||
void update(size_t frameId);
|
||||
|
||||
|
|
|
@ -331,8 +331,8 @@ namespace MWRender
|
|||
// Shadows and radial fog have problems with fixed-function mode.
|
||||
bool forceShaders = Settings::fog().mRadialFog || Settings::fog().mExponentialFog
|
||||
|| Settings::shaders().mSoftParticles || Settings::shaders().mForceShaders
|
||||
|| Settings::Manager::getBool("enable shadows", "Shadows")
|
||||
|| lightingMethod != SceneUtil::LightingMethod::FFP || reverseZ || mSkyBlending || Stereo::getMultiview();
|
||||
|| Settings::shadows().mEnableShadows || lightingMethod != SceneUtil::LightingMethod::FFP || reverseZ
|
||||
|| mSkyBlending || Stereo::getMultiview();
|
||||
resourceSystem->getSceneManager()->setForceShaders(forceShaders);
|
||||
|
||||
// FIXME: calling dummy method because terrain needs to know whether lighting is clamped
|
||||
|
@ -367,22 +367,22 @@ namespace MWRender
|
|||
sceneRoot->setName("Scene Root");
|
||||
|
||||
int shadowCastingTraversalMask = Mask_Scene;
|
||||
if (Settings::Manager::getBool("actor shadows", "Shadows"))
|
||||
if (Settings::shadows().mActorShadows)
|
||||
shadowCastingTraversalMask |= Mask_Actor;
|
||||
if (Settings::Manager::getBool("player shadows", "Shadows"))
|
||||
if (Settings::shadows().mPlayerShadows)
|
||||
shadowCastingTraversalMask |= Mask_Player;
|
||||
|
||||
int indoorShadowCastingTraversalMask = shadowCastingTraversalMask;
|
||||
if (Settings::Manager::getBool("object shadows", "Shadows"))
|
||||
if (Settings::shadows().mObjectShadows)
|
||||
shadowCastingTraversalMask |= (Mask_Object | Mask_Static);
|
||||
if (Settings::Manager::getBool("terrain shadows", "Shadows"))
|
||||
if (Settings::shadows().mTerrainShadows)
|
||||
shadowCastingTraversalMask |= Mask_Terrain;
|
||||
|
||||
mShadowManager = std::make_unique<SceneUtil::ShadowManager>(sceneRoot, mRootNode, shadowCastingTraversalMask,
|
||||
indoorShadowCastingTraversalMask, Mask_Terrain | Mask_Object | Mask_Static,
|
||||
indoorShadowCastingTraversalMask, Mask_Terrain | Mask_Object | Mask_Static, Settings::shadows(),
|
||||
mResourceSystem->getSceneManager()->getShaderManager());
|
||||
|
||||
Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines();
|
||||
Shader::ShaderManager::DefineMap shadowDefines = mShadowManager->getShadowDefines(Settings::shadows());
|
||||
Shader::ShaderManager::DefineMap lightDefines = sceneRoot->getLightDefines();
|
||||
Shader::ShaderManager::DefineMap globalDefines
|
||||
= mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines();
|
||||
|
@ -702,7 +702,7 @@ namespace MWRender
|
|||
{
|
||||
// need to wrap this in a StateUpdater?
|
||||
mSunLight->setDiffuse(diffuse);
|
||||
mSunLight->setSpecular(specular);
|
||||
mSunLight->setSpecular(osg::Vec4f(specular.x(), specular.y(), specular.z(), specular.w() * sunVis));
|
||||
|
||||
mPostProcessor->getStateUpdater()->setSunColor(diffuse);
|
||||
mPostProcessor->getStateUpdater()->setSunVis(sunVis);
|
||||
|
@ -770,7 +770,7 @@ namespace MWRender
|
|||
if (enabled)
|
||||
mShadowManager->enableOutdoorMode();
|
||||
else
|
||||
mShadowManager->enableIndoorMode();
|
||||
mShadowManager->enableIndoorMode(Settings::shadows());
|
||||
mPostProcessor->getStateUpdater()->setIsInterior(!enabled);
|
||||
}
|
||||
|
||||
|
@ -1319,6 +1319,7 @@ namespace MWRender
|
|||
const float lodFactor = Settings::terrain().mLodFactor;
|
||||
const bool groundcover = Settings::groundcover().mEnabled;
|
||||
const bool distantTerrain = Settings::terrain().mDistantTerrain;
|
||||
const double expiryDelay = Settings::cells().mCacheExpiryDelay;
|
||||
if (distantTerrain || groundcover)
|
||||
{
|
||||
const int compMapResolution = Settings::terrain().mCompositeMapResolution;
|
||||
|
@ -1329,7 +1330,7 @@ namespace MWRender
|
|||
const bool debugChunks = Settings::terrain().mDebugChunks;
|
||||
auto quadTreeWorld = std::make_unique<Terrain::QuadTreeWorld>(mSceneRoot, mRootNode, mResourceSystem,
|
||||
mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug, compMapResolution, compMapLevel,
|
||||
lodFactor, vertexLodMod, maxCompGeometrySize, debugChunks, worldspace);
|
||||
lodFactor, vertexLodMod, maxCompGeometrySize, debugChunks, worldspace, expiryDelay);
|
||||
if (Settings::terrain().mObjectPaging)
|
||||
{
|
||||
newChunkMgr.mObjectPaging
|
||||
|
@ -1351,7 +1352,7 @@ namespace MWRender
|
|||
}
|
||||
else
|
||||
newChunkMgr.mTerrain = std::make_unique<Terrain::TerrainGrid>(mSceneRoot, mRootNode, mResourceSystem,
|
||||
mTerrainStorage.get(), Mask_Terrain, worldspace, Mask_PreCompile, Mask_Debug);
|
||||
mTerrainStorage.get(), Mask_Terrain, worldspace, expiryDelay, Mask_PreCompile, Mask_Debug);
|
||||
|
||||
newChunkMgr.mTerrain->setTargetFrameRate(Settings::cells().mTargetFramerate);
|
||||
float distanceMult = std::cos(osg::DegreesToRadians(std::min(mFieldOfView, 140.f)) / 2.f);
|
||||
|
|
|
@ -220,7 +220,7 @@ namespace
|
|||
camera->setNodeMask(MWRender::Mask_RenderToTexture);
|
||||
camera->setCullMask(MWRender::Mask_Sky);
|
||||
camera->addChild(mEarlyRenderBinRoot);
|
||||
SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet());
|
||||
SceneUtil::ShadowManager::disableShadowsForStateSet(Settings::shadows(), *camera->getOrCreateStateSet());
|
||||
}
|
||||
|
||||
private:
|
||||
|
@ -271,7 +271,7 @@ namespace MWRender
|
|||
if (!mSceneManager->getForceShaders())
|
||||
skyroot->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(),
|
||||
osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED | osg::StateAttribute::ON);
|
||||
SceneUtil::ShadowManager::disableShadowsForStateSet(skyroot->getOrCreateStateSet());
|
||||
SceneUtil::ShadowManager::disableShadowsForStateSet(Settings::shadows(), *skyroot->getOrCreateStateSet());
|
||||
parentNode->addChild(skyroot);
|
||||
|
||||
mEarlyRenderBinRoot = new osg::Group;
|
||||
|
|
|
@ -265,7 +265,8 @@ namespace MWRender
|
|||
camera->setNodeMask(Mask_RenderToTexture);
|
||||
|
||||
if (Settings::water().mRefractionScale != 1) // TODO: to be removed with issue #5709
|
||||
SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet());
|
||||
SceneUtil::ShadowManager::disableShadowsForStateSet(
|
||||
Settings::shadows(), *camera->getOrCreateStateSet());
|
||||
}
|
||||
|
||||
void apply(osg::Camera* camera) override
|
||||
|
@ -341,7 +342,7 @@ namespace MWRender
|
|||
camera->addChild(mClipCullNode);
|
||||
camera->setNodeMask(Mask_RenderToTexture);
|
||||
|
||||
SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet());
|
||||
SceneUtil::ShadowManager::disableShadowsForStateSet(Settings::shadows(), *camera->getOrCreateStateSet());
|
||||
}
|
||||
|
||||
void apply(osg::Camera* camera) override
|
||||
|
|
|
@ -111,12 +111,12 @@ MWWorld::ContainerStoreIterator MWWorld::ContainerStore::getState(
|
|||
}
|
||||
|
||||
void MWWorld::ContainerStore::storeEquipmentState(
|
||||
const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const
|
||||
const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const
|
||||
{
|
||||
}
|
||||
|
||||
void MWWorld::ContainerStore::readEquipmentState(
|
||||
const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory)
|
||||
const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -128,7 +128,7 @@ void MWWorld::ContainerStore::storeState(const LiveCellRef<T>& ref, ESM::ObjectS
|
|||
|
||||
template <typename T>
|
||||
void MWWorld::ContainerStore::storeStates(
|
||||
const CellRefList<T>& collection, ESM::InventoryState& inventory, int& index, bool equipable) const
|
||||
const CellRefList<T>& collection, ESM::InventoryState& inventory, size_t& index, bool equipable) const
|
||||
{
|
||||
for (const LiveCellRef<T>& liveCellRef : collection.mList)
|
||||
{
|
||||
|
@ -926,7 +926,7 @@ void MWWorld::ContainerStore::writeState(ESM::InventoryState& state) const
|
|||
{
|
||||
state.mItems.clear();
|
||||
|
||||
int index = 0;
|
||||
size_t index = 0;
|
||||
storeStates(potions, state, index);
|
||||
storeStates(appas, state, index);
|
||||
storeStates(armors, state, index, true);
|
||||
|
@ -947,12 +947,12 @@ void MWWorld::ContainerStore::readState(const ESM::InventoryState& inventory)
|
|||
mModified = true;
|
||||
mResolved = true;
|
||||
|
||||
int index = 0;
|
||||
size_t index = 0;
|
||||
for (const ESM::ObjectState& state : inventory.mItems)
|
||||
{
|
||||
int type = MWBase::Environment::get().getESMStore()->find(state.mRef.mRefID);
|
||||
|
||||
int thisIndex = index++;
|
||||
size_t thisIndex = index++;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
|
|
|
@ -161,16 +161,16 @@ namespace MWWorld
|
|||
void storeState(const LiveCellRef<T>& ref, ESM::ObjectState& state) const;
|
||||
|
||||
template <typename T>
|
||||
void storeStates(
|
||||
const CellRefList<T>& collection, ESM::InventoryState& inventory, int& index, bool equipable = false) const;
|
||||
void storeStates(const CellRefList<T>& collection, ESM::InventoryState& inventory, size_t& index,
|
||||
bool equipable = false) const;
|
||||
|
||||
void updateRechargingItems();
|
||||
|
||||
virtual void storeEquipmentState(
|
||||
const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const;
|
||||
const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const;
|
||||
|
||||
virtual void readEquipmentState(
|
||||
const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory);
|
||||
const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory);
|
||||
|
||||
public:
|
||||
ContainerStore();
|
||||
|
|
|
@ -138,6 +138,59 @@ namespace
|
|||
return npcsToReplace;
|
||||
}
|
||||
|
||||
template <class RecordType>
|
||||
std::vector<RecordType> getSpellsToReplace(
|
||||
const MWWorld::Store<RecordType>& spells, const MWWorld::Store<ESM::MagicEffect>& magicEffects)
|
||||
{
|
||||
std::vector<RecordType> spellsToReplace;
|
||||
|
||||
for (RecordType spell : spells)
|
||||
{
|
||||
if (spell.mEffects.mList.empty())
|
||||
continue;
|
||||
|
||||
bool changed = false;
|
||||
auto iter = spell.mEffects.mList.begin();
|
||||
while (iter != spell.mEffects.mList.end())
|
||||
{
|
||||
const ESM::MagicEffect* mgef = magicEffects.search(iter->mEffectID);
|
||||
if (!mgef)
|
||||
{
|
||||
Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId
|
||||
<< ": dropping invalid effect (index " << iter->mEffectID << ")";
|
||||
iter = spell.mEffects.mList.erase(iter);
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) && iter->mAttribute != -1)
|
||||
{
|
||||
iter->mAttribute = -1;
|
||||
Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId
|
||||
<< ": dropping unexpected attribute argument of "
|
||||
<< ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect";
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) && iter->mSkill != -1)
|
||||
{
|
||||
iter->mSkill = -1;
|
||||
Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId
|
||||
<< ": dropping unexpected skill argument of "
|
||||
<< ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect";
|
||||
changed = true;
|
||||
}
|
||||
|
||||
++iter;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
spellsToReplace.emplace_back(spell);
|
||||
}
|
||||
|
||||
return spellsToReplace;
|
||||
}
|
||||
|
||||
// Custom enchanted items can reference scripts that no longer exist, this doesn't necessarily mean the base item no
|
||||
// longer exists however. So instead of removing the item altogether, we're only removing the script.
|
||||
template <class MapT>
|
||||
|
@ -538,71 +591,24 @@ namespace MWWorld
|
|||
|
||||
removeMissingScripts(getWritable<ESM::Script>(), getWritable<ESM::Creature>().mStatic);
|
||||
|
||||
// Validate spell effects for invalid arguments
|
||||
std::vector<ESM::Spell> spellsToReplace;
|
||||
// Validate spell effects and enchantments for invalid arguments
|
||||
auto& spells = getWritable<ESM::Spell>();
|
||||
for (ESM::Spell spell : spells)
|
||||
{
|
||||
if (spell.mEffects.mList.empty())
|
||||
continue;
|
||||
|
||||
bool changed = false;
|
||||
auto iter = spell.mEffects.mList.begin();
|
||||
while (iter != spell.mEffects.mList.end())
|
||||
{
|
||||
const ESM::MagicEffect* mgef = getWritable<ESM::MagicEffect>().search(iter->mEffectID);
|
||||
if (!mgef)
|
||||
{
|
||||
Log(Debug::Verbose) << "Spell '" << spell.mId << "' has an invalid effect (index "
|
||||
<< iter->mEffectID << ") present. Dropping the effect.";
|
||||
iter = spell.mEffects.mList.erase(iter);
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mgef->mData.mFlags & ESM::MagicEffect::TargetSkill)
|
||||
{
|
||||
if (iter->mAttribute != -1)
|
||||
{
|
||||
iter->mAttribute = -1;
|
||||
Log(Debug::Verbose)
|
||||
<< ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect of spell '" << spell.mId
|
||||
<< "' has an attribute argument present. Dropping the argument.";
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
else if (mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute)
|
||||
{
|
||||
if (iter->mSkill != -1)
|
||||
{
|
||||
iter->mSkill = -1;
|
||||
Log(Debug::Verbose)
|
||||
<< ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect of spell '" << spell.mId
|
||||
<< "' has a skill argument present. Dropping the argument.";
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
else if (iter->mSkill != -1 || iter->mAttribute != -1)
|
||||
{
|
||||
iter->mSkill = -1;
|
||||
iter->mAttribute = -1;
|
||||
Log(Debug::Verbose) << ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect of spell '"
|
||||
<< spell.mId << "' has argument(s) present. Dropping the argument(s).";
|
||||
changed = true;
|
||||
}
|
||||
|
||||
++iter;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
spellsToReplace.emplace_back(spell);
|
||||
}
|
||||
auto& enchantments = getWritable<ESM::Enchantment>();
|
||||
auto& magicEffects = getWritable<ESM::MagicEffect>();
|
||||
|
||||
std::vector<ESM::Spell> spellsToReplace = getSpellsToReplace(spells, magicEffects);
|
||||
for (const ESM::Spell& spell : spellsToReplace)
|
||||
{
|
||||
spells.eraseStatic(spell.mId);
|
||||
spells.insertStatic(spell);
|
||||
}
|
||||
|
||||
std::vector<ESM::Enchantment> enchantmentsToReplace = getSpellsToReplace(enchantments, magicEffects);
|
||||
for (const ESM::Enchantment& enchantment : enchantmentsToReplace)
|
||||
{
|
||||
enchantments.eraseStatic(enchantment.mId);
|
||||
enchantments.insertStatic(enchantment);
|
||||
}
|
||||
}
|
||||
|
||||
void ESMStore::movePlayerRecord()
|
||||
|
|
|
@ -46,32 +46,32 @@ void MWWorld::InventoryStore::initSlots(TSlots& slots_)
|
|||
}
|
||||
|
||||
void MWWorld::InventoryStore::storeEquipmentState(
|
||||
const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const
|
||||
const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const
|
||||
{
|
||||
for (int i = 0; i < static_cast<int>(mSlots.size()); ++i)
|
||||
for (int32_t i = 0; i < MWWorld::InventoryStore::Slots; ++i)
|
||||
{
|
||||
if (mSlots[i].getType() != -1 && mSlots[i]->getBase() == &ref)
|
||||
{
|
||||
inventory.mEquipmentSlots[index] = i;
|
||||
}
|
||||
inventory.mEquipmentSlots[static_cast<uint32_t>(index)] = i;
|
||||
}
|
||||
|
||||
if (mSelectedEnchantItem.getType() != -1 && mSelectedEnchantItem->getBase() == &ref)
|
||||
inventory.mSelectedEnchantItem = index;
|
||||
}
|
||||
|
||||
void MWWorld::InventoryStore::readEquipmentState(
|
||||
const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory)
|
||||
const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory)
|
||||
{
|
||||
if (index == inventory.mSelectedEnchantItem)
|
||||
mSelectedEnchantItem = iter;
|
||||
|
||||
std::map<int, int>::const_iterator found = inventory.mEquipmentSlots.find(index);
|
||||
auto found = inventory.mEquipmentSlots.find(index);
|
||||
if (found != inventory.mEquipmentSlots.end())
|
||||
{
|
||||
if (found->second < 0 || found->second >= MWWorld::InventoryStore::Slots)
|
||||
throw std::runtime_error("Invalid slot index in inventory state");
|
||||
|
||||
// make sure the item can actually be equipped in this slot
|
||||
int slot = found->second;
|
||||
int32_t slot = found->second;
|
||||
std::pair<std::vector<int>, bool> allowedSlots = iter->getClass().getEquipmentSlots(*iter);
|
||||
if (!allowedSlots.first.size())
|
||||
return;
|
||||
|
|
|
@ -81,9 +81,9 @@ namespace MWWorld
|
|||
void fireEquipmentChangedEvent();
|
||||
|
||||
void storeEquipmentState(
|
||||
const MWWorld::LiveCellRefBase& ref, int index, ESM::InventoryState& inventory) const override;
|
||||
const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const override;
|
||||
void readEquipmentState(
|
||||
const MWWorld::ContainerStoreIterator& iter, int index, const ESM::InventoryState& inventory) override;
|
||||
const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory) override;
|
||||
|
||||
ContainerStoreIterator findSlot(int slot) const;
|
||||
|
||||
|
|
|
@ -789,8 +789,7 @@ namespace MWWorld
|
|||
mRendering.configureFog(
|
||||
mResult.mFogDepth, underwaterFog, mResult.mDLFogFactor, mResult.mDLFogOffset / 100.0f, mResult.mFogColor);
|
||||
mRendering.setAmbientColour(mResult.mAmbientColor);
|
||||
mRendering.setSunColour(
|
||||
mResult.mSunColor, mResult.mSunColor * mResult.mGlareView * glareFade, mResult.mGlareView * glareFade);
|
||||
mRendering.setSunColour(mResult.mSunColor, mResult.mSunColor, mResult.mGlareView * glareFade);
|
||||
|
||||
mRendering.getSkyManager()->setWeather(mResult);
|
||||
|
||||
|
|
|
@ -660,8 +660,8 @@ namespace MWWorld
|
|||
|
||||
std::string_view World::getCellName(const MWWorld::Cell& cell) const
|
||||
{
|
||||
if (!cell.isExterior() || !cell.getNameId().empty())
|
||||
return cell.getNameId();
|
||||
if (!cell.isExterior() || !cell.getDisplayName().empty())
|
||||
return cell.getDisplayName();
|
||||
|
||||
return ESM::visit(ESM::VisitOverload{
|
||||
[&](const ESM::Cell& cellIn) -> std::string_view { return getCellName(&cellIn); },
|
||||
|
|
|
@ -411,7 +411,7 @@ namespace
|
|||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
||||
TEST_F(TestBulletNifLoader, for_root_bounding_box_should_return_shape_with_compound_shape_and_box_inside)
|
||||
TEST_F(TestBulletNifLoader, for_root_bounding_box_should_return_shape_with_bounding_box_data)
|
||||
{
|
||||
mNode.mName = "Bounding Box";
|
||||
mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV;
|
||||
|
@ -427,15 +427,11 @@ namespace
|
|||
Resource::BulletShape expected;
|
||||
expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3);
|
||||
expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3);
|
||||
std::unique_ptr<btBoxShape> box(new btBoxShape(btVector3(1, 2, 3)));
|
||||
std::unique_ptr<btCompoundShape> shape(new btCompoundShape);
|
||||
shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release());
|
||||
expected.mCollisionShape.reset(shape.release());
|
||||
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
||||
TEST_F(TestBulletNifLoader, for_child_bounding_box_should_return_shape_with_compound_shape_with_box_inside)
|
||||
TEST_F(TestBulletNifLoader, for_child_bounding_box_should_return_shape_with_bounding_box_data)
|
||||
{
|
||||
mNode.mName = "Bounding Box";
|
||||
mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV;
|
||||
|
@ -453,15 +449,11 @@ namespace
|
|||
Resource::BulletShape expected;
|
||||
expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3);
|
||||
expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3);
|
||||
std::unique_ptr<btBoxShape> box(new btBoxShape(btVector3(1, 2, 3)));
|
||||
std::unique_ptr<btCompoundShape> shape(new btCompoundShape);
|
||||
shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release());
|
||||
expected.mCollisionShape.reset(shape.release());
|
||||
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
||||
TEST_F(TestBulletNifLoader, for_root_with_bounds_and_child_bounding_box_but_should_use_bounding_box)
|
||||
TEST_F(TestBulletNifLoader, for_root_with_bounds_and_child_bounding_box_should_use_bounding_box)
|
||||
{
|
||||
mNode.mName = "Bounding Box";
|
||||
mNode.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV;
|
||||
|
@ -483,10 +475,6 @@ namespace
|
|||
Resource::BulletShape expected;
|
||||
expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3);
|
||||
expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3);
|
||||
std::unique_ptr<btBoxShape> box(new btBoxShape(btVector3(1, 2, 3)));
|
||||
std::unique_ptr<btCompoundShape> shape(new btCompoundShape);
|
||||
shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release());
|
||||
expected.mCollisionShape.reset(shape.release());
|
||||
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
@ -519,10 +507,6 @@ namespace
|
|||
Resource::BulletShape expected;
|
||||
expected.mCollisionBox.mExtents = osg::Vec3f(1, 2, 3);
|
||||
expected.mCollisionBox.mCenter = osg::Vec3f(-1, -2, -3);
|
||||
std::unique_ptr<btBoxShape> box(new btBoxShape(btVector3(1, 2, 3)));
|
||||
std::unique_ptr<btCompoundShape> shape(new btCompoundShape);
|
||||
shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-1, -2, -3)), box.release());
|
||||
expected.mCollisionShape.reset(shape.release());
|
||||
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
@ -555,10 +539,6 @@ namespace
|
|||
Resource::BulletShape expected;
|
||||
expected.mCollisionBox.mExtents = osg::Vec3f(4, 5, 6);
|
||||
expected.mCollisionBox.mCenter = osg::Vec3f(-4, -5, -6);
|
||||
std::unique_ptr<btBoxShape> box(new btBoxShape(btVector3(4, 5, 6)));
|
||||
std::unique_ptr<btCompoundShape> shape(new btCompoundShape);
|
||||
shape->addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(-4, -5, -6)), box.release());
|
||||
expected.mCollisionShape.reset(shape.release());
|
||||
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
@ -977,12 +957,12 @@ namespace
|
|||
}
|
||||
|
||||
TEST_F(TestBulletNifLoader,
|
||||
for_tri_shape_child_node_with_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision)
|
||||
for_root_node_with_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision)
|
||||
{
|
||||
mNiStringExtraData.mData = "NCC__";
|
||||
mNiStringExtraData.recType = Nif::RC_NiStringExtraData;
|
||||
mNiTriShape.mExtra = Nif::ExtraPtr(&mNiStringExtraData);
|
||||
mNiTriShape.mParents.push_back(&mNiNode);
|
||||
mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData);
|
||||
mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) };
|
||||
|
||||
Nif::NIFFile file("test.nif");
|
||||
|
@ -1005,13 +985,13 @@ namespace
|
|||
}
|
||||
|
||||
TEST_F(TestBulletNifLoader,
|
||||
for_tri_shape_child_node_with_not_first_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision)
|
||||
for_root_node_with_not_first_extra_data_string_equal_ncc_should_return_shape_with_cameraonly_collision)
|
||||
{
|
||||
mNiStringExtraData.mNext = Nif::ExtraPtr(&mNiStringExtraData2);
|
||||
mNiStringExtraData2.mData = "NCC__";
|
||||
mNiStringExtraData2.recType = Nif::RC_NiStringExtraData;
|
||||
mNiTriShape.mExtra = Nif::ExtraPtr(&mNiStringExtraData);
|
||||
mNiTriShape.mParents.push_back(&mNiNode);
|
||||
mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData);
|
||||
mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) };
|
||||
|
||||
Nif::NIFFile file("test.nif");
|
||||
|
@ -1032,8 +1012,62 @@ namespace
|
|||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
||||
TEST_F(
|
||||
TestBulletNifLoader, for_root_node_with_extra_data_string_starting_with_nc_should_return_shape_with_nocollision)
|
||||
{
|
||||
mNiStringExtraData.mData = "NC___";
|
||||
mNiStringExtraData.recType = Nif::RC_NiStringExtraData;
|
||||
mNiTriShape.mParents.push_back(&mNiNode);
|
||||
mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData);
|
||||
mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) };
|
||||
|
||||
Nif::NIFFile file("test.nif");
|
||||
file.mRoots.push_back(&mNiNode);
|
||||
file.mHash = mHash;
|
||||
|
||||
const auto result = mLoader.load(file);
|
||||
|
||||
std::unique_ptr<btTriangleMesh> triangles(new btTriangleMesh(false));
|
||||
triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0));
|
||||
std::unique_ptr<btCompoundShape> compound(new btCompoundShape);
|
||||
compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true));
|
||||
|
||||
Resource::BulletShape expected;
|
||||
expected.mCollisionShape.reset(compound.release());
|
||||
expected.mVisualCollisionType = Resource::VisualCollisionType::Default;
|
||||
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
||||
TEST_F(TestBulletNifLoader,
|
||||
for_tri_shape_child_node_with_extra_data_string_starting_with_nc_should_return_shape_with_nocollision)
|
||||
for_root_node_with_not_first_extra_data_string_starting_with_nc_should_return_shape_with_nocollision)
|
||||
{
|
||||
mNiStringExtraData.mNext = Nif::ExtraPtr(&mNiStringExtraData2);
|
||||
mNiStringExtraData2.mData = "NC___";
|
||||
mNiStringExtraData2.recType = Nif::RC_NiStringExtraData;
|
||||
mNiTriShape.mParents.push_back(&mNiNode);
|
||||
mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData);
|
||||
mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) };
|
||||
|
||||
Nif::NIFFile file("test.nif");
|
||||
file.mRoots.push_back(&mNiNode);
|
||||
file.mHash = mHash;
|
||||
|
||||
const auto result = mLoader.load(file);
|
||||
|
||||
std::unique_ptr<btTriangleMesh> triangles(new btTriangleMesh(false));
|
||||
triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0));
|
||||
std::unique_ptr<btCompoundShape> compound(new btCompoundShape);
|
||||
compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true));
|
||||
|
||||
Resource::BulletShape expected;
|
||||
expected.mCollisionShape.reset(compound.release());
|
||||
expected.mVisualCollisionType = Resource::VisualCollisionType::Default;
|
||||
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
||||
TEST_F(TestBulletNifLoader, for_tri_shape_child_node_with_extra_data_string_should_ignore_extra_data)
|
||||
{
|
||||
mNiStringExtraData.mData = "NC___";
|
||||
mNiStringExtraData.recType = Nif::RC_NiStringExtraData;
|
||||
|
@ -1054,35 +1088,6 @@ namespace
|
|||
|
||||
Resource::BulletShape expected;
|
||||
expected.mCollisionShape.reset(compound.release());
|
||||
expected.mVisualCollisionType = Resource::VisualCollisionType::Default;
|
||||
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
||||
TEST_F(TestBulletNifLoader,
|
||||
for_tri_shape_child_node_with_not_first_extra_data_string_starting_with_nc_should_return_shape_with_nocollision)
|
||||
{
|
||||
mNiStringExtraData.mNext = Nif::ExtraPtr(&mNiStringExtraData2);
|
||||
mNiStringExtraData2.mData = "NC___";
|
||||
mNiStringExtraData2.recType = Nif::RC_NiStringExtraData;
|
||||
mNiTriShape.mExtra = Nif::ExtraPtr(&mNiStringExtraData);
|
||||
mNiTriShape.mParents.push_back(&mNiNode);
|
||||
mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) };
|
||||
|
||||
Nif::NIFFile file("test.nif");
|
||||
file.mRoots.push_back(&mNiNode);
|
||||
file.mHash = mHash;
|
||||
|
||||
const auto result = mLoader.load(file);
|
||||
|
||||
std::unique_ptr<btTriangleMesh> triangles(new btTriangleMesh(false));
|
||||
triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0));
|
||||
std::unique_ptr<btCompoundShape> compound(new btCompoundShape);
|
||||
compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true));
|
||||
|
||||
Resource::BulletShape expected;
|
||||
expected.mCollisionShape.reset(compound.release());
|
||||
expected.mVisualCollisionType = Resource::VisualCollisionType::Default;
|
||||
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
@ -1121,33 +1126,13 @@ namespace
|
|||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
||||
TEST_F(TestBulletNifLoader,
|
||||
for_tri_shape_child_node_with_extra_data_string_mrk_should_return_shape_with_null_collision_shape)
|
||||
{
|
||||
mNiStringExtraData.mData = "MRK";
|
||||
mNiStringExtraData.recType = Nif::RC_NiStringExtraData;
|
||||
mNiTriShape.mExtra = Nif::ExtraPtr(&mNiStringExtraData);
|
||||
mNiTriShape.mParents.push_back(&mNiNode);
|
||||
mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) };
|
||||
|
||||
Nif::NIFFile file("test.nif");
|
||||
file.mRoots.push_back(&mNiNode);
|
||||
file.mHash = mHash;
|
||||
|
||||
const auto result = mLoader.load(file);
|
||||
|
||||
Resource::BulletShape expected;
|
||||
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
||||
TEST_F(TestBulletNifLoader, bsx_editor_marker_flag_disables_collision_for_markers)
|
||||
{
|
||||
mNiIntegerExtraData.mData = 34; // BSXFlags "has collision" | "editor marker"
|
||||
mNiIntegerExtraData.recType = Nif::RC_BSXFlags;
|
||||
mNiTriShape.mExtraList.push_back(Nif::ExtraPtr(&mNiIntegerExtraData));
|
||||
mNiTriShape.mParents.push_back(&mNiNode);
|
||||
mNiTriShape.mName = "EditorMarker";
|
||||
mNiIntegerExtraData.mData = 34; // BSXFlags "has collision" | "editor marker"
|
||||
mNiIntegerExtraData.recType = Nif::RC_BSXFlags;
|
||||
mNiNode.mExtraList.push_back(Nif::ExtraPtr(&mNiIntegerExtraData));
|
||||
mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) };
|
||||
|
||||
Nif::NIFFile file("test.nif");
|
||||
|
@ -1162,18 +1147,14 @@ namespace
|
|||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
||||
TEST_F(TestBulletNifLoader,
|
||||
for_tri_shape_child_node_with_extra_data_string_mrk_and_other_collision_node_should_return_shape_with_triangle_mesh_shape_with_all_meshes)
|
||||
TEST_F(TestBulletNifLoader, mrk_editor_marker_flag_disables_collision_for_markers)
|
||||
{
|
||||
mNiTriShape.mParents.push_back(&mNiNode);
|
||||
mNiTriShape.mName = "Tri EditorMarker";
|
||||
mNiStringExtraData.mData = "MRK";
|
||||
mNiStringExtraData.recType = Nif::RC_NiStringExtraData;
|
||||
mNiTriShape.mExtra = Nif::ExtraPtr(&mNiStringExtraData);
|
||||
mNiTriShape.mParents.push_back(&mNiNode2);
|
||||
mNiNode2.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) };
|
||||
mNiNode2.recType = Nif::RC_RootCollisionNode;
|
||||
mNiNode2.mParents.push_back(&mNiNode);
|
||||
mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiNode2) };
|
||||
mNiNode.recType = Nif::RC_NiNode;
|
||||
mNiNode.mExtra = Nif::ExtraPtr(&mNiStringExtraData);
|
||||
mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape) };
|
||||
|
||||
Nif::NIFFile file("test.nif");
|
||||
file.mRoots.push_back(&mNiNode);
|
||||
|
@ -1181,13 +1162,7 @@ namespace
|
|||
|
||||
const auto result = mLoader.load(file);
|
||||
|
||||
std::unique_ptr<btTriangleMesh> triangles(new btTriangleMesh(false));
|
||||
triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0));
|
||||
std::unique_ptr<btCompoundShape> compound(new btCompoundShape);
|
||||
compound->addChildShape(btTransform::getIdentity(), new Resource::TriangleMeshShape(triangles.release(), true));
|
||||
|
||||
Resource::BulletShape expected;
|
||||
expected.mCollisionShape.reset(compound.release());
|
||||
|
||||
EXPECT_EQ(*result, expected);
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ osg::Group {
|
|||
)");
|
||||
}
|
||||
|
||||
std::string formatOsgNodeForShaderProperty(std::string_view shaderPrefix)
|
||||
std::string formatOsgNodeForBSShaderProperty(std::string_view shaderPrefix)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << R"(
|
||||
|
@ -165,6 +165,72 @@ osg::Group {
|
|||
return oss.str();
|
||||
}
|
||||
|
||||
std::string formatOsgNodeForBSLightingShaderProperty(std::string_view shaderPrefix)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << R"(
|
||||
osg::Group {
|
||||
UniqueID 1
|
||||
DataVariance STATIC
|
||||
UserDataContainer TRUE {
|
||||
osg::DefaultUserDataContainer {
|
||||
UniqueID 2
|
||||
UDC_UserObjects 1 {
|
||||
osg::StringValueObject {
|
||||
UniqueID 3
|
||||
Name "fileHash"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Children 1 {
|
||||
osg::Group {
|
||||
UniqueID 4
|
||||
DataVariance STATIC
|
||||
UserDataContainer TRUE {
|
||||
osg::DefaultUserDataContainer {
|
||||
UniqueID 5
|
||||
UDC_UserObjects 3 {
|
||||
osg::UIntValueObject {
|
||||
UniqueID 6
|
||||
Name "recIndex"
|
||||
Value 4294967295
|
||||
}
|
||||
osg::StringValueObject {
|
||||
UniqueID 7
|
||||
Name "shaderPrefix"
|
||||
Value ")"
|
||||
<< shaderPrefix << R"("
|
||||
}
|
||||
osg::BoolValueObject {
|
||||
UniqueID 8
|
||||
Name "shaderRequired"
|
||||
Value TRUE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StateSet TRUE {
|
||||
osg::StateSet {
|
||||
UniqueID 9
|
||||
ModeList 1 {
|
||||
GL_DEPTH_TEST ON
|
||||
}
|
||||
AttributeList 1 {
|
||||
osg::Depth {
|
||||
UniqueID 10
|
||||
}
|
||||
Value OFF
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
struct ShaderPrefixParams
|
||||
{
|
||||
unsigned int mShaderType;
|
||||
|
@ -194,7 +260,7 @@ osg::Group {
|
|||
Nif::NIFFile file("test.nif");
|
||||
file.mRoots.push_back(&node);
|
||||
auto result = Loader::load(file, &mImageManager);
|
||||
EXPECT_EQ(serialize(*result), formatOsgNodeForShaderProperty(GetParam().mExpectedShaderPrefix));
|
||||
EXPECT_EQ(serialize(*result), formatOsgNodeForBSShaderProperty(GetParam().mExpectedShaderPrefix));
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(Params, NifOsgLoaderBSShaderPrefixTest, ValuesIn(NifOsgLoaderBSShaderPrefixTest::sParams));
|
||||
|
@ -218,11 +284,13 @@ osg::Group {
|
|||
property.mTextureSet = nullptr;
|
||||
property.mController = nullptr;
|
||||
property.mType = GetParam().mShaderType;
|
||||
property.mShaderFlags1 |= Nif::BSShaderFlags1::BSSFlag1_DepthTest;
|
||||
property.mShaderFlags2 |= Nif::BSShaderFlags2::BSSFlag2_DepthWrite;
|
||||
node.mProperties.push_back(Nif::RecordPtrT<Nif::NiProperty>(&property));
|
||||
Nif::NIFFile file("test.nif");
|
||||
file.mRoots.push_back(&node);
|
||||
auto result = Loader::load(file, &mImageManager);
|
||||
EXPECT_EQ(serialize(*result), formatOsgNodeForShaderProperty(GetParam().mExpectedShaderPrefix));
|
||||
EXPECT_EQ(serialize(*result), formatOsgNodeForBSLightingShaderProperty(GetParam().mExpectedShaderPrefix));
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
|
|
|
@ -16,10 +16,10 @@ namespace ESM
|
|||
|
||||
struct AIData
|
||||
{
|
||||
unsigned short mHello; // This is the base value for greeting distance [0, 65535]
|
||||
uint16_t mHello; // This is the base value for greeting distance [0, 65535]
|
||||
unsigned char mFight, mFlee, mAlarm; // These are probabilities [0, 100]
|
||||
char mU1, mU2, mU3; // Unknown values
|
||||
int mServices; // See the Services enum
|
||||
int32_t mServices; // See the Services enum
|
||||
|
||||
void blank();
|
||||
///< Set record to default state (does not touch the ID).
|
||||
|
@ -27,8 +27,8 @@ namespace ESM
|
|||
|
||||
struct AIWander
|
||||
{
|
||||
short mDistance;
|
||||
short mDuration;
|
||||
int16_t mDistance;
|
||||
int16_t mDuration;
|
||||
unsigned char mTimeOfDay;
|
||||
unsigned char mIdle[8];
|
||||
unsigned char mShouldRepeat;
|
||||
|
@ -44,7 +44,7 @@ namespace ESM
|
|||
struct AITarget
|
||||
{
|
||||
float mX, mY, mZ;
|
||||
short mDuration;
|
||||
int16_t mDuration;
|
||||
NAME32 mId;
|
||||
unsigned char mShouldRepeat;
|
||||
unsigned char mPadding;
|
||||
|
|
|
@ -196,7 +196,7 @@ namespace ESM
|
|||
int count = 0;
|
||||
while (esm.isNextSub("AIPK"))
|
||||
{
|
||||
int type;
|
||||
int32_t type;
|
||||
esm.getHT(type);
|
||||
|
||||
mPackages.emplace_back();
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace ESM
|
|||
|
||||
struct CreatureLevListState final : public ObjectState
|
||||
{
|
||||
int mSpawnActorId;
|
||||
int32_t mSpawnActorId;
|
||||
bool mSpawn;
|
||||
|
||||
void load(ESMReader& esm) override;
|
||||
|
|
|
@ -47,9 +47,8 @@ namespace ESM
|
|||
std::vector<int> mSummonGraveyard;
|
||||
|
||||
TimeStamp mTradeTime;
|
||||
int mGoldPool;
|
||||
int mActorId;
|
||||
// int mHitAttemptActorId;
|
||||
int32_t mGoldPool;
|
||||
int32_t mActorId;
|
||||
|
||||
enum Flags
|
||||
{
|
||||
|
|
|
@ -24,14 +24,14 @@ namespace ESM
|
|||
Flag_Global = 4 // make available from main menu (i.e. not location specific)
|
||||
};
|
||||
|
||||
unsigned int mRecordFlags;
|
||||
uint32_t mRecordFlags;
|
||||
RefId mId;
|
||||
|
||||
std::string mDescription;
|
||||
|
||||
std::string mScriptText;
|
||||
|
||||
unsigned int mFlags;
|
||||
uint32_t mFlags;
|
||||
|
||||
void load(ESMReader& esm, bool& isDeleted);
|
||||
void save(ESMWriter& esm, bool isDeleted = false) const;
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace ESM
|
|||
|
||||
struct DoorState final : public ObjectState
|
||||
{
|
||||
int mDoorState = 0;
|
||||
int32_t mDoorState = 0;
|
||||
|
||||
void load(ESMReader& esm) override;
|
||||
void save(ESMWriter& esm, bool inInventory = false) const override;
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace ESM
|
|||
|
||||
static constexpr std::string_view getRecordType() { return "Filter"; }
|
||||
|
||||
unsigned int mRecordFlags;
|
||||
uint32_t mRecordFlags;
|
||||
RefId mId;
|
||||
|
||||
std::string mDescription;
|
||||
|
|
|
@ -7,22 +7,25 @@
|
|||
|
||||
namespace ESM
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t sInvalidSlot = static_cast<uint32_t>(-1);
|
||||
}
|
||||
|
||||
void InventoryState::load(ESMReader& esm)
|
||||
{
|
||||
// obsolete
|
||||
int index = 0;
|
||||
uint32_t index = 0;
|
||||
while (esm.isNextSub("IOBJ"))
|
||||
{
|
||||
int unused; // no longer used
|
||||
esm.getHT(unused);
|
||||
esm.skipHT<int32_t>();
|
||||
|
||||
ObjectState state;
|
||||
|
||||
// obsolete
|
||||
if (esm.isNextSub("SLOT"))
|
||||
{
|
||||
int slot;
|
||||
int32_t slot;
|
||||
esm.getHT(slot);
|
||||
mEquipmentSlots[index] = slot;
|
||||
}
|
||||
|
@ -38,9 +41,9 @@ namespace ESM
|
|||
++index;
|
||||
}
|
||||
|
||||
int itemsCount = 0;
|
||||
uint32_t itemsCount = 0;
|
||||
esm.getHNOT(itemsCount, "ICNT");
|
||||
for (int i = 0; i < itemsCount; i++)
|
||||
for (; itemsCount > 0; --itemsCount)
|
||||
{
|
||||
ObjectState state;
|
||||
|
||||
|
@ -62,7 +65,7 @@ namespace ESM
|
|||
{
|
||||
// Get its name
|
||||
ESM::RefId id = esm.getRefId();
|
||||
int count;
|
||||
int32_t count;
|
||||
std::string parentGroup;
|
||||
// Then get its count
|
||||
esm.getHNT(count, "COUN");
|
||||
|
@ -91,9 +94,9 @@ namespace ESM
|
|||
while (esm.isNextSub("EQUI"))
|
||||
{
|
||||
esm.getSubHeader();
|
||||
int equipIndex;
|
||||
int32_t equipIndex;
|
||||
esm.getT(equipIndex);
|
||||
int slot;
|
||||
int32_t slot;
|
||||
esm.getT(slot);
|
||||
mEquipmentSlots[equipIndex] = slot;
|
||||
}
|
||||
|
@ -101,20 +104,24 @@ namespace ESM
|
|||
if (esm.isNextSub("EQIP"))
|
||||
{
|
||||
esm.getSubHeader();
|
||||
int slotsCount = 0;
|
||||
uint32_t slotsCount = 0;
|
||||
esm.getT(slotsCount);
|
||||
for (int i = 0; i < slotsCount; i++)
|
||||
for (; slotsCount > 0; --slotsCount)
|
||||
{
|
||||
int equipIndex;
|
||||
int32_t equipIndex;
|
||||
esm.getT(equipIndex);
|
||||
int slot;
|
||||
int32_t slot;
|
||||
esm.getT(slot);
|
||||
mEquipmentSlots[equipIndex] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
mSelectedEnchantItem = -1;
|
||||
esm.getHNOT(mSelectedEnchantItem, "SELE");
|
||||
uint32_t selectedEnchantItem = sInvalidSlot;
|
||||
esm.getHNOT(selectedEnchantItem, "SELE");
|
||||
if (selectedEnchantItem == sInvalidSlot)
|
||||
mSelectedEnchantItem.reset();
|
||||
else
|
||||
mSelectedEnchantItem = selectedEnchantItem;
|
||||
|
||||
// Old saves had restocking levelled items in a special map
|
||||
// This turns items from that map into negative quantities
|
||||
|
@ -132,7 +139,7 @@ namespace ESM
|
|||
|
||||
void InventoryState::save(ESMWriter& esm) const
|
||||
{
|
||||
int itemsCount = static_cast<int>(mItems.size());
|
||||
uint32_t itemsCount = static_cast<uint32_t>(mItems.size());
|
||||
if (itemsCount > 0)
|
||||
{
|
||||
esm.writeHNT("ICNT", itemsCount);
|
||||
|
@ -149,34 +156,32 @@ namespace ESM
|
|||
esm.writeHNString("LGRP", it->first.second);
|
||||
}
|
||||
|
||||
for (TEffectMagnitudes::const_iterator it = mPermanentMagicEffectMagnitudes.begin();
|
||||
it != mPermanentMagicEffectMagnitudes.end(); ++it)
|
||||
for (const auto& [id, params] : mPermanentMagicEffectMagnitudes)
|
||||
{
|
||||
esm.writeHNRefId("MAGI", it->first);
|
||||
esm.writeHNRefId("MAGI", id);
|
||||
|
||||
const std::vector<std::pair<float, float>>& params = it->second;
|
||||
for (std::vector<std::pair<float, float>>::const_iterator pIt = params.begin(); pIt != params.end(); ++pIt)
|
||||
for (const auto& [rand, mult] : params)
|
||||
{
|
||||
esm.writeHNT("RAND", pIt->first);
|
||||
esm.writeHNT("MULT", pIt->second);
|
||||
esm.writeHNT("RAND", rand);
|
||||
esm.writeHNT("MULT", mult);
|
||||
}
|
||||
}
|
||||
|
||||
int slotsCount = static_cast<int>(mEquipmentSlots.size());
|
||||
uint32_t slotsCount = static_cast<uint32_t>(mEquipmentSlots.size());
|
||||
if (slotsCount > 0)
|
||||
{
|
||||
esm.startSubRecord("EQIP");
|
||||
esm.writeT(slotsCount);
|
||||
for (std::map<int, int>::const_iterator it = mEquipmentSlots.begin(); it != mEquipmentSlots.end(); ++it)
|
||||
for (const auto& [index, slot] : mEquipmentSlots)
|
||||
{
|
||||
esm.writeT(it->first);
|
||||
esm.writeT(it->second);
|
||||
esm.writeT(index);
|
||||
esm.writeT(slot);
|
||||
}
|
||||
esm.endRecord("EQIP");
|
||||
}
|
||||
|
||||
if (mSelectedEnchantItem != -1)
|
||||
esm.writeHNT("SELE", mSelectedEnchantItem);
|
||||
if (mSelectedEnchantItem)
|
||||
esm.writeHNT("SELE", *mSelectedEnchantItem);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#define OPENMW_ESM_INVENTORYSTATE_H
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
#include "objectstate.hpp"
|
||||
#include <components/esm/refid.hpp>
|
||||
|
@ -19,20 +20,15 @@ namespace ESM
|
|||
std::vector<ObjectState> mItems;
|
||||
|
||||
// <Index in mItems, equipment slot>
|
||||
std::map<int, int> mEquipmentSlots;
|
||||
std::map<uint32_t, int32_t> mEquipmentSlots;
|
||||
|
||||
std::map<std::pair<ESM::RefId, std::string>, int> mLevelledItemMap;
|
||||
std::map<std::pair<ESM::RefId, std::string>, int32_t> mLevelledItemMap;
|
||||
|
||||
typedef std::map<ESM::RefId, std::vector<std::pair<float, float>>> TEffectMagnitudes;
|
||||
TEffectMagnitudes mPermanentMagicEffectMagnitudes;
|
||||
std::map<ESM::RefId, std::vector<std::pair<float, float>>> mPermanentMagicEffectMagnitudes;
|
||||
|
||||
int mSelectedEnchantItem; // For inventories only
|
||||
std::optional<uint32_t> mSelectedEnchantItem; // For inventories only
|
||||
|
||||
InventoryState()
|
||||
: mSelectedEnchantItem(-1)
|
||||
{
|
||||
}
|
||||
virtual ~InventoryState() {}
|
||||
virtual ~InventoryState() = default;
|
||||
|
||||
virtual void load(ESMReader& esm);
|
||||
virtual void save(ESMWriter& esm) const;
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace ESM
|
|||
///< Translate 8bit/24bit code (stored in refNum.mIndex) into a proper refNum
|
||||
void adjustRefNum(RefNum& refNum, const ESMReader& reader)
|
||||
{
|
||||
unsigned int local = (refNum.mIndex & 0xff000000) >> 24;
|
||||
uint32_t local = (refNum.mIndex & 0xff000000) >> 24;
|
||||
|
||||
// If we have an index value that does not make sense, assume that it was an addition
|
||||
// by the present plugin (but a faulty one)
|
||||
|
@ -124,7 +124,7 @@ namespace ESM
|
|||
switch (esm.retSubName().toInt())
|
||||
{
|
||||
case fourCC("INTV"):
|
||||
int waterl;
|
||||
int32_t waterl;
|
||||
esm.getHT(waterl);
|
||||
mWater = static_cast<float>(waterl);
|
||||
mWaterInt = true;
|
||||
|
@ -192,7 +192,7 @@ namespace ESM
|
|||
{
|
||||
if (mWaterInt)
|
||||
{
|
||||
int water = (mWater >= 0) ? (int)(mWater + 0.5) : (int)(mWater - 0.5);
|
||||
int32_t water = (mWater >= 0) ? static_cast<int32_t>(mWater + 0.5) : static_cast<int32_t>(mWater - 0.5);
|
||||
esm.writeHNT("INTV", water);
|
||||
}
|
||||
else
|
||||
|
@ -218,13 +218,13 @@ namespace ESM
|
|||
}
|
||||
}
|
||||
|
||||
void Cell::saveTempMarker(ESMWriter& esm, int tempCount) const
|
||||
void Cell::saveTempMarker(ESMWriter& esm, int32_t tempCount) const
|
||||
{
|
||||
if (tempCount != 0)
|
||||
esm.writeHNT("NAM0", tempCount);
|
||||
}
|
||||
|
||||
void Cell::restore(ESMReader& esm, int iCtx) const
|
||||
void Cell::restore(ESMReader& esm, size_t iCtx) const
|
||||
{
|
||||
esm.restoreContext(mContextList.at(iCtx));
|
||||
}
|
||||
|
@ -321,7 +321,7 @@ namespace ESM
|
|||
|
||||
void Cell::blank()
|
||||
{
|
||||
mName = "";
|
||||
mName.clear();
|
||||
mRegion = ESM::RefId();
|
||||
mWater = 0;
|
||||
mWaterInt = false;
|
||||
|
|
|
@ -34,7 +34,7 @@ namespace ESM
|
|||
RefNum mRefNum;
|
||||
|
||||
// Coordinates of target exterior cell
|
||||
int mTarget[2];
|
||||
int32_t mTarget[2];
|
||||
|
||||
// The content file format does not support moving objects to an interior cell.
|
||||
// The save game format does support moving to interior cells, but uses a different mechanism
|
||||
|
@ -153,13 +153,13 @@ namespace ESM
|
|||
ESMReader& esm, bool saveContext = true); // Load everything, except NAME, DATAstruct and references
|
||||
|
||||
void save(ESMWriter& esm, bool isDeleted = false) const;
|
||||
void saveTempMarker(ESMWriter& esm, int tempCount) const;
|
||||
void saveTempMarker(ESMWriter& esm, int32_t tempCount) const;
|
||||
|
||||
bool isExterior() const { return !(mData.mFlags & Interior); }
|
||||
|
||||
int getGridX() const { return mData.mX; }
|
||||
int32_t getGridX() const { return mData.mX; }
|
||||
|
||||
int getGridY() const { return mData.mY; }
|
||||
int32_t getGridY() const { return mData.mY; }
|
||||
|
||||
bool hasWater() const { return ((mData.mFlags & HasWater) != 0) || isExterior(); }
|
||||
|
||||
|
@ -172,7 +172,7 @@ namespace ESM
|
|||
// somewhere other than the file system, you need to pre-open the
|
||||
// ESMReader, and the filename must match the stored filename
|
||||
// exactly.
|
||||
void restore(ESMReader& esm, int iCtx) const;
|
||||
void restore(ESMReader& esm, size_t iCtx) const;
|
||||
|
||||
std::string getDescription() const;
|
||||
///< Return a short string describing the cell (mostly used for debugging/logging purpose)
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace ESM
|
|||
|
||||
struct ContItem
|
||||
{
|
||||
int mCount{ 0 };
|
||||
int32_t mCount{ 0 };
|
||||
ESM::RefId mItem;
|
||||
};
|
||||
|
||||
|
@ -48,12 +48,12 @@ namespace ESM
|
|||
Unknown = 8
|
||||
};
|
||||
|
||||
unsigned int mRecordFlags;
|
||||
uint32_t mRecordFlags;
|
||||
RefId mId, mScript;
|
||||
std::string mName, mModel;
|
||||
|
||||
float mWeight; // Not sure, might be max total weight allowed?
|
||||
int mFlags;
|
||||
int32_t mFlags;
|
||||
InventoryList mInventory;
|
||||
|
||||
void load(ESMReader& esm, bool& isDeleted);
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace ESM
|
|||
/// Return a string descriptor for this record type. Currently used for debugging / error logs only.
|
||||
static std::string_view getRecordType() { return "Global"; }
|
||||
|
||||
unsigned int mRecordFlags;
|
||||
uint32_t mRecordFlags;
|
||||
ESM::RefId mId;
|
||||
Variant mValue;
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ namespace ESM
|
|||
/// Return a string descriptor for this record type. Currently used for debugging / error logs only.
|
||||
static std::string_view getRecordType() { return "GameSetting"; }
|
||||
|
||||
unsigned int mRecordFlags;
|
||||
uint32_t mRecordFlags;
|
||||
RefId mId;
|
||||
|
||||
Variant mValue;
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace ESM
|
|||
break;
|
||||
case fourCC("INDX"):
|
||||
{
|
||||
int length = 0;
|
||||
uint32_t length = 0;
|
||||
esm.getHT(length);
|
||||
mList.resize(length);
|
||||
|
||||
|
@ -87,12 +87,12 @@ namespace ESM
|
|||
|
||||
esm.writeHNT("DATA", mFlags);
|
||||
esm.writeHNT("NNAM", mChanceNone);
|
||||
esm.writeHNT<int>("INDX", mList.size());
|
||||
esm.writeHNT<uint32_t>("INDX", mList.size());
|
||||
|
||||
for (std::vector<LevelItem>::const_iterator it = mList.begin(); it != mList.end(); ++it)
|
||||
for (const auto& item : mList)
|
||||
{
|
||||
esm.writeHNCRefId(recName, it->mId);
|
||||
esm.writeHNT("INTV", it->mLevel);
|
||||
esm.writeHNCRefId(recName, item.mId);
|
||||
esm.writeHNT("INTV", item.mLevel);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,15 +24,15 @@ namespace ESM
|
|||
|
||||
struct LevelledListBase
|
||||
{
|
||||
int mFlags;
|
||||
int32_t mFlags;
|
||||
unsigned char mChanceNone; // Chance that none are selected (0-100)
|
||||
unsigned int mRecordFlags;
|
||||
uint32_t mRecordFlags;
|
||||
RefId mId;
|
||||
|
||||
struct LevelItem
|
||||
{
|
||||
RefId mId;
|
||||
short mLevel;
|
||||
uint16_t mLevel;
|
||||
};
|
||||
|
||||
std::vector<LevelItem> mList;
|
||||
|
|
|
@ -29,7 +29,7 @@ namespace ESM
|
|||
// mId is merely a user friendly name for the texture in the editor.
|
||||
std::string mTexture;
|
||||
RefId mId;
|
||||
int mIndex;
|
||||
int32_t mIndex;
|
||||
|
||||
void load(ESMReader& esm, bool& isDeleted);
|
||||
void save(ESMWriter& esm, bool isDeleted = false) const;
|
||||
|
|
|
@ -36,7 +36,7 @@ namespace ESM
|
|||
|
||||
esm.getSubNameIs("MEDT");
|
||||
esm.getSubHeader();
|
||||
int school;
|
||||
int32_t school;
|
||||
esm.getT(school);
|
||||
mData.mSchool = MagicSchool::indexToSkillRefId(school);
|
||||
esm.getT(mData.mBaseCost);
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace ESM
|
|||
/// Return a string descriptor for this record type. Currently used for debugging / error logs only.
|
||||
static std::string_view getRecordType() { return "MagicEffect"; }
|
||||
|
||||
unsigned int mRecordFlags;
|
||||
uint32_t mRecordFlags;
|
||||
RefId mId;
|
||||
|
||||
enum Flags
|
||||
|
@ -74,9 +74,9 @@ namespace ESM
|
|||
{
|
||||
RefId mSchool; // Skill id
|
||||
float mBaseCost;
|
||||
int mFlags;
|
||||
int32_t mFlags;
|
||||
// Glow color for enchanted items with this effect
|
||||
int mRed, mGreen, mBlue;
|
||||
int32_t mRed, mGreen, mBlue;
|
||||
|
||||
float mUnknown1; // Called "Size X" in CS
|
||||
float mSpeed; // Speed of fired projectile
|
||||
|
@ -107,7 +107,7 @@ namespace ESM
|
|||
// there. They can be redefined in mods by setting the name in GMST
|
||||
// sEffectSummonCreature04/05 creature id in
|
||||
// sMagicCreature04ID/05ID.
|
||||
int mIndex;
|
||||
int32_t mIndex;
|
||||
|
||||
void load(ESMReader& esm, bool& isDeleted);
|
||||
void save(ESMWriter& esm, bool isDeleted = false) const;
|
||||
|
|
|
@ -82,7 +82,7 @@ namespace ESM
|
|||
break;
|
||||
case fourCC("FLAG"):
|
||||
hasFlags = true;
|
||||
int flags;
|
||||
int32_t flags;
|
||||
esm.getHT(flags);
|
||||
mFlags = flags & 0xFF;
|
||||
mBloodType = ((flags >> 8) & 0xFF) >> 2;
|
||||
|
|
|
@ -79,28 +79,28 @@ namespace ESM
|
|||
|
||||
struct NPDTstruct52
|
||||
{
|
||||
short mLevel;
|
||||
int16_t mLevel;
|
||||
unsigned char mStrength, mIntelligence, mWillpower, mAgility, mSpeed, mEndurance, mPersonality, mLuck;
|
||||
|
||||
// mSkill can grow up to 200, it must be unsigned
|
||||
std::array<unsigned char, Skill::Length> mSkills;
|
||||
|
||||
char mUnknown1;
|
||||
unsigned short mHealth, mMana, mFatigue;
|
||||
uint16_t mHealth, mMana, mFatigue;
|
||||
unsigned char mDisposition, mReputation, mRank;
|
||||
char mUnknown2;
|
||||
int mGold;
|
||||
int32_t mGold;
|
||||
}; // 52 bytes
|
||||
|
||||
// Structure for autocalculated characters.
|
||||
// This is only used for load and save operations.
|
||||
struct NPDTstruct12
|
||||
{
|
||||
short mLevel;
|
||||
int16_t mLevel;
|
||||
// see above
|
||||
unsigned char mDisposition, mReputation, mRank;
|
||||
char mUnknown1, mUnknown2, mUnknown3;
|
||||
int mGold;
|
||||
int32_t mGold;
|
||||
}; // 12 bytes
|
||||
#pragma pack(pop)
|
||||
|
||||
|
@ -111,7 +111,7 @@ namespace ESM
|
|||
|
||||
int getFactionRank() const; /// wrapper for mNpdt*, -1 = no rank
|
||||
|
||||
int mBloodType;
|
||||
int32_t mBloodType;
|
||||
unsigned char mFlags;
|
||||
|
||||
InventoryList mInventory;
|
||||
|
@ -125,7 +125,7 @@ namespace ESM
|
|||
|
||||
AIPackageList mAiPackage;
|
||||
|
||||
unsigned int mRecordFlags;
|
||||
uint32_t mRecordFlags;
|
||||
RefId mId, mRace, mClass, mFaction, mScript;
|
||||
std::string mModel, mName;
|
||||
|
||||
|
|
|
@ -27,13 +27,13 @@ namespace ESM
|
|||
|
||||
struct SkillBonus
|
||||
{
|
||||
int mSkill; // SkillEnum
|
||||
int mBonus;
|
||||
int32_t mSkill; // SkillEnum
|
||||
int32_t mBonus;
|
||||
};
|
||||
|
||||
struct MaleFemale
|
||||
{
|
||||
int mMale, mFemale;
|
||||
int32_t mMale, mFemale;
|
||||
|
||||
int getValue(bool male) const;
|
||||
};
|
||||
|
@ -63,13 +63,13 @@ namespace ESM
|
|||
// as 'height' times 128. This has not been tested yet.
|
||||
MaleFemaleF mHeight, mWeight;
|
||||
|
||||
int mFlags; // 0x1 - playable, 0x2 - beast race
|
||||
int32_t mFlags; // 0x1 - playable, 0x2 - beast race
|
||||
|
||||
}; // Size = 140 bytes
|
||||
|
||||
RADTstruct mData;
|
||||
|
||||
unsigned int mRecordFlags;
|
||||
uint32_t mRecordFlags;
|
||||
std::string mName, mDescription;
|
||||
RefId mId;
|
||||
SpellList mPowers;
|
||||
|
|
|
@ -36,9 +36,9 @@ namespace ESM
|
|||
};
|
||||
|
||||
// Type
|
||||
int mType;
|
||||
int32_t mType;
|
||||
|
||||
unsigned int mRecordFlags;
|
||||
uint32_t mRecordFlags;
|
||||
RefId mId, mCreature, mSound;
|
||||
|
||||
void load(ESMReader& esm, bool& isDeleted);
|
||||
|
|
|
@ -28,7 +28,7 @@ namespace ESM
|
|||
static std::string_view getRecordType() { return "StartScript"; }
|
||||
|
||||
std::string mData;
|
||||
unsigned int mRecordFlags;
|
||||
uint32_t mRecordFlags;
|
||||
RefId mId;
|
||||
|
||||
// Load a record and add it to the list
|
||||
|
|
|
@ -31,7 +31,7 @@ namespace ESM
|
|||
/// Return a string descriptor for this record type. Currently used for debugging / error logs only.
|
||||
static std::string_view getRecordType() { return "Static"; }
|
||||
|
||||
unsigned int mRecordFlags;
|
||||
uint32_t mRecordFlags;
|
||||
RefId mId;
|
||||
std::string mModel;
|
||||
|
||||
|
|
|
@ -20,11 +20,11 @@ namespace ESM
|
|||
versions are 1.2 and 1.3. These correspond to:
|
||||
1.2 = 0x3f99999a and 1.3 = 0x3fa66666
|
||||
*/
|
||||
unsigned int version;
|
||||
int type; // 0=esp, 1=esm, 32=ess (unused)
|
||||
uint32_t version;
|
||||
int32_t type; // 0=esp, 1=esm, 32=ess (unused)
|
||||
std::string author; // Author's name
|
||||
std::string desc; // File description
|
||||
int records; // Number of records
|
||||
int32_t records; // Number of records
|
||||
};
|
||||
|
||||
struct GMDT
|
||||
|
|
|
@ -20,8 +20,8 @@ namespace ESM
|
|||
{
|
||||
while (esm.isNextSub("EFID"))
|
||||
{
|
||||
int id;
|
||||
std::pair<int, float> params;
|
||||
int32_t id;
|
||||
std::pair<int32_t, float> params;
|
||||
esm.getHT(id);
|
||||
esm.getHNT(params.first, "BASE");
|
||||
if (esm.getFormatVersion() <= MaxClearModifiersFormatVersion)
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace ESM
|
|||
struct MagicEffects
|
||||
{
|
||||
// <Effect Id, Base value, Modifier>
|
||||
std::map<int, std::pair<int, float>> mEffects;
|
||||
std::map<int32_t, std::pair<int32_t, float>> mEffects;
|
||||
|
||||
void load(ESMReader& esm);
|
||||
void save(ESMWriter& esm) const;
|
||||
|
@ -24,16 +24,16 @@ namespace ESM
|
|||
|
||||
struct SummonKey
|
||||
{
|
||||
SummonKey(int effectId, const ESM::RefId& sourceId, int index)
|
||||
SummonKey(int32_t effectId, const ESM::RefId& sourceId, int32_t index)
|
||||
: mEffectId(effectId)
|
||||
, mSourceId(sourceId)
|
||||
, mEffectIndex(index)
|
||||
{
|
||||
}
|
||||
|
||||
int mEffectId;
|
||||
int32_t mEffectId;
|
||||
ESM::RefId mSourceId;
|
||||
int mEffectIndex;
|
||||
int32_t mEffectIndex;
|
||||
};
|
||||
|
||||
inline auto makeTupleRef(const SummonKey& value) noexcept
|
||||
|
|
|
@ -21,7 +21,7 @@ namespace ESM
|
|||
|
||||
Faction faction;
|
||||
|
||||
int expelled = 0;
|
||||
int32_t expelled = 0;
|
||||
esm.getHNOT(expelled, "FAEX");
|
||||
|
||||
if (expelled)
|
||||
|
@ -75,7 +75,7 @@ namespace ESM
|
|||
esm.getHNOT(hasWerewolfAttributes, "HWAT");
|
||||
if (hasWerewolfAttributes)
|
||||
{
|
||||
StatState<int> dummy;
|
||||
StatState<int32_t> dummy;
|
||||
for (int i = 0; i < ESM::Attribute::Length; ++i)
|
||||
dummy.load(esm, intFallback);
|
||||
mWerewolfDeprecatedData = true;
|
||||
|
@ -130,21 +130,21 @@ namespace ESM
|
|||
|
||||
void NpcStats::save(ESMWriter& esm) const
|
||||
{
|
||||
for (auto iter(mFactions.begin()); iter != mFactions.end(); ++iter)
|
||||
for (const auto& [id, faction] : mFactions)
|
||||
{
|
||||
esm.writeHNRefId("FACT", iter->first);
|
||||
esm.writeHNRefId("FACT", id);
|
||||
|
||||
if (iter->second.mExpelled)
|
||||
if (faction.mExpelled)
|
||||
{
|
||||
int expelled = 1;
|
||||
int32_t expelled = 1;
|
||||
esm.writeHNT("FAEX", expelled);
|
||||
}
|
||||
|
||||
if (iter->second.mRank >= 0)
|
||||
esm.writeHNT("FARA", iter->second.mRank);
|
||||
if (faction.mRank >= 0)
|
||||
esm.writeHNT("FARA", faction.mRank);
|
||||
|
||||
if (iter->second.mReputation)
|
||||
esm.writeHNT("FARE", iter->second.mReputation);
|
||||
if (faction.mReputation)
|
||||
esm.writeHNT("FARE", faction.mReputation);
|
||||
}
|
||||
|
||||
if (mDisposition)
|
||||
|
@ -169,7 +169,7 @@ namespace ESM
|
|||
esm.writeHNT("LPRO", mLevelProgress);
|
||||
|
||||
bool saveSkillIncreases = false;
|
||||
for (int increase : mSkillIncrease)
|
||||
for (int32_t increase : mSkillIncrease)
|
||||
{
|
||||
if (increase != 0)
|
||||
{
|
||||
|
@ -183,8 +183,8 @@ namespace ESM
|
|||
if (mSpecIncreases[0] != 0 || mSpecIncreases[1] != 0 || mSpecIncreases[2] != 0)
|
||||
esm.writeHNT("SPEC", mSpecIncreases);
|
||||
|
||||
for (auto iter(mUsedIds.begin()); iter != mUsedIds.end(); ++iter)
|
||||
esm.writeHNRefId("USED", *iter);
|
||||
for (const RefId& id : mUsedIds)
|
||||
esm.writeHNRefId("USED", id);
|
||||
|
||||
if (mTimeToStartDrowning)
|
||||
esm.writeHNT("DRTI", mTimeToStartDrowning);
|
||||
|
|
|
@ -23,8 +23,8 @@ namespace ESM
|
|||
struct Faction
|
||||
{
|
||||
bool mExpelled;
|
||||
int mRank;
|
||||
int mReputation;
|
||||
int32_t mRank;
|
||||
int32_t mReputation;
|
||||
|
||||
Faction();
|
||||
};
|
||||
|
@ -33,18 +33,18 @@ namespace ESM
|
|||
|
||||
bool mWerewolfDeprecatedData;
|
||||
|
||||
std::map<ESM::RefId, Faction> mFactions; // lower case IDs
|
||||
int mDisposition;
|
||||
std::map<ESM::RefId, Faction> mFactions;
|
||||
int32_t mDisposition;
|
||||
std::array<StatState<float>, ESM::Skill::Length> mSkills;
|
||||
int mBounty;
|
||||
int mReputation;
|
||||
int mWerewolfKills;
|
||||
int mLevelProgress;
|
||||
std::array<int, ESM::Attribute::Length> mSkillIncrease;
|
||||
std::array<int, 3> mSpecIncreases;
|
||||
std::vector<ESM::RefId> mUsedIds; // lower case IDs
|
||||
int32_t mBounty;
|
||||
int32_t mReputation;
|
||||
int32_t mWerewolfKills;
|
||||
int32_t mLevelProgress;
|
||||
std::array<int32_t, ESM::Attribute::Length> mSkillIncrease;
|
||||
std::array<int32_t, 3> mSpecIncreases;
|
||||
std::vector<ESM::RefId> mUsedIds;
|
||||
float mTimeToStartDrowning;
|
||||
int mCrimeId;
|
||||
int32_t mCrimeId;
|
||||
|
||||
/// Initialize to default state
|
||||
void blank();
|
||||
|
|
|
@ -49,7 +49,7 @@ namespace ESM
|
|||
esm.getHNOT(mFlags, "FLAG");
|
||||
|
||||
// obsolete
|
||||
int unused;
|
||||
int32_t unused;
|
||||
esm.getHNOT(unused, "LTIM");
|
||||
|
||||
mAnimationState.load(esm);
|
||||
|
@ -179,6 +179,6 @@ namespace ESM
|
|||
throw std::logic_error(error.str());
|
||||
}
|
||||
|
||||
ObjectState::~ObjectState() {}
|
||||
ObjectState::~ObjectState() = default;
|
||||
|
||||
}
|
||||
|
|
|
@ -32,9 +32,9 @@ namespace ESM
|
|||
Locals mLocals;
|
||||
LuaScripts mLuaScripts;
|
||||
unsigned char mEnabled;
|
||||
int mCount;
|
||||
int32_t mCount;
|
||||
Position mPosition;
|
||||
unsigned int mFlags;
|
||||
uint32_t mFlags;
|
||||
|
||||
// Is there any class-specific state following the ObjectState
|
||||
bool mHasCustomState;
|
||||
|
|
|
@ -27,8 +27,8 @@ namespace ESM
|
|||
RefId mMarkedCell;
|
||||
ESM::RefId mBirthsign;
|
||||
|
||||
int mCurrentCrimeId;
|
||||
int mPaidCrimeId;
|
||||
int32_t mCurrentCrimeId;
|
||||
int32_t mPaidCrimeId;
|
||||
|
||||
float mSaveAttributes[Attribute::Length];
|
||||
float mSaveSkills[Skill::Length];
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace ESM
|
|||
Vector3 mPosition;
|
||||
Quaternion mOrientation;
|
||||
|
||||
int mActorId;
|
||||
int32_t mActorId;
|
||||
|
||||
void load(ESMReader& esm);
|
||||
void save(ESMWriter& esm) const;
|
||||
|
|
|
@ -14,7 +14,7 @@ namespace ESM
|
|||
struct QuestState
|
||||
{
|
||||
ESM::RefId mTopic; // lower case id
|
||||
int mState;
|
||||
int32_t mState;
|
||||
unsigned char mFinished;
|
||||
|
||||
void load(ESMReader& esm);
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace ESM
|
|||
|
||||
std::vector<std::string> mContentFiles;
|
||||
std::string mPlayerName;
|
||||
int mPlayerLevel;
|
||||
int32_t mPlayerLevel;
|
||||
|
||||
// ID of class
|
||||
ESM::RefId mPlayerClassId;
|
||||
|
@ -34,7 +34,7 @@ namespace ESM
|
|||
std::string mDescription;
|
||||
std::vector<char> mScreenshot; // raw jpg-encoded data
|
||||
|
||||
int mCurrentDay = 0;
|
||||
int32_t mCurrentDay = 0;
|
||||
float mCurrentHealth = 0;
|
||||
float mMaximumHealth = 0;
|
||||
|
||||
|
|
|
@ -21,19 +21,19 @@ namespace ESM
|
|||
// We changed stats values from integers to floats; ensure backwards compatibility
|
||||
if (intFallback)
|
||||
{
|
||||
int base = 0;
|
||||
int32_t base = 0;
|
||||
esm.getHNT(base, "STBA");
|
||||
mBase = static_cast<T>(base);
|
||||
|
||||
int mod = 0;
|
||||
int32_t mod = 0;
|
||||
esm.getHNOT(mod, "STMO");
|
||||
mMod = static_cast<T>(mod);
|
||||
|
||||
int current = 0;
|
||||
int32_t current = 0;
|
||||
esm.getHNOT(current, "STCU");
|
||||
mCurrent = static_cast<T>(current);
|
||||
|
||||
int oldDamage = 0;
|
||||
int32_t oldDamage = 0;
|
||||
esm.getHNOT(oldDamage, "STDA");
|
||||
mDamage = static_cast<float>(oldDamage);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace ESM
|
|||
template <typename T, bool orDefault = false>
|
||||
struct GetValue
|
||||
{
|
||||
constexpr T operator()(int value) const { return static_cast<T>(value); }
|
||||
constexpr T operator()(int32_t value) const { return static_cast<T>(value); }
|
||||
|
||||
constexpr T operator()(float value) const { return static_cast<T>(value); }
|
||||
|
||||
|
@ -41,7 +41,7 @@ namespace ESM
|
|||
{
|
||||
}
|
||||
|
||||
void operator()(int& value) const { value = static_cast<int>(mValue); }
|
||||
void operator()(int32_t& value) const { value = static_cast<int32_t>(mValue); }
|
||||
|
||||
void operator()(float& value) const { value = static_cast<float>(mValue); }
|
||||
|
||||
|
@ -58,9 +58,9 @@ namespace ESM
|
|||
return std::get<std::string>(mData);
|
||||
}
|
||||
|
||||
int Variant::getInteger() const
|
||||
int32_t Variant::getInteger() const
|
||||
{
|
||||
return std::visit(GetValue<int>{}, mData);
|
||||
return std::visit(GetValue<int32_t>{}, mData);
|
||||
}
|
||||
|
||||
float Variant::getFloat() const
|
||||
|
@ -194,17 +194,17 @@ namespace ESM
|
|||
|
||||
case VT_Short:
|
||||
|
||||
stream << "variant short: " << std::get<int>(mData);
|
||||
stream << "variant short: " << std::get<int32_t>(mData);
|
||||
break;
|
||||
|
||||
case VT_Int:
|
||||
|
||||
stream << "variant int: " << std::get<int>(mData);
|
||||
stream << "variant int: " << std::get<int32_t>(mData);
|
||||
break;
|
||||
|
||||
case VT_Long:
|
||||
|
||||
stream << "variant long: " << std::get<int>(mData);
|
||||
stream << "variant long: " << std::get<int32_t>(mData);
|
||||
break;
|
||||
|
||||
case VT_Float:
|
||||
|
@ -259,7 +259,7 @@ namespace ESM
|
|||
std::get<std::string>(mData) = std::move(value);
|
||||
}
|
||||
|
||||
void Variant::setInteger(int value)
|
||||
void Variant::setInteger(int32_t value)
|
||||
{
|
||||
std::visit(SetValue(value), mData);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace ESM
|
|||
class Variant
|
||||
{
|
||||
VarType mType;
|
||||
std::variant<std::monostate, int, float, std::string> mData;
|
||||
std::variant<std::monostate, int32_t, float, std::string> mData;
|
||||
|
||||
public:
|
||||
enum Format
|
||||
|
@ -54,7 +54,7 @@ namespace ESM
|
|||
{
|
||||
}
|
||||
|
||||
explicit Variant(int value)
|
||||
explicit Variant(int32_t value)
|
||||
: mType(VT_Long)
|
||||
, mData(value)
|
||||
{
|
||||
|
@ -71,7 +71,7 @@ namespace ESM
|
|||
const std::string& getString() const;
|
||||
///< Will throw an exception, if value can not be represented as a string.
|
||||
|
||||
int getInteger() const;
|
||||
int32_t getInteger() const;
|
||||
///< Will throw an exception, if value can not be represented as an integer (implicit
|
||||
/// casting of float values is permitted).
|
||||
|
||||
|
@ -93,7 +93,7 @@ namespace ESM
|
|||
void setString(std::string&& value);
|
||||
///< Will throw an exception, if type is not compatible with string.
|
||||
|
||||
void setInteger(int value);
|
||||
void setInteger(int32_t value);
|
||||
///< Will throw an exception, if type is not compatible with integer.
|
||||
|
||||
void setFloat(float value);
|
||||
|
|
|
@ -46,7 +46,7 @@ namespace ESM
|
|||
esm.writeHNString("STRV", in);
|
||||
}
|
||||
|
||||
void readESMVariantValue(ESMReader& esm, Variant::Format format, VarType type, int& out)
|
||||
void readESMVariantValue(ESMReader& esm, Variant::Format format, VarType type, int32_t& out)
|
||||
{
|
||||
if (type != VT_Short && type != VT_Long && type != VT_Int)
|
||||
throw std::logic_error("not an integer type");
|
||||
|
@ -60,9 +60,9 @@ namespace ESM
|
|||
if (std::isnan(value))
|
||||
out = 0;
|
||||
else
|
||||
out = static_cast<short>(value);
|
||||
out = static_cast<int16_t>(value);
|
||||
else if (type == VT_Long)
|
||||
out = static_cast<int>(value);
|
||||
out = static_cast<int32_t>(value);
|
||||
else
|
||||
esm.fail("unsupported global variable integer type");
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ namespace ESM
|
|||
{
|
||||
if (type == VT_Short)
|
||||
{
|
||||
short value;
|
||||
int16_t value;
|
||||
esm.getHT(value);
|
||||
out = value;
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ namespace ESM
|
|||
}
|
||||
}
|
||||
|
||||
void writeESMVariantValue(ESMWriter& esm, Variant::Format format, VarType type, int in)
|
||||
void writeESMVariantValue(ESMWriter& esm, Variant::Format format, VarType type, int32_t in)
|
||||
{
|
||||
if (type != VT_Short && type != VT_Long && type != VT_Int)
|
||||
throw std::logic_error("not an integer type");
|
||||
|
@ -126,7 +126,7 @@ namespace ESM
|
|||
else if (format == Variant::Format_Local)
|
||||
{
|
||||
if (type == VT_Short)
|
||||
esm.writeHNT("STTV", static_cast<short>(in));
|
||||
esm.writeHNT("STTV", static_cast<int16_t>(in));
|
||||
else if (type == VT_Int)
|
||||
esm.writeHNT("INTV", in);
|
||||
else
|
||||
|
|
|
@ -12,13 +12,13 @@ namespace ESM
|
|||
|
||||
void readESMVariantValue(ESMReader& reader, Variant::Format format, VarType type, float& value);
|
||||
|
||||
void readESMVariantValue(ESMReader& reader, Variant::Format format, VarType type, int& value);
|
||||
void readESMVariantValue(ESMReader& reader, Variant::Format format, VarType type, int32_t& value);
|
||||
|
||||
void writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, const std::string& value);
|
||||
|
||||
void writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, float value);
|
||||
|
||||
void writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, int value);
|
||||
void writeESMVariantValue(ESMWriter& writer, Variant::Format format, VarType type, int32_t value);
|
||||
|
||||
struct ReadESMVariantValue
|
||||
{
|
||||
|
|
|
@ -76,6 +76,9 @@ void ESM4::LandTexture::load(ESM4::Reader& reader)
|
|||
case ESM4::SUB_MNAM:
|
||||
reader.getFormId(mMaterial);
|
||||
break; // TES5, FO4
|
||||
case ESM4::SUB_INAM:
|
||||
reader.get(mMaterialFlags);
|
||||
break; // SSE
|
||||
default:
|
||||
throw std::runtime_error("ESM4::LTEX::load - Unknown subrecord " + ESM::printName(subHdr.typeId));
|
||||
}
|
||||
|
|
|
@ -63,6 +63,17 @@ namespace ESM4
|
|||
|
||||
// ----------------------
|
||||
|
||||
// ------ SSE -----------
|
||||
|
||||
enum MaterialFlags
|
||||
{
|
||||
Flag_IsSnow = 0x1,
|
||||
};
|
||||
|
||||
std::uint32_t mMaterialFlags;
|
||||
|
||||
// ----------------------
|
||||
|
||||
void load(ESM4::Reader& reader);
|
||||
// void save(ESM4::Writer& writer) const;
|
||||
|
||||
|
|
|
@ -116,9 +116,11 @@ void ESM4::Npc::load(ESM4::Reader& reader)
|
|||
{
|
||||
switch (subHdr.dataSize)
|
||||
{
|
||||
case 20: // FO4
|
||||
mIsFO4 = true;
|
||||
[[fallthrough]];
|
||||
case 16: // TES4
|
||||
case 24: // FO3/FNV, TES5
|
||||
case 20: // FO4
|
||||
reader.get(&mBaseConfig, subHdr.dataSize);
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -78,6 +78,7 @@ namespace ESM4
|
|||
FO3_NoRotateHead = 0x40000000
|
||||
};
|
||||
|
||||
// In FO4 flags seem to be the same.
|
||||
enum ACBS_TES5
|
||||
{
|
||||
TES5_Female = 0x00000001,
|
||||
|
@ -101,27 +102,32 @@ namespace ESM4
|
|||
TES5_Invulnerable = 0x80000000
|
||||
};
|
||||
|
||||
// All FO3+ games.
|
||||
enum Template_Flags
|
||||
{
|
||||
TES5_UseTraits = 0x0001, // Destructible Object; Traits tab, including race, gender, height, weight,
|
||||
// voice type, death item; Sounds tab; Animation tab; Character Gen tabs
|
||||
TES5_UseStats = 0x0002, // Stats tab, including level, autocalc, skills, health/magicka/stamina,
|
||||
// speed, bleedout, class
|
||||
TES5_UseFactions = 0x0004, // both factions and assigned crime faction
|
||||
TES5_UseSpellList = 0x0008, // both spells and perks
|
||||
TES5_UseAIData = 0x0010, // AI Data tab, including aggression/confidence/morality, combat style and
|
||||
// gift filter
|
||||
TES5_UseAIPackage = 0x0020, // only the basic Packages listed on the AI Packages tab;
|
||||
// rest of tab controlled by Def Pack List
|
||||
TES5_UseBaseData = 0x0080, // including name and short name, and flags for Essential, Protected,
|
||||
// Respawn, Summonable, Simple Actor, and Doesn't affect stealth meter
|
||||
TES5_UseInventory = 0x0100, // Inventory tab, including all outfits and geared-up item
|
||||
// -- but not death item
|
||||
TES5_UseScript = 0x0200,
|
||||
TES5_UseDefined = 0x0400, // Def Pack List (the dropdown-selected package lists on the AI Packages tab)
|
||||
TES5_UseAtkData = 0x0800, // Attack Data tab, including override from behavior graph race,
|
||||
// events, and data)
|
||||
TES5_UseKeywords = 0x1000
|
||||
Template_UseTraits = 0x0001, // Destructible Object; Traits tab, including race, gender, height, weight,
|
||||
// voice type, death item; Sounds tab; Animation tab; Character Gen tabs
|
||||
Template_UseStats = 0x0002, // Stats tab, including level, autocalc, skills, health/magicka/stamina,
|
||||
// speed, bleedout, class
|
||||
Template_UseFactions = 0x0004, // both factions and assigned crime faction
|
||||
Template_UseSpellList = 0x0008, // both spells and perks
|
||||
Template_UseAIData = 0x0010, // AI Data tab, including aggression/confidence/morality, combat style and
|
||||
// gift filter
|
||||
Template_UseAIPackage = 0x0020, // only the basic Packages listed on the AI Packages tab;
|
||||
// rest of tab controlled by Def Pack List
|
||||
Template_UseModel = 0x0040, // FO3, FONV; probably not used in TES5+
|
||||
Template_UseBaseData = 0x0080, // including name and short name, and flags for Essential, Protected,
|
||||
// Respawn, Summonable, Simple Actor, and Doesn't affect stealth meter
|
||||
Template_UseInventory = 0x0100, // Inventory tab, including all outfits and geared-up item,
|
||||
// but not death item
|
||||
Template_UseScript = 0x0200,
|
||||
|
||||
// The following flags were added in TES5+:
|
||||
|
||||
Template_UseDefined = 0x0400, // Def Pack List (the dropdown-selected package lists on the AI Packages tab)
|
||||
Template_UseAtkData = 0x0800, // Attack Data tab, including override from behavior graph race,
|
||||
// events, and data)
|
||||
Template_UseKeywords = 0x1000
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
@ -172,6 +178,7 @@ namespace ESM4
|
|||
|
||||
bool mIsTES4;
|
||||
bool mIsFONV;
|
||||
bool mIsFO4 = false;
|
||||
|
||||
std::string mEditorId;
|
||||
std::string mFullName;
|
||||
|
|
|
@ -110,7 +110,7 @@ namespace ESM4
|
|||
ESM::FormId mId; // from the header
|
||||
std::uint32_t mFlags; // from the header, see enum type RecordFlag for details
|
||||
|
||||
bool mIsTES5;
|
||||
bool mIsTES5 = false;
|
||||
|
||||
std::string mEditorId;
|
||||
std::string mFullName;
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
#include <osg/StateSet>
|
||||
|
||||
#include <components/resource/scenemanager.hpp>
|
||||
#include <components/sceneutil/clearcolor.hpp>
|
||||
#include <components/sceneutil/lightmanager.hpp>
|
||||
#include <components/settings/values.hpp>
|
||||
#include <components/stereo/multiview.hpp>
|
||||
|
@ -190,6 +189,11 @@ mat4 omw_InvProjectionMatrix()
|
|||
#endif
|
||||
}
|
||||
|
||||
vec3 omw_GetNormalsWorldSpace(vec2 uv)
|
||||
{
|
||||
return (vec4(omw_GetNormals(uv), 0.0) * omw.viewMatrix).rgb;
|
||||
}
|
||||
|
||||
vec3 omw_GetWorldPosFromUV(vec2 uv)
|
||||
{
|
||||
float depth = omw_GetDepth(uv);
|
||||
|
@ -321,9 +325,6 @@ float omw_EstimateFogCoverageFromUV(vec2 uv)
|
|||
|
||||
if (mBlendEq)
|
||||
stateSet->setAttributeAndModes(new osg::BlendEquation(mBlendEq.value()));
|
||||
|
||||
if (mClearColor)
|
||||
stateSet->setAttributeAndModes(new SceneUtil::ClearColor(mClearColor.value(), GL_COLOR_BUFFER_BIT));
|
||||
}
|
||||
|
||||
void Pass::dirty()
|
||||
|
@ -339,7 +340,7 @@ float omw_EstimateFogCoverageFromUV(vec2 uv)
|
|||
if (mCompiled)
|
||||
return;
|
||||
|
||||
mLegacyGLSL = technique.getGLSLVersion() != 330;
|
||||
mLegacyGLSL = technique.getGLSLVersion() < 330;
|
||||
|
||||
if (mType == Type::Pixel)
|
||||
{
|
||||
|
|
|
@ -72,7 +72,6 @@ namespace fx
|
|||
std::array<std::string, 3> mRenderTargets;
|
||||
|
||||
std::string mTarget;
|
||||
std::optional<osg::Vec4f> mClearColor;
|
||||
|
||||
std::optional<osg::BlendFunc::BlendFuncMode> mBlendSource;
|
||||
std::optional<osg::BlendFunc::BlendFuncMode> mBlendDest;
|
||||
|
|
|
@ -279,6 +279,7 @@ namespace fx
|
|||
rt.mTarget->setSourceType(GL_UNSIGNED_BYTE);
|
||||
rt.mTarget->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE);
|
||||
rt.mTarget->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE);
|
||||
rt.mTarget->setName(std::string(mBlockName));
|
||||
|
||||
while (!isNext<Lexer::Close_bracket>() && !isNext<Lexer::Eof>())
|
||||
{
|
||||
|
@ -312,6 +313,8 @@ namespace fx
|
|||
rt.mTarget->setSourceFormat(parseSourceFormat());
|
||||
else if (key == "mipmaps")
|
||||
rt.mMipMap = parseBool();
|
||||
else if (key == "clear_color")
|
||||
rt.mClearColor = parseVec<osg::Vec4f, Lexer::Vec4>();
|
||||
else
|
||||
error(Misc::StringUtils::format("unexpected key '%s'", std::string(key)));
|
||||
|
||||
|
@ -797,9 +800,6 @@ namespace fx
|
|||
if (!pass)
|
||||
pass = std::make_shared<fx::Pass>();
|
||||
|
||||
bool clear = true;
|
||||
osg::Vec4f clearColor = { 1, 1, 1, 1 };
|
||||
|
||||
while (!isNext<Lexer::Eof>())
|
||||
{
|
||||
expect<Lexer::Literal>("invalid key in block header");
|
||||
|
@ -843,10 +843,6 @@ namespace fx
|
|||
if (blendEq != osg::BlendEquation::FUNC_ADD)
|
||||
pass->mBlendEq = blendEq;
|
||||
}
|
||||
else if (key == "clear")
|
||||
clear = parseBool();
|
||||
else if (key == "clear_color")
|
||||
clearColor = parseVec<osg::Vec4f, Lexer::Vec4>();
|
||||
else
|
||||
error(Misc::StringUtils::format("unrecognized key '%s' in block header", std::string(key)));
|
||||
|
||||
|
@ -864,9 +860,6 @@ namespace fx
|
|||
return;
|
||||
}
|
||||
|
||||
if (clear)
|
||||
pass->mClearColor = clearColor;
|
||||
|
||||
error("malformed block header");
|
||||
}
|
||||
|
||||
|
|
|
@ -54,10 +54,14 @@ namespace fx
|
|||
osg::ref_ptr<osg::FrameBufferObject> mRenderTarget;
|
||||
osg::ref_ptr<osg::Texture2D> mRenderTexture;
|
||||
bool mResolve = false;
|
||||
Types::SizeProxy mSize;
|
||||
bool mMipMap;
|
||||
|
||||
SubPass(const SubPass& other, const osg::CopyOp& copyOp = osg::CopyOp::SHALLOW_COPY)
|
||||
: mStateSet(new osg::StateSet(*other.mStateSet, copyOp))
|
||||
, mResolve(other.mResolve)
|
||||
, mSize(other.mSize)
|
||||
, mMipMap(other.mMipMap)
|
||||
{
|
||||
if (other.mRenderTarget)
|
||||
mRenderTarget = new osg::FrameBufferObject(*other.mRenderTarget, copyOp);
|
||||
|
|
|
@ -29,6 +29,16 @@ namespace fx
|
|||
std::optional<int> mWidth;
|
||||
std::optional<int> mHeight;
|
||||
|
||||
SizeProxy() = default;
|
||||
|
||||
SizeProxy(const SizeProxy& other)
|
||||
: mWidthRatio(other.mWidthRatio)
|
||||
, mHeightRatio(other.mHeightRatio)
|
||||
, mWidth(other.mWidth)
|
||||
, mHeight(other.mHeight)
|
||||
{
|
||||
}
|
||||
|
||||
std::tuple<int, int> get(int width, int height) const
|
||||
{
|
||||
int scaledWidth = width;
|
||||
|
@ -53,6 +63,17 @@ namespace fx
|
|||
osg::ref_ptr<osg::Texture2D> mTarget = new osg::Texture2D;
|
||||
SizeProxy mSize;
|
||||
bool mMipMap = false;
|
||||
osg::Vec4f mClearColor = osg::Vec4f(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
RenderTarget() = default;
|
||||
|
||||
RenderTarget(const RenderTarget& other)
|
||||
: mTarget(other.mTarget)
|
||||
, mSize(other.mSize)
|
||||
, mMipMap(other.mMipMap)
|
||||
, mClearColor(other.mClearColor)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
|
|
|
@ -8,9 +8,7 @@ namespace LuaUi
|
|||
{
|
||||
void LuaTileRect::_setAlign(const MyGUI::IntSize& _oldsize)
|
||||
{
|
||||
mCurrentCoord.set(0, 0, mCroppedParent->getWidth(), mCroppedParent->getHeight());
|
||||
mAlign = MyGUI::Align::Stretch;
|
||||
MyGUI::TileRect::_setAlign(_oldsize);
|
||||
mCoord.set(0, 0, mCroppedParent->getWidth(), mCroppedParent->getHeight());
|
||||
mTileSize = mSetTileSize;
|
||||
|
||||
// zero tilesize stands for not tiling
|
||||
|
@ -25,6 +23,8 @@ namespace LuaUi
|
|||
mTileSize.width = 1e7;
|
||||
if (mTileSize.height <= 0)
|
||||
mTileSize.height = 1e7;
|
||||
|
||||
MyGUI::TileRect::_updateView();
|
||||
}
|
||||
|
||||
void LuaImage::initialize()
|
||||
|
@ -55,13 +55,13 @@ namespace LuaUi
|
|||
if (texture != nullptr)
|
||||
textureSize = MyGUI::IntSize(texture->getWidth(), texture->getHeight());
|
||||
|
||||
mTileRect->updateSize(MyGUI::IntSize(tileH ? textureSize.width : 0, tileV ? textureSize.height : 0));
|
||||
setImageTile(textureSize);
|
||||
|
||||
if (atlasCoord.width == 0)
|
||||
atlasCoord.width = textureSize.width;
|
||||
if (atlasCoord.height == 0)
|
||||
atlasCoord.height = textureSize.height;
|
||||
|
||||
mTileRect->updateSize(MyGUI::IntSize(tileH ? atlasCoord.width : 0, tileV ? atlasCoord.height : 0));
|
||||
setImageTile(atlasCoord.size());
|
||||
setImageCoord(atlasCoord);
|
||||
|
||||
setColour(propertyValue("color", MyGUI::Colour(1, 1, 1, 1)));
|
||||
|
|
|
@ -8,6 +8,7 @@ namespace LuaUi
|
|||
{
|
||||
mEditBox = createWidget<MyGUI::EditBox>("LuaTextEdit", MyGUI::IntCoord(0, 0, 0, 0), MyGUI::Align::Default);
|
||||
mEditBox->eventEditTextChange += MyGUI::newDelegate(this, &LuaTextEdit::textChange);
|
||||
mEditBox->setMaxTextLength(std::numeric_limits<std::size_t>::max());
|
||||
registerEvents(mEditBox);
|
||||
WidgetExtension::initialize();
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue