Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions relaytestapp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,18 @@ list(APPEND src_files
src/globals.h
src/app.cpp
src/app.h
src/coverage.cpp
src/coverage.h
src/game.cpp
src/game.h
src/globalChat.cpp
src/globalChat.h
src/leaderboardPanel.cpp
src/leaderboardPanel.h
src/lobby.cpp
src/lobby.h
src/matchSummary.cpp
src/matchSummary.h
src/login.cpp
src/login.h
src/loading.cpp
Expand Down
1 change: 1 addition & 0 deletions relaytestapp/src/BCCallback.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
// })
// );
//-----------------------------------------------------------------------------
#pragma once

// Thirdparty includes
#include <braincloud/IServerCallback.h>
Expand Down
856 changes: 794 additions & 62 deletions relaytestapp/src/app.cpp

Large diffs are not rendered by default.

37 changes: 35 additions & 2 deletions relaytestapp/src/app.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// Desc: Interface for main application logic
// Author: David St-Louis
//-----------------------------------------------------------------------------
#pragma once

// brainCloud
#include <braincloud/BrainCloudRelay.h>
Expand Down Expand Up @@ -46,6 +47,17 @@ void app_reconnect();
// Find lobby
void app_play(BrainCloud::eRelayConnectionType protocol);

// Enables RTT so main-menu chat works (idempotent — safe to call any time the
// MainMenu screen is reached; no-ops if RTT is already connected).
void app_enableChatRTT();

// Sends a chat message to everyone in the current lobby, via Lobby service signals.
void app_sendLobbySignal(const std::string &text);

// Fetches this player's own worldwide rank (coverage leaderboard) and shares it via
// the lobby's extra field. Safe to call repeatedly.
void app_fetchWorldwideRank();

// Cancel lobby search or leave lobby. Go back to main menu without logging out.
void app_cancelLobby();

Expand All @@ -58,6 +70,20 @@ void app_endMatch();
// Ready up and signals RTT service we can start the game
void app_startGame();

// Non-host lobby members have no "Start" button (only the host can start the round),
// but still need a way to signal they're ready before the host starts — toggles this
// player's own ready state. Not used during the rematch flow (see app_setRematchReady).
void app_toggleReady();

// Marks this player queued for a rematch and takes them back to the Lobby screen —
// used by both the Match Summary screen's button and its own auto-timeout.
void app_setRematchReady(bool ready);

// Host-only: starts the next round once everyone has queued for a rematch, or the
// 15s auto-rematch timer elapses. Called once per frame from both lobby_update() and
// matchSummary_update() (whichever screen the host is currently on).
void app_tickRematchGate();

// User changes his player color
void app_changeUserColor(int colorIndex);

Expand All @@ -67,5 +93,12 @@ void app_mouseMoved(const Point& pos);
// User clicked mouse in the play area
void app_shockwave(const Point& pos);

// Host clears all splotches on every client
void app_clearSplotches();
// Drives coverage/ranking recompute + the host-authoritative match-end + leaderboard-post
// flow. Called once per frame from game_update() while on the Game screen.
void app_tickMatch();

// Non-host: polls for the host's PostMatchResults results (a GlobalEntity, not a relay
// broadcast — see the function definition in app.cpp) once a round's matchResult is valid.
// Safe/cheap to call every frame; self-throttles. Called once per frame from
// matchSummary_update() while on the Match Summary screen.
void app_tickMatchResultsPoll();
145 changes: 145 additions & 0 deletions relaytestapp/src/coverage.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//-----------------------------------------------------------------------------
// Copyright 2026 bitHeads inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//-----------------------------------------------------------------------------
// File: coverage.cpp
// Desc: Canvas coverage % + win-ranking calculation (BCLOUD-14472 / BCLOUD-14490).
//-----------------------------------------------------------------------------

#include "coverage.h"

#include <algorithm>
#include <cmath>
#include <map>

std::vector<CoverageEntry> computeCoverage(const std::vector<Splotch> &splotches,
const std::vector<User> &members)
{
std::vector<CoverageEntry> result;
result.reserve(members.size());

std::map<std::string, int> indexByCxId;
for (const auto &m : members)
{
indexByCxId[m.cxId] = (int)result.size();
CoverageEntry e;
e.cxId = m.cxId;
e.colorIndex = m.colorIndex;
result.push_back(e);
}

const int N = (int)splotches.size();
if (N > 0)
{
// Ownership grid: each cell records which player (by result[] index) most
// recently painted over it. Splotches are stamped in paint order (a filled circle
// of radius SPLOTCH_RADIUS), so a later splotch always overwrites an earlier one
// wherever they overlap — exactly matching what's rendered on screen (paint order
// = draw order = "last one wins"). -1 = unpainted, or painted by something that
// couldn't be attributed to any current member (still overwrites the grid, so it
// correctly obscures whoever was there before, it just credits no one).
const float CELL = COVERAGE_GRID_CELL_SIZE;
const int gridW = (int)(CANVAS_W / CELL);
const int gridH = (int)(CANVAS_H / CELL);

static std::vector<int> owner; // reused across calls (this runs every ~250ms mid-match)
owner.assign((size_t)gridW * gridH, -1);

const float R2 = SPLOTCH_RADIUS * SPLOTCH_RADIUS;
const int cellRadius = (int)std::ceil(SPLOTCH_RADIUS / CELL);

for (int i = 0; i < N; ++i)
{
const Splotch &s = splotches[i];

int idx = -1;
if (!s.ownerCxId.empty())
{
auto it = indexByCxId.find(s.ownerCxId);
if (it != indexByCxId.end())
idx = it->second;
}
if (idx < 0)
{
// Unattributed (legacy sender / departed player) — fall back to a
// colorIndex match against a live member, matching pre-attribution behavior.
for (size_t k = 0; k < result.size(); ++k)
{
if (result[k].colorIndex == s.colorIndex)
{
idx = (int)k;
break;
}
}
}

int cx = (int)(s.pos.x / CELL);
int cy = (int)(s.pos.y / CELL);
for (int dy = -cellRadius; dy <= cellRadius; ++dy)
{
int gy = cy + dy;
if (gy < 0 || gy >= gridH) continue;
float py = (gy + 0.5f) * CELL;
float ddy = py - (float)s.pos.y;
for (int dx = -cellRadius; dx <= cellRadius; ++dx)
{
int gx = cx + dx;
if (gx < 0 || gx >= gridW) continue;
float px = (gx + 0.5f) * CELL;
float ddx = px - (float)s.pos.x;
if (ddx * ddx + ddy * ddy <= R2)
owner[gx + gy * gridW] = idx; // may be -1 — still overwrites, credits no one
}
}
}

for (int c = 0, count = gridW * gridH; c < count; ++c)
{
int idx = owner[c];
if (idx >= 0)
result[idx].visibleCount++;
}
}

const float cellArea = COVERAGE_GRID_CELL_SIZE * COVERAGE_GRID_CELL_SIZE;
const float canvasArea = CANVAS_W * CANVAS_H;
for (auto &e : result)
e.coveragePct = std::min(100.0f, e.visibleCount * cellArea / canvasArea * 100.0f);

std::sort(result.begin(), result.end(), [](const CoverageEntry &a, const CoverageEntry &b) {
if (a.coveragePct != b.coveragePct) return a.coveragePct > b.coveragePct;
if (a.visibleCount != b.visibleCount) return a.visibleCount > b.visibleCount;
return a.cxId < b.cxId;
});

for (size_t i = 0; i < result.size(); ++i)
{
if (i > 0 &&
result[i].coveragePct == result[i - 1].coveragePct &&
result[i].visibleCount == result[i - 1].visibleCount)
result[i].rank = result[i - 1].rank; // tie shares rank
else
result[i].rank = (int)i + 1;
}

for (size_t i = 0; i < result.size(); ++i)
{
int beaten = 0;
for (size_t j = 0; j < result.size(); ++j)
if (result[j].rank > result[i].rank) ++beaten;
result[i].beaten = beaten;
}

return result;
}
48 changes: 48 additions & 0 deletions relaytestapp/src/coverage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//-----------------------------------------------------------------------------
// Copyright 2026 bitHeads inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//-----------------------------------------------------------------------------
// File: coverage.h
// Desc: Canvas coverage % + win-ranking calculation (BCLOUD-14472 / BCLOUD-14490).
// Pure logic — no ImGui, no brainCloud, no globals — so this is the exact
// artifact other RelayTestApp ports (dotnet/godot/java/react) should copy.
//-----------------------------------------------------------------------------
#pragma once

#include "globals.h"

// Computes each member's canvas coverage and ranks them.
//
// Algorithm: rasterize a COVERAGE_GRID_CELL_SIZE-resolution ownership grid over the canvas
// by stamping every splotch, in paint order, as a filled circle of radius SPLOTCH_RADIUS
// centered on it — so a later splotch always overwrites an earlier one wherever they
// overlap. This is exactly what ends up rendered on screen (paint order = draw order =
// "last one wins"), not an approximation of it — it replaced an earlier center-point/
// obscure-radius heuristic that could diverge sharply from the actual visible area once
// splotches were dense/overlapping (e.g. a long stress-test match).
//
// coveragePct is each player's owned-cell-count * cell-area / canvas-area (clamped to 100)
// — ABSOLUTE canvas-area coverage, NOT a share of painted area, so a solo match doesn't
// trivially score 100%. Splotches whose ownerCxId doesn't match any current member (a
// not-yet-ported legacy sender, or a departed player) fall back to a colorIndex match
// against a live member; if that also fails, the splotch still overwrites the grid
// (correctly obscuring whatever was under it) but credits no one.
//
// Every current member gets a seeded zero-coverage entry, so painters-of-nothing still
// appear on the board, ranked last. Result is sorted best-first (coveragePct desc,
// visibleCount desc, cxId asc for determinism); ties share a rank and don't "beat" each
// other in CoverageEntry::beaten. (visibleCount is now an owned-cell count, not a splotch
// count — the field wasn't renamed since nothing outside this file inspects it directly.)
std::vector<CoverageEntry> computeCoverage(const std::vector<Splotch> &splotches,
const std::vector<User> &members);
Loading