mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 02:23:56 +00:00
Add player name bubbles above remote players' heads in multiplayer (#7)
Renders a billboard text bubble showing each remote player's display name. Includes a WASM export to toggle visibility from the frontend. - Bitmap font renderer generates paletted textures for name labels - Billboard quad faces the camera each frame via orientation matrix - Bubble visibility managed globally by NetworkManager toggle - Fix miniwin D3DRMIMAGE constructor code style (static_cast, const)
This commit is contained in:
parent
ed4e248be4
commit
c943d2455d
@ -534,6 +534,7 @@ if (ISLE_EXTENSIONS)
|
|||||||
extensions/src/multiplayer.cpp
|
extensions/src/multiplayer.cpp
|
||||||
extensions/src/multiplayer/animutils.cpp
|
extensions/src/multiplayer/animutils.cpp
|
||||||
extensions/src/multiplayer/charactercloner.cpp
|
extensions/src/multiplayer/charactercloner.cpp
|
||||||
|
extensions/src/multiplayer/namebubblerenderer.cpp
|
||||||
extensions/src/multiplayer/networkmanager.cpp
|
extensions/src/multiplayer/networkmanager.cpp
|
||||||
extensions/src/multiplayer/protocol.cpp
|
extensions/src/multiplayer/protocol.cpp
|
||||||
extensions/src/multiplayer/remoteplayer.cpp
|
extensions/src/multiplayer/remoteplayer.cpp
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace Tgl
|
||||||
|
{
|
||||||
|
class Group;
|
||||||
|
class MeshBuilder;
|
||||||
|
class Mesh;
|
||||||
|
class Texture;
|
||||||
|
} // namespace Tgl
|
||||||
|
|
||||||
|
class LegoROI;
|
||||||
|
|
||||||
|
namespace Multiplayer
|
||||||
|
{
|
||||||
|
|
||||||
|
class NameBubbleRenderer {
|
||||||
|
public:
|
||||||
|
NameBubbleRenderer();
|
||||||
|
~NameBubbleRenderer();
|
||||||
|
|
||||||
|
// Create the 3D billboard with the given name text.
|
||||||
|
// Must be called after the player's ROI is spawned.
|
||||||
|
void Create(const char* p_name);
|
||||||
|
|
||||||
|
// Remove from scene and release all resources.
|
||||||
|
void Destroy();
|
||||||
|
|
||||||
|
// Update billboard position (above p_roi) and orientation (face camera).
|
||||||
|
void Update(LegoROI* p_roi);
|
||||||
|
|
||||||
|
// Show or hide the billboard.
|
||||||
|
void SetVisible(bool p_visible);
|
||||||
|
|
||||||
|
bool IsCreated() const { return m_group != nullptr; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void GenerateTexture(const char* p_name);
|
||||||
|
void CreateQuadMesh();
|
||||||
|
|
||||||
|
Tgl::Group* m_group;
|
||||||
|
Tgl::MeshBuilder* m_meshBuilder;
|
||||||
|
Tgl::Mesh* m_mesh;
|
||||||
|
Tgl::Texture* m_texture;
|
||||||
|
uint8_t* m_texelData;
|
||||||
|
bool m_visible;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Multiplayer
|
||||||
@ -55,6 +55,9 @@ class NetworkManager : public MxCore {
|
|||||||
void RequestSetWalkAnimation(uint8_t p_index) { m_pendingWalkAnim.store(p_index, std::memory_order_relaxed); }
|
void RequestSetWalkAnimation(uint8_t p_index) { m_pendingWalkAnim.store(p_index, std::memory_order_relaxed); }
|
||||||
void RequestSetIdleAnimation(uint8_t p_index) { m_pendingIdleAnim.store(p_index, std::memory_order_relaxed); }
|
void RequestSetIdleAnimation(uint8_t p_index) { m_pendingIdleAnim.store(p_index, std::memory_order_relaxed); }
|
||||||
void RequestSendEmote(uint8_t p_emoteId) { m_pendingEmote.store(p_emoteId, std::memory_order_relaxed); }
|
void RequestSendEmote(uint8_t p_emoteId) { m_pendingEmote.store(p_emoteId, std::memory_order_relaxed); }
|
||||||
|
void RequestToggleNameBubbles() { m_pendingToggleNameBubbles.store(true, std::memory_order_relaxed); }
|
||||||
|
|
||||||
|
bool GetShowNameBubbles() const { return m_showNameBubbles; }
|
||||||
|
|
||||||
void OnWorldEnabled(LegoWorld* p_world);
|
void OnWorldEnabled(LegoWorld* p_world);
|
||||||
void OnWorldDisabled(LegoWorld* p_world);
|
void OnWorldDisabled(LegoWorld* p_world);
|
||||||
@ -108,10 +111,13 @@ class NetworkManager : public MxCore {
|
|||||||
bool m_registered;
|
bool m_registered;
|
||||||
|
|
||||||
std::atomic<bool> m_pendingToggleThirdPerson;
|
std::atomic<bool> m_pendingToggleThirdPerson;
|
||||||
|
std::atomic<bool> m_pendingToggleNameBubbles;
|
||||||
std::atomic<int> m_pendingWalkAnim;
|
std::atomic<int> m_pendingWalkAnim;
|
||||||
std::atomic<int> m_pendingIdleAnim;
|
std::atomic<int> m_pendingIdleAnim;
|
||||||
std::atomic<int> m_pendingEmote;
|
std::atomic<int> m_pendingEmote;
|
||||||
|
|
||||||
|
bool m_showNameBubbles;
|
||||||
|
|
||||||
static const uint32_t BROADCAST_INTERVAL_MS = 66; // ~15Hz
|
static const uint32_t BROADCAST_INTERVAL_MS = 66; // ~15Hz
|
||||||
static const uint32_t TIMEOUT_MS = 5000; // 5 second timeout
|
static const uint32_t TIMEOUT_MS = 5000; // 5 second timeout
|
||||||
static const int EXIT_ROOM_FULL = 10;
|
static const int EXIT_ROOM_FULL = 10;
|
||||||
|
|||||||
@ -81,6 +81,7 @@ struct PlayerStateMsg {
|
|||||||
float speed;
|
float speed;
|
||||||
uint8_t walkAnimId; // Index into walk animation table (0 = default)
|
uint8_t walkAnimId; // Index into walk animation table (0 = default)
|
||||||
uint8_t idleAnimId; // Index into idle animation table (0 = default)
|
uint8_t idleAnimId; // Index into idle animation table (0 = default)
|
||||||
|
char name[8]; // Player display name (7 chars + null terminator)
|
||||||
uint8_t displayActorIndex; // Index into g_actorInfoInit (0-65)
|
uint8_t displayActorIndex; // Index into g_actorInfoInit (0-65)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,8 @@ class LegoTreeNode;
|
|||||||
namespace Multiplayer
|
namespace Multiplayer
|
||||||
{
|
{
|
||||||
|
|
||||||
|
class NameBubbleRenderer;
|
||||||
|
|
||||||
class RemotePlayer {
|
class RemotePlayer {
|
||||||
public:
|
public:
|
||||||
RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex);
|
RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex);
|
||||||
@ -35,9 +37,11 @@ class RemotePlayer {
|
|||||||
bool IsVisible() const { return m_visible; }
|
bool IsVisible() const { return m_visible; }
|
||||||
int8_t GetWorldId() const { return m_targetWorldId; }
|
int8_t GetWorldId() const { return m_targetWorldId; }
|
||||||
uint32_t GetLastUpdateTime() const { return m_lastUpdateTime; }
|
uint32_t GetLastUpdateTime() const { return m_lastUpdateTime; }
|
||||||
|
|
||||||
void SetVisible(bool p_visible);
|
void SetVisible(bool p_visible);
|
||||||
void TriggerEmote(uint8_t p_emoteId);
|
void TriggerEmote(uint8_t p_emoteId);
|
||||||
|
void SetNameBubbleVisible(bool p_visible);
|
||||||
|
void CreateNameBubble();
|
||||||
|
void DestroyNameBubble();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
using AnimCache = AnimUtils::AnimCache;
|
using AnimCache = AnimUtils::AnimCache;
|
||||||
@ -54,6 +58,7 @@ class RemotePlayer {
|
|||||||
uint8_t m_actorId;
|
uint8_t m_actorId;
|
||||||
uint8_t m_displayActorIndex;
|
uint8_t m_displayActorIndex;
|
||||||
char m_uniqueName[32];
|
char m_uniqueName[32];
|
||||||
|
char m_displayName[8];
|
||||||
|
|
||||||
LegoROI* m_roi;
|
LegoROI* m_roi;
|
||||||
bool m_spawned;
|
bool m_spawned;
|
||||||
@ -99,6 +104,8 @@ class RemotePlayer {
|
|||||||
|
|
||||||
LegoROI* m_vehicleROI;
|
LegoROI* m_vehicleROI;
|
||||||
int8_t m_currentVehicleType;
|
int8_t m_currentVehicleType;
|
||||||
|
|
||||||
|
NameBubbleRenderer* m_nameBubble;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Multiplayer
|
} // namespace Multiplayer
|
||||||
|
|||||||
335
extensions/src/multiplayer/namebubblerenderer.cpp
Normal file
335
extensions/src/multiplayer/namebubblerenderer.cpp
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
#include "extensions/multiplayer/namebubblerenderer.h"
|
||||||
|
|
||||||
|
#include "3dmanager/lego3dmanager.h"
|
||||||
|
#include "legovideomanager.h"
|
||||||
|
#include "misc.h"
|
||||||
|
#include "roi/legoroi.h"
|
||||||
|
#include "tgl/tgl.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_stdinc.h>
|
||||||
|
#include <vec.h>
|
||||||
|
|
||||||
|
using namespace Multiplayer;
|
||||||
|
|
||||||
|
// 5x5 bitmap font for A-Z (each row is a byte with bits 4..0 representing columns)
|
||||||
|
// clang-format off
|
||||||
|
static const uint8_t g_letterFont[26][5] = {
|
||||||
|
{0x0E, 0x11, 0x1F, 0x11, 0x11}, // A
|
||||||
|
{0x1E, 0x11, 0x1E, 0x11, 0x1E}, // B
|
||||||
|
{0x0F, 0x10, 0x10, 0x10, 0x0F}, // C
|
||||||
|
{0x1E, 0x11, 0x11, 0x11, 0x1E}, // D
|
||||||
|
{0x1F, 0x10, 0x1E, 0x10, 0x1F}, // E
|
||||||
|
{0x1F, 0x10, 0x1E, 0x10, 0x10}, // F
|
||||||
|
{0x0F, 0x10, 0x13, 0x11, 0x0F}, // G
|
||||||
|
{0x11, 0x11, 0x1F, 0x11, 0x11}, // H
|
||||||
|
{0x0E, 0x04, 0x04, 0x04, 0x0E}, // I
|
||||||
|
{0x01, 0x01, 0x01, 0x11, 0x0E}, // J
|
||||||
|
{0x11, 0x12, 0x1C, 0x12, 0x11}, // K
|
||||||
|
{0x10, 0x10, 0x10, 0x10, 0x1F}, // L
|
||||||
|
{0x11, 0x1B, 0x15, 0x11, 0x11}, // M
|
||||||
|
{0x11, 0x19, 0x15, 0x13, 0x11}, // N
|
||||||
|
{0x0E, 0x11, 0x11, 0x11, 0x0E}, // O
|
||||||
|
{0x1E, 0x11, 0x1E, 0x10, 0x10}, // P
|
||||||
|
{0x0E, 0x11, 0x15, 0x12, 0x0D}, // Q
|
||||||
|
{0x1E, 0x11, 0x1E, 0x12, 0x11}, // R
|
||||||
|
{0x0F, 0x10, 0x0E, 0x01, 0x1E}, // S
|
||||||
|
{0x1F, 0x04, 0x04, 0x04, 0x04}, // T
|
||||||
|
{0x11, 0x11, 0x11, 0x11, 0x0E}, // U
|
||||||
|
{0x11, 0x11, 0x11, 0x0A, 0x04}, // V
|
||||||
|
{0x11, 0x11, 0x15, 0x1B, 0x11}, // W
|
||||||
|
{0x11, 0x0A, 0x04, 0x0A, 0x11}, // X
|
||||||
|
{0x11, 0x0A, 0x04, 0x04, 0x04}, // Y
|
||||||
|
{0x1F, 0x02, 0x04, 0x08, 0x1F}, // Z
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
// Texture dimensions (must be power of 2)
|
||||||
|
static const int TEX_WIDTH = 64;
|
||||||
|
static const int TEX_HEIGHT = 16;
|
||||||
|
|
||||||
|
// Palette indices
|
||||||
|
static const uint8_t PAL_TRANSPARENT = 0;
|
||||||
|
static const uint8_t PAL_BLACK = 1;
|
||||||
|
static const uint8_t PAL_WHITE = 2;
|
||||||
|
|
||||||
|
// Billboard world-space size
|
||||||
|
static const float BUBBLE_WIDTH = 1.2f;
|
||||||
|
static const float BUBBLE_HEIGHT = 0.3f;
|
||||||
|
|
||||||
|
// Vertical offset above ROI bounding sphere
|
||||||
|
static const float BUBBLE_Y_OFFSET = 0.15f;
|
||||||
|
|
||||||
|
NameBubbleRenderer::NameBubbleRenderer()
|
||||||
|
: m_group(nullptr), m_meshBuilder(nullptr), m_mesh(nullptr), m_texture(nullptr), m_texelData(nullptr),
|
||||||
|
m_visible(true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
NameBubbleRenderer::~NameBubbleRenderer()
|
||||||
|
{
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NameBubbleRenderer::GenerateTexture(const char* p_name)
|
||||||
|
{
|
||||||
|
m_texelData = new uint8_t[TEX_WIDTH * TEX_HEIGHT];
|
||||||
|
SDL_memset(m_texelData, PAL_TRANSPARENT, TEX_WIDTH * TEX_HEIGHT);
|
||||||
|
|
||||||
|
int nameLen = (int) SDL_strlen(p_name);
|
||||||
|
if (nameLen <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each letter is 5px wide + 1px spacing; 3px horizontal and 2px vertical padding
|
||||||
|
int bubbleW = nameLen * 6 - 1 + 6;
|
||||||
|
int bubbleH = 9;
|
||||||
|
int bx = SDL_max((TEX_WIDTH - bubbleW) / 2, 0);
|
||||||
|
int by = SDL_max((TEX_HEIGHT - bubbleH) / 2, 0);
|
||||||
|
|
||||||
|
// Draw white bubble background with rounded corners
|
||||||
|
for (int y = by; y < by + bubbleH && y < TEX_HEIGHT; y++) {
|
||||||
|
for (int x = bx; x < bx + bubbleW && x < TEX_WIDTH; x++) {
|
||||||
|
int lx = x - bx;
|
||||||
|
int ly = y - by;
|
||||||
|
if ((lx == 0 || lx == bubbleW - 1) && (ly == 0 || ly == bubbleH - 1)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_texelData[y * TEX_WIDTH + x] = PAL_WHITE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw black border (top/bottom edges, then left/right edges)
|
||||||
|
for (int x = bx + 1; x < bx + bubbleW - 1 && x < TEX_WIDTH; x++) {
|
||||||
|
m_texelData[by * TEX_WIDTH + x] = PAL_BLACK;
|
||||||
|
m_texelData[(by + bubbleH - 1) * TEX_WIDTH + x] = PAL_BLACK;
|
||||||
|
}
|
||||||
|
for (int y = by + 1; y < by + bubbleH - 1 && y < TEX_HEIGHT; y++) {
|
||||||
|
m_texelData[y * TEX_WIDTH + bx] = PAL_BLACK;
|
||||||
|
m_texelData[y * TEX_WIDTH + bx + bubbleW - 1] = PAL_BLACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw text (black on white bubble)
|
||||||
|
int textX = bx + 3;
|
||||||
|
int textY = by + 2;
|
||||||
|
|
||||||
|
for (int i = 0; i < nameLen; i++) {
|
||||||
|
char ch = SDL_toupper(p_name[i]);
|
||||||
|
if (ch < 'A' || ch > 'Z') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int row = 0; row < 5; row++) {
|
||||||
|
uint8_t bits = g_letterFont[ch - 'A'][row];
|
||||||
|
for (int col = 0; col < 5; col++) {
|
||||||
|
if (bits & (1 << (4 - col))) {
|
||||||
|
int px = textX + i * 6 + col;
|
||||||
|
int py = textY + row;
|
||||||
|
if (px < TEX_WIDTH && py < TEX_HEIGHT) {
|
||||||
|
m_texelData[py * TEX_WIDTH + px] = PAL_BLACK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NameBubbleRenderer::CreateQuadMesh()
|
||||||
|
{
|
||||||
|
Tgl::Renderer* renderer = VideoManager()->GetRenderer();
|
||||||
|
if (!renderer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_meshBuilder = renderer->CreateMeshBuilder();
|
||||||
|
if (!m_meshBuilder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float halfW = BUBBLE_WIDTH * 0.5f;
|
||||||
|
float halfH = BUBBLE_HEIGHT * 0.5f;
|
||||||
|
|
||||||
|
// Vertex order chosen so that triangles (0,1,2) and (0,2,3) have CW winding
|
||||||
|
// when viewed from +Z, matching the renderer's glFrontFace(GL_CW) setting.
|
||||||
|
float positions[4][3] = {
|
||||||
|
{-halfW, -halfH, 0.0f}, // 0: bottom-left
|
||||||
|
{-halfW, halfH, 0.0f}, // 1: top-left
|
||||||
|
{ halfW, halfH, 0.0f}, // 2: top-right
|
||||||
|
{ halfW, -halfH, 0.0f} // 3: bottom-right
|
||||||
|
};
|
||||||
|
|
||||||
|
float normals[4][3] = {
|
||||||
|
{0.0f, 0.0f, 1.0f},
|
||||||
|
{0.0f, 0.0f, 1.0f},
|
||||||
|
{0.0f, 0.0f, 1.0f},
|
||||||
|
{0.0f, 0.0f, 1.0f}
|
||||||
|
};
|
||||||
|
|
||||||
|
float texCoords[4][2] = {
|
||||||
|
{0.0f, 1.0f}, // 0: bottom-left of texture
|
||||||
|
{0.0f, 0.0f}, // 1: top-left
|
||||||
|
{1.0f, 0.0f}, // 2: top-right
|
||||||
|
{1.0f, 1.0f} // 3: bottom-right
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tgl::CreateMesh expects packed face indices where each uint32 encodes:
|
||||||
|
// low 16 bits = position vertex index
|
||||||
|
// high 16 bits = normal vertex index | 0x8000 (bit 15 = "packed vertex" flag)
|
||||||
|
// Without the 0x8000 flag, the entry is a simple reference to an already-created
|
||||||
|
// vertex (no new vertex is allocated). Each packed entry creates a new vertex,
|
||||||
|
// so shared vertices (0 and 2, used in both triangles) must use simple refs in
|
||||||
|
// the second triangle to stay within the p_numVertices allocation.
|
||||||
|
unsigned int faceIndices[2][3] = {
|
||||||
|
{0x80000000, 0x80010001, 0x80020002}, // create vertices 0, 1, 2
|
||||||
|
{0x00000000, 0x00000002, 0x80030003} // reuse 0, reuse 2, create vertex 3
|
||||||
|
};
|
||||||
|
|
||||||
|
unsigned int texIndices[2][3] = {
|
||||||
|
{0, 1, 2},
|
||||||
|
{0, 0, 3} // only index 5 (value 3) is read; indices 3-4 are simple refs
|
||||||
|
};
|
||||||
|
|
||||||
|
m_mesh = m_meshBuilder->CreateMesh(2, 4, positions, normals, texCoords, faceIndices, texIndices, Tgl::Flat);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NameBubbleRenderer::Create(const char* p_name)
|
||||||
|
{
|
||||||
|
if (m_group || !p_name || p_name[0] == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tgl::Renderer* renderer = VideoManager()->GetRenderer();
|
||||||
|
if (!renderer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the name texture
|
||||||
|
GenerateTexture(p_name);
|
||||||
|
|
||||||
|
// Create Tgl texture from pixel data
|
||||||
|
Tgl::PaletteEntry palette[3];
|
||||||
|
palette[PAL_TRANSPARENT] = {255, 255, 255};
|
||||||
|
palette[PAL_BLACK] = {0, 0, 0};
|
||||||
|
palette[PAL_WHITE] = {255, 255, 255};
|
||||||
|
|
||||||
|
m_texture = renderer->CreateTexture(TEX_WIDTH, TEX_HEIGHT, 8, m_texelData, TRUE, 3, palette);
|
||||||
|
if (!m_texture) {
|
||||||
|
Destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the quad mesh
|
||||||
|
CreateQuadMesh();
|
||||||
|
if (!m_mesh) {
|
||||||
|
Destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply texture to mesh
|
||||||
|
m_mesh->SetTexture(m_texture);
|
||||||
|
m_mesh->SetShadingModel(Tgl::Flat);
|
||||||
|
// Set alpha < 1.0 so the renderer treats this as transparent (deferred draw
|
||||||
|
// with blending enabled). The actual per-pixel alpha comes from the texture.
|
||||||
|
m_mesh->SetColor(1.0f, 1.0f, 1.0f, 254.0f / 255.0f);
|
||||||
|
|
||||||
|
// Create a group (D3DRM frame) to hold the billboard
|
||||||
|
Tgl::Group* scene = VideoManager()->Get3DManager()->GetScene();
|
||||||
|
m_group = renderer->CreateGroup(scene);
|
||||||
|
if (!m_group) {
|
||||||
|
Destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_group->Add(m_meshBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NameBubbleRenderer::Destroy()
|
||||||
|
{
|
||||||
|
if (m_group) {
|
||||||
|
if (m_visible) {
|
||||||
|
Tgl::Group* scene = VideoManager()->Get3DManager()->GetScene();
|
||||||
|
if (scene) {
|
||||||
|
scene->Remove(m_group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete m_group;
|
||||||
|
m_group = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_meshBuilder) {
|
||||||
|
delete m_meshBuilder;
|
||||||
|
m_meshBuilder = nullptr;
|
||||||
|
m_mesh = nullptr; // owned by meshBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_texture) {
|
||||||
|
delete m_texture;
|
||||||
|
m_texture = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_texelData) {
|
||||||
|
delete[] m_texelData;
|
||||||
|
m_texelData = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NameBubbleRenderer::SetVisible(bool p_visible)
|
||||||
|
{
|
||||||
|
if (m_visible == p_visible || !m_group) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_visible = p_visible;
|
||||||
|
|
||||||
|
Tgl::Group* scene = VideoManager()->Get3DManager()->GetScene();
|
||||||
|
if (!scene) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p_visible) {
|
||||||
|
scene->Add(m_group);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
scene->Remove(m_group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NameBubbleRenderer::Update(LegoROI* p_roi)
|
||||||
|
{
|
||||||
|
if (!m_group || !p_roi || !m_visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LegoROI* viewROI = VideoManager()->GetViewROI();
|
||||||
|
if (!viewROI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Billboard normal = camera's backward-z direction (faces toward camera)
|
||||||
|
const float* normal = viewROI->GetWorldDirection();
|
||||||
|
const float* camUp = viewROI->GetWorldUp();
|
||||||
|
|
||||||
|
// Build billboard basis vectors
|
||||||
|
float right[3], up[3];
|
||||||
|
VXV3(right, camUp, normal);
|
||||||
|
float rLen = SDL_sqrtf(NORMSQRD3(right));
|
||||||
|
if (rLen > 0.0001f) {
|
||||||
|
VDS3(right, right, rLen);
|
||||||
|
}
|
||||||
|
VXV3(up, normal, right);
|
||||||
|
|
||||||
|
// Position above the player's bounding sphere
|
||||||
|
const BoundingSphere& sphere = p_roi->GetWorldBoundingSphere();
|
||||||
|
float pos[3];
|
||||||
|
SET3(pos, sphere.Center());
|
||||||
|
pos[1] += sphere.Radius() + BUBBLE_Y_OFFSET;
|
||||||
|
|
||||||
|
// Build transformation: rows are right, up, normal, position
|
||||||
|
Tgl::FloatMatrix4 mat = {};
|
||||||
|
SET3(mat[0], right);
|
||||||
|
SET3(mat[1], up);
|
||||||
|
SET3(mat[2], normal);
|
||||||
|
SET3(mat[3], pos);
|
||||||
|
mat[3][3] = 1.0f;
|
||||||
|
|
||||||
|
m_group->SetTransformation(mat);
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
#include "extensions/multiplayer/networkmanager.h"
|
#include "extensions/multiplayer/networkmanager.h"
|
||||||
|
|
||||||
|
#include "legogamestate.h"
|
||||||
#include "legomain.h"
|
#include "legomain.h"
|
||||||
#include "legopathactor.h"
|
#include "legopathactor.h"
|
||||||
#include "legoworld.h"
|
#include "legoworld.h"
|
||||||
@ -32,8 +33,8 @@ NetworkManager::NetworkManager()
|
|||||||
: m_transport(nullptr), m_callbacks(nullptr), m_localPeerId(0), m_hostPeerId(0), m_sequence(0),
|
: m_transport(nullptr), m_callbacks(nullptr), m_localPeerId(0), m_hostPeerId(0), m_sequence(0),
|
||||||
m_lastBroadcastTime(0), m_lastValidActorId(0), m_localWalkAnimId(0), m_localIdleAnimId(0),
|
m_lastBroadcastTime(0), m_lastValidActorId(0), m_localWalkAnimId(0), m_localIdleAnimId(0),
|
||||||
m_localDisplayActorIndex(DISPLAY_ACTOR_NONE), m_inIsleWorld(false),
|
m_localDisplayActorIndex(DISPLAY_ACTOR_NONE), m_inIsleWorld(false),
|
||||||
m_registered(false), m_pendingToggleThirdPerson(false), m_pendingWalkAnim(-1), m_pendingIdleAnim(-1),
|
m_registered(false), m_pendingToggleThirdPerson(false), m_pendingToggleNameBubbles(false),
|
||||||
m_pendingEmote(-1)
|
m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), m_pendingEmote(-1), m_showNameBubbles(true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +150,7 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world)
|
|||||||
|
|
||||||
if (player->GetWorldId() == (int8_t) LegoOmni::e_act1) {
|
if (player->GetWorldId() == (int8_t) LegoOmni::e_act1) {
|
||||||
player->SetVisible(true);
|
player->SetVisible(true);
|
||||||
|
player->SetNameBubbleVisible(m_showNameBubbles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,6 +172,7 @@ void NetworkManager::OnWorldDisabled(LegoWorld* p_world)
|
|||||||
m_worldSync.SetInIsleWorld(false);
|
m_worldSync.SetInIsleWorld(false);
|
||||||
for (auto& [peerId, player] : m_remotePlayers) {
|
for (auto& [peerId, player] : m_remotePlayers) {
|
||||||
player->SetVisible(false);
|
player->SetVisible(false);
|
||||||
|
player->SetNameBubbleVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotifyPlayerCountChanged();
|
NotifyPlayerCountChanged();
|
||||||
@ -206,6 +209,13 @@ void NetworkManager::ProcessPendingRequests()
|
|||||||
if (emote >= 0) {
|
if (emote >= 0) {
|
||||||
SendEmote(static_cast<uint8_t>(emote));
|
SendEmote(static_cast<uint8_t>(emote));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_pendingToggleNameBubbles.exchange(false, std::memory_order_relaxed)) {
|
||||||
|
m_showNameBubbles = !m_showNameBubbles;
|
||||||
|
for (auto& [peerId, player] : m_remotePlayers) {
|
||||||
|
player->SetNameBubbleVisible(m_showNameBubbles);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkManager::BroadcastLocalState()
|
void NetworkManager::BroadcastLocalState()
|
||||||
@ -266,6 +276,26 @@ void NetworkManager::BroadcastLocalState()
|
|||||||
msg.walkAnimId = m_localWalkAnimId;
|
msg.walkAnimId = m_localWalkAnimId;
|
||||||
msg.idleAnimId = m_localIdleAnimId;
|
msg.idleAnimId = m_localIdleAnimId;
|
||||||
|
|
||||||
|
// Convert Username letters (0-25 = A-Z) to ASCII string.
|
||||||
|
// The active player is always at m_players[0] after RegisterPlayer/SwitchPlayer.
|
||||||
|
SDL_memset(msg.name, 0, sizeof(msg.name));
|
||||||
|
LegoGameState* gs = GameState();
|
||||||
|
if (gs && gs->m_playerCount > 0) {
|
||||||
|
const LegoGameState::Username& username = gs->m_players[0];
|
||||||
|
for (int i = 0; i < 7; i++) {
|
||||||
|
MxS16 letter = username.m_letters[i];
|
||||||
|
if (letter < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (letter <= 25) {
|
||||||
|
msg.name[i] = (char) ('A' + letter);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
msg.name[i] = '?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t displayIndex = m_localDisplayActorIndex;
|
uint8_t displayIndex = m_localDisplayActorIndex;
|
||||||
if (displayIndex == DISPLAY_ACTOR_NONE) {
|
if (displayIndex == DISPLAY_ACTOR_NONE) {
|
||||||
displayIndex = actorId - 1; // actorId already validated above
|
displayIndex = actorId - 1; // actorId already validated above
|
||||||
@ -436,6 +466,7 @@ void NetworkManager::HandleState(const PlayerStateMsg& p_msg)
|
|||||||
bool bothInIsle = m_inIsleWorld && (p_msg.worldId == (int8_t) LegoOmni::e_act1);
|
bool bothInIsle = m_inIsleWorld && (p_msg.worldId == (int8_t) LegoOmni::e_act1);
|
||||||
if (it->second->IsSpawned()) {
|
if (it->second->IsSpawned()) {
|
||||||
it->second->SetVisible(bothInIsle);
|
it->second->SetVisible(bothInIsle);
|
||||||
|
it->second->SetNameBubbleVisible(bothInIsle && m_showNameBubbles);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool wasInIsle = (oldWorldId == (int8_t) LegoOmni::e_act1);
|
bool wasInIsle = (oldWorldId == (int8_t) LegoOmni::e_act1);
|
||||||
|
|||||||
@ -42,6 +42,14 @@ extern "C"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE void mp_toggle_name_bubbles()
|
||||||
|
{
|
||||||
|
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
|
||||||
|
if (mgr) {
|
||||||
|
mgr->RequestToggleNameBubbles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
#include "extensions/multiplayer/remoteplayer.h"
|
#include "extensions/multiplayer/remoteplayer.h"
|
||||||
|
|
||||||
|
#include "extensions/multiplayer/namebubblerenderer.h"
|
||||||
|
|
||||||
#include "3dmanager/lego3dmanager.h"
|
#include "3dmanager/lego3dmanager.h"
|
||||||
#include "anim/legoanim.h"
|
#include "anim/legoanim.h"
|
||||||
#include "extensions/multiplayer/charactercloner.h"
|
#include "extensions/multiplayer/charactercloner.h"
|
||||||
@ -14,6 +16,7 @@
|
|||||||
#include "realtime/realtime.h"
|
#include "realtime/realtime.h"
|
||||||
#include "roi/legoroi.h"
|
#include "roi/legoroi.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
#include <SDL3/SDL_stdinc.h>
|
#include <SDL3/SDL_stdinc.h>
|
||||||
#include <SDL3/SDL_timer.h>
|
#include <SDL3/SDL_timer.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@ -28,8 +31,9 @@ RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displ
|
|||||||
m_walkAnimCache(nullptr), m_idleAnimCache(nullptr), m_animTime(0.0f), m_idleTime(0.0f), m_idleAnimTime(0.0f),
|
m_walkAnimCache(nullptr), m_idleAnimCache(nullptr), m_animTime(0.0f), m_idleTime(0.0f), m_idleAnimTime(0.0f),
|
||||||
m_wasMoving(false), m_emoteAnimCache(nullptr), m_emoteTime(0.0f), m_emoteDuration(0.0f), m_emoteActive(false),
|
m_wasMoving(false), m_emoteAnimCache(nullptr), m_emoteTime(0.0f), m_emoteDuration(0.0f), m_emoteActive(false),
|
||||||
m_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_vehicleROI(nullptr),
|
m_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_vehicleROI(nullptr),
|
||||||
m_currentVehicleType(VEHICLE_NONE)
|
m_currentVehicleType(VEHICLE_NONE), m_nameBubble(nullptr)
|
||||||
{
|
{
|
||||||
|
m_displayName[0] = '\0';
|
||||||
const char* displayName = GetDisplayActorName();
|
const char* displayName = GetDisplayActorName();
|
||||||
SDL_snprintf(m_uniqueName, sizeof(m_uniqueName), "%s_mp_%u", displayName, p_peerId);
|
SDL_snprintf(m_uniqueName, sizeof(m_uniqueName), "%s_mp_%u", displayName, p_peerId);
|
||||||
|
|
||||||
@ -82,6 +86,11 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld)
|
|||||||
// Build initial walk and idle animation caches
|
// Build initial walk and idle animation caches
|
||||||
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
|
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
|
||||||
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
|
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
|
||||||
|
|
||||||
|
// Create name bubble if we already have a name
|
||||||
|
if (m_displayName[0] != '\0') {
|
||||||
|
CreateNameBubble();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RemotePlayer::Despawn()
|
void RemotePlayer::Despawn()
|
||||||
@ -90,6 +99,7 @@ void RemotePlayer::Despawn()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DestroyNameBubble();
|
||||||
ExitVehicle();
|
ExitVehicle();
|
||||||
|
|
||||||
if (m_roi) {
|
if (m_roi) {
|
||||||
@ -136,6 +146,27 @@ void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg)
|
|||||||
m_hasReceivedUpdate = true;
|
m_hasReceivedUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update display name (can change when player switches save file)
|
||||||
|
char newName[8];
|
||||||
|
SDL_memcpy(newName, p_msg.name, sizeof(newName));
|
||||||
|
newName[sizeof(newName) - 1] = '\0';
|
||||||
|
if (SDL_strcmp(m_displayName, newName) != 0) {
|
||||||
|
SDL_Log(
|
||||||
|
"RemotePlayer[%u] name changed: '%s' -> '%s' (spawned=%d)",
|
||||||
|
m_peerId,
|
||||||
|
m_displayName,
|
||||||
|
newName,
|
||||||
|
m_spawned
|
||||||
|
);
|
||||||
|
SDL_memcpy(m_displayName, newName, sizeof(m_displayName));
|
||||||
|
|
||||||
|
// Recreate bubble with new name (or create for the first time)
|
||||||
|
if (m_spawned) {
|
||||||
|
DestroyNameBubble();
|
||||||
|
CreateNameBubble();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Swap walk animation if changed
|
// Swap walk animation if changed
|
||||||
if (p_msg.walkAnimId != m_walkAnimId && p_msg.walkAnimId < g_walkAnimCount) {
|
if (p_msg.walkAnimId != m_walkAnimId && p_msg.walkAnimId < g_walkAnimCount) {
|
||||||
m_walkAnimId = p_msg.walkAnimId;
|
m_walkAnimId = p_msg.walkAnimId;
|
||||||
@ -158,6 +189,11 @@ void RemotePlayer::Tick(float p_deltaTime)
|
|||||||
UpdateVehicleState();
|
UpdateVehicleState();
|
||||||
UpdateTransform(p_deltaTime);
|
UpdateTransform(p_deltaTime);
|
||||||
UpdateAnimation(p_deltaTime);
|
UpdateAnimation(p_deltaTime);
|
||||||
|
|
||||||
|
// Update name bubble position and billboard orientation
|
||||||
|
if (m_nameBubble) {
|
||||||
|
m_nameBubble->Update(m_roi);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RemotePlayer::ReAddToScene()
|
void RemotePlayer::ReAddToScene()
|
||||||
@ -487,3 +523,28 @@ void RemotePlayer::ExitVehicle()
|
|||||||
m_animTime = 0.0f;
|
m_animTime = 0.0f;
|
||||||
m_wasMoving = false;
|
m_wasMoving = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RemotePlayer::CreateNameBubble()
|
||||||
|
{
|
||||||
|
if (m_nameBubble || m_displayName[0] == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_nameBubble = new NameBubbleRenderer();
|
||||||
|
m_nameBubble->Create(m_displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemotePlayer::DestroyNameBubble()
|
||||||
|
{
|
||||||
|
if (m_nameBubble) {
|
||||||
|
delete m_nameBubble;
|
||||||
|
m_nameBubble = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemotePlayer::SetNameBubbleVisible(bool p_visible)
|
||||||
|
{
|
||||||
|
if (m_nameBubble) {
|
||||||
|
m_nameBubble->SetVisible(p_visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,9 +1,49 @@
|
|||||||
#include "d3drmtexture_impl.h"
|
#include "d3drmtexture_impl.h"
|
||||||
|
#include "ddsurface_impl.h"
|
||||||
#include "miniwin.h"
|
#include "miniwin.h"
|
||||||
|
|
||||||
Direct3DRMTextureImpl::Direct3DRMTextureImpl(D3DRMIMAGE* image)
|
Direct3DRMTextureImpl::Direct3DRMTextureImpl(D3DRMIMAGE* image) : m_holdsRef(true)
|
||||||
{
|
{
|
||||||
MINIWIN_NOT_IMPLEMENTED();
|
if (!image || !image->buffer1 || image->width == 0 || image->height == 0) {
|
||||||
|
m_surface = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* ddsurface = new DirectDrawSurfaceImpl(image->width, image->height, SDL_PIXELFORMAT_RGBA32);
|
||||||
|
if (!ddsurface->m_surface) {
|
||||||
|
ddsurface->Release();
|
||||||
|
m_surface = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* dst = static_cast<uint8_t*>(ddsurface->m_surface->pixels);
|
||||||
|
int dstPitch = ddsurface->m_surface->pitch;
|
||||||
|
|
||||||
|
if (!image->rgb && image->palette && image->palette_size > 0) {
|
||||||
|
uint8_t* indices = static_cast<uint8_t*>(image->buffer1);
|
||||||
|
for (int y = 0; y < image->height; y++) {
|
||||||
|
for (int x = 0; x < image->width; x++) {
|
||||||
|
uint8_t idx = indices[y * image->bytes_per_line + x];
|
||||||
|
uint8_t* pixel = dst + y * dstPitch + x * 4;
|
||||||
|
if (idx < image->palette_size) {
|
||||||
|
const D3DRMPALETTEENTRY& e = image->palette[idx];
|
||||||
|
pixel[0] = e.red;
|
||||||
|
pixel[1] = e.green;
|
||||||
|
pixel[2] = e.blue;
|
||||||
|
// Palette index 0 is transparent by convention (color key)
|
||||||
|
pixel[3] = (idx == 0) ? 0 : 255;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pixel[0] = pixel[1] = pixel[2] = pixel[3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_memset(dst, 0, dstPitch * image->height);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_surface = static_cast<IDirectDrawSurface*>(ddsurface);
|
||||||
}
|
}
|
||||||
|
|
||||||
Direct3DRMTextureImpl::Direct3DRMTextureImpl(IDirectDrawSurface* surface, bool holdsRef)
|
Direct3DRMTextureImpl::Direct3DRMTextureImpl(IDirectDrawSurface* surface, bool holdsRef)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user