/* Copyright (C) 2019 Mr Goldberg
This file is part of the Goldberg Emulator
The Goldberg Emulator is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.
The Goldberg Emulator is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the Goldberg Emulator; if not, see
. */
#include "dll/steam_timeline.h"
const std::chrono::system_clock::time_point& Steam_Timeline::TimelineEvent_t::get_time_added() const
{
return time_added;
}
const std::chrono::system_clock::time_point& Steam_Timeline::TimelineState_t::get_time_added() const
{
return time_added;
}
const std::chrono::system_clock::time_point& Steam_Timeline::TimelineGamePhase_t::get_time_added() const
{
return time_added;
}
void Steam_Timeline::steam_callback(void *object, Common_Message *msg)
{
// PRINT_DEBUG_ENTRY();
auto instance = (Steam_Timeline *)object;
instance->Callback(msg);
}
void Steam_Timeline::steam_run_every_runcb(void *object)
{
// PRINT_DEBUG_ENTRY();
auto instance = (Steam_Timeline *)object;
instance->RunCallbacks();
}
Steam_Timeline::Steam_Timeline(class Settings *settings, class Networking *network, class SteamCallResults *callback_results, class SteamCallBacks *callbacks, class RunEveryRunCB *run_every_runcb)
{
this->settings = settings;
this->network = network;
this->callback_results = callback_results;
this->callbacks = callbacks;
this->run_every_runcb = run_every_runcb;
// this->network->setCallback(CALLBACK_ID_USER_STATUS, settings->get_local_steam_id(), &Steam_Timeline::steam_callback, this);
this->run_every_runcb->add(&Steam_Timeline::steam_run_every_runcb, this);
// timeline starts with a default event as seen here: https://www.youtube.com/watch?v=YwBD0E4-EsI
PRINT_DEBUG("adding an initial game mode");
SetTimelineGameMode(ETimelineGameMode::k_ETimelineGameMode_Invalid);
}
Steam_Timeline::~Steam_Timeline()
{
// this->network->rmCallback(CALLBACK_ID_USER_STATUS, settings->get_local_steam_id(), &Steam_Timeline::steam_callback, this);
this->run_every_runcb->remove(&Steam_Timeline::steam_run_every_runcb, this);
}
// Sets a description for the current game state in the timeline. These help the user to find specific
// moments in the timeline when saving clips. Setting a new state description replaces any previous
// description.
//
// Examples could include:
// * Where the user is in the world in a single player game
// * Which round is happening in a multiplayer game
// * The current score for a sports game
//
// Parameters:
// - pchDescription: provide a localized string in the language returned by SteamUtils()->GetSteamUILanguage()
// - flTimeDelta: The time offset in seconds to apply to this event. Negative times indicate an
// event that happened in the past.
void Steam_Timeline::SetTimelineTooltip( const char *pchDescription, float flTimeDelta )
{
PRINT_DEBUG("'%s' %f", pchDescription, flTimeDelta);
std::lock_guard lock(timeline_mutex);
const auto target_timepoint = std::chrono::system_clock::now() + std::chrono::milliseconds(static_cast(flTimeDelta * 1000));
// reverse iterators to find last/nearest match in recent time
auto event_it = std::find_if(timeline_states.rbegin(), timeline_states.rend(), [this, &target_timepoint](const TimelineState_t &item) {
return target_timepoint >= item.get_time_added();
});
if (timeline_states.rend() != event_it) {
PRINT_DEBUG("setting timeline state description");
if (pchDescription) {
event_it->description = pchDescription;
} else {
event_it->description.clear();
}
}
}
void Steam_Timeline::ClearTimelineTooltip( float flTimeDelta )
{
PRINT_DEBUG("%f", flTimeDelta);
std::lock_guard lock(timeline_mutex);
const auto target_timepoint = std::chrono::system_clock::now() + std::chrono::milliseconds(static_cast(flTimeDelta * 1000));
// reverse iterators to find last/nearest match in recent time
auto event_it = std::find_if(timeline_states.rbegin(), timeline_states.rend(), [this, &target_timepoint](const TimelineState_t &item) {
return target_timepoint >= item.get_time_added();
});
if (timeline_states.rend() != event_it) {
PRINT_DEBUG("clearing timeline state description");
event_it->description.clear();
}
}
void Steam_Timeline::SetTimelineStateDescription( const char *pchDescription, float flTimeDelta )
{
PRINT_DEBUG("old v1");
SetTimelineTooltip(pchDescription, flTimeDelta);
}
void Steam_Timeline::ClearTimelineStateDescription( float flTimeDelta )
{
PRINT_DEBUG("old v1");
ClearTimelineTooltip(flTimeDelta);
}
void Steam_Timeline::SetTimelineGameMode( ETimelineGameMode eMode )
{
PRINT_DEBUG("%i", (int)eMode);
std::lock_guard lock(timeline_mutex);
auto &new_timeline_state = timeline_states.emplace_back(TimelineState_t{});
new_timeline_state.bar_color = eMode;
}
TimelineEventHandle_t Steam_Timeline::AddInstantaneousTimelineEvent( const char *pchTitle, const char *pchDescription, const char *pchIcon, uint32 unIconPriority, float flStartOffsetSeconds, ETimelineEventClipPriority ePossibleClip )
{
PRINT_DEBUG_TODO();
return AddRangeTimelineEvent(pchTitle, pchDescription, pchIcon, unIconPriority, flStartOffsetSeconds, 0, ePossibleClip);
}
TimelineEventHandle_t Steam_Timeline::AddRangeTimelineEvent( const char *pchTitle, const char *pchDescription, const char *pchIcon, uint32 unIconPriority, float flStartOffsetSeconds, float flDuration, ETimelineEventClipPriority ePossibleClip)
{
PRINT_DEBUG("'%s' ('%s') icon='%s', %u, [%f, %f) %i", pchTitle, pchDescription, pchIcon, unIconPriority, flStartOffsetSeconds, flDuration, (int)ePossibleClip);
std::lock_guard lock(timeline_mutex);
auto event_id = StartRangeTimelineEvent(pchTitle, pchDescription, pchIcon, unIconPriority, flStartOffsetSeconds, ePossibleClip);
if (!event_id || event_id > timeline_events.size()) return 0;
auto& my_event = timeline_events[static_cast(event_id - 1)];
my_event.ended = true; // ranged and instantaneous events are ended/closed events, they can't be modified later according to docs
// make events last at least 1 sec
if (static_cast(flDuration * 1000) < 1000LL) { // < 1000ms
flDuration = 1;
}
// for events with priority=ETimelineEventClipPriority::k_ETimelineEventClipPriority_Featured steam creates ~30 sec clip
if (flDuration < PRIORITY_CLIP_MIN_SEC && ePossibleClip == ETimelineEventClipPriority::k_ETimelineEventClipPriority_Featured) {
flDuration = PRIORITY_CLIP_MIN_SEC;
}
if (flDuration > k_flMaxTimelineEventDuration) {
flDuration = k_flMaxTimelineEventDuration;
}
my_event.flDurationSeconds = flDuration;
return event_id;
}
TimelineEventHandle_t Steam_Timeline::AddTimelineEvent( const char *pchTitle, const char *pchDescription, const char *pchIcon, uint32 unIconPriority, float flStartOffsetSeconds, float flDurationSeconds, ETimelineEventClipPriority ePossibleClip )
{
PRINT_DEBUG("undocumented v2/v3");
// this is how actual steamclient64.dll implements it
if (flDurationSeconds > 0) {
return AddRangeTimelineEvent(pchTitle, pchDescription, pchIcon, unIconPriority, flStartOffsetSeconds, flDurationSeconds, ePossibleClip);
} else {
return AddInstantaneousTimelineEvent(pchTitle, pchDescription, pchIcon, unIconPriority, flStartOffsetSeconds, ePossibleClip);
}
}
// Use this to mark an event on the Timeline. The event can be instantaneous or take some amount of time
// to complete, depending on the value passed in flDurationSeconds
//
// Examples could include:
// * a boss battle
// * a cut scene
// * a large team fight
// * picking up a new weapon or ammunition
// * scoring a goal
//
// Parameters:
//
// - pchIcon: specify the name of the icon uploaded through the Steamworks Partner Site for your title
// or one of the provided icons that start with steam_
// - pchTitle & pchDescription: provide a localized string in the language returned by
// SteamUtils()->GetSteamUILanguage()
// - unPriority: specify how important this range is compared to other markers provided by the game.
// Ranges with larger priority values will be displayed more prominently in the UI. This value
// may be between 0 and k_unMaxTimelinePriority.
// - flStartOffsetSeconds: The time that this range started relative to now. Negative times
// indicate an event that happened in the past.
// - flDurationSeconds: How long the time range should be in seconds. For instantaneous events, this
// should be 0
// - ePossibleClip: By setting this parameter to Featured or Standard, the game indicates to Steam that it
// would be appropriate to offer this range as a clip to the user. For instantaneous events, the
// suggested clip will be for a short time before and after the event itself.
void Steam_Timeline::AddTimelineEvent_old( const char *pchIcon, const char *pchTitle, const char *pchDescription, uint32 unPriority, float flStartOffsetSeconds, float flDurationSeconds, ETimelineEventClipPriority ePossibleClip )
{
PRINT_DEBUG("old v1");
// this is how actual steamclient64.dll implements it
if (flDurationSeconds > 0) {
AddRangeTimelineEvent(pchTitle, pchDescription, pchIcon, unPriority, flStartOffsetSeconds, flDurationSeconds, ePossibleClip);
} else {
AddInstantaneousTimelineEvent(pchTitle, pchDescription, pchIcon, unPriority, flStartOffsetSeconds, ePossibleClip);
}
}
TimelineEventHandle_t Steam_Timeline::StartRangeTimelineEvent( const char *pchTitle, const char *pchDescription, const char *pchIcon, uint32 unPriority, float flStartOffsetSeconds, ETimelineEventClipPriority ePossibleClip )
{
PRINT_DEBUG("'%s' ('%s') icon='%s', %u, @[%f]sec %i", pchTitle, pchDescription, pchIcon, unPriority, flStartOffsetSeconds, (int)ePossibleClip);
std::lock_guard lock(timeline_mutex);
// this adds a new event, but the duration is set once EndRangeTimelineEvent is called
// also its "ended" flag is not set
auto &new_event = timeline_events.emplace_back(TimelineEvent_t{});
new_event.pchTitle = pchTitle ? pchTitle : "";
new_event.pchDescription = pchDescription ? pchDescription : "";
new_event.pchIcon = pchIcon ? pchIcon : "";
new_event.unPriority = unPriority;
new_event.flStartOffsetSeconds = flStartOffsetSeconds;
new_event.ePossibleClip = ePossibleClip;
auto new_event_id = timeline_events.size(); // never return 0, most APIs in other interfaces use it for invalid IDs
PRINT_DEBUG(" new event ID = [%zu]", new_event_id);
return static_cast(new_event_id);
}
void Steam_Timeline::UpdateRangeTimelineEvent( TimelineEventHandle_t ulEvent, const char *pchTitle, const char *pchDescription, const char *pchIcon, uint32 unPriority, ETimelineEventClipPriority ePossibleClip )
{
PRINT_DEBUG("[%llu] '%s' ('%s') | icon='%s', %u, %i", ulEvent, pchTitle, pchDescription, pchIcon, unPriority, (int)ePossibleClip);
std::lock_guard lock(timeline_mutex);
if (!ulEvent || ulEvent > timeline_events.size()) return;
auto& my_event = timeline_events[static_cast(ulEvent - 1)];
if (my_event.ended) return;
if (pchTitle) {
my_event.pchTitle = pchTitle;
} else {
my_event.pchTitle.clear();
}
if (pchDescription) {
my_event.pchDescription = pchDescription;
} else {
my_event.pchDescription.clear();
}
if (pchIcon) {
my_event.pchIcon = pchIcon;
} else {
my_event.pchIcon.clear();
}
my_event.unPriority = unPriority;
my_event.ePossibleClip = ePossibleClip;
PRINT_DEBUG(" updated event");
}
void Steam_Timeline::EndRangeTimelineEvent( TimelineEventHandle_t ulEvent, float flEndOffsetSeconds )
{
PRINT_DEBUG("[%llu] %f", ulEvent, flEndOffsetSeconds);
std::lock_guard lock(timeline_mutex);
if (!ulEvent || ulEvent > timeline_events.size()) return;
auto& my_event = timeline_events[static_cast(ulEvent - 1)];
if (my_event.ended) return;
my_event.ended = true;
auto end_timepoint = std::chrono::system_clock::now();
auto start_timepoint = my_event.get_time_added() + std::chrono::milliseconds(static_cast(my_event.flStartOffsetSeconds * 1000));
auto duration_ms = std::chrono::duration_cast(end_timepoint - start_timepoint);
my_event.flDurationSeconds = duration_ms.count() / 1000.0f;
PRINT_DEBUG(" ended event // TODO show in the UI");
}
void Steam_Timeline::RemoveTimelineEvent( TimelineEventHandle_t ulEvent )
{
PRINT_DEBUG("[%llu]", ulEvent);
std::lock_guard lock(timeline_mutex);
if (!ulEvent || ulEvent > timeline_events.size()) return;
timeline_events.erase(timeline_events.begin() + static_cast(ulEvent - 1));
PRINT_DEBUG(" removed event // TODO remove from the UI");
}
STEAM_CALL_RESULT( SteamTimelineEventRecordingExists_t )
SteamAPICall_t Steam_Timeline::DoesEventRecordingExist(TimelineEventHandle_t ulEvent)
{
PRINT_DEBUG("[%llu] // TODO", ulEvent);
std::lock_guard lock(timeline_mutex);
if (!ulEvent || ulEvent > timeline_events.size()) {
SteamTimelineEventRecordingExists_t data_invalid{};
data_invalid.m_bRecordingExists = false;
data_invalid.m_ulEventID = ulEvent;
auto ret = callback_results->addCallResult(data_invalid.k_iCallback, &data_invalid, sizeof(data_invalid));
callbacks->addCBResult(data_invalid.k_iCallback, &data_invalid, sizeof(data_invalid));
return ret;
}
auto& my_event = timeline_events[static_cast(ulEvent - 1)];
auto recordings_count = my_event.recordings.size();
SteamTimelineEventRecordingExists_t data{};
data.m_bRecordingExists = !my_event.recordings.empty();
data.m_ulEventID = ulEvent;
auto ret = callback_results->addCallResult(data.k_iCallback, &data, sizeof(data));
callbacks->addCBResult(data.k_iCallback, &data, sizeof(data));
return ret;
}
void Steam_Timeline::StartGamePhase()
{
PRINT_DEBUG_ENTRY();
std::lock_guard lock(timeline_mutex);
timeline_game_phases.emplace_back(TimelineGamePhase_t{});
}
void Steam_Timeline::EndGamePhase()
{
PRINT_DEBUG_ENTRY();
std::lock_guard lock(timeline_mutex);
if (timeline_game_phases.empty()) return;
auto &last_game_phase = timeline_game_phases.back();
last_game_phase.ended = true;
}
void Steam_Timeline::SetGamePhaseID( const char *pchPhaseID )
{
PRINT_DEBUG("['%s']", pchPhaseID);
std::lock_guard lock(timeline_mutex);
if (timeline_game_phases.empty()) return;
auto &last_game_phase = timeline_game_phases.back();
if (last_game_phase.ended) return;
last_game_phase.pchPhaseID = pchPhaseID ? pchPhaseID : "";
PRINT_DEBUG(" changed phase ID");
}
STEAM_CALL_RESULT( SteamTimelineGamePhaseRecordingExists_t )
SteamAPICall_t Steam_Timeline::DoesGamePhaseRecordingExist( const char *pchPhaseID )
{
PRINT_DEBUG("'%s' // TODO", pchPhaseID);
std::lock_guard lock(timeline_mutex);
if (!pchPhaseID) pchPhaseID = "";
std::string_view game_phase_id_view(pchPhaseID);
const auto trigger_failure = [game_phase_id_view, this]() {
SteamTimelineGamePhaseRecordingExists_t data_invalid{};
auto chars_copied = game_phase_id_view.copy(data_invalid.m_rgchPhaseID, sizeof(data_invalid.m_rgchPhaseID) - 1);
data_invalid.m_rgchPhaseID[chars_copied] = 0;
data_invalid.m_ulLongestClipMS = 0;
data_invalid.m_ulRecordingMS = 0;
data_invalid.m_unClipCount = 0;
data_invalid.m_unScreenshotCount = 0;
auto ret = callback_results->addCallResult(data_invalid.k_iCallback, &data_invalid, sizeof(data_invalid));
callbacks->addCBResult(data_invalid.k_iCallback, &data_invalid, sizeof(data_invalid));
return ret;
};
if (timeline_game_phases.empty()) {
return trigger_failure();
}
auto phase_it = std::find_if(timeline_game_phases.begin(), timeline_game_phases.end(), [game_phase_id_view](const TimelineGamePhase_t &item){
return game_phase_id_view == item.pchPhaseID;
});
if (timeline_game_phases.end() == phase_it) {
return trigger_failure();
}
// TODO return actual count ?
auto recordings_count = phase_it->recordings.size();
return trigger_failure();
}
void Steam_Timeline::AddGamePhaseTag( const char *pchTagName, const char *pchTagIcon, const char *pchTagGroup, uint32 unPriority )
{
PRINT_DEBUG("['%s']: '%s' '%s' <%u>", pchTagGroup, pchTagName, pchTagIcon, unPriority);
std::lock_guard lock(timeline_mutex);
if (timeline_game_phases.empty()) return;
auto &last_game_phase = timeline_game_phases.back();
if (last_game_phase.ended) return;
if (!pchTagGroup) pchTagGroup = "";
auto &phase_tag = last_game_phase.tags[pchTagGroup].emplace_back(Steam_Timeline::TimelineGamePhase_t::Tag_t{});
phase_tag.pchTagName = pchTagName ? pchTagName : "";
phase_tag.pchTagIcon = pchTagIcon ? pchTagIcon : "";
phase_tag.unPriority = unPriority;
PRINT_DEBUG(" added phase tag");
}
void Steam_Timeline::SetGamePhaseAttribute( const char *pchAttributeGroup, const char *pchAttributeValue, uint32 unPriority )
{
PRINT_DEBUG("['%s']: '%s' <%u>", pchAttributeGroup, pchAttributeValue, unPriority);
std::lock_guard lock(timeline_mutex);
if (timeline_game_phases.empty()) return;
auto &last_game_phase = timeline_game_phases.back();
if (last_game_phase.ended) return;
if (!pchAttributeGroup) pchAttributeGroup = "";
auto &phase_att = last_game_phase.attributes[pchAttributeGroup];
phase_att.pchAttributeValue = pchAttributeValue ? pchAttributeValue : "";
phase_att.unPriority = unPriority;
PRINT_DEBUG(" changed phase attribute");
}
void Steam_Timeline::OpenOverlayToGamePhase( const char *pchPhaseID )
{
PRINT_DEBUG("['%s'] // TODO", pchPhaseID);
std::lock_guard lock(timeline_mutex);
}
void Steam_Timeline::OpenOverlayToTimelineEvent( const TimelineEventHandle_t ulEvent )
{
PRINT_DEBUG("[%llu] // TODO", ulEvent);
std::lock_guard lock(timeline_mutex);
}
uint32 Steam_Timeline::unknown_ret0_1()
{
PRINT_DEBUG_TODO();
return 0;
}
uint32 Steam_Timeline::unknown_ret0_2()
{
PRINT_DEBUG_TODO();
return 0;
}
void Steam_Timeline::unknown_nop_3()
{
PRINT_DEBUG_TODO();
}
void Steam_Timeline::unknown_nop_4()
{
PRINT_DEBUG_TODO();
}
void Steam_Timeline::unknown_nop_5()
{
PRINT_DEBUG_TODO();
}
void Steam_Timeline::unknown_nop_6()
{
PRINT_DEBUG_TODO();
}
void Steam_Timeline::unknown_nop_7()
{
PRINT_DEBUG_TODO();
}
void Steam_Timeline::RunCallbacks()
{
}
void Steam_Timeline::Callback(Common_Message *msg)
{
if (msg->has_low_level()) {
if (msg->low_level().type() == Low_Level::CONNECT) {
}
if (msg->low_level().type() == Low_Level::DISCONNECT) {
}
}
}