From 5ba6588d899d55093c0a119542acca60fc752905 Mon Sep 17 00:00:00 2001 From: albertywang Date: Fri, 29 Nov 2024 17:27:44 -0600 Subject: [PATCH 1/4] Matches support in gamelink sdk --- include/config_footer.h | 47 ++++++- include/gamelink.h | 59 +++++++++ schema/matches.h | 267 +++++++++++++++++++++++++++++++++++++++ schema/poll.h | 8 +- schema/schema.h | 1 + src/gamelink.cpp | 10 ++ src/gamelink_matches.cpp | 116 +++++++++++++++++ test/matches.cpp | 200 +++++++++++++++++++++++++++++ vcpkg.json | 2 +- 9 files changed, 707 insertions(+), 3 deletions(-) create mode 100644 schema/matches.h create mode 100644 src/gamelink_matches.cpp create mode 100644 test/matches.cpp diff --git a/include/config_footer.h b/include/config_footer.h index d6e6625..34acc3d 100644 --- a/include/config_footer.h +++ b/include/config_footer.h @@ -16,4 +16,49 @@ namespace nlohmann } }; } -#endif \ No newline at end of file + +template<> +struct std::hash +{ + inline std::size_t operator()(const gamelink::string& str) const + { + return std::hash< + std::string_view + >()(std::string_view(str.c_str(), str.size())); + } +}; +#endif + +namespace nlohmann +{ + template + struct adl_serializer> + { + static inline void to_json(json& js, const std::unordered_map& s) + { + for (auto it = s.begin(); it != s.end(); ++it) + { + js[json::string_t(it->first.c_str())] = it->second; + } + } + + static inline void from_json(const json& j, std::unordered_map& s) + { + if (!j.is_object()) + { + return; + } + + s.clear(); + for (auto it = j.begin(); it != j.end(); ++it) + { + gamelink::string key(it.key().c_str()); + + T value; + it.value().get_to(value); + + s.insert(std::make_pair(key, std::move(value))); + } + } + }; +} \ No newline at end of file diff --git a/include/gamelink.h b/include/gamelink.h index d0308b8..b1d3994 100644 --- a/include/gamelink.h +++ b/include/gamelink.h @@ -1616,6 +1616,63 @@ namespace gamelink Event& OnMatchmakingQueueInvite(); #pragma endregion +#pragma region Matches + // Creates a match, which is a collection of users that can have some operations performed on them + // as an entire collection, instead of channel by channel. + // Match manipulation functions are only supported by a server Gamelink connection. Calling these + // from a client will result in an authorization faiure. + + // Creates a match with the given ID + RequestId CreateMatch(const string& id); + + // Keeps a match alive + RequestId KeepMatchAlive(const string& id); + + // Adds channels to a match + RequestId AddChannelsToMatch(const string& id, const std::vector& channels); + + // Removes channels from a match. + RequestId RemoveChannelsFromMatch(const string& id, const std::vector& channels); + + // Runs a poll in a match. Very similar to RunPoll, except for the callback type, as it receives + // information for all matches. + + RequestId RunMatchPoll( + const string& matchId, + const string& pollId, + const string& prompt, + const PollConfiguration& config, + const string* optionsBegin, + const string* optionsEnd, + std::function onUpdateCallback, + std::function onFinishCallback + ); + + RequestId RunMatchPoll( + const string& matchId, + const string& pollId, + const string& prompt, + const PollConfiguration& config, + const std::vector& opts, + std::function onUpdateCallback, + std::function onFinishCallback + ); + + // Stops a poll in a match + RequestId StopMatchPoll(const string& matchId, const string& pollId); + + /// Sends a broadcast to all viewers on all channels in the match + template + RequestId SendMatchBroadcast(const string& matchId, const string& topic, const T& value) + { + schema::BroadcastMatchRequest payload(matchId, topic, value); + return queuePayload(payload); + } + + /// Sends a broadcast to all viewers on all channels in the match + RequestId SendMatchBroadcast(const string& matchId, const string& topic, const nlohmann::json& message); +#pragma endregion + /// Sends a request to set your games metadata. You're expected to fill out a gamelink::GameMetadata struct with your games metadata /// and provide it. /// @return RequestId of the generated request @@ -1683,6 +1740,8 @@ namespace gamelink Event _onGetDrops; Event _onMatchmakingUpdate; + + Event _onMatchPollUpdate; }; // Implementation of Event::Remove is here because of completeness requirements of SDK. diff --git a/schema/matches.h b/schema/matches.h new file mode 100644 index 0000000..47149c2 --- /dev/null +++ b/schema/matches.h @@ -0,0 +1,267 @@ +#pragma once +#ifndef MUXY_GAMELINK_SCHEMA_MATCHES_H +#define MUXY_GAMELINK_SCHEMA_MATCHES_H +#include "schema/envelope.h" +#include "schema/subscription.h" +#include "schema/poll.h" + +namespace gamelink +{ + namespace schema + { + struct CreateMatchRequestBody + { + gamelink::string matchId; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_1(CreateMatchRequestBody, + "id", matchId + ); + }; + + struct MUXY_GAMELINK_API CreateMatchRequest : SendEnvelope + { + explicit inline CreateMatchRequest(const string& id) + { + this->action = string("create"); + this->params.target = string("match"); + + this->data.matchId = id; + } + }; + + struct KeepMatchAliveBody + { + gamelink::string matchId; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_1(KeepMatchAliveBody, + "id", matchId + ); + }; + + struct MUXY_GAMELINK_API KeepMatchAliveRequest : SendEnvelope + { + explicit inline KeepMatchAliveRequest(const string& id) + { + this->action = string("keepalive"); + this->params.target = string("match"); + + this->data.matchId = id; + } + }; + + struct AddOrRemoveMatchChannelsRequestBody + { + gamelink::string matchId; + std::vector channels; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_2(AddOrRemoveMatchChannelsRequestBody, + "id", matchId, + "channel_ids", channels + ); + }; + + struct MUXY_GAMELINK_API AddMatchChannelsRequest : SendEnvelope + { + explicit inline AddMatchChannelsRequest(const string& id, const std::vector& channels) + { + this->action = string("add_channels"); + this->params.target = string("match"); + + this->data.matchId = id; + this->data.channels = channels; + } + }; + + struct MUXY_GAMELINK_API RemoveMatchChannelsRequest : SendEnvelope + { + explicit inline RemoveMatchChannelsRequest(const string& id, const std::vector& channels) + { + this->action = string("remove_channels"); + this->params.target = string("match"); + + this->data.matchId = id; + this->data.channels = channels; + } + }; + + struct CreateMatchPollRequestBody + { + gamelink::string matchId; + CreatePollWithConfigurationRequestBody poll; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_2(CreateMatchPollRequestBody, + "id", matchId, + "poll", poll + ); + }; + + struct MUXY_GAMELINK_API CreateMatchPollRequest : SendEnvelope + { + explicit inline CreateMatchPollRequest(const string& match, const CreatePollWithConfigurationRequestBody& poll) + { + this->action = string("create"); + this->params.target = string("match_poll"); + + this->data.matchId = match; + this->data.poll = poll; + } + }; + + + struct DeleteMatchPollRequestBody + { + gamelink::string matchId; + gamelink::string pollId; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_2(DeleteMatchPollRequestBody, + "id", matchId, + "poll_id", pollId + ); + }; + + struct MUXY_GAMELINK_API DeleteMatchPollRequest : SendEnvelope + { + explicit inline DeleteMatchPollRequest(const string& match, const string& poll) + { + this->action = string("delete"); + this->params.target = string("match_poll"); + + this->data.matchId = match; + this->data.pollId = poll; + } + }; + + template + struct ReconfigureMatchPollRequestBody + { + gamelink::string matchId; + gamelink::string pollId; + Config config; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_3(ReconfigureMatchPollRequestBody, + "id", matchId, + "poll_id", pollId, + "config", config + ); + }; + + struct MUXY_GAMELINK_API ExpireMatchPollRequest : SendEnvelope> + { + explicit inline ExpireMatchPollRequest(const string& id, const string& pollId) + { + this->action = string("reconfigure"); + this->params.target = string("match_poll"); + + this->data.matchId = id; + this->data.pollId = pollId; + this->data.config.endsAt = -1; + } + }; + + struct MUXY_GAMELINK_API SetMatchPollDisableRequest : SendEnvelope> + { + explicit inline SetMatchPollDisableRequest(const string& id, const string& pollId, bool status) + { + this->action = string("reconfigure"); + this->params.target = string("match_poll"); + + this->data.matchId = id; + this->data.pollId = pollId; + this->data.config.disabled = status; + } + }; + + struct MUXY_GAMELINK_API SubscribeMatchPollRequest : SendEnvelope + { + explicit inline SubscribeMatchPollRequest(const string& matchId) + { + this->action = string("subscribe"); + this->params.target = string("match_poll"); + this->data.topic_id = string(matchId); + } + }; + + struct MUXY_GAMELINK_API UnsubscribeMatchPollRequest : SendEnvelope + { + explicit inline UnsubscribeMatchPollRequest(const string& matchId) + { + this->action = string("unsubscribe"); + this->params.target = string("match_poll"); + this->data.topic_id = string(matchId); + } + }; + + struct MatchPollUpdateInformation + { + string matchId; + string pollId; + string status; + + std::unordered_map results; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_4(MatchPollUpdateInformation, + "match_id", matchId, + "poll_id", pollId, + "status", status, + "results", results + ); + }; + + struct MUXY_GAMELINK_API MatchPollUpdate : ReceiveEnvelope + {}; + + + struct MatchPollUpdateInformationInternal + { + string matchId; + string pollId; + string status; + + nlohmann::json results; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_4(MatchPollUpdateInformationInternal, + "match_id", matchId, + "poll_id", pollId, + "status", status, + "results", results + ); + }; + + struct MUXY_GAMELINK_API MatchPollUpdateInternal : ReceiveEnvelope + {}; + + template + struct BroadcastMatchRequestBody + { + string matchId; + string topic; + + T data; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_3(BroadcastMatchRequestBody, + "match_id", matchId, + "topic", topic, + "data", data + ); + }; + + template + struct BroadcastMatchRequest : SendEnvelope> + { + BroadcastMatchRequest(const string& matchId, const string& topic, const T& data) + { + this->action = string("broadcast"); + this->params.target = string("match"); + + this->data.matchId = matchId; + this->data.topic = topic; + this->data.data = data; + } + }; + + struct MUXY_GAMELINK_API BroadcastMatchResponse : ReceiveEnvelope + {}; + } +} + +#endif \ No newline at end of file diff --git a/schema/poll.h b/schema/poll.h index a7f5bc0..738dc5a 100644 --- a/schema/poll.h +++ b/schema/poll.h @@ -203,7 +203,13 @@ namespace gamelink /// Number of responses, including ones that outside the [0, 32) range. int32_t count; - MUXY_GAMELINK_SERIALIZE_INTRUSIVE_5(PollUpdateBody, "poll", poll, "results", results, "mean", mean, "sum", sum, "count", count); + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_5(PollUpdateBody, + "poll", poll, + "results", results, + "mean", mean, + "sum", sum, + "count", count + ); }; template diff --git a/schema/schema.h b/schema/schema.h index 6a69eb0..9624102 100644 --- a/schema/schema.h +++ b/schema/schema.h @@ -15,5 +15,6 @@ #include "game_config.h" #include "drops.h" #include "matchmaking.h" +#include "matches.h" #include "game_metadata.h" #endif diff --git a/src/gamelink.cpp b/src/gamelink.cpp index 576744e..68984a8 100644 --- a/src/gamelink.cpp +++ b/src/gamelink.cpp @@ -173,6 +173,7 @@ namespace gamelink , _onGetOutstandingTransactions(this, "OnGetOutstandingTransactions", 11) , _onGetDrops(this, "OnGetDrops", 12) , _onMatchmakingUpdate(this, "OnMatchmakingUpdate", 13) + , _onMatchPollUpdate(this, "OnMatchPollUpdate", 14) {} SDK::~SDK() @@ -508,6 +509,15 @@ namespace gamelink _onMatchmakingUpdate.Invoke(resp); } } + else if (env.meta.target == "match_poll") + { + schema::MatchPollUpdate resp; + success = schema::ParseResponse(bytes, length, resp); + if (success) + { + _onMatchPollUpdate.Invoke(resp); + } + } } else { diff --git a/src/gamelink_matches.cpp b/src/gamelink_matches.cpp new file mode 100644 index 0000000..dd1f6b7 --- /dev/null +++ b/src/gamelink_matches.cpp @@ -0,0 +1,116 @@ +#include "gamelink.h" + +namespace gamelink +{ + RequestId SDK::CreateMatch(const string& id) + { + schema::CreateMatchRequest req(id); + return queuePayload(req); + } + + RequestId SDK::KeepMatchAlive(const string& id) + { + schema::KeepMatchAliveRequest req(id); + return queuePayload(req); + } + + RequestId SDK::AddChannelsToMatch(const string& id, const std::vector& channels) + { + schema::AddMatchChannelsRequest req(id, channels); + return queuePayload(req); + } + + RequestId SDK::RemoveChannelsFromMatch(const string& id, const std::vector& channels) + { + schema::RemoveMatchChannelsRequest req(id, channels); + return queuePayload(req); + } + + RequestId SDK::StopMatchPoll(const string& id, const string& pollId) + { + schema::ExpireMatchPollRequest req(id, pollId); + return queuePayload(req); + } + + RequestId SDK::SendMatchBroadcast(const string& matchId, const string& topic, const nlohmann::json& message) + { + schema::BroadcastMatchRequest payload(matchId, topic, message); + return queuePayload(payload); + } + + RequestId SDK::RunMatchPoll( + const string& matchId, + const string& pollId, + const string& prompt, + const PollConfiguration& config, + const std::vector& opts, + std::function onUpdateCallback, + std::function onFinishCallback) + { + schema::DeleteMatchPollRequest delRequest(matchId, pollId); + RequestId del = queuePayload(delRequest); + WaitForResponse(del); + + schema::SubscribeMatchPollRequest subRequest(matchId); + WaitForResponse(queuePayload(subRequest)); + + if (!VerifyPollLimits(prompt, opts)) + { + return gamelink::REJECTED_REQUEST_ID; + } + + schema::CreatePollWithConfigurationRequest createPollRequest(pollId, prompt, config, opts); + schema::CreateMatchPollRequest createMatchPollRequest(matchId, createPollRequest.data); + + RequestId create = queuePayload(createMatchPollRequest); + + char buffer[128]; + snprintf(buffer, 128, "_%s", pollId.c_str()); + + gamelink::string callbackName = gamelink::string(buffer); + + bool hasCalledOnFinish = false; + _onMatchPollUpdate.AddUnique(callbackName, [=](const schema::MatchPollUpdate& update) mutable + { + bool matches = update.data.matchId == matchId && update.data.pollId == pollId; + if (!matches) + { + return; + } + + if (update.data.status == gamelink::string("expired")) + { + if (!hasCalledOnFinish) + { + onFinishCallback(update); + + schema::UnsubscribeMatchPollRequest unsubRequest(matchId); + this->queuePayload(unsubRequest); + this->_onMatchPollUpdate.RemoveByName(callbackName); + hasCalledOnFinish = true; + } + } + else + { + onUpdateCallback(update); + } + }); + + return ANY_REQUEST_ID; + } + + RequestId SDK::RunMatchPoll( + const string& matchId, + const string& pollId, + const string& prompt, + const PollConfiguration& config, + const string* optionsBegin, + const string* optionsEnd, + std::function onUpdateCallback, + std::function onFinishCallback + ) + { + std::vector opts(optionsBegin, optionsEnd); + return RunMatchPoll(matchId, pollId, prompt, config, opts, std::move(onUpdateCallback), std::move(onFinishCallback)); + } +} \ No newline at end of file diff --git a/test/matches.cpp b/test/matches.cpp new file mode 100644 index 0000000..6965839 --- /dev/null +++ b/test/matches.cpp @@ -0,0 +1,200 @@ +#include "catch2/catch.hpp" +#include "util.h" + +#include "gamelink.h" + +TEST_CASE("Matches operations", "[matches]") +{ + gamelink::SDK sdk; + + sdk.CreateMatch("my-cool-match"); + validateSinglePayload(sdk, R"({ + "action": "create", + "data": { + "id": "my-cool-match" + }, + "params": { + "request_id": 65535, + "target": "match" + } + })"); + + sdk.AddChannelsToMatch("my-cool-match", { + "1001", "1002", "1003", + }); + validateSinglePayload(sdk, R"({ + "action": "add_channels", + "data": { + "id": "my-cool-match", + "channel_ids": ["1001", "1002", "1003"] + }, + "params": { + "request_id": 65535, + "target": "match" + } + })"); + + sdk.KeepMatchAlive("my-cool-match"); + validateSinglePayload(sdk, R"({ + "action": "keepalive", + "data": { + "id": "my-cool-match" + }, + "params": { + "request_id": 65535, + "target": "match" + } + })"); + + sdk.RemoveChannelsFromMatch("my-cool-match", { + "1001" + }); + + validateSinglePayload(sdk, R"({ + "action": "remove_channels", + "data": { + "id": "my-cool-match", + "channel_ids": ["1001"] + }, + "params": { + "request_id": 65535, + "target": "match" + } + })"); +} + +namespace gs = gamelink::schema; +TEST_CASE("Match polls operations", "[matches]") +{ + gamelink::SDK sdk; + + sdk.CreateMatch("my-cool-match"); + validateSinglePayload(sdk, R"({ + "action": "create", + "data": { + "id": "my-cool-match" + }, + "params": { + "request_id": 65535, + "target": "match" + } + })"); + + sdk.AddChannelsToMatch("my-cool-match", { + "1001", "1002", "1003", + }); + validateSinglePayload(sdk, R"({ + "action": "add_channels", + "data": { + "id": "my-cool-match", + "channel_ids": ["1001", "1002", "1003"] + }, + "params": { + "request_id": 65535, + "target": "match" + } + })"); + + gamelink::PollConfiguration config; + config.disabled = false; + config.distinctOptionsPerUser = 1; + config.totalVotesPerUser = 1024; + config.userIdVoting = true; + config.startsAt = 10; + config.endsAt = 20; + config.votesPerOption = 1024; + + int updateCalls = 0; + std::function update = [&updateCalls](const gs::MatchPollUpdate& resp) + { + REQUIRE(resp.data.pollId == "test-poll"); + REQUIRE(resp.data.matchId == "my-cool-match"); + + REQUIRE(resp.data.results.count("1001") == 1); + REQUIRE(resp.data.results.find("1001")->second.results[0] == 1); + REQUIRE(resp.data.results.find("1001")->second.results[1] == 3); + REQUIRE(resp.data.results.count("1002") == 0); + REQUIRE(resp.data.results.count("1003") == 0); + updateCalls++; + }; + + int finishCalls = 0; + std::function finish = [&finishCalls](const gs::MatchPollUpdate& resp) + { + REQUIRE(resp.data.pollId == "test-poll"); + finishCalls++; + }; + + const char* updateMessage = R"({ + "meta": { + "action": "update", + "target": "match_poll" + }, + "data": { + "poll_id": "test-poll", + "match_id": "my-cool-match", + "status": "active", + + "results": { + "1001": { + "count": 4, + "mean": 0.0, + "sum": 0.0, + + "results": [1, 3], + "poll": { + "poll_id": "test-poll", + "prompt": "Is a hot dog a sandwich?", + "options": ["Yes", "No"], + "status": "active" + } + } + } + } + })"; + + const char * finishMessage = R"({ + "meta": { + "action": "update", + "target": "match_poll" + }, + "data": { + "poll_id": "test-poll", + "match_id": "my-cool-match", + "status": "expired", + + "results": { + "1001": { + "count": 4, + "mean": 0.0, + "sum": 0.0, + + "results": [1, 3], + "poll": { + "poll_id": "test-poll", + "prompt": "Is a hot dog a sandwich?", + "options": ["Yes", "No"], + "status": "expired" + } + } + } + } + })"; + + sdk.RunMatchPoll("my-cool-match", "test-poll", + "Is a hot dog a sandwich?", + config, + { "Yes", "No" }, + update, finish); + + + sdk.ReceiveMessage(updateMessage, strlen(updateMessage)); + sdk.ReceiveMessage(updateMessage, strlen(updateMessage)); + sdk.ReceiveMessage(updateMessage, strlen(updateMessage)); + sdk.ReceiveMessage(finishMessage, strlen(finishMessage)); + sdk.ReceiveMessage(finishMessage, strlen(finishMessage)); + sdk.ReceiveMessage(finishMessage, strlen(finishMessage)); + + REQUIRE(updateCalls == 3); + REQUIRE(finishCalls == 1); +} \ No newline at end of file diff --git a/vcpkg.json b/vcpkg.json index 91e448b..541039f 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -10,6 +10,6 @@ "websockets" ] } - ], + ], "builtin-baseline": "94bfbda514961bf2e29ef803c975ec36379d75e5" } \ No newline at end of file From 418f078121ffcb940900435bf58a6baa02ca77d1 Mon Sep 17 00:00:00 2001 From: albertywang Date: Fri, 29 Nov 2024 23:49:02 -0600 Subject: [PATCH 2/4] Integration tests to show match voting works --- schema/matches.h | 1 - test/integration.cpp | 52 +++++++++++++++++++++++++++++++-- test/integration_utils.cpp | 46 ++++++++++++++--------------- test/integration_utils.h | 5 ++-- websocket_network/websocket.cpp | 19 +++++++----- 5 files changed, 85 insertions(+), 38 deletions(-) diff --git a/schema/matches.h b/schema/matches.h index 47149c2..6a658dc 100644 --- a/schema/matches.h +++ b/schema/matches.h @@ -210,7 +210,6 @@ namespace gamelink struct MUXY_GAMELINK_API MatchPollUpdate : ReceiveEnvelope {}; - struct MatchPollUpdateInformationInternal { string matchId; diff --git a/test/integration.cpp b/test/integration.cpp index 1aa31ec..871f579 100644 --- a/test/integration.cpp +++ b/test/integration.cpp @@ -373,7 +373,6 @@ TEST_CASE_METHOD(IntegrationTestFixture, "Transactions Support", "[.][integratio REQUIRE(calls == 6); } - TEST_CASE_METHOD(IntegrationTestFixture, "Transactions Support through gateway", "[.][integration][t]") { Connect(); @@ -384,14 +383,14 @@ TEST_CASE_METHOD(IntegrationTestFixture, "Transactions Support through gateway", // This should get 1 call, to purchase 50 coins. REQUIRE(used.SKU == "muxy-bits-50"); REQUIRE(used.Bits == 50); - + bitsCalls++; }); size_t coinCalls = 0; gateway.OnActionUsed([&](const gateway::ActionUsed& used) { - // This should get 5 calls, which are all coins + // This should get 5 calls, which are all coins REQUIRE(used.ActionID == "costs-ten"); REQUIRE(used.Cost == 10); @@ -519,6 +518,53 @@ TEST_CASE_METHOD(IntegrationTestFixture, "Datastream operations", "[.][integrati REQUIRE(events == 6); } +TEST_CASE_METHOD(IntegrationTestFixture, "Matches and polls", "[.][integration]") +{ + Connect(); + + sdk.CreateMatch("my-cool-match"); + + sdk.AddChannelsToMatch("my-cool-match", { + "26052853", "89319907", "89368629", "89368745", "124708734" + }); + + size_t updateCalls = 0; + auto update = [&updateCalls](const gamelink::schema::MatchPollUpdate& update) + { + updateCalls++; + }; + + size_t finishCalls = 0; + auto finish = [&finishCalls](const gamelink::schema::MatchPollUpdate& finish) + { + finishCalls++; + }; + + gamelink::PollConfiguration config; + config.startsIn = 0; + config.endsIn = 5; + + gamelink::RequestId waiter = sdk.RunMatchPoll("my-cool-match", "what-to-eat", + "How many pizzas should I buy?", + config, + { "one", "two", "twenty" }, + update, + finish + ); + + Sleep(1); + + nlohmann::json voteValue; + voteValue["value"] = 1; + nlohmann::json unused; + + Request("POST", "vote?id=what-to-eat", &voteValue, &unused); + Sleep(6); + + REQUIRE(updateCalls > 0); + REQUIRE(finishCalls == 1); +} + TEST_CASE_METHOD(IntegrationTestFixture, "Client IP access", "[.][integration]") { int code; diff --git a/test/integration_utils.cpp b/test/integration_utils.cpp index ed99fb8..af1689d 100644 --- a/test/integration_utils.cpp +++ b/test/integration_utils.cpp @@ -59,7 +59,7 @@ void IntegrationTestFixture::Connect() std::string token = resp["token"]; gamelink::RequestId req = sdk.AuthenticateWithGameIDAndPIN( - gamelink::string(client.c_str()), + gamelink::string(client.c_str()), gamelink::string("Mystery Fun House"), gamelink::string(token.c_str()) ); @@ -68,7 +68,7 @@ void IntegrationTestFixture::Connect() else { gamelink::RequestId req = sdk.AuthenticateWithGameIDAndRefreshToken( - gamelink::string(client.c_str()), + gamelink::string(client.c_str()), gamelink::string("Mystery Fun House"), gamelink::string(refreshToken.c_str()) ); @@ -251,28 +251,28 @@ void IntegrationTestFixture::LoadEnvironment() } target = "sandbox"; - targetDomain = "sandbox.muxy.io"; + apiUrl = "https://sandbox.muxy.io"; + websocketUrl = "gamelink.sandbox.muxy.io"; + const char * targetEnv = std::getenv("MUXY_INTEGRATION_TARGET"); if (targetEnv) { - if (std::string(targetEnv) != "production") + target = std::string(targetEnv); + if (target == "localhost") { - target = std::string(targetEnv); - targetDomain = target + ".muxy.io"; - } else + apiUrl = "http://localhost:5050"; + websocketUrl = "localhost:5051"; + } + else if (std::string(targetEnv) != "production") { - target = "production"; - targetDomain = "muxy.io"; + apiUrl = "https://" + target + ".muxy.io"; + websocketUrl = "gamelink." + target + ".muxy.io"; + } + else + { + apiUrl = "https://api.muxy.io"; + websocketUrl = "gamelink.muxy.io"; } - } - REQUIRE(targetDomain.size()); - - targetPrefix = ""; - const char * prefixEnv = std::getenv("MUXY_INTEGRATION_OLD_URLS"); - if (prefixEnv && std::string(prefixEnv) == "true") - { - targetPrefix = target + "."; - targetDomain = "muxy.io"; } signature = ""; @@ -288,9 +288,8 @@ void IntegrationTestFixture::Reconnect() ForceDisconnect(); char buffer[256]; - int output = snprintf(buffer, 256, "%sgamelink.%s/%d.%d.%d/%s", - targetPrefix.c_str(), - targetDomain.c_str(), + int output = snprintf(buffer, 256, "%s/%d.%d.%d/%s", + websocketUrl.c_str(), MUXY_GAMELINK_VERSION_MAJOR, MUXY_GAMELINK_VERSION_MINOR, MUXY_GAMELINK_VERSION_PATCH, client.c_str()); @@ -342,9 +341,8 @@ int IntegrationTestFixture::Request(const char* method, const char* endpoint, co } char buffer[256]; - int writtenBytes = snprintf(buffer, 256, "https://%sapi.%s/v1/e/%s", - targetPrefix.c_str(), - targetDomain.c_str(), + int writtenBytes = snprintf(buffer, 256, "%s/v1/e/%s", + apiUrl.c_str(), endpoint); REQUIRE(writtenBytes > 0); diff --git a/test/integration_utils.h b/test/integration_utils.h index f470fa8..2537a93 100644 --- a/test/integration_utils.h +++ b/test/integration_utils.h @@ -40,9 +40,8 @@ class IntegrationTestFixture void LoadEnvironment(); std::string target; - - std::string targetDomain; - std::string targetPrefix; + std::string apiUrl; + std::string websocketUrl; std::string authenticationHeader; std::unique_ptr connection; diff --git a/websocket_network/websocket.cpp b/websocket_network/websocket.cpp index 1cee9ea..6dba5c7 100644 --- a/websocket_network/websocket.cpp +++ b/websocket_network/websocket.cpp @@ -45,7 +45,7 @@ size_t writeResponse(char* ptr, size_t size, size_t nmemb, void* data) char* start = impl->messageFragment.data() + oldSize; memcpy(start, ptr, size * nmemb); - + curl_ws_frame* frame = curl_ws_meta(impl->connection); if (frame->bytesleft > 0) { @@ -94,6 +94,11 @@ WebsocketConnection::WebsocketConnection(const std::string& url, uint16_t port) CURL* curl = curl_easy_init(); std::string protocolURL = "wss://" + url; + if (url.rfind("localhost", 0) == 0) + { + protocolURL = "ws://" + url; + } + curl_easy_setopt(curl, CURLOPT_URL, protocolURL.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeResponse); curl_easy_setopt(curl, CURLOPT_WRITEDATA, impl); @@ -185,11 +190,11 @@ int WebsocketConnection::run() size_t sent = 0; CURLcode result = curl_ws_send( - impl->connection, - msg->data.data(), - msg->data.size(), - &sent, - 0, + impl->connection, + msg->data.data(), + msg->data.size(), + &sent, + 0, flags ); @@ -218,7 +223,7 @@ void WebsocketConnection::send(const char* bytes, uint32_t length) std::unique_ptr msg = std::unique_ptr(new Message()); msg->binary = false; msg->data = std::vector(bytes, bytes + length); - + std::lock_guard lock(impl->lock); impl->messages.push_back(std::move(msg)); } From 365ba58ad81a7a480c0c71dd2b0664c3f959903d Mon Sep 17 00:00:00 2001 From: albertywang Date: Sat, 30 Nov 2024 11:04:03 -0600 Subject: [PATCH 3/4] Gateway support for matches --- src/gateway_matches.cpp | 187 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/gateway_matches.cpp diff --git a/src/gateway_matches.cpp b/src/gateway_matches.cpp new file mode 100644 index 0000000..971b4f2 --- /dev/null +++ b/src/gateway_matches.cpp @@ -0,0 +1,187 @@ +#include "gateway.h" + +namespace gateway +{ + void SDK::CreateMatch(const string& str) + { + Base.CreateMatch(str); + } + + void SDK::KeepMatchAlive(const string& str) + { + Base.KeepMatchAlive(str); + } + + void SDK::AddChannelsToMatch(const string& id, const string* start, const string* end) + { + std::vector channels(start, end); + Base.AddChannelsToMatch(id, channels); + } + + void SDK::RemoveChannelsFromMatch(const string& id, const string* start, const string* end) + { + std::vector channels(start, end); + Base.RemoveChannelsFromMatch(id, channels); + } + + void SDK::RunMatchPoll(const string& match, const MatchPollConfiguration& cfg) + { + RunMatchPollWithID(match, string("default"), cfg); + } + + void SDK::StopMatchPoll(const string& match) + { + StopMatchPollWithID(match, string("default")); + } + + void SDK::StopMatchPollWithID(const string& match, const string& id) + { + Base.StopMatchPoll(match, id); + } + + void SDK::RunMatchPollWithID(const string& match, const string& id, const MatchPollConfiguration& cfg) + { + gamelink::PollConfiguration config; + + config.userIdVoting = true; + if (cfg.Mode == PollMode::Chaos) + { + config.totalVotesPerUser = 1024; + config.distinctOptionsPerUser = 258; + config.votesPerOption = 1024; + } + else if (cfg.Mode == PollMode::Order) + { + config.totalVotesPerUser = 1; + config.distinctOptionsPerUser = 1; + config.votesPerOption = 1; + } + + if (cfg.Duration > 0) + { + config.endsIn = cfg.Duration; + } + + config.userData = cfg.UserData; + + Base.RunMatchPoll( + match, + id, + cfg.Prompt, + config, + cfg.Options, + [=](const gamelink::schema::MatchPollUpdate& response) + { + MatchPollUpdate matchUpdate; + + std::vector overall; + overall.resize(32); + + for (auto it = response.data.results.begin(); it != response.data.results.end(); ++it) + { + const gamelink::schema::PollUpdateBody& upd = it->second; + PollUpdate update; + + uint32_t idx = gamelink::GetPollWinnerIndex(upd.results); + update.Winner = static_cast(idx); + update.WinningVoteCount = upd.results[idx]; + update.Results = upd.results; + update.Mean = upd.mean; + update.Count = upd.count; + update.IsFinal = false; + + for (size_t i = 0; i < upd.results.size(); ++i) + { + if (i < overall.size()) + { + overall[i] += upd.results[i]; + } + } + + matchUpdate.perChannel.insert(std::make_pair(it->first, std::move(update))); + } + + uint32_t idx = gamelink::GetPollWinnerIndex(overall); + matchUpdate.overall.Winner = idx; + matchUpdate.overall.WinningVoteCount = overall[idx]; + matchUpdate.overall.Results = std::move(overall); + + double accumulator = 0; + uint32_t count = 0; + for (size_t i = 0; i < matchUpdate.overall.Results.size(); ++i) + { + count += matchUpdate.overall.Results[i]; + accumulator += matchUpdate.overall.Results[i] * i; + } + + matchUpdate.overall.Mean = accumulator / static_cast(count); + matchUpdate.overall.Count = count; + matchUpdate.overall.IsFinal = false; + + if (cfg.OnUpdate) + { + cfg.OnUpdate(matchUpdate); + } + }, + [=](const gamelink::schema::MatchPollUpdate& response) + { + MatchPollUpdate matchFinish; + + std::vector overall; + overall.resize(32); + + for (auto it = response.data.results.begin(); it != response.data.results.end(); ++it) + { + const gamelink::schema::PollUpdateBody& upd = it->second; + PollUpdate update; + + uint32_t idx = gamelink::GetPollWinnerIndex(upd.results); + update.Winner = static_cast(idx); + update.WinningVoteCount = upd.results[idx]; + update.Results = upd.results; + update.Mean = upd.mean; + update.Count = upd.count; + update.IsFinal = false; + + for (size_t i = 0; i < upd.results.size(); ++i) + { + if (i < overall.size()) + { + overall[i] += upd.results[i]; + } + } + + matchFinish.perChannel.insert(std::make_pair(it->first, std::move(update))); + } + + uint32_t idx = gamelink::GetPollWinnerIndex(overall); + matchFinish.overall.Winner = idx; + matchFinish.overall.WinningVoteCount = overall[idx]; + matchFinish.overall.Results = std::move(overall); + + double accumulator = 0; + uint32_t count = 0; + for (size_t i = 0; i < matchFinish.overall.Results.size(); ++i) + { + count += matchFinish.overall.Results[i]; + accumulator += matchFinish.overall.Results[i] * i; + } + + matchFinish.overall.Mean = accumulator / static_cast(count); + matchFinish.overall.Count = count; + matchFinish.overall.IsFinal = true; + + + if (cfg.OnUpdate) + { + cfg.OnUpdate(matchFinish); + } + + if (cfg.OnComplete) + { + cfg.OnComplete(matchFinish); + } + } + ); + } +} \ No newline at end of file From 72b46ad246872d5291d50c83aa2bc17950b3262b Mon Sep 17 00:00:00 2001 From: albertywang Date: Sat, 30 Nov 2024 11:12:06 -0600 Subject: [PATCH 4/4] Update gamelink_single.hpp --- amalgamate.in | 3 +- gamelink_single.hpp | 740 +++++++++++++++++++++++++++++++++++++++- include/config.h | 4 + include/config_footer.h | 13 +- include/gateway.h | 42 +++ 5 files changed, 795 insertions(+), 7 deletions(-) diff --git a/amalgamate.in b/amalgamate.in index b9b5926..60897a4 100755 --- a/amalgamate.in +++ b/amalgamate.in @@ -7,7 +7,7 @@ INPUT_HEADERS=( ./schema/serialization.h ./schema/envelope.h ./schema/subscription.h - ./schema/consts.h + ./schema/consts.h ./schema/authentication.h ./schema/broadcast.h @@ -18,6 +18,7 @@ INPUT_HEADERS=( ./schema/state.h ./schema/game_config.h ./schema/drops.h + ./schema/matches.h ./schema/matchmaking.h ./schema/game_metadata.h diff --git a/gamelink_single.hpp b/gamelink_single.hpp index ebf7f79..6daef70 100644 --- a/gamelink_single.hpp +++ b/gamelink_single.hpp @@ -10,6 +10,10 @@ #define MUXY_GAMELINK_VERSION_MINOR 2 #define MUXY_GAMELINK_VERSION_PATCH 0 +#include +#include +#include + /* Do this: #define MUXY_GAMELINK_SINGLE_IMPL @@ -26162,7 +26166,59 @@ namespace nlohmann } }; } + +template<> +struct std::hash +{ + inline std::size_t operator()(const gamelink::string& str) const + { + // FNV-1a hash + uint64_t hash = 14695981039346656037ull; + const char* data = str.c_str(); + for (size_t i = 0; i < str.size(); ++i) + { + hash ^= static_cast(data[i]); + hash *= 1099511628211; + } + + return static_cast(hash); + } +}; #endif + +namespace nlohmann +{ + template + struct adl_serializer> + { + static inline void to_json(json& js, const std::unordered_map& s) + { + for (auto it = s.begin(); it != s.end(); ++it) + { + js[json::string_t(it->first.c_str())] = it->second; + } + } + + static inline void from_json(const json& j, std::unordered_map& s) + { + if (!j.is_object()) + { + return; + } + + s.clear(); + for (auto it = j.begin(); it != j.end(); ++it) + { + gamelink::string key(it.key().c_str()); + + T value; + it.value().get_to(value); + + s.insert(std::make_pair(key, std::move(value))); + } + } + }; +} #endif #ifndef MUXY_GAMELINK_SCHEMA_SERIALIZATION_H @@ -27358,7 +27414,13 @@ namespace gamelink /// Number of responses, including ones that outside the [0, 32) range. int32_t count; - MUXY_GAMELINK_SERIALIZE_INTRUSIVE_5(PollUpdateBody, "poll", poll, "results", results, "mean", mean, "sum", sum, "count", count); + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_5(PollUpdateBody, + "poll", poll, + "results", results, + "mean", mean, + "sum", sum, + "count", count + ); }; template @@ -27940,6 +28002,268 @@ namespace gamelink } #endif +#ifndef MUXY_GAMELINK_SCHEMA_MATCHES_H +#define MUXY_GAMELINK_SCHEMA_MATCHES_H + +namespace gamelink +{ + namespace schema + { + struct CreateMatchRequestBody + { + gamelink::string matchId; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_1(CreateMatchRequestBody, + "id", matchId + ); + }; + + struct MUXY_GAMELINK_API CreateMatchRequest : SendEnvelope + { + explicit inline CreateMatchRequest(const string& id) + { + this->action = string("create"); + this->params.target = string("match"); + + this->data.matchId = id; + } + }; + + struct KeepMatchAliveBody + { + gamelink::string matchId; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_1(KeepMatchAliveBody, + "id", matchId + ); + }; + + struct MUXY_GAMELINK_API KeepMatchAliveRequest : SendEnvelope + { + explicit inline KeepMatchAliveRequest(const string& id) + { + this->action = string("keepalive"); + this->params.target = string("match"); + + this->data.matchId = id; + } + }; + + struct AddOrRemoveMatchChannelsRequestBody + { + gamelink::string matchId; + std::vector channels; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_2(AddOrRemoveMatchChannelsRequestBody, + "id", matchId, + "channel_ids", channels + ); + }; + + struct MUXY_GAMELINK_API AddMatchChannelsRequest : SendEnvelope + { + explicit inline AddMatchChannelsRequest(const string& id, const std::vector& channels) + { + this->action = string("add_channels"); + this->params.target = string("match"); + + this->data.matchId = id; + this->data.channels = channels; + } + }; + + struct MUXY_GAMELINK_API RemoveMatchChannelsRequest : SendEnvelope + { + explicit inline RemoveMatchChannelsRequest(const string& id, const std::vector& channels) + { + this->action = string("remove_channels"); + this->params.target = string("match"); + + this->data.matchId = id; + this->data.channels = channels; + } + }; + + struct CreateMatchPollRequestBody + { + gamelink::string matchId; + CreatePollWithConfigurationRequestBody poll; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_2(CreateMatchPollRequestBody, + "id", matchId, + "poll", poll + ); + }; + + struct MUXY_GAMELINK_API CreateMatchPollRequest : SendEnvelope + { + explicit inline CreateMatchPollRequest(const string& match, const CreatePollWithConfigurationRequestBody& poll) + { + this->action = string("create"); + this->params.target = string("match_poll"); + + this->data.matchId = match; + this->data.poll = poll; + } + }; + + struct DeleteMatchPollRequestBody + { + gamelink::string matchId; + gamelink::string pollId; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_2(DeleteMatchPollRequestBody, + "id", matchId, + "poll_id", pollId + ); + }; + + struct MUXY_GAMELINK_API DeleteMatchPollRequest : SendEnvelope + { + explicit inline DeleteMatchPollRequest(const string& match, const string& poll) + { + this->action = string("delete"); + this->params.target = string("match_poll"); + + this->data.matchId = match; + this->data.pollId = poll; + } + }; + + template + struct ReconfigureMatchPollRequestBody + { + gamelink::string matchId; + gamelink::string pollId; + Config config; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_3(ReconfigureMatchPollRequestBody, + "id", matchId, + "poll_id", pollId, + "config", config + ); + }; + + struct MUXY_GAMELINK_API ExpireMatchPollRequest : SendEnvelope> + { + explicit inline ExpireMatchPollRequest(const string& id, const string& pollId) + { + this->action = string("reconfigure"); + this->params.target = string("match_poll"); + + this->data.matchId = id; + this->data.pollId = pollId; + this->data.config.endsAt = -1; + } + }; + + struct MUXY_GAMELINK_API SetMatchPollDisableRequest : SendEnvelope> + { + explicit inline SetMatchPollDisableRequest(const string& id, const string& pollId, bool status) + { + this->action = string("reconfigure"); + this->params.target = string("match_poll"); + + this->data.matchId = id; + this->data.pollId = pollId; + this->data.config.disabled = status; + } + }; + + struct MUXY_GAMELINK_API SubscribeMatchPollRequest : SendEnvelope + { + explicit inline SubscribeMatchPollRequest(const string& matchId) + { + this->action = string("subscribe"); + this->params.target = string("match_poll"); + this->data.topic_id = string(matchId); + } + }; + + struct MUXY_GAMELINK_API UnsubscribeMatchPollRequest : SendEnvelope + { + explicit inline UnsubscribeMatchPollRequest(const string& matchId) + { + this->action = string("unsubscribe"); + this->params.target = string("match_poll"); + this->data.topic_id = string(matchId); + } + }; + + struct MatchPollUpdateInformation + { + string matchId; + string pollId; + string status; + + std::unordered_map results; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_4(MatchPollUpdateInformation, + "match_id", matchId, + "poll_id", pollId, + "status", status, + "results", results + ); + }; + + struct MUXY_GAMELINK_API MatchPollUpdate : ReceiveEnvelope + {}; + + struct MatchPollUpdateInformationInternal + { + string matchId; + string pollId; + string status; + + nlohmann::json results; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_4(MatchPollUpdateInformationInternal, + "match_id", matchId, + "poll_id", pollId, + "status", status, + "results", results + ); + }; + + struct MUXY_GAMELINK_API MatchPollUpdateInternal : ReceiveEnvelope + {}; + + template + struct BroadcastMatchRequestBody + { + string matchId; + string topic; + + T data; + + MUXY_GAMELINK_SERIALIZE_INTRUSIVE_3(BroadcastMatchRequestBody, + "match_id", matchId, + "topic", topic, + "data", data + ); + }; + + template + struct BroadcastMatchRequest : SendEnvelope> + { + BroadcastMatchRequest(const string& matchId, const string& topic, const T& data) + { + this->action = string("broadcast"); + this->params.target = string("match"); + + this->data.matchId = matchId; + this->data.topic = topic; + this->data.data = data; + } + }; + + struct MUXY_GAMELINK_API BroadcastMatchResponse : ReceiveEnvelope + {}; + } +} + +#endif + #ifndef MUXY_GAMELINK_SCHEMA_MATCHMAKING_H #define MUXY_GAMELINK_SCHEMA_MATCHMAKING_H @@ -29675,6 +29999,63 @@ namespace gamelink Event& OnMatchmakingQueueInvite(); #pragma endregion +#pragma region Matches + // Creates a match, which is a collection of users that can have some operations performed on them + // as an entire collection, instead of channel by channel. + // Match manipulation functions are only supported by a server Gamelink connection. Calling these + // from a client will result in an authorization faiure. + + // Creates a match with the given ID + RequestId CreateMatch(const string& id); + + // Keeps a match alive + RequestId KeepMatchAlive(const string& id); + + // Adds channels to a match + RequestId AddChannelsToMatch(const string& id, const std::vector& channels); + + // Removes channels from a match. + RequestId RemoveChannelsFromMatch(const string& id, const std::vector& channels); + + // Runs a poll in a match. Very similar to RunPoll, except for the callback type, as it receives + // information for all matches. + + RequestId RunMatchPoll( + const string& matchId, + const string& pollId, + const string& prompt, + const PollConfiguration& config, + const string* optionsBegin, + const string* optionsEnd, + std::function onUpdateCallback, + std::function onFinishCallback + ); + + RequestId RunMatchPoll( + const string& matchId, + const string& pollId, + const string& prompt, + const PollConfiguration& config, + const std::vector& opts, + std::function onUpdateCallback, + std::function onFinishCallback + ); + + // Stops a poll in a match + RequestId StopMatchPoll(const string& matchId, const string& pollId); + + /// Sends a broadcast to all viewers on all channels in the match + template + RequestId SendMatchBroadcast(const string& matchId, const string& topic, const T& value) + { + schema::BroadcastMatchRequest payload(matchId, topic, value); + return queuePayload(payload); + } + + /// Sends a broadcast to all viewers on all channels in the match + RequestId SendMatchBroadcast(const string& matchId, const string& topic, const nlohmann::json& message); +#pragma endregion + /// Sends a request to set your games metadata. You're expected to fill out a gamelink::GameMetadata struct with your games metadata /// and provide it. /// @return RequestId of the generated request @@ -29742,6 +30123,8 @@ namespace gamelink Event _onGetDrops; Event _onMatchmakingUpdate; + + Event _onMatchPollUpdate; }; // Implementation of Event::Remove is here because of completeness requirements of SDK. @@ -29862,6 +30245,12 @@ namespace gateway bool IsFinal; }; + struct MatchPollUpdate + { + PollUpdate overall; + std::unordered_map perChannel; + }; + struct PollConfiguration { string Prompt; @@ -29885,6 +30274,29 @@ namespace gateway std::function OnComplete; }; + struct MatchPollConfiguration + { + string Prompt; + std::vector Options; + + PollMode Mode = PollMode::Order; + PollLocation Location = PollLocation::Default; + + // Duration of the poll, in seconds. + // If set to a negative or zero duration, the poll lasts until a call + // to StopPoll + int32_t Duration = 0; + + // Arbitrary user data to send. Should be small. + nlohmann::json UserData; + + // Called regularly as poll results are streamed in from the server + std::function OnUpdate; + + // Called after the poll completes. This is called right after + std::function OnComplete; + }; + enum class ActionCategory { Neutral = 0, @@ -30037,14 +30449,14 @@ namespace gateway template void UpdateGameStatePathWithArray(const string& path, const T* begin, const T* end) { - Base.UpdateStateWithArray(gamelink::StateTarget::Channel, path, begin, end); + Base.UpdateStateWithArray(gamelink::StateTarget::Channel, gamelink::Operation::Add, path, begin, end); } // Low level set game access. Sets an serializable object to the json path. template void UpdateGameStatePathWithObject(const string& path, const T& obj) { - Base.UpdateStateWithObject(gamelink::StateTarget::Channel, path, obj); + Base.UpdateStateWithObject(gamelink::StateTarget::Channel, gamelink::Operation::Add, path, obj); } RequestID SetGameMetadata(GameMetadata Meta); @@ -30060,6 +30472,18 @@ namespace gateway void AcceptAction(const gateway::ActionUsed& used, const gamelink::string& Details); void RefundAction(const gateway::ActionUsed& used, const gamelink::string& Details); + + // Matches + void CreateMatch(const string& match); + void KeepMatchAlive(const string& match); + void AddChannelsToMatch(const string& match, const string* start, const string* end); + void RemoveChannelsFromMatch(const string& match, const string* start, const string* end); + + void RunMatchPoll(const string& match, const MatchPollConfiguration& cfg); + void StopMatchPoll(const string& match); + + void RunMatchPollWithID(const string& match, const string& id, const MatchPollConfiguration& cfg); + void StopMatchPollWithID(const string& match, const string& id); private: gamelink::SDK Base; @@ -30749,6 +31173,7 @@ namespace gamelink , _onGetOutstandingTransactions(this, "OnGetOutstandingTransactions", 11) , _onGetDrops(this, "OnGetDrops", 12) , _onMatchmakingUpdate(this, "OnMatchmakingUpdate", 13) + , _onMatchPollUpdate(this, "OnMatchPollUpdate", 14) {} SDK::~SDK() @@ -31084,6 +31509,15 @@ namespace gamelink _onMatchmakingUpdate.Invoke(resp); } } + else if (env.meta.target == "match_poll") + { + schema::MatchPollUpdate resp; + success = schema::ParseResponse(bytes, length, resp); + if (success) + { + _onMatchPollUpdate.Invoke(resp); + } + } } else { @@ -31673,6 +32107,121 @@ namespace gamelink } } +namespace gamelink +{ + RequestId SDK::CreateMatch(const string& id) + { + schema::CreateMatchRequest req(id); + return queuePayload(req); + } + + RequestId SDK::KeepMatchAlive(const string& id) + { + schema::KeepMatchAliveRequest req(id); + return queuePayload(req); + } + + RequestId SDK::AddChannelsToMatch(const string& id, const std::vector& channels) + { + schema::AddMatchChannelsRequest req(id, channels); + return queuePayload(req); + } + + RequestId SDK::RemoveChannelsFromMatch(const string& id, const std::vector& channels) + { + schema::RemoveMatchChannelsRequest req(id, channels); + return queuePayload(req); + } + + RequestId SDK::StopMatchPoll(const string& id, const string& pollId) + { + schema::ExpireMatchPollRequest req(id, pollId); + return queuePayload(req); + } + + RequestId SDK::SendMatchBroadcast(const string& matchId, const string& topic, const nlohmann::json& message) + { + schema::BroadcastMatchRequest payload(matchId, topic, message); + return queuePayload(payload); + } + + RequestId SDK::RunMatchPoll( + const string& matchId, + const string& pollId, + const string& prompt, + const PollConfiguration& config, + const std::vector& opts, + std::function onUpdateCallback, + std::function onFinishCallback) + { + schema::DeleteMatchPollRequest delRequest(matchId, pollId); + RequestId del = queuePayload(delRequest); + WaitForResponse(del); + + schema::SubscribeMatchPollRequest subRequest(matchId); + WaitForResponse(queuePayload(subRequest)); + + if (!VerifyPollLimits(prompt, opts)) + { + return gamelink::REJECTED_REQUEST_ID; + } + + schema::CreatePollWithConfigurationRequest createPollRequest(pollId, prompt, config, opts); + schema::CreateMatchPollRequest createMatchPollRequest(matchId, createPollRequest.data); + + RequestId create = queuePayload(createMatchPollRequest); + + char buffer[128]; + snprintf(buffer, 128, "_%s", pollId.c_str()); + + gamelink::string callbackName = gamelink::string(buffer); + + bool hasCalledOnFinish = false; + _onMatchPollUpdate.AddUnique(callbackName, [=](const schema::MatchPollUpdate& update) mutable + { + bool matches = update.data.matchId == matchId && update.data.pollId == pollId; + if (!matches) + { + return; + } + + if (update.data.status == gamelink::string("expired")) + { + if (!hasCalledOnFinish) + { + onFinishCallback(update); + + schema::UnsubscribeMatchPollRequest unsubRequest(matchId); + this->queuePayload(unsubRequest); + this->_onMatchPollUpdate.RemoveByName(callbackName); + hasCalledOnFinish = true; + } + } + else + { + onUpdateCallback(update); + } + }); + + return ANY_REQUEST_ID; + } + + RequestId SDK::RunMatchPoll( + const string& matchId, + const string& pollId, + const string& prompt, + const PollConfiguration& config, + const string* optionsBegin, + const string* optionsEnd, + std::function onUpdateCallback, + std::function onFinishCallback + ) + { + std::vector opts(optionsBegin, optionsEnd); + return RunMatchPoll(matchId, pollId, prompt, config, opts, std::move(onUpdateCallback), std::move(onFinishCallback)); + } +} + namespace gamelink { RequestId SDK::SubscribeToMatchmakingQueueInvite() @@ -32976,6 +33525,191 @@ namespace gateway } } +namespace gateway +{ + void SDK::CreateMatch(const string& str) + { + Base.CreateMatch(str); + } + + void SDK::KeepMatchAlive(const string& str) + { + Base.KeepMatchAlive(str); + } + + void SDK::AddChannelsToMatch(const string& id, const string* start, const string* end) + { + std::vector channels(start, end); + Base.AddChannelsToMatch(id, channels); + } + + void SDK::RemoveChannelsFromMatch(const string& id, const string* start, const string* end) + { + std::vector channels(start, end); + Base.RemoveChannelsFromMatch(id, channels); + } + + void SDK::RunMatchPoll(const string& match, const MatchPollConfiguration& cfg) + { + RunMatchPollWithID(match, string("default"), cfg); + } + + void SDK::StopMatchPoll(const string& match) + { + StopMatchPollWithID(match, string("default")); + } + + void SDK::StopMatchPollWithID(const string& match, const string& id) + { + Base.StopMatchPoll(match, id); + } + + void SDK::RunMatchPollWithID(const string& match, const string& id, const MatchPollConfiguration& cfg) + { + gamelink::PollConfiguration config; + + config.userIdVoting = true; + if (cfg.Mode == PollMode::Chaos) + { + config.totalVotesPerUser = 1024; + config.distinctOptionsPerUser = 258; + config.votesPerOption = 1024; + } + else if (cfg.Mode == PollMode::Order) + { + config.totalVotesPerUser = 1; + config.distinctOptionsPerUser = 1; + config.votesPerOption = 1; + } + + if (cfg.Duration > 0) + { + config.endsIn = cfg.Duration; + } + + config.userData = cfg.UserData; + + Base.RunMatchPoll( + match, + id, + cfg.Prompt, + config, + cfg.Options, + [=](const gamelink::schema::MatchPollUpdate& response) + { + MatchPollUpdate matchUpdate; + + std::vector overall; + overall.resize(32); + + for (auto it = response.data.results.begin(); it != response.data.results.end(); ++it) + { + const gamelink::schema::PollUpdateBody& upd = it->second; + PollUpdate update; + + uint32_t idx = gamelink::GetPollWinnerIndex(upd.results); + update.Winner = static_cast(idx); + update.WinningVoteCount = upd.results[idx]; + update.Results = upd.results; + update.Mean = upd.mean; + update.Count = upd.count; + update.IsFinal = false; + + for (size_t i = 0; i < upd.results.size(); ++i) + { + if (i < overall.size()) + { + overall[i] += upd.results[i]; + } + } + + matchUpdate.perChannel.insert(std::make_pair(it->first, std::move(update))); + } + + uint32_t idx = gamelink::GetPollWinnerIndex(overall); + matchUpdate.overall.Winner = idx; + matchUpdate.overall.WinningVoteCount = overall[idx]; + matchUpdate.overall.Results = std::move(overall); + + double accumulator = 0; + uint32_t count = 0; + for (size_t i = 0; i < matchUpdate.overall.Results.size(); ++i) + { + count += matchUpdate.overall.Results[i]; + accumulator += matchUpdate.overall.Results[i] * i; + } + + matchUpdate.overall.Mean = accumulator / static_cast(count); + matchUpdate.overall.Count = count; + matchUpdate.overall.IsFinal = false; + + if (cfg.OnUpdate) + { + cfg.OnUpdate(matchUpdate); + } + }, + [=](const gamelink::schema::MatchPollUpdate& response) + { + MatchPollUpdate matchFinish; + + std::vector overall; + overall.resize(32); + + for (auto it = response.data.results.begin(); it != response.data.results.end(); ++it) + { + const gamelink::schema::PollUpdateBody& upd = it->second; + PollUpdate update; + + uint32_t idx = gamelink::GetPollWinnerIndex(upd.results); + update.Winner = static_cast(idx); + update.WinningVoteCount = upd.results[idx]; + update.Results = upd.results; + update.Mean = upd.mean; + update.Count = upd.count; + update.IsFinal = false; + + for (size_t i = 0; i < upd.results.size(); ++i) + { + if (i < overall.size()) + { + overall[i] += upd.results[i]; + } + } + + matchFinish.perChannel.insert(std::make_pair(it->first, std::move(update))); + } + + uint32_t idx = gamelink::GetPollWinnerIndex(overall); + matchFinish.overall.Winner = idx; + matchFinish.overall.WinningVoteCount = overall[idx]; + matchFinish.overall.Results = std::move(overall); + + double accumulator = 0; + uint32_t count = 0; + for (size_t i = 0; i < matchFinish.overall.Results.size(); ++i) + { + count += matchFinish.overall.Results[i]; + accumulator += matchFinish.overall.Results[i] * i; + } + + matchFinish.overall.Mean = accumulator / static_cast(count); + matchFinish.overall.Count = count; + matchFinish.overall.IsFinal = true; + + if (cfg.OnUpdate) + { + cfg.OnUpdate(matchFinish); + } + + if (cfg.OnComplete) + { + cfg.OnComplete(matchFinish); + } + } + ); + } +} + namespace gateway { void SDK::OnBitsUsed(std::function Callback) diff --git a/include/config.h b/include/config.h index 7981e91..24020bf 100644 --- a/include/config.h +++ b/include/config.h @@ -4,6 +4,10 @@ #define MUXY_GAMELINK_VERSION_MINOR 2 #define MUXY_GAMELINK_VERSION_PATCH 0 +#include +#include +#include + /* Do this: #define MUXY_GAMELINK_SINGLE_IMPL diff --git a/include/config_footer.h b/include/config_footer.h index 34acc3d..3425aca 100644 --- a/include/config_footer.h +++ b/include/config_footer.h @@ -22,9 +22,16 @@ struct std::hash { inline std::size_t operator()(const gamelink::string& str) const { - return std::hash< - std::string_view - >()(std::string_view(str.c_str(), str.size())); + // FNV-1a hash + uint64_t hash = 14695981039346656037ull; + const char* data = str.c_str(); + for (size_t i = 0; i < str.size(); ++i) + { + hash ^= static_cast(data[i]); + hash *= 1099511628211; + } + + return static_cast(hash); } }; #endif diff --git a/include/gateway.h b/include/gateway.h index 47d2d04..b64b289 100644 --- a/include/gateway.h +++ b/include/gateway.h @@ -93,6 +93,12 @@ namespace gateway bool IsFinal; }; + struct MatchPollUpdate + { + PollUpdate overall; + std::unordered_map perChannel; + }; + struct PollConfiguration { string Prompt; @@ -116,6 +122,30 @@ namespace gateway std::function OnComplete; }; + struct MatchPollConfiguration + { + string Prompt; + std::vector Options; + + PollMode Mode = PollMode::Order; + PollLocation Location = PollLocation::Default; + + // Duration of the poll, in seconds. + // If set to a negative or zero duration, the poll lasts until a call + // to StopPoll + int32_t Duration = 0; + + // Arbitrary user data to send. Should be small. + nlohmann::json UserData; + + // Called regularly as poll results are streamed in from the server + std::function OnUpdate; + + // Called after the poll completes. This is called right after + std::function OnComplete; + }; + + enum class ActionCategory { Neutral = 0, @@ -291,6 +321,18 @@ namespace gateway void AcceptAction(const gateway::ActionUsed& used, const gamelink::string& Details); void RefundAction(const gateway::ActionUsed& used, const gamelink::string& Details); + + // Matches + void CreateMatch(const string& match); + void KeepMatchAlive(const string& match); + void AddChannelsToMatch(const string& match, const string* start, const string* end); + void RemoveChannelsFromMatch(const string& match, const string* start, const string* end); + + void RunMatchPoll(const string& match, const MatchPollConfiguration& cfg); + void StopMatchPoll(const string& match); + + void RunMatchPollWithID(const string& match, const string& id, const MatchPollConfiguration& cfg); + void StopMatchPollWithID(const string& match, const string& id); private: gamelink::SDK Base;