Skip to content

Commit

Permalink
[Replay] #359: Fix Showing dead players in whole timeline (#367)
Browse files Browse the repository at this point in the history
* [Replay] Properly initialize in Airship

* [Replay] Render paths properly when using time filters

* [Replay] #359: Fix Showing dead players in whole timeline

* [Settings] Ignore missing keys

* [Log] Add local time to logs

* [Log] Reduce log noise

* Ignore some game bugs

* [Replay] Fixed ambiguous play/pause state

* [Bug] Fixed an issue in "IsInGame"

The game will crash when closing the door at beginning of match (still black screen)

* [Bug] Fixed an issue causing the screen to freeze during a meeting

* Remove FlipSkeld in Airship and PolusShip

* simplified code
  • Loading branch information
cddjr committed May 31, 2022
1 parent ed84685 commit 9ead3b2
Show file tree
Hide file tree
Showing 16 changed files with 253 additions and 197 deletions.
2 changes: 2 additions & 0 deletions appdata/il2cpp-functions.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,5 @@ DO_APP_FUNC(bool, SaveManager_GetPurchase, (String* itemKey, String* bundleKey,

DO_APP_FUNC(void, PlayerControl_TurnOnProtection, (PlayerControl* __this, bool visible, int32_t colorId, MethodInfo* method), "Assembly-CSharp, System.Void PlayerControl::TurnOnProtection(System.Boolean, System.Int32)");
DO_APP_FUNC(void, PlayerControl_RemoveProtection, (PlayerControl* __this, MethodInfo* method), "Assembly-CSharp, System.Void PlayerControl::RemoveProtection()");

DO_APP_FUNC(bool, Object_1_op_Implicit, (Object_1* exists, MethodInfo* method), "UnityEngine.CoreModule, System.Boolean UnityEngine.Object::op_Implicit(UnityEngine.Object)");
61 changes: 45 additions & 16 deletions framework/il2cpp-helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,46 +18,75 @@ namespace app {
template<typename E>
class Dictionary {
public:
Dictionary(E* dict) : _Ptr(dict) {}
size_t size() const {
using iterator = decltype(&E::fields.entries->vector[0]);
using key_type = decltype(E::fields.entries->vector->key);
using value_type = decltype(E::fields.entries->vector->value);
constexpr Dictionary(E* dict) : _Ptr(dict) {}
constexpr size_t size() const {
if (!_Ptr) return 0;
auto pDict = (Dictionary_2_SystemTypes_ISystemType_*)_Ptr;
return ((size_t(*)(void*, const void*))(pDict->klass->vtable.get_Count.methodPtr))(pDict, pDict->klass->vtable.get_Count.method);
}
auto begin() { return _Ptr->fields.entries->vector; }
auto end() { return _Ptr->fields.entries->vector + size(); }
constexpr iterator begin() const {
if (!_Ptr) return nullptr;
return _Ptr->fields.entries->vector;
}
constexpr iterator end() const { return begin() + size(); }
constexpr auto operator[](key_type&& _Keyval) const {
for (auto& kvp : *this) {
if (kvp.key == _Keyval) {
return kvp.value;
}
}
return (value_type)nullptr;
}
constexpr E* get() const { return _Ptr; }
protected:
E* _Ptr;
};
template<typename E>
class Array {
public:
Array(E* arr) : _Ptr(arr) {}
size_t size() const {
using iterator = decltype(&E::vector[0]);
constexpr Array(E* arr) : _Ptr(arr) {}
constexpr size_t size() const {
if (!_Ptr) return 0;
if (_Ptr->bounds)
return _Ptr->bounds->length;
return _Ptr->max_length;
}
auto begin() { return _Ptr->vector; }
auto end() { return _Ptr->vector + size(); }
auto& operator[](const size_t _Pos) { return begin()[_Pos]; }
constexpr iterator begin() const {
if (!_Ptr) return nullptr;
return _Ptr->vector;
}
constexpr iterator end() const { return begin() + size(); }
constexpr auto& operator[](const size_t _Pos) const { return begin()[_Pos]; }
constexpr E* get() const { return _Ptr; }
protected:
E* _Ptr;
};
template<typename E>
class List {
public:
List(E* list) : _Ptr(list) {}
size_t size() const {
using iterator = decltype(&E::fields._items->vector[0]);
constexpr List(E* list) : _Ptr(list) {}
constexpr size_t size() const {
if (!_Ptr) return 0;
auto pList = (List_1_PlayerTask_*)_Ptr;
return ((size_t(*)(void*, const void*))(pList->klass->vtable.get_Count.methodPtr))(pList, pList->klass->vtable.get_Count.method);
}
auto clear() {
constexpr void clear() {
if (!_Ptr) return;
auto pList = (List_1_PlayerTask_*)_Ptr;
((void(*)(void*, const void*))(pList->klass->vtable.Clear.methodPtr))(pList, pList->klass->vtable.get_Count.method);
((void(*)(void*, const void*))(pList->klass->vtable.Clear.methodPtr))(pList, pList->klass->vtable.Clear.method);
}
constexpr iterator begin() const {
if (!_Ptr) return nullptr;
return _Ptr->fields._items->vector;
}
auto begin() { return _Ptr->fields._items->vector; }
auto end() { return _Ptr->fields._items->vector + size(); }
auto& operator[](const size_t _Pos) { return begin()[_Pos]; }
constexpr iterator end() const { return begin() + size(); }
constexpr auto& operator[](const size_t _Pos) const { return begin()[_Pos]; }
constexpr E* get() const { return _Ptr; }
protected:
E* _Ptr;
};
Expand Down
55 changes: 29 additions & 26 deletions gui/gui-helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,20 +216,22 @@ bool SliderChrono(const char* label, void* p_data, const void* p_min, const void
if (window->SkipItems)
return false;

if (ImGui::ImageButton((void*)icons.at(ICON_TYPES::PLAY).iconImage.shaderResourceView,
ImVec2(icons.at(ICON_TYPES::PLAY).iconImage.imageWidth * icons.at(ICON_TYPES::PLAY).scale,
icons.at(ICON_TYPES::PLAY).iconImage.imageHeight * icons.at(ICON_TYPES::PLAY).scale)))
{
State.Replay_IsPlaying = true;
if (!State.Replay_IsLive && !State.Replay_IsPlaying) {
const auto& playIcon = icons.at(ICON_TYPES::PLAY);
const auto& iconSize = ImVec2((float)playIcon.iconImage.imageWidth, (float)playIcon.iconImage.imageHeight) * playIcon.scale;
if (ImGui::ImageButton((ImTextureID)playIcon.iconImage.shaderResourceView, iconSize))
{
State.Replay_IsPlaying = true;
}
}

ImGui::SameLine(0.0f * State.dpiScale, 1.0f * State.dpiScale);

if (ImGui::ImageButton((void*)icons.at(ICON_TYPES::PAUSE).iconImage.shaderResourceView,
ImVec2(icons.at(ICON_TYPES::PAUSE).iconImage.imageWidth * icons.at(ICON_TYPES::PAUSE).scale,
icons.at(ICON_TYPES::PAUSE).iconImage.imageHeight * icons.at(ICON_TYPES::PAUSE).scale)))
{
State.Replay_IsPlaying = State.Replay_IsLive = false;
else {
// Live or Playing
const auto& pauseIcon = icons.at(ICON_TYPES::PAUSE);
const auto& iconSize = ImVec2((float)pauseIcon.iconImage.imageWidth, (float)pauseIcon.iconImage.imageHeight) * pauseIcon.scale;
if (ImGui::ImageButton((ImTextureID)pauseIcon.iconImage.shaderResourceView, iconSize))
{
State.Replay_IsPlaying = State.Replay_IsLive = false;
}
}

ImGui::SameLine(0.0f * State.dpiScale, 1.0f * State.dpiScale);
Expand Down Expand Up @@ -278,21 +280,22 @@ bool SliderChrono(const char* label, void* p_data, const void* p_min, const void
// Slider behavior
ImRect grab_bb;
const bool value_changed = SliderBehavior(frame_bb, id, ImGuiDataType_S64, p_data, p_min, p_max, nullptr, flags | ImGuiSliderFlags_NoRoundToFormat, &grab_bb);
if (value_changed)
if (value_changed) {
MarkItemEdited(id);

// check if new current timestamp is matching the live timestamp
// this logic makes sure that we can switch between live and replay mode
auto newMatchCurrent = std::chrono::time_point_cast<std::chrono::seconds>(State.MatchCurrent).time_since_epoch().count();
auto matchLiveMs = std::chrono::time_point_cast<std::chrono::seconds>(State.MatchLive).time_since_epoch().count();
if (newMatchCurrent == matchLiveMs)
{
State.Replay_IsLive = true;
State.Replay_IsPlaying = false;
}
else
{
State.Replay_IsLive = false;
// check if new current timestamp is matching the live timestamp
// this logic makes sure that we can switch between live and replay mode
auto newMatchCurrent = std::chrono::time_point_cast<std::chrono::seconds>(State.MatchCurrent);
auto matchLiveMs = std::chrono::time_point_cast<std::chrono::seconds>(State.MatchLive);
if (newMatchCurrent == matchLiveMs)
{
State.Replay_IsLive = true;
State.Replay_IsPlaying = false;
}
else
{
State.Replay_IsLive = false;
}
}

// Render grab
Expand Down
55 changes: 29 additions & 26 deletions gui/replay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ namespace Replay
pair.second.simplifiedTimeStamps.clear();
}

for (int plyIdx = 0; plyIdx < MAX_PLAYERS; plyIdx++)
for (size_t plyIdx = 0; plyIdx < MAX_PLAYERS; plyIdx++)
{
State.lastWalkEventPosPerPlayer[plyIdx] = ImVec2(0.f, 0.f);
State.replayDeathTimePerPlayer[plyIdx] = (std::chrono::system_clock::time_point::max)();// TODO: #define NOMINMAX
}

// Set this to true as the default value
Expand Down Expand Up @@ -108,44 +109,44 @@ namespace Replay
}

// earliestTimeIndex will be the very first event to be shown, lastTimeIndex will be the very last even to be shown
size_t earliestTimeIndex = 0, lastTimeIndex = 0;
size_t earliestTimeIndex = 0, lastTimeIndex = points.size() - 1;
bool collectionHasElementsToFilterMin = false, collectionHasElementsToFilterMax = false;
if ((isUsingMinTimeFilter == true) || (isUsingMaxTimeFilter))
{
// now we figure out the last index that matches the minTimeFilter
// then we'll do some quik pointer mafs to pass to the AddPolyline call
for (size_t index = 0; index < timeStamps.size() - 1; index++)
for (size_t index = 0; isUsingMinTimeFilter && index < timeStamps.size(); index++)
{
const std::chrono::system_clock::time_point& timestamp = timeStamps.at(index);
if ((timestamp > minTimeFilter) && (collectionHasElementsToFilterMin == false))
if (timestamp > minTimeFilter)
{
// the first element that *matches* the minTimeFilter is where we begin drawing from
earliestTimeIndex = index;
collectionHasElementsToFilterMin = true;
break;
}
if ((timestamp > maxTimeFilter) && (collectionHasElementsToFilterMax == false))
}
for (ptrdiff_t index = timeStamps.size() - 1; isUsingMaxTimeFilter && index >= (ptrdiff_t)earliestTimeIndex; index--)
{
const std::chrono::system_clock::time_point& timestamp = timeStamps.at(index);
if (timestamp < maxTimeFilter)
{
// the first element that *exceeds* the maxTimeFilter is where we stop drawing
lastTimeIndex = index;
collectionHasElementsToFilterMax = true;
break;
}
}

uintptr_t startPtr = 0, endPtr = (points.size() - 1) * sizeof(ImVec2);
if (collectionHasElementsToFilterMin == true)
{
if (!isUsingMinTimeFilter || collectionHasElementsToFilterMin) {
// some events occurred before the specified time filter
// so we want to draw only a portion of the total collection
// this portion starts from the index of the last matching event and should continue to the end of the collection
// since we're modifying the *pointer*, we have to multiply by the size of each element in the collection.
startPtr = earliestTimeIndex * sizeof(ImVec2);
}
if (collectionHasElementsToFilterMax == true)
{
endPtr = lastTimeIndex * sizeof(ImVec2);
if (!isUsingMaxTimeFilter || collectionHasElementsToFilterMax) {
size_t numPoints = lastTimeIndex - earliestTimeIndex + 1;
drawList->AddPolyline(points.data() + earliestTimeIndex, (int)numPoints, GetReplayPlayerColor(colorId), false, 1.f * State.dpiScale);
}
}
size_t numPoints = (endPtr - startPtr) / sizeof(ImVec2);
drawList->AddPolyline((ImVec2*)((uintptr_t)points.data() + startPtr), (int)numPoints, GetReplayPlayerColor(colorId), false, 1.f * State.dpiScale);
}
else
{
Expand Down Expand Up @@ -247,7 +248,7 @@ namespace Replay
playerPos = plrLineData.pendingPoints[lastTimeIndex];
if ((isUsingMinTimeFilter == true) && (timestamp < minTimeFilter))
{
STREAM_DEBUG("(not critical) Found a point matching maxTimeFilter, but does not match minTimeFilter. Add check that min < max once free time available.");
//STREAM_DEBUG("(not critical) Found a point matching maxTimeFilter, but does not match minTimeFilter. Add check that min < max once free time available.");
}
foundMatchingPlayerPos = true;
break;
Expand All @@ -265,7 +266,7 @@ namespace Replay
playerPos = plrLineData.simplifiedPoints[lastTimeIndex];
if ((isUsingMinTimeFilter == true) && (timestamp < minTimeFilter))
{
STREAM_DEBUG("(not critical) Found a point matching maxTimeFilter, but does not match minTimeFilter. Add check that min < max once free time available.");
//STREAM_DEBUG("(not critical) Found a point matching maxTimeFilter, but does not match minTimeFilter. Add check that min < max once free time available.");
}
foundMatchingPlayerPos = true;
break;
Expand All @@ -285,7 +286,7 @@ namespace Replay
}
if (foundMatchingPlayerPos == false)
{
STREAM_DEBUG("Could not find replay position for player#" << plrIdx << ". Time filter was likely too strict or player hasn't moved yet.");
//STREAM_DEBUG("Could not find replay position for player#" << plrIdx << ". Time filter was likely too strict or player hasn't moved yet.");
continue;
}

Expand Down Expand Up @@ -315,7 +316,8 @@ namespace Replay
if ((plrInfo != NULL) &&
((plrInfo->fields.IsDead) ||
((plrInfo->fields.Role != NULL) &&
(plrInfo->fields.Role->fields.Role == RoleTypes__Enum::GuardianAngel))))
(plrInfo->fields.Role->fields.Role == RoleTypes__Enum::GuardianAngel))) &&
(!isUsingMaxTimeFilter || maxTimeFilter >= State.replayDeathTimePerPlayer[plrLineData.playerId]))
drawList->AddImage((void*)icons.at(ICON_TYPES::CROSS).iconImage.shaderResourceView,
ImVec2(getMapXOffsetSkeld(player_mapX), player_mapY) * State.dpiScale + ImVec2(cursorPosX, cursorPosY),
ImVec2(getMapXOffsetSkeld(player_mapXMax), player_mapYMax) * State.dpiScale + ImVec2(cursorPosX, cursorPosY),
Expand Down Expand Up @@ -353,7 +355,7 @@ namespace Replay
if ((isUsingEventFilter == true) && (Replay::event_filter[(int)evtType].second == false))
continue;
if ((isUsingPlayerFilter == true) &&
((evtPlayerSource.playerId < 0) || (evtPlayerSource.playerId > Replay::player_filter.size() - 1) ||
((evtPlayerSource.playerId < 0) || (evtPlayerSource.playerId >= Replay::player_filter.size()) ||
(Replay::player_filter[evtPlayerSource.playerId].second == false) ||
(Replay::player_filter[evtPlayerSource.playerId].first.has_value() == false)))
continue;
Expand Down Expand Up @@ -511,11 +513,12 @@ namespace Replay
minTimeFilter = State.MatchCurrent - seconds;
}

std::lock_guard<std::mutex> replayLock(Replay::replayEventMutex);
RenderWalkPaths(drawList, cursorPosX, cursorPosY, State.mapType, isUsingEventFilter, isUsingPlayerFilter, State.Replay_ShowOnlyLastSeconds, minTimeFilter, true, State.MatchCurrent);
RenderPlayerIcons(drawList, cursorPosX, cursorPosY, State.mapType, isUsingEventFilter, isUsingPlayerFilter, State.Replay_ShowOnlyLastSeconds, minTimeFilter, true, State.MatchCurrent);
RenderEventIcons(drawList, cursorPosX, cursorPosY, State.mapType, isUsingEventFilter, isUsingPlayerFilter, State.Replay_ShowOnlyLastSeconds, minTimeFilter, true, State.MatchCurrent);

{
std::lock_guard<std::mutex> replayLock(Replay::replayEventMutex);
RenderWalkPaths(drawList, cursorPosX, cursorPosY, State.mapType, isUsingEventFilter, isUsingPlayerFilter, State.Replay_ShowOnlyLastSeconds, minTimeFilter, true, State.MatchCurrent);
RenderPlayerIcons(drawList, cursorPosX, cursorPosY, State.mapType, isUsingEventFilter, isUsingPlayerFilter, State.Replay_ShowOnlyLastSeconds, minTimeFilter, true, State.MatchCurrent);
RenderEventIcons(drawList, cursorPosX, cursorPosY, State.mapType, isUsingEventFilter, isUsingPlayerFilter, State.Replay_ShowOnlyLastSeconds, minTimeFilter, true, State.MatchCurrent);
}
ImGui::EndChild();

ImGui::Separator();
Expand Down
5 changes: 5 additions & 0 deletions hooks/AirshipStatus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ void dAirshipStatus_OnEnable(AirshipStatus* __this, MethodInfo* method)
{
AirshipStatus_OnEnable(__this, method);

Replay::Reset();

State.MatchStart = std::chrono::system_clock::now();
State.MatchCurrent = State.MatchStart;

State.selectedDoor = SystemTypes__Enum::Hallway;
State.mapDoors.clear();
State.pinnedDoors.clear();
Expand Down
4 changes: 4 additions & 0 deletions hooks/HudManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ void dHudManager_Update(HudManager* __this, MethodInfo* method) {
//HudManager_SetHudActive(__this, State.ShowHud, NULL);
if (IsInGame()) {
auto localData = GetPlayerData(*Game::pLocalPlayer);
if (!localData) {
// oops: game bug
return;
}
GameObject* shadowLayerObject = Component_get_gameObject((Component_1*)__this->fields.ShadowQuad, NULL);
GameObject_SetActive(shadowLayerObject,
!(State.FreeCam || State.EnableZoom || State.playerToFollow.has_value() || State.Wallhack) && !localData->fields.IsDead,
Expand Down
21 changes: 16 additions & 5 deletions hooks/MeetingHud.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@
#include "logger.h"
#include <chrono>

static app::Type* voteSpreaderType;
static app::Type* voteSpreaderType = nullptr;
constexpr Settings::VotedFor HasNotVoted = 255, MissedVote = 254, SkippedVote = 253, DeadVote = 252;

void dMeetingHud_Awake(MeetingHud* __this, MethodInfo* method) {
State.voteMonitor.clear();
State.InMeeting = true;

if (!voteSpreaderType) {
voteSpreaderType = app::Type_GetType(convert_to_string(translate_type_name("VoteSpreader, Assembly-CSharp")), nullptr);
}
static std::string strVoteSpreaderType = translate_type_name("VoteSpreader, Assembly-CSharp");
voteSpreaderType = app::Type_GetType(convert_to_string(strVoteSpreaderType), nullptr);

MeetingHud_Awake(__this, method);
}
Expand Down Expand Up @@ -75,6 +74,10 @@ static void Transform_RevealAnonymousVotes(app::Transform* transform, Settings::
void dMeetingHud_PopulateResults(MeetingHud* __this, Il2CppArraySize* states, MethodInfo* method) {
// remove all votes before populating results
for (auto votedForArea : il2cpp::Array(__this->fields.playerStates)) {
if (!votedForArea) {
// oops: game bug
continue;
}
auto transform = app::Component_get_transform((app::Component_1*)votedForArea, nullptr);
Transform_RemoveAllVotes(transform);
}
Expand Down Expand Up @@ -115,6 +118,10 @@ void RevealAnonymousVotes() {
void dMeetingHud_Update(MeetingHud* __this, MethodInfo* method) {
il2cpp::Array playerStates(__this->fields.playerStates);
for (auto playerVoteArea : playerStates) {
if (!playerVoteArea) {
// oops: game bug
continue;
}
auto playerData = GetPlayerDataById(playerVoteArea->fields.TargetPlayerId);
auto localData = GetPlayerData(*Game::pLocalPlayer);
auto playerNameTMP = playerVoteArea->fields.NameText;
Expand Down Expand Up @@ -145,7 +152,7 @@ void dMeetingHud_Update(MeetingHud* __this, MethodInfo* method) {
bool isVotingState = !isDiscussionState &&
((__this->fields.discussionTimer - (*Game::pGameOptionsData)->fields.DiscussionTime) < (*Game::pGameOptionsData)->fields.VotingTime); //Voting phase

if (playerVoteArea && playerData)
if (playerData)
{
bool didVote = (playerVoteArea->fields.VotedFor != HasNotVoted);
// We are goign to check to see if they voted, then we are going to check to see who they voted for, finally we are going to check to see if we already recorded a vote for them
Expand Down Expand Up @@ -205,6 +212,10 @@ void dMeetingHud_Update(MeetingHud* __this, MethodInfo* method) {
}

for (auto votedForArea : playerStates) {
if (!votedForArea) {
// oops: game bug
continue;
}
auto transform = app::Component_get_transform((app::Component_1*)votedForArea, nullptr);
auto voteSpreader = (VoteSpreader*)app::Component_GetComponent((app::Component_1*)transform, voteSpreaderType, nullptr);
if (!voteSpreader) continue;
Expand Down
1 change: 1 addition & 0 deletions hooks/PlayerControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ void dPlayerControl_MurderPlayer(PlayerControl* __this, PlayerControl* target, M

State.rawEvents.push_back(std::make_unique<KillEvent>(GetEventPlayerControl(__this).value(), GetEventPlayerControl(target).value(), PlayerControl_GetTruePosition(__this, NULL), PlayerControl_GetTruePosition(target, NULL)));
State.liveReplayEvents.push_back(std::make_unique<KillEvent>(GetEventPlayerControl(__this).value(), GetEventPlayerControl(target).value(), PlayerControl_GetTruePosition(__this, NULL), PlayerControl_GetTruePosition(target, NULL)));
State.replayDeathTimePerPlayer[target->fields.PlayerId] = std::chrono::system_clock::now();

PlayerControl_MurderPlayer(__this, target, method);
}
Expand Down

0 comments on commit 9ead3b2

Please sign in to comment.