Merge pull request #669 from h1-mod/server-rpc

server information for discord RPC
This commit is contained in:
fed 2023-12-18 02:07:58 +01:00 committed by GitHub
commit aef84a037e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 576 additions and 471 deletions

View File

@ -2,7 +2,7 @@ if (game:issingleplayer() or Engine.InFrontend()) then
return return
end end
local container = LUI.UIVerticalList.new({ local container = LUI.UIElement.new({
topAnchor = true, topAnchor = true,
rightAnchor = true, rightAnchor = true,
top = 20, top = 20,
@ -11,20 +11,6 @@ local container = LUI.UIVerticalList.new({
spacing = 5 spacing = 5
}) })
function canasktojoin(userid)
history = history or {}
if (history[userid] ~= nil) then
return false
end
history[userid] = true
game:ontimeout(function()
history[userid] = nil
end, 15000)
return true
end
function truncatename(name, length) function truncatename(name, length)
if (#name <= length - 3) then if (#name <= length - 3) then
return name return name
@ -33,27 +19,57 @@ function truncatename(name, length)
return name:sub(1, length - 3) .. "..." return name:sub(1, length - 3) .. "..."
end end
local requestlist = {}
local requestcount = 0
function addrequest(request) function addrequest(request)
if (not canasktojoin(request.userid)) then for i = 1, #requestlist do
if (requestlist[i].userid == request.userid or #requestlist > 5) then
return return
end end
if (container.temp) then
container:removeElement(container.temp)
container.temp = nil
end end
request.id = requestcount
requestcount = requestcount + 1
local yoffset = #requestlist * (75 + 5)
local invite = LUI.UIElement.new({ local invite = LUI.UIElement.new({
leftAnchor = true, leftAnchor = true,
rightAnchor = true, rightAnchor = true,
height = 75 height = 75,
top = yoffset
}) })
local getcurrentindex = function()
for i = 1, #requestlist do
if (requestlist[i].id == request.id) then
return i
end
end
return 0
end
invite:registerEventHandler("update_position", function()
yoffset = (getcurrentindex() - 1) * (75 + 5)
local state = {
leftAnchor = true,
height = 75,
width = 200,
left = -220,
top = yoffset
}
invite:registerAnimationState("default", state)
invite:animateToState("default", 50)
end)
invite:registerAnimationState("move_in", { invite:registerAnimationState("move_in", {
leftAnchor = true, leftAnchor = true,
height = 75, height = 75,
width = 200, width = 200,
left = -220 left = -220,
top = yoffset
}) })
invite:animateToState("move_in", 100) invite:animateToState("move_in", 100)
@ -105,7 +121,7 @@ function addrequest(request)
width = 32, width = 32,
height = 32, height = 32,
left = 1, left = 1,
material = RegisterMaterial(avatarmaterial) material = avatarmaterial
}) })
local username = LUI.UIText.new({ local username = LUI.UIText.new({
@ -119,8 +135,14 @@ function addrequest(request)
font = CoD.TextSettings.BodyFontBold.Font font = CoD.TextSettings.BodyFontBold.Font
}) })
username:setText(string.format("%s^7#%s requested to join your game!", truncatename(request.username, 18), local requesttext = nil
request.discriminator)) if (request.discriminator == "0") then
requesttext = Engine.Localize("LUA_MENU_DISCORD_REQUEST", truncatename(request.username, 18))
else
requesttext = Engine.Localize("LUA_MENU_DISCORD_REQUEST_DISCRIMINATOR", truncatename(request.username, 18), request.discriminator)
end
username:setText(requesttext)
local buttons = LUI.UIElement.new({ local buttons = LUI.UIElement.new({
leftAnchor = true, leftAnchor = true,
@ -156,52 +178,55 @@ function addrequest(request)
return button return button
end end
buttons:addElement(createbutton("[F1] Accept", true)) local accepttext = Engine.Localize("LUA_MENU_DISCORD_ACCEPT", Engine.GetBinding("discord_accept"))
buttons:addElement(createbutton("[F2] Deny")) local denytext = Engine.Localize("LUA_MENU_DISCORD_DENY", Engine.GetBinding("discord_deny"))
buttons:addElement(createbutton(accepttext, true))
buttons:addElement(createbutton(denytext))
local fadeouttime = 50 local fadeouttime = 50
local timeout = 10 * 1000 - fadeouttime local timeout = 10 * 1000 - fadeouttime
local function close() local function close()
container:processEvent({ table.remove(requestlist, getcurrentindex())
name = "update_navigation",
dispatchToChildren = true
})
invite:animateToState("fade_out", fadeouttime)
invite:addElement(LUI.UITimer.new(fadeouttime + 50, "remove"))
invite:registerEventHandler("remove", function()
container:removeElement(invite)
if (container.temp) then
container:removeElement(container.temp)
container.temp = nil
end
local temp = LUI.UIElement.new({})
container.temp = temp
container:addElement(temp)
end)
end
buttons:registerEventHandler("keydown_", function(element, event)
if (event.key == "F1") then
close()
discord.respond(request.userid, discord.reply.yes)
end
if (event.key == "F2") then
close()
discord.respond(request.userid, discord.reply.no)
end
end)
invite:registerAnimationState("fade_out", { invite:registerAnimationState("fade_out", {
leftAnchor = true, leftAnchor = true,
rightAnchor = true, rightAnchor = true,
height = 75, height = 75,
alpha = 0, alpha = 0,
left = 0 left = 0,
top = yoffset
}) })
invite:animateToState("fade_out", fadeouttime)
invite:addElement(LUI.UITimer.new(fadeouttime + 50, "remove"))
invite:registerEventHandler("remove", function()
container:removeElement(invite)
container:processEvent({
name = "update_position",
dispatchToChildren = true
})
end)
end
local closed = false
request.handleresponse = function(event)
if (closed) then
return
end
if (event.accept) then
discord.respond(request.userid, discord.reply.yes)
else
discord.respond(request.userid, discord.reply.no)
end
closed = true
close()
end
invite:addElement(LUI.UITimer.new(timeout, "end_invite")) invite:addElement(LUI.UITimer.new(timeout, "end_invite"))
invite:registerEventHandler("end_invite", function() invite:registerEventHandler("end_invite", function()
close() close()
@ -236,7 +261,7 @@ function addrequest(request)
avatar:registerEventHandler("update", function() avatar:registerEventHandler("update", function()
local avatarmaterial = discord.getavatarmaterial(request.userid) local avatarmaterial = discord.getavatarmaterial(request.userid)
avatar:setImage(RegisterMaterial(avatarmaterial)) avatar:setImage(avatarmaterial)
end) end)
avatar:addElement(LUI.UITimer.new(100, "update")) avatar:addElement(LUI.UITimer.new(100, "update"))
@ -250,19 +275,17 @@ function addrequest(request)
padding:addElement(buttons) padding:addElement(buttons)
container:addElement(invite) container:addElement(invite)
table.insert(requestlist, request)
end end
container:registerEventHandler("keydown", function(element, event) LUI.roots.UIRoot0:registerEventHandler("discord_response", function(element, event)
local first = container:getFirstChild() if (#requestlist <= 0) then
if (not first) then
return return
end end
first:processEvent({ local request = requestlist[1]
name = "keydown_", request.handleresponse(event)
key = event.key
})
end) end)
LUI.roots.UIRoot0:registerEventHandler("discord_join_request", function(element, event) LUI.roots.UIRoot0:registerEventHandler("discord_join_request", function(element, event)

View File

@ -2,5 +2,10 @@
"LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "Unlock All Missions and Intel", "LUA_MENU_CAMPAIGN_UNLOCKED_ALL_TITLE": "Unlock All Missions and Intel",
"LUA_MENU_CANCEL_UNLOCK_CAPS": "Cancel Unlock All Missions", "LUA_MENU_CANCEL_UNLOCK_CAPS": "Cancel Unlock All Missions",
"LUA_MENU_CHOOSE_LANGUAGE_DESC": "Choose your language.", "LUA_MENU_CHOOSE_LANGUAGE_DESC": "Choose your language.",
"MENU_APPLY_LANGUAGE_SETTINGS": "Apply language settings?" "MENU_APPLY_LANGUAGE_SETTINGS": "Apply language settings?",
"LUA_MENU_DISCORD_REQUEST": "&&1^7 requested to join your game!",
"LUA_MENU_DISCORD_REQUEST_DISCRIMINATOR": "&&1^7#&&2 requested to join your game!",
"LUA_MENU_DISCORD_ACCEPT": "[&&1] Accept",
"LUA_MENU_DISCORD_DENY": "[&&1] Deny"
} }

View File

@ -4,9 +4,7 @@
#include "console.hpp" #include "console.hpp"
#include "command.hpp" #include "command.hpp"
#include "discord.hpp" #include "discord.hpp"
#include "fastfiles.hpp"
#include "materials.hpp" #include "materials.hpp"
#include "network.hpp"
#include "party.hpp" #include "party.hpp"
#include "scheduler.hpp" #include "scheduler.hpp"
@ -29,124 +27,181 @@ namespace discord
{ {
namespace namespace
{ {
DiscordRichPresence discord_presence; struct discord_presence_state_t
void update_discord()
{ {
if (!game::CL_IsCgameInitialized() || game::VirtualLobby_Loaded()) int start_timestamp;
int party_size;
int party_max;
};
struct discord_presence_strings_t
{
std::string state;
std::string details;
std::string small_image_key;
std::string small_image_text;
std::string large_image_key;
std::string large_image_text;
std::string party_id;
std::string join_secret;
};
DiscordRichPresence discord_presence{};
discord_presence_strings_t discord_strings;
std::mutex avatar_map_mutex;
std::unordered_map<std::string, game::Material*> avatar_material_map;
game::Material* default_avatar_material{};
void update_discord_frontend()
{ {
discord_presence.details = SELECT_VALUE("Singleplayer", "Multiplayer"); discord_presence.details = SELECT_VALUE("Singleplayer", "Multiplayer");
discord_presence.state = "Main Menu";
discord_presence.partySize = 0;
discord_presence.partyMax = 0;
discord_presence.startTimestamp = 0; discord_presence.startTimestamp = 0;
discord_presence.largeImageKey = SELECT_VALUE("menu_singleplayer", "menu_multiplayer");
discord_presence.matchSecret = ""; static const auto in_firing_range = game::Dvar_FindVar("virtualLobbyInFiringRange");
discord_presence.joinSecret = ""; if (in_firing_range != nullptr && in_firing_range->current.enabled == 1)
discord_presence.partyId = "";
const auto in_firing_range = game::Dvar_FindVar("virtualLobbyInFiringRange");
if (in_firing_range && in_firing_range->current.enabled == 1)
{ {
discord_presence.state = "Firing Range"; discord_presence.state = "Firing Range";
discord_presence.largeImageKey = "mp_vlobby_room"; discord_presence.largeImageKey = "mp_vlobby_room";
} }
}
else else
{ {
static char details[0x80] = {0}; discord_presence.state = "Main Menu";
const auto map = game::Dvar_FindVar("mapname")->current.string; discord_presence.largeImageKey = SELECT_VALUE("menu_singleplayer", "menu_multiplayer");
const auto key = utils::string::va("PRESENCE_%s%s", SELECT_VALUE("SP_", ""), map); }
const char* mapname = map;
if (game::DB_XAssetExists(game::ASSET_TYPE_LOCALIZE, key) && !game::DB_IsXAssetDefault(game::ASSET_TYPE_LOCALIZE, key)) Discord_UpdatePresence(&discord_presence);
}
void update_discord_ingame()
{ {
mapname = game::UI_SafeTranslateString(key); static const auto mapname_dvar = game::Dvar_FindVar("mapname");
auto mapname = mapname_dvar->current.string;
discord_strings.large_image_key = mapname;
const auto presence_key = utils::string::va("PRESENCE_%s%s", SELECT_VALUE("SP_", ""), mapname);
if (game::DB_XAssetExists(game::ASSET_TYPE_LOCALIZE, presence_key) &&
!game::DB_IsXAssetDefault(game::ASSET_TYPE_LOCALIZE, presence_key))
{
mapname = game::UI_SafeTranslateString(presence_key);
} }
if (game::environment::is_mp()) if (game::environment::is_mp())
{ {
static char clean_gametype[0x80] = {0}; static const auto gametype_dvar = game::Dvar_FindVar("g_gametype");
const auto gametype = game::UI_GetGameTypeDisplayName( static const auto max_clients_dvar = game::Dvar_FindVar("sv_maxclients");
game::Dvar_FindVar("g_gametype")->current.string); static const auto hostname_dvar = game::Dvar_FindVar("sv_hostname");
utils::string::strip(gametype,
clean_gametype, sizeof(clean_gametype));
strcpy_s(details, 0x80, utils::string::va("%s on %s", clean_gametype, mapname));
static char clean_hostname[0x80] = {0}; const auto gametype_display_name = game::UI_GetGameTypeDisplayName(gametype_dvar->current.string);
utils::string::strip(game::Dvar_FindVar("sv_hostname")->current.string, const auto gametype = utils::string::strip(gametype_display_name);
clean_hostname, sizeof(clean_hostname));
auto max_clients = party::server_client_count();
if (game::SV_Loaded()) discord_strings.details = std::format("{} on {}", gametype, mapname);
{
strcpy_s(clean_hostname, "Private Match");
max_clients = game::Dvar_FindVar("sv_maxclients")->current.integer;
discord_presence.partyPrivacy = DISCORD_PARTY_PRIVATE;
}
else
{
const auto server_net_info = party::get_state_host();
const auto server_ip_port = utils::string::va("%i.%i.%i.%i:%i",
static_cast<int>(server_net_info.ip[0]),
static_cast<int>(server_net_info.ip[1]),
static_cast<int>(server_net_info.ip[2]),
static_cast<int>(server_net_info.ip[3]),
static_cast<int>(ntohs(server_net_info.port))
);
static char join_secret[0x80] = {0};
strcpy_s(join_secret, 0x80, server_ip_port);
static char party_id[0x80] = {0};
const auto server_ip_port_hash = utils::cryptography::sha1::compute(server_ip_port, true).substr(0, 8);
strcpy_s(party_id, 0x80, server_ip_port_hash.data());
discord_presence.partyId = party_id;
discord_presence.joinSecret = join_secret;
discord_presence.partyPrivacy = DISCORD_PARTY_PUBLIC;
}
const auto client_state = *game::mp::client_state; const auto client_state = *game::mp::client_state;
if (client_state != nullptr) if (client_state != nullptr)
{ {
discord_presence.partySize = client_state->num_players; discord_presence.partySize = client_state->num_players;
} }
if (game::SV_Loaded())
{
discord_strings.state = "Private Match";
discord_presence.partyMax = max_clients_dvar->current.integer;
discord_presence.partyPrivacy = DISCORD_PARTY_PRIVATE;
}
else else
{ {
discord_presence.partySize = 0; discord_strings.state = utils::string::strip(hostname_dvar->current.string);
const auto server_connection_state = party::get_server_connection_state();
const auto server_ip_port = std::format("{}.{}.{}.{}:{}",
static_cast<int>(server_connection_state.host.ip[0]),
static_cast<int>(server_connection_state.host.ip[1]),
static_cast<int>(server_connection_state.host.ip[2]),
static_cast<int>(server_connection_state.host.ip[3]),
static_cast<int>(ntohs(server_connection_state.host.port))
);
discord_strings.party_id = utils::cryptography::sha1::compute(server_ip_port, true).substr(0, 8);
discord_presence.partyMax = server_connection_state.max_clients;
discord_presence.partyPrivacy = DISCORD_PARTY_PUBLIC;
discord_strings.join_secret = server_ip_port;
} }
discord_presence.partyMax = max_clients; auto server_discord_info = party::get_server_discord_info();
discord_presence.state = clean_hostname; if (server_discord_info.has_value())
discord_presence.largeImageKey = map;
if (!fastfiles::is_stock_map(map))
{ {
discord_presence.largeImageKey = "menu_multiplayer"; discord_strings.small_image_key = server_discord_info->image;
discord_strings.small_image_text = server_discord_info->image_text;
} }
} }
else if (game::environment::is_sp()) else if (game::environment::is_sp())
{ {
discord_presence.state = ""; discord_strings.details = mapname;
discord_presence.largeImageKey = map;
strcpy_s(details, 0x80, mapname);
} }
discord_presence.details = details; if (discord_presence.startTimestamp == 0)
if (!discord_presence.startTimestamp)
{ {
discord_presence.startTimestamp = std::chrono::duration_cast<std::chrono::seconds>( discord_presence.startTimestamp = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count(); std::chrono::system_clock::now().time_since_epoch()).count();
} }
}
discord_presence.state = discord_strings.state.data();
discord_presence.details = discord_strings.details.data();
discord_presence.smallImageKey = discord_strings.small_image_key.data();
discord_presence.smallImageText = discord_strings.small_image_text.data();
discord_presence.largeImageKey = discord_strings.large_image_key.data();
discord_presence.largeImageText = discord_strings.large_image_text.data();
discord_presence.partyId = discord_strings.party_id.data();
discord_presence.joinSecret = discord_strings.join_secret.data();
Discord_UpdatePresence(&discord_presence); Discord_UpdatePresence(&discord_presence);
} }
void update_discord()
{
const auto saved_time = discord_presence.startTimestamp;
discord_presence = {};
discord_presence.startTimestamp = saved_time;
if (!game::CL_IsCgameInitialized() || game::VirtualLobby_Loaded())
{
update_discord_frontend();
}
else
{
update_discord_ingame();
}
}
game::Material* create_avatar_material(const std::string& name, const std::string& data)
{
const auto material = materials::create_material(name);
try
{
if (!materials::setup_material_image(material, data))
{
materials::free_material(material);
return nullptr;
}
{
std::lock_guard _0(avatar_map_mutex);
avatar_material_map.insert(std::make_pair(name, material));
}
return material;
}
catch (const std::exception& e)
{
materials::free_material(material);
console::error("Failed to load user avatar image: %s\n", e.what());
}
return nullptr;
}
void download_user_avatar(const std::string& id, const std::string& avatar) void download_user_avatar(const std::string& id, const std::string& avatar)
{ {
const auto data = utils::http::get_data( const auto data = utils::http::get_data(
@ -162,10 +217,10 @@ namespace discord
return; return;
} }
materials::add(utils::string::va(AVATAR, id.data()), value.buffer); const auto name = utils::string::va(AVATAR, id.data());
create_avatar_material(name, value.buffer);
} }
bool has_default_avatar = false;
void download_default_avatar() void download_default_avatar()
{ {
const auto data = utils::http::get_data(DEFAULT_AVATAR_URL); const auto data = utils::http::get_data(DEFAULT_AVATAR_URL);
@ -180,25 +235,128 @@ namespace discord
return; return;
} }
has_default_avatar = true; default_avatar_material = create_avatar_material(DEFAULT_AVATAR, value.buffer);
materials::add(DEFAULT_AVATAR, value.buffer); }
void ready(const DiscordUser* request)
{
DiscordRichPresence presence{};
presence.instance = 1;
presence.state = "";
console::info("Discord: Ready on %s (%s)\n", request->username, request->userId);
Discord_UpdatePresence(&presence);
}
void errored(const int error_code, const char* message)
{
console::error("Discord: %s (%i)\n", message, error_code);
}
void join_game(const char* join_secret)
{
console::debug("Discord: join_game called with secret '%s'\n", join_secret);
scheduler::once([=]
{
game::netadr_s target{};
if (game::NET_StringToAdr(join_secret, &target))
{
console::info("Discord: Connecting to server '%s'\n", join_secret);
party::connect(target);
}
}, scheduler::pipeline::main);
}
void join_request(const DiscordUser* request)
{
console::debug("Discord: Join request from %s (%s)\n", request->username, request->userId);
if (game::Com_InFrontend() || !ui_scripting::lui_running())
{
Discord_Respond(request->userId, DISCORD_REPLY_IGNORE);
return;
}
static std::unordered_map<std::string, std::chrono::high_resolution_clock::time_point> last_requests;
const std::string user_id = request->userId;
const std::string avatar = request->avatar;
const std::string discriminator = request->discriminator;
const std::string username = request->username;
const auto now = std::chrono::high_resolution_clock::now();
auto iter = last_requests.find(user_id);
if (iter != last_requests.end())
{
if ((now - iter->second) < 15s)
{
return;
}
else
{
iter->second = now;
}
}
else
{
last_requests.insert(std::make_pair(user_id, now));
}
scheduler::once([=]
{
const ui_scripting::table request_table{};
request_table.set("avatar", avatar);
request_table.set("discriminator", discriminator);
request_table.set("userid", user_id);
request_table.set("username", username);
ui_scripting::notify("discord_join_request",
{
{"request", request_table}
});
}, scheduler::pipeline::lui);
const auto material_name = utils::string::va(AVATAR, user_id.data());
if (!avatar.empty() && !avatar_material_map.contains(material_name))
{
download_user_avatar(user_id, avatar);
} }
} }
std::string get_avatar_material(const std::string& id) void set_default_bindings()
{ {
const auto avatar_name = utils::string::va(AVATAR, id.data()); const auto set_binding = [](const std::string& command, const game::keyNum_t key)
if (materials::exists(avatar_name))
{ {
return avatar_name; const auto binding = game::Key_GetBindingForCmd(command.data());
for (auto i = 0; i < 256; i++)
{
if (game::playerKeys[0].keys[i].binding == binding)
{
return;
}
} }
if (has_default_avatar) if (game::playerKeys[0].keys[key].binding == 0)
{ {
return DEFAULT_AVATAR; game::Key_SetBinding(0, key, binding);
}
};
set_binding("discord_accept", game::K_F1);
set_binding("discord_deny", game::K_F2);
}
} }
return "black"; game::Material* get_avatar_material(const std::string& id)
{
const auto material_name = utils::string::va(AVATAR, id.data());
const auto iter = avatar_material_map.find(material_name);
if (iter == avatar_material_map.end())
{
return default_avatar_material;
}
return iter->second;
} }
void respond(const std::string& id, int reply) void respond(const std::string& id, int reply)
@ -212,15 +370,14 @@ namespace discord
class component final : public component_interface class component final : public component_interface
{ {
public: public:
void post_load() override void post_unpack() override
{ {
if (game::environment::is_dedi()) if (game::environment::is_dedi())
{ {
return; return;
} }
DiscordEventHandlers handlers; DiscordEventHandlers handlers{};
ZeroMemory(&handlers, sizeof(handlers));
handlers.ready = ready; handlers.ready = ready;
handlers.errored = errored; handlers.errored = errored;
handlers.disconnected = errored; handlers.disconnected = errored;
@ -239,16 +396,29 @@ namespace discord
Discord_Initialize("947125042930667530", &handlers, 1, nullptr); Discord_Initialize("947125042930667530", &handlers, 1, nullptr);
scheduler::once(download_default_avatar, scheduler::pipeline::async); if (game::environment::is_mp())
scheduler::once([]
{ {
scheduler::once(update_discord, scheduler::pipeline::async); scheduler::on_game_initialized([]
scheduler::loop(update_discord, scheduler::pipeline::async, 5s); {
scheduler::loop(Discord_RunCallbacks, scheduler::pipeline::async, 1s); scheduler::once(download_default_avatar, scheduler::async);
}, scheduler::pipeline::main); set_default_bindings();
}, scheduler::main);
}
scheduler::loop(Discord_RunCallbacks, scheduler::async, 500ms);
scheduler::loop(update_discord, scheduler::async, 5s);
initialized_ = true; initialized_ = true;
command::add("discord_accept", []()
{
ui_scripting::notify("discord_response", {{"accept", true}});
});
command::add("discord_deny", []()
{
ui_scripting::notify("discord_response", {{"accept", false}});
});
} }
void pre_destroy() override void pre_destroy() override
@ -263,67 +433,6 @@ namespace discord
private: private:
bool initialized_ = false; bool initialized_ = false;
static void ready(const DiscordUser* request)
{
ZeroMemory(&discord_presence, sizeof(discord_presence));
discord_presence.instance = 1;
console::info("Discord: Ready on %s (%s)\n", request->username, request->userId);
Discord_UpdatePresence(&discord_presence);
}
static void errored(const int error_code, const char* message)
{
console::error("Discord: Error (%i): %s\n", error_code, message);
}
static void join_game(const char* join_secret)
{
scheduler::once([=]
{
game::netadr_s target{};
if (game::NET_StringToAdr(join_secret, &target))
{
console::info("Discord: Connecting to server: %s\n", join_secret);
party::connect(target);
}
}, scheduler::pipeline::main);
}
static void join_request(const DiscordUser* request)
{
console::info("Discord: Join request from %s (%s)\n", request->username, request->userId);
if (game::Com_InFrontend() || !ui_scripting::lui_running())
{
Discord_Respond(request->userId, DISCORD_REPLY_IGNORE);
return;
}
std::string user_id = request->userId;
std::string avatar = request->avatar;
std::string discriminator = request->discriminator;
std::string username = request->username;
scheduler::once([=]
{
const ui_scripting::table request_table{};
request_table.set("avatar", avatar);
request_table.set("discriminator", discriminator);
request_table.set("userid", user_id);
request_table.set("username", username);
ui_scripting::notify("discord_join_request",
{
{"request", request_table}
});
}, scheduler::pipeline::lui);
if (!materials::exists(utils::string::va(AVATAR, user_id.data())))
{
download_user_avatar(user_id, avatar);
}
}
}; };
} }

View File

@ -1,7 +1,9 @@
#pragma once #pragma once
#include "game/game.hpp"
namespace discord namespace discord
{ {
std::string get_avatar_material(const std::string& id); game::Material* get_avatar_material(const std::string& id);
void respond(const std::string& id, int reply); void respond(const std::string& id, int reply);
} }

View File

@ -170,7 +170,7 @@ namespace download
auto data = utils::http::get_data(url, {}, {}, &progress_callback); auto data = utils::http::get_data(url, {}, {}, &progress_callback);
if (!data.has_value()) if (!data.has_value())
{ {
menu_error("Download failed: An unknown error occurred, please try again."); menu_error(utils::string::va("Download failed: An unknown error occurred when getting data from '%s', please try again.", url));
return; return;
} }
@ -182,6 +182,13 @@ namespace download
auto& result = data.value(); auto& result = data.value();
if (result.code != CURLE_OK) if (result.code != CURLE_OK)
{ {
if (result.code == CURLE_COULDNT_CONNECT)
{
menu_error(utils::string::va("Download failed: Couldn't connect to server '%s' (%i)\n",
url, result.code));
return;
}
menu_error(utils::string::va("Download failed: %s (%i)\n", menu_error(utils::string::va("Download failed: %s (%i)\n",
curl_easy_strerror(result.code), result.code)); curl_easy_strerror(result.code), result.code));
return; return;
@ -189,7 +196,7 @@ namespace download
if (result.response_code >= 400) if (result.response_code >= 400)
{ {
menu_error(utils::string::va("Download failed: Server returned bad response code %i\n", menu_error(utils::string::va("Download failed: Server returned bad response code (%i)\n",
result.response_code)); result.response_code));
return; return;
} }
@ -197,7 +204,7 @@ namespace download
const auto hash = utils::hash::get_buffer_hash(result.buffer, file.name); const auto hash = utils::hash::get_buffer_hash(result.buffer, file.name);
if (hash != file.hash) if (hash != file.hash)
{ {
menu_error(utils::string::va("Download failed: file hash doesn't match the server's (%s: %s != %s)\n", menu_error(utils::string::va("Download failed: File hash doesn't match the server's (%s: %s != %s)\n",
file.name.data(), hash.data(), file.hash.data())); file.name.data(), hash.data(), file.hash.data()));
return; return;
} }
@ -233,7 +240,7 @@ namespace download
scheduler::once([] scheduler::once([]
{ {
ui_scripting::notify("mod_download_done", {}); ui_scripting::notify("mod_download_done", {});
party::menu_error("Download for server mod has been cancelled."); party::menu_error("Download failed: Aborted");
}, scheduler::pipeline::lui); }, scheduler::pipeline::lui);
} }

View File

@ -17,15 +17,6 @@ namespace input
void cl_char_event_stub(const int local_client_num, const int key) void cl_char_event_stub(const int local_client_num, const int key)
{ {
if (game::environment::is_sp() && ui_scripting::lui_running())
{
ui_scripting::notify("keypress",
{
{"keynum", key},
{"key", game::Key_KeynumToString(key, 0, 1)},
});
}
if (!game_console::console_char_event(local_client_num, key)) if (!game_console::console_char_event(local_client_num, key))
{ {
return; return;
@ -36,15 +27,6 @@ namespace input
void cl_key_event_stub(const int local_client_num, const int key, const int down) void cl_key_event_stub(const int local_client_num, const int key, const int down)
{ {
if (game::environment::is_sp() && ui_scripting::lui_running())
{
ui_scripting::notify(down ? "keydown" : "keyup",
{
{"keynum", key},
{"key", game::Key_KeynumToString(key, 0, 1)},
});
}
if (!game_console::console_key_event(local_client_num, key, down)) if (!game_console::console_key_event(local_client_num, key, down))
{ {
return; return;

View File

@ -21,7 +21,6 @@ namespace materials
namespace namespace
{ {
utils::hook::detour db_material_streaming_fail_hook; utils::hook::detour db_material_streaming_fail_hook;
utils::hook::detour material_register_handle_hook;
utils::hook::detour db_get_material_index_hook; utils::hook::detour db_get_material_index_hook;
#ifdef DEBUG #ifdef DEBUG
@ -31,120 +30,8 @@ namespace materials
const game::dvar_t* debug_materials = nullptr; const game::dvar_t* debug_materials = nullptr;
#endif #endif
struct material_data_t
{
std::unordered_map<std::string, game::Material*> materials;
std::unordered_map<std::string, std::string> images;
};
char constant_table[0x20] = {}; char constant_table[0x20] = {};
utils::concurrency::container<material_data_t> material_data;
game::GfxImage* setup_image(game::GfxImage* image, const utils::image& raw_image)
{
image->imageFormat = 0x1000003;
image->resourceSize = -1;
D3D11_SUBRESOURCE_DATA data{};
data.SysMemPitch = raw_image.get_width() * 4;
data.SysMemSlicePitch = data.SysMemPitch * raw_image.get_height();
data.pSysMem = raw_image.get_buffer();
game::Image_Setup(image, raw_image.get_width(), raw_image.get_height(), image->depth, image->numElements,
image->imageFormat, DXGI_FORMAT_R8G8B8A8_UNORM, image->name, &data);
return image;
}
game::Material* create_material(const std::string& name, const std::string& data)
{
const auto white = material_register_handle_hook.invoke<game::Material*>("white");
const auto material = utils::memory::get_allocator()->allocate<game::Material>();
const auto texture_table = utils::memory::get_allocator()->allocate<game::MaterialTextureDef>();
const auto image = utils::memory::get_allocator()->allocate<game::GfxImage>();
std::memcpy(material, white, sizeof(game::Material));
std::memcpy(texture_table, white->textureTable, sizeof(game::MaterialTextureDef));
std::memcpy(image, white->textureTable->u.image, sizeof(game::GfxImage));
material->constantTable = &constant_table;
material->name = utils::memory::get_allocator()->duplicate_string(name);
image->name = material->name;
material->textureTable = texture_table;
material->textureTable->u.image = setup_image(image, data);
return material;
}
void free_material(game::Material* material)
{
material->textureTable->u.image->textures.___u0.map->Release();
material->textureTable->u.image->textures.shaderView->Release();
utils::memory::get_allocator()->free(material->textureTable->u.image);
utils::memory::get_allocator()->free(material->textureTable);
utils::memory::get_allocator()->free(material->name);
utils::memory::get_allocator()->free(material);
}
game::Material* load_material(const std::string& name)
{
return material_data.access<game::Material*>([&](material_data_t& data_) -> game::Material*
{
if (const auto i = data_.materials.find(name); i != data_.materials.end())
{
return i->second;
}
std::string data{};
if (const auto i = data_.images.find(name); i != data_.images.end())
{
data = i->second;
}
if (data.empty() && !filesystem::read_file(utils::string::va("materials/%s.png", name.data()), &data))
{
data_.materials[name] = nullptr;
return nullptr;
}
const auto material = create_material(name, data);
data_.materials[name] = material;
return material;
});
}
game::Material* try_load_material(const std::string& name)
{
if (name == "white")
{
return nullptr;
}
try
{
return load_material(name);
}
catch (const std::exception& e)
{
console::error("Failed to load material %s: %s\n", name.data(), e.what());
}
return nullptr;
}
game::Material* material_register_handle_stub(const char* name)
{
auto result = try_load_material(name);
if (result == nullptr)
{
result = material_register_handle_hook.invoke<game::Material*>(name);
}
return result;
}
int db_material_streaming_fail_stub(game::Material* material) int db_material_streaming_fail_stub(game::Material* material)
{ {
if (material->constantTable == &constant_table) if (material->constantTable == &constant_table)
@ -238,38 +125,73 @@ namespace materials
#endif #endif
} }
void add(const std::string& name, const std::string& data) bool setup_material_image(game::Material* material, const std::string& data)
{ {
material_data.access([&](material_data_t& data_) if (*game::d3d11_device == nullptr)
{ {
data_.images[name] = data; console::error("Tried to create texture while d3d11 device isn't initialized\n");
}); return false;
} }
bool exists(const std::string& name) const auto image = material->textureTable->u.image;
{ image->imageFormat = 0x1000003;
return material_data.access<bool>([&](material_data_t& data_) image->resourceSize = -1;
{
return data_.images.find(name) != data_.images.end(); auto raw_image = utils::image{data};
});
D3D11_SUBRESOURCE_DATA resource_data{};
resource_data.SysMemPitch = raw_image.get_width() * 4;
resource_data.SysMemSlicePitch = resource_data.SysMemPitch * raw_image.get_height();
resource_data.pSysMem = raw_image.get_buffer();
game::Image_Setup(image, raw_image.get_width(), raw_image.get_height(), image->depth, image->numElements,
image->imageFormat, DXGI_FORMAT_R8G8B8A8_UNORM, image->name, &resource_data);
return true;
} }
void clear() game::Material* create_material(const std::string& name)
{ {
material_data.access([&](material_data_t& data_) const auto white = game::Material_RegisterHandle("$white");
{ const auto material = utils::memory::allocate<game::Material>();
for (auto& material : data_.materials) const auto texture_table = utils::memory::allocate<game::MaterialTextureDef>();
{ const auto image = utils::memory::allocate<game::GfxImage>();
if (material.second == nullptr)
{ std::memcpy(material, white, sizeof(game::Material));
continue; std::memcpy(texture_table, white->textureTable, sizeof(game::MaterialTextureDef));
std::memcpy(image, white->textureTable->u.image, sizeof(game::GfxImage));
material->constantTable = &constant_table;
material->name = utils::memory::duplicate_string(name);
image->name = material->name;
image->textures.map = nullptr;
image->textures.shaderView = nullptr;
image->textures.shaderViewAlternate = nullptr;
material->textureTable = texture_table;
return material;
} }
free_material(material.second); void free_material(game::Material* material)
{
const auto try_release = []<typename T>(T** resource)
{
if (*resource != nullptr)
{
(*resource)->Release();
*resource = nullptr;
} }
};
data_.materials.clear(); try_release(&material->textureTable->u.image->textures.map);
}); try_release(&material->textureTable->u.image->textures.shaderView);
try_release(&material->textureTable->u.image->textures.shaderViewAlternate);
utils::memory::free(material->textureTable->u.image);
utils::memory::free(material->textureTable);
utils::memory::free(material->name);
utils::memory::free(material);
} }
class component final : public component_interface class component final : public component_interface
@ -282,7 +204,6 @@ namespace materials
return; return;
} }
material_register_handle_hook.create(game::Material_RegisterHandle, material_register_handle_stub);
db_material_streaming_fail_hook.create(SELECT_VALUE(0x1FB400_b, 0x3A1600_b), db_material_streaming_fail_stub); db_material_streaming_fail_hook.create(SELECT_VALUE(0x1FB400_b, 0x3A1600_b), db_material_streaming_fail_stub);
db_get_material_index_hook.create(SELECT_VALUE(0x1F1D80_b, 0x396000_b), db_get_material_index_stub); db_get_material_index_hook.create(SELECT_VALUE(0x1F1D80_b, 0x396000_b), db_get_material_index_stub);
@ -296,7 +217,7 @@ namespace materials
scheduler::once([] scheduler::once([]
{ {
debug_materials = dvars::register_bool("debug_materials", 0, 0x0, "Print current material and images"); debug_materials = dvars::register_bool("debug_materials", false, game::DVAR_FLAG_NONE, "Print current material and images");
}, scheduler::main); }, scheduler::main);
} }
#endif #endif

View File

@ -1,8 +1,10 @@
#pragma once #pragma once
#include "game/game.hpp"
namespace materials namespace materials
{ {
void add(const std::string& name, const std::string& data); bool setup_material_image(game::Material* material, const std::string& data);
bool exists(const std::string& name); game::Material* create_material(const std::string& name);
void clear(); void free_material(game::Material* material);
} }

View File

@ -30,7 +30,6 @@ namespace mods
{ {
if (release_assets) if (release_assets)
{ {
materials::clear();
fonts::clear(); fonts::clear();
} }

View File

@ -32,15 +32,8 @@ namespace party
{ {
namespace namespace
{ {
struct connection_state server_connection_state{};
{ std::optional<discord_information> server_discord_info{};
game::netadr_s host{};
std::string challenge{};
bool hostDefined{false};
} connect_state;
std::string sv_motd;
int sv_maxclients;
struct usermap_file struct usermap_file
{ {
@ -165,11 +158,11 @@ namespace party
const char* get_didyouknow_stub(void* table, int row, int column) const char* get_didyouknow_stub(void* table, int row, int column)
{ {
if (party::sv_motd.empty()) if (server_connection_state.motd.empty())
{ {
return utils::hook::invoke<const char*>(0x5A0AC0_b, table, row, column); return utils::hook::invoke<const char*>(0x5A0AC0_b, table, row, column);
} }
return utils::string::va("%s", party::sv_motd.data()); return utils::string::va("%s", server_connection_state.motd.data());
} }
void disconnect() void disconnect()
@ -498,7 +491,7 @@ namespace party
command::execute("disconnect"); command::execute("disconnect");
scheduler::once([] scheduler::once([]
{ {
connect(connect_state.host); connect(server_connection_state.host);
}, scheduler::pipeline::main); }, scheduler::pipeline::main);
return; return;
} }
@ -614,7 +607,7 @@ namespace party
void clear_sv_motd() void clear_sv_motd()
{ {
party::sv_motd.clear(); server_connection_state.motd.clear();
} }
int get_client_num_by_name(const std::string& name) int get_client_num_by_name(const std::string& name)
@ -636,9 +629,9 @@ namespace party
return -1; return -1;
} }
void reset_connect_state() void reset_server_connection_state()
{ {
connect_state = {}; server_connection_state = {};
} }
int get_client_count() int get_client_count()
@ -691,16 +684,11 @@ namespace party
command::execute("lui_open_popup popup_acceptinginvite", false); command::execute("lui_open_popup popup_acceptinginvite", false);
connect_state.host = target; server_connection_state.host = target;
connect_state.challenge = utils::cryptography::random::get_challenge(); server_connection_state.challenge = utils::cryptography::random::get_challenge();
connect_state.hostDefined = true; server_connection_state.hostDefined = true;
network::send(target, "getInfo", connect_state.challenge); network::send(target, "getInfo", server_connection_state.challenge);
}
game::netadr_s get_state_host()
{
return connect_state.host;
} }
void start_map(const std::string& mapname, bool dev) void start_map(const std::string& mapname, bool dev)
@ -762,9 +750,14 @@ namespace party
} }
} }
int server_client_count() connection_state get_server_connection_state()
{ {
return party::sv_maxclients; return server_connection_state;
}
std::optional<discord_information> get_server_discord_info()
{
return server_discord_info;
} }
class component final : public component_interface class component final : public component_interface
@ -777,7 +770,7 @@ namespace party
return; return;
} }
// detour CL_Disconnect to clear motd // clear motd & usermap
cl_disconnect_hook.create(0x12F080_b, cl_disconnect_stub); cl_disconnect_hook.create(0x12F080_b, cl_disconnect_stub);
if (game::environment::is_mp()) if (game::environment::is_mp())
@ -849,7 +842,7 @@ namespace party
command::add("reconnect", [](const command::params& argument) command::add("reconnect", [](const command::params& argument)
{ {
if (!connect_state.hostDefined) if (!server_connection_state.hostDefined)
{ {
console::info("Cannot connect to server.\n"); console::info("Cannot connect to server.\n");
return; return;
@ -862,7 +855,7 @@ namespace party
} }
else else
{ {
connect(connect_state.host); connect(server_connection_state.host);
} }
}); });
@ -965,7 +958,7 @@ namespace party
scheduler::once([]() scheduler::once([]()
{ {
sv_say_name = dvars::register_string("sv_sayName", "console", game::DvarFlags::DVAR_FLAG_NONE, ""); sv_say_name = dvars::register_string("sv_sayName", "console", game::DvarFlags::DVAR_FLAG_NONE, "Custom name for RCON console");
}, scheduler::pipeline::main); }, scheduler::pipeline::main);
command::add("tell", [](const command::params& params) command::add("tell", [](const command::params& params)
@ -1060,6 +1053,8 @@ namespace party
info.set("sv_running", utils::string::va("%i", get_dvar_bool("sv_running") && !game::VirtualLobby_Loaded())); info.set("sv_running", utils::string::va("%i", get_dvar_bool("sv_running") && !game::VirtualLobby_Loaded()));
info.set("dedicated", utils::string::va("%i", get_dvar_bool("dedicated"))); info.set("dedicated", utils::string::va("%i", get_dvar_bool("dedicated")));
info.set("sv_wwwBaseUrl", get_dvar_string("sv_wwwBaseUrl")); info.set("sv_wwwBaseUrl", get_dvar_string("sv_wwwBaseUrl"));
info.set("sv_discordImageUrl", get_dvar_string("sv_discordImageUrl"));
info.set("sv_discordImageText", get_dvar_string("sv_discordImageText"));
if (!fastfiles::is_stock_map(mapname)) if (!fastfiles::is_stock_map(mapname))
{ {
@ -1092,7 +1087,7 @@ namespace party
const utils::info_string info(data); const utils::info_string info(data);
server_list::handle_info_response(target, info); server_list::handle_info_response(target, info);
if (connect_state.host != target) if (server_connection_state.host != target)
{ {
return; return;
} }
@ -1108,7 +1103,7 @@ namespace party
return; return;
} }
if (info.get("challenge") != connect_state.challenge) if (info.get("challenge") != server_connection_state.challenge)
{ {
menu_error("Connection failed: Invalid challenge."); menu_error("Connection failed: Invalid challenge.");
return; return;
@ -1154,8 +1149,17 @@ namespace party
return; return;
} }
party::sv_motd = info.get("sv_motd"); server_connection_state.motd = info.get("sv_motd");
party::sv_maxclients = std::stoi(info.get("sv_maxclients")); server_connection_state.max_clients = std::stoi(info.get("sv_maxclients"));
server_connection_state.base_url = info.get("sv_wwwBaseUrl");
discord_information discord_info{};
discord_info.image = info.get("sv_discordImageUrl");
discord_info.image_text = info.get("sv_discordImageText");
if (!discord_info.image.empty() || !discord_info.image_text.empty())
{
server_discord_info.emplace(discord_info);
}
connect_to_party(target, mapname, gametype); connect_to_party(target, mapname, gametype);
}); });

View File

@ -3,19 +3,34 @@
namespace party namespace party
{ {
std::string get_www_url(); struct connection_state
{
game::netadr_s host;
std::string challenge;
bool hostDefined;
std::string motd;
int max_clients;
std::string base_url;
};
struct discord_information
{
std::string image;
std::string image_text;
};
void user_download_response(bool response); void user_download_response(bool response);
void menu_error(const std::string& error); void menu_error(const std::string& error);
void reset_connect_state(); void reset_server_connection_state();
void connect(const game::netadr_s& target); void connect(const game::netadr_s& target);
void start_map(const std::string& mapname, bool dev = false); void start_map(const std::string& mapname, bool dev = false);
void clear_sv_motd(); void clear_sv_motd();
game::netadr_s get_state_host(); connection_state get_server_connection_state();
int server_client_count(); std::optional<discord_information> get_server_discord_info();
int get_client_num_by_name(const std::string& name); int get_client_num_by_name(const std::string& name);

View File

@ -77,7 +77,7 @@ namespace server_list
server_list_page = 0; server_list_page = 0;
} }
party::reset_connect_state(); party::reset_server_connection_state();
if (get_master_server(master_state.address)) if (get_master_server(master_state.address))
{ {

View File

@ -9,6 +9,7 @@
#include "localized_strings.hpp" #include "localized_strings.hpp"
#include "console.hpp" #include "console.hpp"
#include "discord.hpp"
#include "download.hpp" #include "download.hpp"
#include "game_module.hpp" #include "game_module.hpp"
#include "fps.hpp" #include "fps.hpp"
@ -33,6 +34,8 @@
#include "steam/steam.hpp" #include "steam/steam.hpp"
#include <discord_rpc.h>
namespace ui_scripting namespace ui_scripting
{ {
namespace namespace
@ -367,7 +370,29 @@ namespace ui_scripting
download_table["abort"] = download::stop_download; download_table["abort"] = download::stop_download;
download_table["userdownloadresponse"] = party::user_download_response; download_table["userdownloadresponse"] = party::user_download_response;
download_table["getwwwurl"] = party::get_www_url; download_table["getwwwurl"] = party::get_server_connection_state().base_url;
auto discord_table = table();
lua["discord"] = discord_table;
discord_table["respond"] = discord::respond;
discord_table["getavatarmaterial"] = [](const std::string& id)
-> script_value
{
const auto material = discord::get_avatar_material(id);
if (material == nullptr)
{
return {};
}
return lightuserdata(material);
};
discord_table["reply"] = table();
discord_table["reply"]["yes"] = DISCORD_REPLY_YES;
discord_table["reply"]["ignore"] = DISCORD_REPLY_IGNORE;
discord_table["reply"]["no"] = DISCORD_REPLY_NO;
} }
void start() void start()

View File

@ -1542,7 +1542,9 @@ namespace game
char data[1]; char data[1];
}; };
union $3FA29451CE6F1FA138A5ABAB84BE9676 struct GfxTexture
{
union
{ {
ID3D11Texture1D* linemap; ID3D11Texture1D* linemap;
ID3D11Texture2D* map; ID3D11Texture2D* map;
@ -1550,10 +1552,6 @@ namespace game
ID3D11Texture2D* cubemap; ID3D11Texture2D* cubemap;
GfxImageLoadDef* loadDef; GfxImageLoadDef* loadDef;
}; };
struct GfxTexture
{
$3FA29451CE6F1FA138A5ABAB84BE9676 ___u0;
ID3D11ShaderResourceView* shaderView; ID3D11ShaderResourceView* shaderView;
ID3D11ShaderResourceView* shaderViewAlternate; ID3D11ShaderResourceView* shaderViewAlternate;
}; };

View File

@ -125,6 +125,8 @@ namespace game
WEAK symbol<char*(char* string)> I_CleanStr{0x4293E0, 0x5AF2E0}; WEAK symbol<char*(char* string)> I_CleanStr{0x4293E0, 0x5AF2E0};
WEAK symbol<const char*(int, int, int)> Key_KeynumToString{0x1AC410, 0x199990}; WEAK symbol<const char*(int, int, int)> Key_KeynumToString{0x1AC410, 0x199990};
WEAK symbol<int(const char* cmd)> Key_GetBindingForCmd{0x377280, 0x1572B0};
WEAK symbol<void(int local_client_num, int keynum, int binding)> Key_SetBinding{0x1AC570, 0x199AE0};
WEAK symbol<unsigned int(int)> Live_SyncOnlineDataFlags{0x0, 0x1A5C10}; WEAK symbol<unsigned int(int)> Live_SyncOnlineDataFlags{0x0, 0x1A5C10};
@ -347,6 +349,8 @@ namespace game
WEAK symbol<map_t> maps{0x7CE5A0, 0x926C80}; WEAK symbol<map_t> maps{0x7CE5A0, 0x926C80};
WEAK symbol<ID3D11Device*> d3d11_device{0x1163B98, 0x12DFBF8};
namespace mp namespace mp
{ {
WEAK symbol<gentity_s> g_entities{0x0, 0x71F19E0}; WEAK symbol<gentity_s> g_entities{0x0, 0x71F19E0};

View File

@ -131,6 +131,14 @@ namespace utils::string
*out = '\0'; *out = '\0';
} }
std::string strip(const std::string& string)
{
std::string new_string;
new_string.resize(string.size(), 0);
strip(string.data(), new_string.data(), static_cast<int>(new_string.size()));
return new_string;
}
std::string convert(const std::wstring& wstr) std::string convert(const std::wstring& wstr)
{ {
std::string result; std::string result;

View File

@ -92,6 +92,7 @@ namespace utils::string
std::string get_clipboard_data(); std::string get_clipboard_data();
void strip(const char* in, char* out, int max); void strip(const char* in, char* out, int max);
std::string strip(const std::string& string);
std::string convert(const std::wstring& wstr); std::string convert(const std::wstring& wstr);
std::wstring convert(const std::string& str); std::wstring convert(const std::string& str);