diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE
new file mode 100644
index 0000000..5474ada
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/.github/assets/download.png b/.github/assets/download.png
new file mode 100644
index 0000000..cc88b50
Binary files /dev/null and b/.github/assets/download.png differ
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..1fa452e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "nuget" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
\ No newline at end of file
diff --git a/.github/workflows/msbuild.yml b/.github/workflows/msbuild.yml
new file mode 100644
index 0000000..ec7eca2
--- /dev/null
+++ b/.github/workflows/msbuild.yml
@@ -0,0 +1,36 @@
+name: MSBuild
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+env:
+ SOLUTION_FILE_PATH: .
+
+ BUILD_PLATFORM: x64
+ BUILD_CONFIGURATION: Release
+
+jobs:
+ build:
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Add MSBuild to PATH
+ uses: microsoft/setup-msbuild@v1.1
+
+ - name: Restore NuGet packages
+ working-directory: ${{env.GITHUB_WORKSPACE}}
+ run: nuget restore ${{env.SOLUTION_FILE_PATH}}
+
+ - name: Build
+ working-directory: ${{env.GITHUB_WORKSPACE}}
+ run: msbuild /m /p:Platform=${{env.BUILD_PLATFORM}} /p:Configuration=${{env.BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}}
+
+ - uses: actions/upload-artifact@v3
+ with:
+ name: Amalgam
+ path: ${{env.SOLUTION_FILE_PATH}}/output/${{env.BUILD_PLATFORM}}/${{env.BUILD_CONFIGURATION}}/*.dll
\ No newline at end of file
diff --git a/Amalgam/Amalgam.vcxproj b/Amalgam/Amalgam.vcxproj
index 6029559..a42ff54 100644
--- a/Amalgam/Amalgam.vcxproj
+++ b/Amalgam/Amalgam.vcxproj
@@ -243,7 +243,7 @@
-
+
@@ -494,7 +494,7 @@
-
+
diff --git a/Amalgam/Amalgam.vcxproj.filters b/Amalgam/Amalgam.vcxproj.filters
index 9ce4cad..b2443db 100644
--- a/Amalgam/Amalgam.vcxproj.filters
+++ b/Amalgam/Amalgam.vcxproj.filters
@@ -6,7 +6,7 @@
-
+
@@ -174,7 +174,7 @@
-
+
diff --git a/Amalgam/src/Features/CheaterDetection/CheaterDetection.cpp b/Amalgam/src/Features/CheaterDetection/CheaterDetection.cpp
index 9ca343e..6923c6a 100644
--- a/Amalgam/src/Features/CheaterDetection/CheaterDetection.cpp
+++ b/Amalgam/src/Features/CheaterDetection/CheaterDetection.cpp
@@ -1,7 +1,7 @@
#include "CheaterDetection.h"
#include "../Players/PlayerUtils.h"
-#include "../Logs/Logs.h"
+#include "../Records/Records.h"
bool CCheaterDetection::ShouldScan()
{
@@ -82,7 +82,7 @@ void CCheaterDetection::Infract(CTFPlayer* pEntity, std::string sReason)
mData[pEntity].iDetections++;
const bool bMark = mData[pEntity].iDetections >= Vars::CheaterDetection::DetectionsRequired.Value;
- F::Logs.CheatDetection(mData[pEntity].sName, bMark ? "marked" : "infracted", sReason);
+ F::Records.CheatDetection(mData[pEntity].sName, bMark ? "marked" : "infracted", sReason);
if (bMark)
{
mData[pEntity].iDetections = 0;
diff --git a/Amalgam/src/Features/Players/PlayerUtils.cpp b/Amalgam/src/Features/Players/PlayerUtils.cpp
index 70ffc65..23018b2 100644
--- a/Amalgam/src/Features/Players/PlayerUtils.cpp
+++ b/Amalgam/src/Features/Players/PlayerUtils.cpp
@@ -1,7 +1,7 @@
#include "PlayerUtils.h"
#include "../../SDK/Definitions/Types.h"
-#include "../Logs/Logs.h"
+#include "../Records/Records.h"
uint32_t GetFriendsID(int iIndex)
{
@@ -40,7 +40,7 @@ void CPlayerlistUtils::AddTag(uint32_t friendsID, std::string sTag, bool bSave,
{
PriorityLabel_t plTag;
if (GetTag(sTag, &plTag))
- F::Logs.TagsChanged(sName, "Added", plTag.Color.ToHexA(), sTag);
+ F::Records.TagsChanged(sName, "Added", plTag.Color.ToHexA(), sTag);
}
}
}
@@ -67,7 +67,7 @@ void CPlayerlistUtils::RemoveTag(uint32_t friendsID, std::string sTag, bool bSav
{
PriorityLabel_t plTag;
if (GetTag(sTag, &plTag))
- F::Logs.TagsChanged(sName, "Removed", plTag.Color.ToHexA(), sTag);
+ F::Records.TagsChanged(sName, "Removed", plTag.Color.ToHexA(), sTag);
}
break;
}
diff --git a/Amalgam/src/Features/Records/Records.cpp b/Amalgam/src/Features/Records/Records.cpp
new file mode 100644
index 0000000..250a8bb
--- /dev/null
+++ b/Amalgam/src/Features/Records/Records.cpp
@@ -0,0 +1,255 @@
+#include "Records.h"
+
+#include "../Visuals/Notifications/Notifications.h"
+#include "../Players/PlayerUtils.h"
+
+static std::string white({ '\x7', 'F', 'F', 'F', 'F', 'F', 'F' }); //FFFFFF
+static std::string red({ '\x7', 'F', 'F', '3', 'A', '3', 'A' }); //FF3A3A
+static std::string green({ '\x7', '3', 'A', 'F', 'F', '4', 'D' }); //3AFF4D
+static std::string blue({ '\x7', '0', 'D', '9', '2', 'F', 'F' }); //0D92FF
+static std::string yellow({ '\x7', 'C', '8', 'A', '9', '0', '0' }); //C8A900
+static std::string gsred({ '\x7', '7', '5', '7', '5', '7', '5' }); //757575
+static std::string gsgreen({ '\x7', 'B', '0', 'B', '0', 'B', '0' }); //b0b0b0
+static std::string gsblue({ '\x7', '7', '6', '7', '6', '7', '6' }); //767676
+static std::string gsyellow({ '\x7', 'C', 'A', 'C', 'A', 'C', 'A' }); //CACACA
+
+void CRecords::OutputInfo(int flags, std::string name, std::string string, std::string cstring)
+{
+ if (flags & 1 << 0)
+ F::Notifications.Add(string);
+ if (flags & 1 << 1)
+ I::ClientModeShared->m_pChatElement->ChatPrintf(0, std::format("{}{}\x1 {}", Vars::Menu::Theme::Accent.Value.ToHex(), Vars::Menu::CheatPrefix.Value, cstring).c_str());
+ if (flags & 1 << 2)
+ I::EngineClient->ClientCmd_Unrestricted(std::format("tf_party_chat \"{}\"", string).c_str());
+ if (flags & 1 << 3)
+ SDK::Output(name.c_str(), string.c_str(), Vars::Menu::Theme::Accent.Value);
+}
+
+// Event info
+void CRecords::Event(IGameEvent* pEvent, const FNV1A_t uHash, CTFPlayer* pLocal)
+{
+ if (uHash == FNV1A::HashConst("game_newmap"))
+ {
+ bTagsOnJoin = true;
+ return;
+ }
+
+ if (!I::EngineClient->IsConnected() || !I::EngineClient->IsInGame() || !pLocal)
+ return;
+
+ switch (uHash)
+ {
+ case FNV1A::HashConst("vote_cast"): // Voting
+ {
+ if (!(Vars::Logging::Logs.Value & 1 << 1))
+ return;
+
+ const int iIndex = pEvent->GetInt("entityid");
+ const auto pEntity = I::ClientEntityList->GetClientEntity(iIndex);
+ if (!pEntity || pEntity->GetClassID() != ETFClassID::CTFPlayer)
+ return;
+
+ const bool bVotedYes = pEvent->GetInt("vote_option") == 0;
+ const bool bSameTeam = pEntity->As()->m_iTeamNum() == pLocal->m_iTeamNum();
+
+ PlayerInfo_t pi{};
+ if (!I::EngineClient->GetPlayerInfo(iIndex, &pi))
+ return;
+
+ std::string string = std::format("{}{} voted {}", (bSameTeam ? "" : "[Enemy] "), (pi.name), (bVotedYes ? "Yes" : "No"));
+ std::string cstring = std::format("{}{}{}\x1 voted {}{}", (bSameTeam ? "" : "[Enemy] "), (yellow), (pi.name), (bVotedYes ? green : red), (bVotedYes ? "Yes" : "No"));
+ OutputInfo(Vars::Logging::VoteCast::LogTo.Value, "Vote Cast", string, cstring);
+
+ return;
+ }
+ case FNV1A::HashConst("player_changeclass"): // Class change
+ {
+ if (!(Vars::Logging::Logs.Value & 1 << 2))
+ return;
+
+ const int iIndex = I::EngineClient->GetPlayerForUserID(pEvent->GetInt("userid"));
+ const auto pEntity = I::ClientEntityList->GetClientEntity(iIndex);
+ if (!pEntity || iIndex == pLocal->entindex())
+ return;
+
+ const bool bSameTeam = pEntity->As()->m_iTeamNum() == pLocal->m_iTeamNum();
+
+ PlayerInfo_t pi{};
+ if (!I::EngineClient->GetPlayerInfo(iIndex, &pi) || pi.fakeplayer)
+ return; // dont spam chat by giving class changes for bots
+
+ std::string string = std::format("{}{} changed class to {}", (bSameTeam ? "" : "[Enemy] "), (pi.name), (SDK::GetClassByIndex(pEvent->GetInt("class"))));
+ std::string cstring = std::format("{}{}{}\x1 changed class to {}{}", (bSameTeam ? "" : "[Enemy] "), (yellow), (pi.name), (yellow), (SDK::GetClassByIndex(pEvent->GetInt("class"))));
+ OutputInfo(Vars::Logging::ClassChange::LogTo.Value, "Class Change", string, cstring);
+
+ return;
+ }
+ case FNV1A::HashConst("player_hurt"): // Damage
+ {
+ if (!(Vars::Logging::Logs.Value & 1 << 3))
+ return;
+
+ const int iIndex = I::EngineClient->GetPlayerForUserID(pEvent->GetInt("userid"));
+ const auto pEntity = I::ClientEntityList->GetClientEntity(iIndex);
+ if (!pEntity || iIndex == pLocal->entindex())
+ return;
+
+ const int nAttacker = pEvent->GetInt("attacker");
+ const int nHealth = pEvent->GetInt("health");
+ const int nDamage = pEvent->GetInt("damageamount");
+ const bool bCrit = pEvent->GetBool("crit");
+ const int maxHealth = pEntity->As()->m_iMaxHealth();
+
+ PlayerInfo_t pi{};
+ if (!I::EngineClient->GetPlayerInfo(I::EngineClient->GetLocalPlayer(), &pi) || nAttacker != pi.userID ||
+ !I::EngineClient->GetPlayerInfo(iIndex, &pi))
+ return;
+
+ std::string string = std::format("You hit {} for {} damage{}({} / {})", (pi.name), (nDamage), (bCrit ? " (crit) " : " "), (nHealth), (maxHealth));
+ std::string cstring = std::format("You hit {}{}\x1 for {}{} damage{}{}({} / {})", (yellow), (pi.name), (red), (nDamage), (bCrit ? " (crit) " : " "), (yellow), (nHealth), (maxHealth));
+ OutputInfo(Vars::Logging::Damage::LogTo.Value, "Damage", string, cstring);
+
+ return;
+ }
+ case FNV1A::HashConst("player_activate"): // Tags (player join)
+ {
+ if (!(Vars::Logging::Logs.Value & 1 << 5) || bTagsOnJoin)
+ return;
+
+ const int iIndex = I::EngineClient->GetPlayerForUserID(pEvent->GetInt("userid"));
+ if (iIndex == pLocal->entindex())
+ return;
+
+ PlayerInfo_t pi{};
+ if (!I::EngineClient->GetPlayerInfo(iIndex, &pi) || pi.fakeplayer)
+ return;
+
+ TagsOnJoin(pi.name, pi.friendsID);
+
+ return;
+ }
+ case FNV1A::HashConst("player_spawn"): // Tags (local join)
+ {
+ if (!(Vars::Logging::Logs.Value & 1 << 5) || !bTagsOnJoin)
+ return;
+
+ const int iIndex = I::EngineClient->GetPlayerForUserID(pEvent->GetInt("userid"));
+ if (iIndex != pLocal->entindex())
+ return;
+
+ bTagsOnJoin = false;
+ for (int n = 1; n <= I::EngineClient->GetMaxClients(); n++)
+ {
+ PlayerInfo_t pi{};
+ if (n == pLocal->entindex() || !I::EngineClient->GetPlayerInfo(n, &pi) || pi.fakeplayer)
+ continue;
+
+ TagsOnJoin(pi.name, pi.friendsID);
+ }
+ }
+ }
+}
+
+// Vote start
+void CRecords::UserMessage(bf_read& msgData)
+{
+ if (!(Vars::Logging::Logs.Value & 1 << 0))
+ return;
+
+ auto pLocal = H::Entities.GetLocal();
+ if (!pLocal)
+ return;
+
+ const int team = msgData.ReadByte();
+ const int voteID = msgData.ReadLong();
+ const int caller = msgData.ReadByte();
+ char reason[64], voteTarget[64];
+ msgData.ReadString(reason, 64);
+ msgData.ReadString(voteTarget, 64);
+ const int target = static_cast(msgData.ReadByte()) >> 1;
+ const bool bSameTeam = team == pLocal->m_iTeamNum();
+
+ PlayerInfo_t piTarget{}, piCaller{};
+ if (!caller || !target || !I::EngineClient->GetPlayerInfo(target, &piTarget) || !I::EngineClient->GetPlayerInfo(caller, &piCaller))
+ return;
+
+ std::string string = std::format("{}{} called a vote on {}", (bSameTeam ? "" : "[Enemy] "), (piCaller.name), (piTarget.name));
+ std::string cstring = std::format("{}{}{}\x1 called a vote on {}{}", (bSameTeam ? "" : "[Enemy] "), (yellow), (piCaller.name), (yellow), (piTarget.name));
+ OutputInfo(Vars::Logging::VoteStart::LogTo.Value, "Vote Start", string, cstring);
+}
+
+// Cheat detection
+void CRecords::CheatDetection(std::string name, std::string action, std::string reason)
+{
+ if (!(Vars::Logging::Logs.Value & 1 << 4))
+ return;
+
+ std::string string = std::format("{} {} for {}", (name), (action), (reason));
+ std::string cstring = std::format("{}{}\x1 {} for {}{}", (yellow), (name), (action), (yellow), (reason));
+ OutputInfo(Vars::Logging::CheatDetection::LogTo.Value, "Cheat Detection", string, cstring);
+}
+
+// Tags
+void CRecords::TagsOnJoin(std::string name, uint32_t friendsID)
+{
+ const auto& vTags = F::PlayerUtils.mPlayerTags[friendsID];
+ std::vector> vColorsTags = {};
+ for (auto& sTag : vTags)
+ {
+ PriorityLabel_t lbTag;
+ if (!F::PlayerUtils.GetTag(sTag, &lbTag))
+ continue;
+ vColorsTags.push_back({ lbTag.Color.ToHexA(), sTag });
+ }
+
+ std::string tagtext, ctagtext;
+ switch (vColorsTags.size())
+ {
+ case 0: return;
+ case 1:
+ {
+ auto& pColorTag = *vColorsTags.begin();
+ tagtext = pColorTag.second;
+ ctagtext = std::format("{}{}", pColorTag.first, pColorTag.second);
+ break;
+ }
+ case 2:
+ {
+ auto& pColorTag1 = *vColorsTags.begin(), &pColorTag2 = *(vColorsTags.begin() + 1);
+ tagtext = std::format("{} and ", pColorTag1.second, pColorTag2.second);
+ ctagtext = std::format("{}{}\x1 and {}{}", pColorTag1.first, pColorTag1.second, pColorTag2.first, pColorTag2.second);
+ break;
+ }
+ default:
+ {
+ for (auto it = vColorsTags.begin(); it != vColorsTags.end(); it++)
+ {
+ auto& pColorTag = *it;
+ if (it + 1 != vColorsTags.end())
+ {
+ tagtext += std::format("{}, ", pColorTag.second);
+ ctagtext += std::format("{}{}\x1, ", pColorTag.first, pColorTag.second);
+ }
+ else
+ {
+ tagtext += std::format("and {}", pColorTag.second);
+ ctagtext += std::format("and {}{}", pColorTag.first, pColorTag.second);
+ }
+ }
+ }
+ }
+
+ std::string string = std::format("{} has the {} {}", (name), (vColorsTags.size() == 1 ? "tag" : "tags"), (tagtext));
+ std::string cstring = std::format("{}{}\x1 has the {} {}", (yellow), (name), (vColorsTags.size() == 1 ? "tag" : "tags"), (ctagtext));
+ OutputInfo(Vars::Logging::Tags::LogTo.Value, "Tags", string, cstring);
+}
+void CRecords::TagsChanged(std::string name, std::string action, std::string color, std::string tag)
+{
+ if (!(Vars::Logging::Logs.Value & 1 << 5))
+ return;
+
+ auto uHash = FNV1A::Hash(action.c_str());
+ std::string string = std::format("{} tag {} {} {}", (action), (tag), (uHash == FNV1A::HashConst("Added") ? "to" : "from"), (name));
+ std::string cstring = std::format("{} tag {}{}\x1 {} {}{}", (action), (color), (tag), (uHash == FNV1A::HashConst("Added") ? "to" : "from"), (yellow), (name));
+ OutputInfo(Vars::Logging::Tags::LogTo.Value, "Tags", string, cstring);
+}
\ No newline at end of file
diff --git a/Amalgam/src/Features/Records/Records.h b/Amalgam/src/Features/Records/Records.h
new file mode 100644
index 0000000..5d0fc70
--- /dev/null
+++ b/Amalgam/src/Features/Records/Records.h
@@ -0,0 +1,18 @@
+#pragma once
+#include "../../SDK/SDK.h"
+
+class CRecords
+{
+ void OutputInfo(int flags, std::string name, std::string string, std::string cstring);
+
+ void TagsOnJoin(std::string name, uint32_t friendsID);
+ bool bTagsOnJoin = false;
+
+public:
+ void Event(IGameEvent* pEvent, FNV1A_t uHash, CTFPlayer* pLocal);
+ void UserMessage(bf_read& msgData);
+ void CheatDetection(std::string name, std::string action, std::string reason);
+ void TagsChanged(std::string name, std::string action, std::string color, std::string tag);
+};
+
+ADD_FEATURE(CRecords, Records)
\ No newline at end of file
diff --git a/Amalgam/src/Hooks/BaseClientDLL_DispatchUserMessage.cpp b/Amalgam/src/Hooks/BaseClientDLL_DispatchUserMessage.cpp
index a53a4aa..2fb0939 100644
--- a/Amalgam/src/Hooks/BaseClientDLL_DispatchUserMessage.cpp
+++ b/Amalgam/src/Hooks/BaseClientDLL_DispatchUserMessage.cpp
@@ -1,7 +1,7 @@
#include "../SDK/SDK.h"
#include "../Features/Misc/Misc.h"
-#include "../Features/Logs/Logs.h"
+#include "../Features/Records/Records.h"
#include "../Features/NoSpread/NoSpreadHitscan/NoSpreadHitscan.h"
#include
#include
@@ -16,7 +16,7 @@ MAKE_HOOK(BaseClientDLL_DispatchUserMessage, U::Memory.GetVFunc(I::BaseClientDLL
switch (type)
{
case VoteStart:
- F::Logs.UserMessage(msgData);
+ F::Records.UserMessage(msgData);
break;
/*
diff --git a/Amalgam/src/Hooks/CGameEventManager_FireEventIntern.cpp b/Amalgam/src/Hooks/CGameEventManager_FireEventIntern.cpp
index ffda7a0..2e27f67 100644
--- a/Amalgam/src/Hooks/CGameEventManager_FireEventIntern.cpp
+++ b/Amalgam/src/Hooks/CGameEventManager_FireEventIntern.cpp
@@ -3,7 +3,7 @@
#include "../Features/Backtrack/Backtrack.h"
#include "../Features/CheaterDetection/CheaterDetection.h"
#include "../Features/CritHack/CritHack.h"
-#include "../Features/Logs/Logs.h"
+#include "../Features/Records/Records.h"
#include "../Features/Misc/Misc.h"
#include "../Features/PacketManip/AntiAim/AntiAim.h"
#include "../Features/Resolver/Resolver.h"
@@ -22,7 +22,7 @@ MAKE_HOOK(CGameEventManager_FireEventIntern, S::CGameEventManager_FireEventInter
auto pLocal = H::Entities.GetLocal();
auto uHash = FNV1A::Hash(pEvent->GetName());
- F::Logs.Event(pEvent, uHash, pLocal);
+ F::Records.Event(pEvent, uHash, pLocal);
F::CritHack.Event(pEvent, uHash, pLocal);
F::Misc.Event(pEvent, uHash);
switch (uHash)
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..ad1989f
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,13 @@
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ Version 2, December 2004
+
+ Copyright (C) 2004 Sam Hocevar
+
+ Everyone is permitted to copy and distribute verbatim or modified
+ copies of this license document, and changing it is allowed as long
+ as the name is changed.
+
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
deleted file mode 100644
index 8aa2645..0000000
--- a/LICENSE.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) [year] [fullname]
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.