Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/windows-playable-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
push:
branches:
- master
- nonplayable-beast-race
workflow_call:

jobs:
Expand All @@ -16,7 +17,7 @@ jobs:
strategy:
fail-fast: false
matrix:
skyrim-version: ['latest', '1.6.1170']
skyrim-version: ['latest']
uses: ./.github/workflows/windows.yml
with:
build-mode: release
Expand Down
26 changes: 26 additions & 0 deletions Code/client/Games/References.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,32 @@ float CalculateRealDamage(Actor* apHittee, float aDamage, bool aKillMove) noexce
return realDamage;
}

float GetPartyDamageMultiplier(Actor* apHitter) noexcept
{
const float cScale = World::Get().GetServerSettings().PartyDamageScale;
if (cScale <= 0.f || !apHitter)
return 1.f;

const auto& partyService = World::Get().GetPartyService();
if (!partyService.IsInParty())
return 1.f;

const size_t cMembers = partyService.GetPartyMembers().size();
if (cMembers < 2)
return 1.f;

Actor* pSource = apHitter;
if (!pSource->GetExtension()->IsPlayer())
{
if (Actor* pCommander = pSource->GetCommandingActor())
pSource = pCommander;
}
if (!pSource->GetExtension()->IsPlayer())
return 1.f;

return 1.f / (1.f + cScale * static_cast<float>(cMembers - 1));
}

} // namespace GameplayFormulas

void FadeOutGame(bool aFadingOut, bool aBlackFade, float aFadeDuration, bool aRemainVisible, float aSecondsToFade) noexcept
Expand Down
1 change: 1 addition & 0 deletions Code/client/Games/References.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
namespace GameplayFormulas
{
float CalculateRealDamage(Actor* apHittee, float aDamage, bool aKillMove) noexcept;
float GetPartyDamageMultiplier(Actor* apHitter) noexcept;
}

namespace Settings
Expand Down
2 changes: 2 additions & 0 deletions Code/client/Games/Skyrim/Actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,8 @@ bool TP_MAKE_THISCALL(HookDamageActor, Actor, float aDamage, Actor* apHitter, bo
if (apHitter)
World::Get().GetRunner().Trigger(HitEvent(apHitter->formID, apThis->formID));

aDamage *= GameplayFormulas::GetPartyDamageMultiplier(apHitter);

float realDamage = GameplayFormulas::CalculateRealDamage(apThis, aDamage, aKillMove);

float currentHealth = apThis->GetActorValue(ActorValueInfo::kHealth);
Expand Down
12 changes: 12 additions & 0 deletions Code/client/Games/Skyrim/Forms/TESRace.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,16 @@

struct TESRace : TESForm, TESFullName
{
// RACE_DATA::Flag
enum : uint32_t
{
kPlayable = 1 << 0,
};

[[nodiscard]] bool IsPlayable() const noexcept { return (raceFlags & kPlayable) != 0; }

uint8_t unk30[0x108 - 0x30];
uint32_t raceFlags; // RACE_DATA (DATA) at 0xE8, its flags at +0x20
};

static_assert(offsetof(TESRace, raceFlags) == 0x108);
13 changes: 10 additions & 3 deletions Code/client/Services/Generic/PlayerService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -402,9 +402,16 @@ void PlayerService::RunBeastFormDetection() const noexcept
static uint32_t lastRaceFormID = 0;
static std::chrono::steady_clock::time_point lastSendTimePoint;
constexpr auto cDelayBetweenUpdates = 250ms;
// Werewolf (0xCDD84) and Vampire Lord (0x200283A)
constexpr auto isBeastRace = [](uint32_t aFormId) noexcept {
return aFormId == 0xCDD84 || aFormId == 0x200283A;
// Werewolf (0xCDD84) and Vampire Lord (0x200283A), and any other non-playable race: mods transform the
// player with SetRace into their own beast races (e.g. Enderal's Theriantrophist wolf form), and without
// the respawn the remote puppet keeps its old body on the new race's skeleton and behavior. Playable races
// are excluded so race changes in the RaceSex menu don't respawn the puppet.
const auto isBeastRace = [](uint32_t aFormId) noexcept {
if (aFormId == 0xCDD84 || aFormId == 0x200283A)
return true;

const TESRace* pRace = Cast<TESRace>(TESForm::GetById(aFormId));
return pRace && !pRace->IsPlayable();
};

const auto now = std::chrono::steady_clock::now();
Expand Down
4 changes: 3 additions & 1 deletion Code/encoding/Structs/ServerSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ using TiltedPhoques::Serialization;

bool ServerSettings::operator==(const ServerSettings& acRhs) const noexcept
{
return Difficulty == acRhs.Difficulty && GreetingsEnabled == acRhs.GreetingsEnabled && PvpEnabled == acRhs.PvpEnabled && SyncPlayerHomes == acRhs.SyncPlayerHomes && DeathSystemEnabled == acRhs.DeathSystemEnabled && AutoPartyJoin == acRhs.AutoPartyJoin && ItemDropsEnabled == acRhs.ItemDropsEnabled && ConsoleCommandsAllowed == acRhs.ConsoleCommandsAllowed && VoipEnabled == acRhs.VoipEnabled;
return Difficulty == acRhs.Difficulty && GreetingsEnabled == acRhs.GreetingsEnabled && PvpEnabled == acRhs.PvpEnabled && SyncPlayerHomes == acRhs.SyncPlayerHomes && DeathSystemEnabled == acRhs.DeathSystemEnabled && AutoPartyJoin == acRhs.AutoPartyJoin && ItemDropsEnabled == acRhs.ItemDropsEnabled && ConsoleCommandsAllowed == acRhs.ConsoleCommandsAllowed && VoipEnabled == acRhs.VoipEnabled && PartyDamageScale == acRhs.PartyDamageScale;
}

bool ServerSettings::operator!=(const ServerSettings& acRhs) const noexcept
Expand All @@ -24,6 +24,7 @@ void ServerSettings::Serialize(TiltedPhoques::Buffer::Writer& aWriter) const noe
Serialization::WriteBool(aWriter, AutoPartyJoin);
Serialization::WriteBool(aWriter, ItemDropsEnabled);
Serialization::WriteBool(aWriter, ConsoleCommandsAllowed);
Serialization::WriteFloat(aWriter, PartyDamageScale);
Serialization::WriteBool(aWriter, VoipEnabled);
}

Expand All @@ -38,5 +39,6 @@ void ServerSettings::Deserialize(TiltedPhoques::Buffer::Reader& aReader) noexcep
AutoPartyJoin = Serialization::ReadBool(aReader);
ItemDropsEnabled = Serialization::ReadBool(aReader);
ConsoleCommandsAllowed = Serialization::ReadBool(aReader);
PartyDamageScale = Serialization::ReadFloat(aReader);
VoipEnabled = Serialization::ReadBool(aReader);
}
1 change: 1 addition & 0 deletions Code/encoding/Structs/ServerSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct ServerSettings
bool AutoPartyJoin{};
bool ItemDropsEnabled{};
bool ConsoleCommandsAllowed{true};
float PartyDamageScale{};
// Built-in proximity voice chat, players also need Connect to voip on
bool VoipEnabled{};
};
19 changes: 19 additions & 0 deletions Code/server/GameServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Console::Setting uTimeScale{"Gameplay:uTimeScale", "How many seconds pass ingame
Console::Setting bSyncPlayerCalendar{"Gameplay:bSyncPlayerCalendar", "Syncs up all player calendars to be the same day, month, and year. This uses the date of the player with the furthest ahead date at connection.", false};
Console::Setting bAutoPartyJoin{"Gameplay:bAutoPartyJoin", "Join parties automatically, as long as there is only one party in the server", true};
Console::Setting bEnableItemDrops{"Gameplay:bEnableItemDrops", "Syncs items dropped by players", true};
Console::Setting fPartyDamageScale{"Gameplay:fPartyDamageScale", "Longer fights in a party: damage from players, their spells and their summons is divided by 1 + this x (party members - 1). 1 = like enemies having x party members health, 0 = off. NPCs and followers keep full damage", 0.0f};
Console::Setting bAllowConsoleCommands{"Gameplay:bAllowConsoleCommands", "Lets players run console commands they type while connected. Turn it off to stop console trolling such as spawning enemies or killing NPCs. The console still opens for reading ids, and mods can still run commands", true};
// ModPolicy Stuff
Console::Setting bEnableModCheck{"ModPolicy:bEnableModCheck", "Refuse clients whose mods do not match Data/loadorder.txt", false, Console::SettingsFlags::kLocked};
Expand Down Expand Up @@ -77,6 +78,23 @@ Console::Command<> ToggleConsoleCommands(
GameServer::Get()->UpdateSettings();
});

Console::Command<std::string> SetPartyDamageScale(
"SetPartyDamageScale", "Set Gameplay:fPartyDamageScale (0 turns it off, 1 is like enemies having party size x health)",
[](Console::ArgStack& aStack)
{
const auto& cValue = aStack.Pop<TiltedPhoques::String>();
char* pEnd = nullptr;
const float cScale = std::strtof(cValue.c_str(), &pEnd);
if (pEnd == cValue.c_str() || cScale < 0.0f || cScale > 10.0f)
{
spdlog::get("ConOut")->error("Party damage scale must be a number from 0 to 10, e.g. 1 or 0.75");
return;
}
fPartyDamageScale = cScale;
spdlog::get("ConOut")->info("Party damage scale has been set to {}.", cScale);
GameServer::Get()->UpdateSettings();
});

Console::Command<int64_t> SetDifficulty(
"SetDifficulty", "Set server difficulty (0 being Novice and 5 being Legendary; default is 4)",
[](Console::ArgStack& aStack)
Expand Down Expand Up @@ -156,6 +174,7 @@ ServerSettings GetSettings()
settings.AutoPartyJoin = bAutoPartyJoin;
settings.ItemDropsEnabled = bEnableItemDrops;
settings.ConsoleCommandsAllowed = bAllowConsoleCommands;
settings.PartyDamageScale = std::clamp(fPartyDamageScale.value_as<float>(), 0.0f, 10.0f);
settings.VoipEnabled = bVoip;
return settings;
}
Expand Down