Initial voice HUD indicator setup

This commit is contained in:
Kae 2023-07-17 22:20:39 +10:00
parent 848b11399f
commit 34bb0b5422
16 changed files with 333 additions and 24 deletions

View File

@ -1,4 +1,6 @@
{
"universeScriptContexts" : { "opensb" : ["/scripts/universeClient/opensb.lua"] },
// Disables scissoring and letterboxing on vanilla and modded warp cinematics
"warpCinematicBase" : {
"scissor" : false,

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,29 @@
submodules = {}
require "/scripts/universeClient/opensb/voice_manager.lua"
local submodules, type = submodules, type
local function call(func, ...)
if type(func) == "function" then
return func(...)
end
end
function init(...)
script.setUpdateDelta(1)
for i, module in pairs(submodules) do
call(module.init, ...)
end
end
function update(...)
for i, module in pairs(submodules) do
call(module.update, ...)
end
end
function uninit(...)
for i, module in pairs(submodules) do
call(module.uninit, ...)
end
end

View File

@ -0,0 +1,216 @@
-- Manages the voice HUD indicators and click-to-mute/unmute.
local fmt = string.format
local sqrt = math.sqrt
local module = {}
submodules.voice_manager = module
--constants
local INDICATOR_PATH = "/interface/voicechat/indicator/"
local BACK_INDICATOR_IMAGE = INDICATOR_PATH .. "back.png"
local FRONT_INDICATOR_IMAGE = INDICATOR_PATH .. "front.png"
local FRONT_MUTED_INDICATOR_IMAGE = INDICATOR_PATH .. "front_muted.png"
local INDICATOR_SIZE = {300, 48}
local LINE_PADDING = 12
local LINE_WIDTH = 296
local LINE_WIDTH_PADDED = LINE_WIDTH - LINE_PADDING
local LINE_COLOR = {50, 210, 255, 255}
local FONT_DIRECTIVES = "?border=1;333;3337?border=1;333;3330"
local NAME_PREFIX = "^noshadow,white,set;"
local canvas
local linePaddingDrawable = {
image = BACK_INDICATOR_IMAGE,
position = {0, 0},
color = LINE_COLOR,
centered = false
}
local function getLinePadding(a, b)
linePaddingDrawable.image = BACK_INDICATOR_IMAGE .. fmt("?crop=%i;%i;%i;%i?fade=fff;1", a, 0, b, INDICATOR_SIZE[2])
linePaddingDrawable.position[1] = a
return linePaddingDrawable;
end
local lineDrawable = {
line = {{LINE_PADDING, 24}, {10, 24}},
width = 48,
color = LINE_COLOR
}
local function drawTheLine(pos, value)
local width = math.floor((LINE_WIDTH * value) + 0.5)
LINE_COLOR[4] = 255 * math.min(1, sqrt(width / 350))
if width > 0 then
canvas:drawDrawable(getLinePadding(0, math.min(12, width)), pos)
if width > 12 then
lineDrawable.line[2][1] = math.min(width, LINE_WIDTH_PADDED)
canvas:drawDrawable(lineDrawable, pos)
if width > LINE_WIDTH_PADDED then
canvas:drawDrawable(getLinePadding(LINE_WIDTH_PADDED, width), pos)
end
end
end
end
local drawable = {
image = BACK_INDICATOR_IMAGE,
centered = false
}
local textPositioning = {
position = {0, 0},
horizontalAnchor = "left",
verticalAnchor = "mid"
}
local hoveredSpeaker = nil
local hoveredSpeakerIndex = 1
local hoveredSpeakerPosition = {0, 0}
local function mouseOverSpeaker(mouse, pos, expand)
expand = tonumber(expand) or 0
return (mouse[1] > pos[1] - expand and mouse[1] < pos[1] + 300 + expand)
and (mouse[2] > pos[2] - expand and mouse[2] < pos[2] + 48 + expand)
end
local function drawSpeakerBar(mouse, pos, speaker, i)
drawable.image = BACK_INDICATOR_IMAGE
canvas:drawDrawable(drawable, pos)
drawTheLine(pos, 1 - math.sqrt(math.min(1, math.max(0, speaker.loudness / -50))))
local hovering = not speaker.isLocal and mouseOverSpeaker(mouse, pos)
textPositioning.position = {pos[1] + 49, pos[2] + 24}
textPositioning.horizontalAnchor = "left"
local text = NAME_PREFIX ..
(hovering and (speaker.muted and "^#31d2f7;Unmute^reset; " or "^#f43030;Mute^reset; ") or "")
.. speaker.name
canvas:drawText(text, textPositioning, 16, nil, nil, nil, FONT_DIRECTIVES)
drawable.image = speaker.muted and FRONT_MUTED_INDICATOR_IMAGE or FRONT_INDICATOR_IMAGE
canvas:drawDrawable(drawable, pos)
if hovering then
hoveredSpeaker = speaker
hoveredSpeakerIndex = i
hoveredSpeakerPosition = pos
--if input.key("LShift") then
-- textPositioning.position = {pos[1] + 288, pos[2] + 24}
-- textPositioning.horizontalAnchor = "right"
-- canvas:drawText("^#fff7;" .. tostring(speaker.speakerId), textPositioning, 16, nil, nil, nil, FONT_DIRECTIVES)
--end
--
--if input.mouseDown("MouseLeft") then
-- local muted = not voice.muted(speaker.speakerId)
-- interface.queueMessage((muted and "^#f43030;Muted^reset; " or "^#31d2f7;Unmuted^reset; ") .. speaker.name, 4, 0.5)
-- voice.setMuted(speaker.speakerId, muted)
--end
end
end
local speakersTime = {}
local function simulateSpeakers()
local speakers = {}
for i = 2, 5 + math.floor((math.sin(os.clock()) * 4) + .5) do
speakers[i] = {
speakerId = i,
entityId = -65536 * i,
name = "Player " .. i,
loudness = -48 + 48 * math.sin(os.clock() + (i * 0.5)),
muted = false
}
end
return speakers
end
local function drawIndicators()
canvas:clear()
local screenSize = canvas:size()
local mousePosition = canvas:mousePosition()
sb.setLogMap("mousePosition", sb.printJson(mousePosition))
local basePos = {screenSize[1] - 350, 50}
-- sort it ourselves for now
local speakersRemaining, speakersSorted = {}, {}
local hoveredSpeakerId = nil
if hoveredSpeaker then
if not mouseOverSpeaker(mousePosition, hoveredSpeakerPosition, 16) then
hoveredSpeaker = nil
else
hoveredSpeakerId = hoveredSpeaker.speakerId
end
end
--local speakers = voice.speakers()
local speakers = { -- just testing before implementing voice lua functions
{
speakerId = 1,
entityId = -65536,
loudness = -96 + math.random() * 96,
muted = false,
name = "theres a pipe bomb up my ass"
}
}
local sortI = 0
local now = os.clock()
for i, speaker in pairs(speakers) do
local speakerId = speaker.speakerId
speakersRemaining[speakerId] = true
local t = speakersTime[speakerId]
if not t then
t = now
speakersTime[speakerId] = t
end
speaker.startTime = t
if speakerId == hoveredSpeakerId then
hoveredSpeaker = speaker
else
sortI = sortI + 1
speakersSorted[sortI] = speaker
end
end
for i, v in pairs(speakersTime) do
if not speakersRemaining[i] then
speakersTime[i] = nil
end
end
table.sort(speakersSorted, function(a, b)
if a.startTime == b.startTime then
return a.speakerId < b.speakerId
else
return a.startTime < b.startTime
end
end)
if hoveredSpeaker then
local len = #speakersSorted
if hoveredSpeakerIndex > len then
for i = len + 1, hoveredSpeakerIndex - 1 do
speakersSorted[i] = false
end
speakersSorted[hoveredSpeakerIndex] = hoveredSpeaker
else
table.insert(speakersSorted, hoveredSpeakerIndex, hoveredSpeaker)
end
end
for i, v in pairs(speakersSorted) do
if v then
local entityId = v.entityId
local loudness = v.loudness
local pos = {basePos[1], basePos[2] + (i - 1) * 52}
drawSpeakerBar(mousePosition, pos, v, i)
end
end
end
function module.init()
canvas = interface.bindCanvas("voice", true)
end
function module.update()
drawIndicators()
end

View File

@ -614,6 +614,7 @@ void ClientApplication::changeState(MainAppState newState) {
m_worldPainter = make_shared<WorldPainter>();
m_mainInterface = make_shared<MainInterface>(m_universeClient, m_worldPainter, m_cinematicOverlay);
m_universeClient->setLuaCallbacks("interface", LuaBindings::makeInterfaceCallbacks(m_mainInterface.get()));
m_universeClient->startLua();
m_mainMixer->setWorldPainter(m_worldPainter);

View File

@ -27,6 +27,11 @@ LuaCallbacks LuaBindings::makeInterfaceCallbacks(MainInterface* mainInterface) {
return GuiContext::singleton().interfaceScale();
});
callbacks.registerCallback("queueMessage", [mainInterface](String const& message, Maybe<float> cooldown, Maybe<float> springState) {
mainInterface->queueMessage(message, cooldown, springState.value(0));
});
return callbacks;
}

View File

@ -62,8 +62,8 @@ namespace Star {
GuiMessage::GuiMessage() : message(), cooldown(), springState() {}
GuiMessage::GuiMessage(String const& message, float cooldown)
: message(message), cooldown(cooldown), springState(0) {}
GuiMessage::GuiMessage(String const& message, float cooldown, float spring)
: message(message), cooldown(cooldown), springState(spring) {}
MainInterface::MainInterface(UniverseClientPtr client, WorldPainterPtr painter, CinematicPtr cinematicOverlay) {
m_state = Running;
@ -369,6 +369,9 @@ bool MainInterface::handleInputEvent(InputEvent const& event) {
player->endTrigger();
}
for (auto& pair : m_canvases)
pair.second->sendEvent(event);
return true;
}
@ -863,11 +866,15 @@ void MainInterface::doChat(String const& chat, bool addToHistory) {
m_chat->addHistory(chat);
}
void MainInterface::queueMessage(String const& message) {
auto guiMessage = make_shared<GuiMessage>(message, m_config->messageTime);
void MainInterface::queueMessage(String const& message, Maybe<float> cooldown, float spring) {
auto guiMessage = make_shared<GuiMessage>(message, cooldown.value(m_config->messageTime), spring);
m_messages.append(guiMessage);
}
void MainInterface::queueMessage(String const& message) {
queueMessage(message, m_config->messageTime, 0.0f);
}
void MainInterface::queueJoinRequest(pair<String, RpcPromiseKeeper<P2PJoinRequestReply>> request)
{
m_queuedJoinRequests.push_back(request);
@ -927,18 +934,21 @@ void MainInterface::warpTo(WarpAction const& warpAction) {
}
CanvasWidgetPtr MainInterface::fetchCanvas(String const& canvasName, bool ignoreInterfaceScale) {
CanvasWidgetPtr canvas;
if (auto canvasPtr = m_canvases.ptr(canvasName))
return *canvasPtr;
canvas = *canvasPtr;
else {
CanvasWidgetPtr canvas = m_canvases.emplace(canvasName, make_shared<CanvasWidget>()).first->second;
m_canvases.emplace(canvasName, canvas = make_shared<CanvasWidget>());
canvas->setPosition(Vec2I());
if (ignoreInterfaceScale)
canvas->setSize(Vec2I(m_guiContext->windowSize()));
else
canvas->setSize(Vec2I(m_guiContext->windowInterfaceSize()));
}
canvas->setIgnoreInterfaceScale(ignoreInterfaceScale);
return canvas;
}
}
PanePtr MainInterface::createEscapeDialog() {

View File

@ -49,7 +49,7 @@ STAR_CLASS(MainInterface);
struct GuiMessage {
GuiMessage();
GuiMessage(String const& message, float cooldown);
GuiMessage(String const& message, float cooldown, float spring = 0);
String message;
float cooldown;
@ -105,7 +105,9 @@ public:
void doChat(String const& chat, bool addToHistory);
void queueMessage(String const& message, Maybe<float> cooldown, float spring);
void queueMessage(String const& message);
void queueItemPickupText(ItemPtr const& item);
void queueJoinRequest(pair<String, RpcPromiseKeeper<P2PJoinRequestReply>> request);

View File

@ -30,6 +30,7 @@ UniverseClient::UniverseClient(PlayerStoragePtr playerStorage, StatisticsPtr sta
m_playerStorage = move(playerStorage);
m_statistics = move(statistics);
m_pause = false;
m_luaRoot = make_shared<LuaRoot>();
reset();
}
@ -86,7 +87,7 @@ Maybe<String> UniverseClient::connect(UniverseConnection connection, bool allowA
return String(strf("Join failed! Server does not support connections with protocol version {}", StarProtocolVersion));
connection.pushSingle(make_shared<ClientConnectPacket>(Root::singleton().assets()->digest(), allowAssetsMismatch, m_mainPlayer->uuid(), m_mainPlayer->name(),
m_mainPlayer->species(), m_playerStorage->loadShipData(m_mainPlayer->uuid()), ShipUpgrades(m_mainPlayer->shipUpgrades()),
m_mainPlayer->species(), m_playerStorage->loadShipData(m_mainPlayer->uuid()), m_mainPlayer->shipUpgrades(),
m_mainPlayer->log()->introComplete(), account));
connection.sendAll(timeout);
@ -219,8 +220,11 @@ void UniverseClient::update() {
m_statistics->update();
if (!m_pause)
if (!m_pause) {
m_worldClient->update();
for (auto& p : m_scriptContexts)
p.second->update();
}
m_connection->push(m_worldClient->getOutgoingPackets());
if (!m_pause)
@ -444,6 +448,28 @@ void UniverseClient::setLuaCallbacks(String const& groupName, LuaCallbacks const
m_worldClient->setLuaCallbacks(groupName, callbacks);
}
void UniverseClient::startLua() {
auto assets = Root::singleton().assets();
for (auto& p : assets->json("/client.config:universeScriptContexts").toObject()) {
auto scriptComponent = make_shared<ScriptComponent>();
scriptComponent->setLuaRoot(m_luaRoot);
scriptComponent->setScripts(jsonToStringList(p.second.toArray()));
for (auto& pair : m_luaCallbacks)
scriptComponent->addCallbacks(pair.first, pair.second);
m_scriptContexts.set(p.first, scriptComponent);
scriptComponent->init();
}
}
void UniverseClient::stopLua() {
for (auto& p : m_scriptContexts)
p.second->uninit();
m_scriptContexts.clear();
}
ClockConstPtr UniverseClient::universeClock() const {
return m_universeClock;
}
@ -539,6 +565,8 @@ void UniverseClient::handlePackets(List<PacketPtr> const& packets) {
}
void UniverseClient::reset() {
stopLua();
m_universeClock.reset();
m_worldClient.reset();
m_celestialDatabase.reset();

View File

@ -10,6 +10,7 @@
#include "StarAiTypes.hpp"
#include "StarSky.hpp"
#include "StarUniverseConnection.hpp"
#include "StarLuaComponents.hpp"
namespace Star {
@ -29,8 +30,8 @@ STAR_CLASS(CelestialDatabase);
STAR_CLASS(JsonRpcInterface);
STAR_CLASS(TeamClient);
STAR_CLASS(QuestManager);
STAR_CLASS(UniverseClient);
STAR_CLASS(LuaRoot);
class UniverseClient {
public:
@ -86,6 +87,8 @@ public:
uint16_t maxPlayers();
void setLuaCallbacks(String const& groupName, LuaCallbacks const& callbacks);
void startLua();
void stopLua();
ClockConstPtr universeClock() const;
CelestialLogConstPtr celestialLog() const;
@ -141,6 +144,12 @@ private:
List<ChatReceivedMessage> m_pendingMessages;
Maybe<String> m_disconnectReason;
LuaRootPtr m_luaRoot;
typedef LuaUpdatableComponent<LuaBaseComponent> ScriptComponent;
typedef shared_ptr<ScriptComponent> ScriptComponentPtr;
StringMap<ScriptComponentPtr> m_scriptContexts;
};
}

View File

@ -88,20 +88,21 @@ bool CanvasWidget::sendEvent(InputEvent const& event) {
return false;
auto& context = GuiContext::singleton();
int interfaceScale = m_ignoreInterfaceScale ? 1 : context.interfaceScale();
if (auto mouseButtonDown = event.ptr<MouseButtonDownEvent>()) {
if (inMember(*context.mousePosition(event)) && m_captureMouse) {
m_clickEvents.append({*context.mousePosition(event) - screenPosition(), mouseButtonDown->mouseButton, true});
if (inMember(*context.mousePosition(event, interfaceScale)) && m_captureMouse) {
m_clickEvents.append({*context.mousePosition(event, interfaceScale) - screenPosition(), mouseButtonDown->mouseButton, true});
m_clickEvents.limitSizeBack(MaximumEventBuffer);
return true;
}
} else if (auto mouseButtonUp = event.ptr<MouseButtonUpEvent>()) {
if (m_captureMouse) {
m_clickEvents.append({*context.mousePosition(event) - screenPosition(), mouseButtonUp->mouseButton, false});
m_clickEvents.append({*context.mousePosition(event, interfaceScale) - screenPosition(), mouseButtonUp->mouseButton, false});
m_clickEvents.limitSizeBack(MaximumEventBuffer);
return true;
}
} else if (event.is<MouseMoveEvent>()) {
m_mousePosition = *context.mousePosition(event) - screenPosition();
m_mousePosition = *context.mousePosition(event, interfaceScale) - screenPosition();
return false;
} else if (auto keyDown = event.ptr<KeyDownEvent>()) {
if (m_captureKeyboard) {
@ -258,7 +259,7 @@ void CanvasWidget::renderTriangles(Vec2F const& renderingOffset, List<tuple<Vec2
void CanvasWidget::renderText(Vec2F const& renderingOffset, String const& s, TextPositioning const& position, unsigned fontSize, Vec4B const& color, FontMode mode, float lineSpacing, String const& font, String const& directives) {
auto& context = GuiContext::singleton();
context.setFontProcessingDirectives(directives);
context.setFontSize(fontSize);
context.setFontSize(fontSize, m_ignoreInterfaceScale ? 1 : context.interfaceScale());
context.setFontColor(color);
context.setFontMode(mode);
context.setFont(font);

View File

@ -98,9 +98,9 @@ void GuiContext::setInterfaceScale(int interfaceScale) {
m_interfaceScale = interfaceScale;
}
Maybe<Vec2I> GuiContext::mousePosition(InputEvent const& event) const {
auto getInterfacePosition = [this](Vec2I pos) {
return Vec2I(pos) / interfaceScale();
Maybe<Vec2I> GuiContext::mousePosition(InputEvent const& event, int pixelRatio) const {
auto getInterfacePosition = [pixelRatio](Vec2I pos) {
return Vec2I(pos) / pixelRatio;
};
if (auto mouseMoveEvent = event.ptr<MouseMoveEvent>())
@ -115,6 +115,10 @@ Maybe<Vec2I> GuiContext::mousePosition(InputEvent const& event) const {
return {};
}
Maybe<Vec2I> GuiContext::mousePosition(InputEvent const& event) const {
return mousePosition(event, interfaceScale());
}
Set<InterfaceAction> GuiContext::actions(InputEvent const& event) const {
return m_keyBindings.actions(event);
}

View File

@ -50,6 +50,7 @@ public:
int interfaceScale() const;
void setInterfaceScale(int interfaceScale);
Maybe<Vec2I> mousePosition(InputEvent const& event, int pixelRatio) const;
Maybe<Vec2I> mousePosition(InputEvent const& event) const;
Set<InterfaceAction> actions(InputEvent const& event) const;

View File

@ -25,13 +25,14 @@ LuaMethods<CanvasWidgetPtr> LuaUserDataMethods<CanvasWidgetPtr>::make() {
methods.registerMethodWithSignature<void, CanvasWidgetPtr>("clear", mem_fn(&CanvasWidget::clear));
methods.registerMethod("drawDrawable", [](CanvasWidgetPtr canvasWidget, Drawable drawable) {
canvasWidget->drawDrawable(move(drawable), Vec2F());
methods.registerMethod("drawDrawable", [](CanvasWidgetPtr canvasWidget, Drawable drawable, Maybe<Vec2F> screenPos) {
canvasWidget->drawDrawable(move(drawable), screenPos.value(Vec2F()));
});
methods.registerMethod("drawDrawables", [](CanvasWidgetPtr canvasWidget, List<Drawable> drawables) {
methods.registerMethod("drawDrawables", [](CanvasWidgetPtr canvasWidget, List<Drawable> drawables, Maybe<Vec2F> screenPos) {
Vec2F pos = screenPos.value(Vec2F());
for (auto& drawable : drawables)
canvasWidget->drawDrawable(move(drawable), Vec2F());
canvasWidget->drawDrawable(move(drawable), pos);
});
methods.registerMethod("drawImage",