Chat
This commit is contained in:
parent
686472aa27
commit
9a095a09ac
4
TODO.md
4
TODO.md
|
@ -9,7 +9,8 @@
|
|||
## Hello other
|
||||
|
||||
- [~] Multiplayer
|
||||
- [ ] Chat
|
||||
- [~] Chat
|
||||
- Private messages
|
||||
- [~] Authentication
|
||||
- [x] Compression
|
||||
- [ ] Encryption
|
||||
|
@ -64,6 +65,7 @@
|
|||
|
||||
- [ ] Slash screen
|
||||
- [ ] QUIC protocal
|
||||
- [ ] Use in memory protocol (to replace server_handle)
|
||||
- [ ] Octree
|
||||
- [ ] Better Lod
|
||||
- [ ] VK
|
||||
|
|
|
@ -29,6 +29,11 @@ void Client::run(server_handle* const localHandle) {
|
|||
world->onTeleport = [&](voxel_pos pos) {
|
||||
state.position = player.position = pos;
|
||||
};
|
||||
world->onMessage = [&](const std::string &text) {
|
||||
// MAYBE: rolling buffer
|
||||
const auto yellow = std::make_optional(0xFF43F5F5);
|
||||
state.console.lines.push_back(state::state::line{text, text[0] == '>' ? yellow : std::nullopt});
|
||||
};
|
||||
|
||||
do {
|
||||
window.startFrame();
|
||||
|
@ -116,6 +121,14 @@ void Client::run(server_handle* const localHandle) {
|
|||
if(actions && render::UI::Actions::FillMode) {
|
||||
pipeline->setFillMode(options.renderer.wireframe);
|
||||
}
|
||||
if(actions && render::UI::Actions::Message) {
|
||||
char *s = state.console.buffer.data();
|
||||
//Strtrim(s);
|
||||
//if (s[0])
|
||||
// ExecCommand(s);
|
||||
world->emit(world::action::Message(s));
|
||||
strcpy(s, "");
|
||||
}
|
||||
}
|
||||
{ // Rendering
|
||||
ZoneScopedN("Render");
|
||||
|
|
|
@ -62,8 +62,6 @@ bool Window::create(const CreateInfo &opt) {
|
|||
}
|
||||
glfwMakeContextCurrent(ptr);
|
||||
|
||||
// Ensure we can capture the escape key being pressed below
|
||||
glfwSetInputMode(ptr, GLFW_STICKY_KEYS, GL_TRUE);
|
||||
// Hide the mouse and enable unlimited mouvement
|
||||
glfwSetInputMode(ptr, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
|
||||
|
||||
|
|
|
@ -82,6 +82,10 @@ public:
|
|||
editor.tool.material = config["editor"]["tool"]["material"].value_or(editor.tool.material);
|
||||
editor.tool.emptyAir = config["editor"]["tool"]["empty_air"].value_or(editor.tool.emptyAir);
|
||||
|
||||
console.visible = config["console"]["visible"].value_or(console.visible);
|
||||
console.scroll = config["console"]["scroll"].value_or(console.scroll);
|
||||
console.opacity = config["console"]["opacity"].value_or(console.opacity);
|
||||
|
||||
debugMenu.bar = config["debug_menu"]["bar"].value_or(debugMenu.bar);
|
||||
debugMenu.render = config["debug_menu"]["render"].value_or(debugMenu.render);
|
||||
debugMenu.world = config["debug_menu"]["world"].value_or(debugMenu.world);
|
||||
|
@ -155,6 +159,11 @@ public:
|
|||
{"empty_air", editor.tool.emptyAir}
|
||||
})}
|
||||
}));
|
||||
config.insert_or_assign("console", toml::table({
|
||||
{"visible", console.visible},
|
||||
{"scroll", console.scroll},
|
||||
{"opacity", console.opacity}
|
||||
}));
|
||||
config.insert_or_assign("debug_menu", toml::table({
|
||||
{"bar", debugMenu.bar},
|
||||
{"render", debugMenu.render},
|
||||
|
@ -200,6 +209,12 @@ public:
|
|||
} tool;
|
||||
} editor;
|
||||
|
||||
struct {
|
||||
bool visible = true;
|
||||
bool scroll = true;
|
||||
float opacity = .8f;
|
||||
} console;
|
||||
|
||||
struct {
|
||||
bool visible = true;
|
||||
int corner = 3;
|
||||
|
|
|
@ -50,6 +50,7 @@ UI::Actions UI::draw(config::client::options &options, state::state &state, cons
|
|||
ImGui::EndMenu();
|
||||
}
|
||||
ImGui::Checkbox("Editor", &options.editor.visible);
|
||||
ImGui::Checkbox("Console", &options.console.visible);
|
||||
|
||||
if(ImGui::MenuItem("Close"))
|
||||
options.debugMenu.bar = false;
|
||||
|
@ -230,36 +231,94 @@ UI::Actions UI::draw(config::client::options &options, state::state &state, cons
|
|||
ImGui::End();
|
||||
}
|
||||
|
||||
/*if (show_console) {
|
||||
ImGui::SetNextWindowPos(ImVec2(UI_MARGIN, 500), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(200, 100), ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("Console", &show_console, ImGuiWindowFlags_MenuBar);
|
||||
if (options.console.visible) {
|
||||
{
|
||||
const auto SIZE = ImVec2(300, 200);
|
||||
ImGui::SetNextWindowSize(SIZE, ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowPos(ImVec2(UI_MARGIN, io.DisplaySize.y - SIZE.y - UI_MARGIN), ImGuiCond_FirstUseEver);
|
||||
}
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, options.console.opacity);
|
||||
ImGui::Begin("Console", &options.console.visible, ImGuiWindowFlags_MenuBar);
|
||||
ImGui::BeginMenuBar();
|
||||
ImGui::MenuItem("Auto-scrool", NULL, &console_scrool);
|
||||
if(ImGui::BeginMenu("Options")) {
|
||||
ImGui::Checkbox("Auto-scrool", &options.console.scroll);
|
||||
ImGui::SliderFloat("Opacity", &options.console.opacity, 0.1, 1);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::Button("Clear"))
|
||||
state.console.lines.clear();
|
||||
const bool copy_to_clipboard = ImGui::Button("Copy");
|
||||
ImGui::EndMenuBar();
|
||||
|
||||
// TODO: text
|
||||
// Reserve enough left-over height for 1 separator + 1 input text
|
||||
const float footer_height_to_reserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing();
|
||||
ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), false, ImGuiWindowFlags_HorizontalScrollbar);
|
||||
/*if (ImGui::BeginPopupContextWindow()) {
|
||||
if (ImGui::Selectable("Clear")) ClearLog();
|
||||
ImGui::EndPopup();
|
||||
}*/
|
||||
|
||||
// Display every line as a separate entry so we can change their color or add custom widgets.
|
||||
// If you only want raw text you can use ImGui::TextUnformatted(log.begin(), log.end());
|
||||
// NB- if you have thousands of entries this approach may be too inefficient and may require user-side clipping
|
||||
// to only process visible items. The clipper will automatically measure the height of your first item and then
|
||||
// "seek" to display only items in the visible area.
|
||||
// To use the clipper we can replace your standard loop:
|
||||
// for (int i = 0; i < Items.Size; i++)
|
||||
// With:
|
||||
// ImGuiListClipper clipper(Items.Size);
|
||||
// while (clipper.Step())
|
||||
// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||
// - That your items are evenly spaced (same height)
|
||||
// - That you have cheap random access to your elements (you can access them given their index,
|
||||
// without processing all the ones before)
|
||||
// You cannot this code as-is if a filter is active because it breaks the 'cheap random-access' property.
|
||||
// We would need random-access on the post-filtered list.
|
||||
// A typical application wanting coarse clipping and filtering may want to pre-compute an array of indices
|
||||
// or offsets of items that passed the filtering test, recomputing this array when user changes the filter,
|
||||
// and appending newly elements as they are inserted. This is left as a task to the user until we can manage
|
||||
// to improve this example code!
|
||||
// If your items are of variable height:
|
||||
// - Split them into same height items would be simpler and facilitate random-seeking into your list.
|
||||
// - Consider using manual call to IsRectVisible() and skipping extraneous decoration from your items.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1)); // Tighten spacing
|
||||
if (copy_to_clipboard)
|
||||
ImGui::LogToClipboard();
|
||||
for (const auto& item: state.console.lines) {
|
||||
/*if (!Filter.PassFilter(item))
|
||||
continue;*/
|
||||
|
||||
if (item.color.has_value())
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, item.color.value());
|
||||
ImGui::TextUnformatted(item.text.c_str());
|
||||
if (item.color.has_value())
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
if (copy_to_clipboard)
|
||||
ImGui::LogFinish();
|
||||
|
||||
if (options.console.scroll || ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::EndChild();
|
||||
ImGui::Separator();
|
||||
|
||||
// Command-line
|
||||
bool reclaim_focus = false;
|
||||
// TODO: completion callback
|
||||
if (ImGui::InputText("Input", console_buffer, IM_ARRAYSIZE(console_buffer), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory)) {
|
||||
char *s = console_buffer;
|
||||
//Strtrim(s);
|
||||
//if (s[0])
|
||||
// ExecCommand(s);
|
||||
strcpy(s, "");
|
||||
reclaim_focus = true;
|
||||
}
|
||||
const auto flags = ImGuiInputTextFlags_EnterReturnsTrue;
|
||||
// | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory;
|
||||
ImGui::GetColumnWidth();
|
||||
if (ImGui::InputText("", state.console.buffer.data(), state.console.buffer.size(), flags /*, TODO: callback, context*/) && strlen(state.console.buffer.data()) > 0)
|
||||
actions |= Actions::Message;
|
||||
|
||||
// Auto-focus on window apparition
|
||||
ImGui::SetItemDefaultFocus();
|
||||
if (reclaim_focus)
|
||||
if (actions && Actions::Message)
|
||||
ImGui::SetKeyboardFocusHere(-1); // Auto focus previous widget
|
||||
|
||||
ImGui::End();
|
||||
}*/
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
if (options.overlay.visible) {
|
||||
if (options.overlay.corner != -1) {
|
||||
|
|
|
@ -36,6 +36,7 @@ public:
|
|||
Camera = 1 << 6,
|
||||
Control = 1 << 7,
|
||||
FillMode = 1 << 8,
|
||||
Message = 1 << 9,
|
||||
};
|
||||
friend inline void operator|=(Actions& a, Actions b) {
|
||||
a = static_cast<Actions>(static_cast<int>(a) | static_cast<int>(b));
|
||||
|
|
|
@ -12,7 +12,14 @@ struct state {
|
|||
|
||||
contouring::Abstract* contouring;
|
||||
|
||||
std::array<char, 256> console_buffer;
|
||||
struct line {
|
||||
std::string text;
|
||||
std::optional<uint32_t> color;
|
||||
};
|
||||
struct {
|
||||
std::array<char, 256> buffer;
|
||||
std::vector<line> lines;
|
||||
} console;
|
||||
};
|
||||
|
||||
/// Readonly metrics
|
||||
|
|
|
@ -102,6 +102,12 @@ void DistantUniverse::pullNetwork(voxel_pos pos) {
|
|||
break;
|
||||
}
|
||||
|
||||
case server_packet_type::MESSAGE: {
|
||||
const auto ref = PacketReader(packet, true).remaning();
|
||||
onMessage(std::string(static_cast<const char*>(ref.data()), ref.size()));
|
||||
break;
|
||||
}
|
||||
|
||||
case server_packet_type::AREAS: {
|
||||
auto reader = PacketReader(packet, true);
|
||||
while(!reader.isFull()) {
|
||||
|
@ -210,6 +216,8 @@ void DistantUniverse::pullNetwork(voxel_pos pos) {
|
|||
void DistantUniverse::emit(const action::packet &action) {
|
||||
if(const auto move = std::get_if<action::Move>(&action)) {
|
||||
peer.send(net::client_packet_type::MOVE, move->pos, net::channel_type::NOTIFY);
|
||||
} else if(const auto message = std::get_if<action::Message>(&action)) {
|
||||
peer.send(net::client_packet_type::MESSAGE, message->text.data(), message->text.size(), net::channel_type::RELIABLE);
|
||||
} else if(const auto fillCube = std::get_if<action::FillCube>(&action)) {
|
||||
peer.send(net::client_packet_type::FILL_CUBE, *fillCube, net::channel_type::RELIABLE);
|
||||
} else {
|
||||
|
|
|
@ -28,6 +28,10 @@ void LocalUniverse::update(voxel_pos pos, float) {
|
|||
onTeleport(pos);
|
||||
handle->teleport = std::nullopt;
|
||||
}
|
||||
if (handle->message.has_value() && onMessage) {
|
||||
onMessage(handle->message.value());
|
||||
handle->message = std::nullopt;
|
||||
}
|
||||
|
||||
const auto cur_chunk = glm::divide(pos);
|
||||
const auto chunkChange = cur_chunk != last_chunk;
|
||||
|
|
|
@ -26,6 +26,8 @@ namespace world::client {
|
|||
virtual void emit(const action::packet &) = 0;
|
||||
/// When server teleport player
|
||||
std::function<void(voxel_pos)> onTeleport;
|
||||
/// On chat message
|
||||
std::function<void(const std::string&)> onMessage;
|
||||
|
||||
/// Get current contouring worker
|
||||
contouring::Abstract* getContouring() const {
|
||||
|
|
|
@ -50,6 +50,10 @@ enum class server_packet_type: enet_uint8 {
|
|||
/// Server capabilities
|
||||
/// ushort(loadDistance), MAYBE: more reliable
|
||||
CAPABILITIES = 25,
|
||||
|
||||
/// Public chat message
|
||||
/// char[] (not null terminated) reliable
|
||||
MESSAGE = 29,
|
||||
};
|
||||
enum class client_packet_type: enet_uint8 {
|
||||
/// Interact with voxels
|
||||
|
@ -60,6 +64,10 @@ enum class client_packet_type: enet_uint8 {
|
|||
/// area_id, chunk_pos[] reliable
|
||||
MISSING_CHUNKS = 8,
|
||||
|
||||
/// Send public text message
|
||||
/// char[] (not null terminated) reliable
|
||||
MESSAGE = 9,
|
||||
|
||||
/// Position update
|
||||
/// voxel_pos notify
|
||||
MOVE = 16,
|
||||
|
|
|
@ -11,4 +11,5 @@ struct server_handle {
|
|||
std::function<void(const world::action::packet &packet)> emit;
|
||||
std::function<world::Universe::ray_result(const geometry::Ray &ray)> raycast;
|
||||
std::optional<voxel_pos> teleport;
|
||||
std::optional<std::string> message;
|
||||
};
|
||||
|
|
|
@ -27,6 +27,11 @@ struct Move: part::Ping {
|
|||
|
||||
const voxel_pos pos;
|
||||
};
|
||||
struct Message: part::Ping {
|
||||
Message(const std::string& text): text(text) { }
|
||||
|
||||
using packet = std::variant<Move, Fill, FillCube>;
|
||||
const std::string text;
|
||||
};
|
||||
|
||||
using packet = std::variant<Move, Message, Fill, FillCube>;
|
||||
}
|
|
@ -17,6 +17,8 @@ SharedUniverse::SharedUniverse(const options &o, server_handle *const localHandl
|
|||
this->set(fill->pos, fill->val);
|
||||
} else if(const auto fillCube = std::get_if<world::action::FillCube>(&packet)) {
|
||||
this->setCube(fillCube->pos, fillCube->val, fillCube->radius);
|
||||
} else if(const auto message = std::get_if<world::action::Message>(&packet)) {
|
||||
this->broadcastMessage("Player" + std::to_string(id.index) + ": " + message->text);
|
||||
} else if(const auto move = std::get_if<world::action::Move>(&packet)) {
|
||||
if(!movePlayer(PLAYER_ENTITY_ID, move->pos)) {
|
||||
LOG_W("Bad move of player " << PLAYER_ENTITY_ID.index);
|
||||
|
@ -44,6 +46,11 @@ void SharedUniverse::updateChunk(area_map::iterator &it, world::ChunkContainer::
|
|||
}
|
||||
}
|
||||
|
||||
void SharedUniverse::broadcastMessage(const std::string& text) {
|
||||
localHandle->message = text;
|
||||
Universe::broadcastMessage(text);
|
||||
}
|
||||
|
||||
std::shared_ptr<Chunk> SharedUniverse::createChunk(const chunk_pos &pos, const std::unique_ptr<generator::Abstract> &rnd) const {
|
||||
return std::make_shared<SharedChunk>(pos, rnd);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ namespace world::server {
|
|||
void loadChunk(area_<chunk_pos>, chunk_pos, const world::ChunkContainer &) override;
|
||||
void updateChunk(area_map::iterator &, world::ChunkContainer::iterator &, chunk_pos, float deltaTime) override;
|
||||
|
||||
void broadcastMessage(const std::string&) override;
|
||||
|
||||
private:
|
||||
server_handle *const localHandle;
|
||||
};
|
||||
|
|
|
@ -389,12 +389,14 @@ void Universe::pullNetwork() {
|
|||
host.sendTo(peer, server_packet_type::TELEPORT, player->pos.as_voxel(), channel_type::RELIABLE);
|
||||
movedPlayers.insert(client->instanceId);
|
||||
}
|
||||
broadcastMessage("> Player" + std::to_string(client->instanceId.index) + " has joined us");
|
||||
broadcastAreas();
|
||||
},
|
||||
[&](peer_t *peer, disconnect_reason reason) {
|
||||
ZoneScopedN("Disconnect");
|
||||
LOG_I("Client disconnect from " << peer->address << " with " << (enet_uint32)reason);
|
||||
if (const auto data = Server::GetPeerData<net_client>(peer)) {
|
||||
broadcastMessage("> Player" + std::to_string(data->instanceId.index) + " has left our universe");
|
||||
entities.at(PLAYER_ENTITY_ID).instances.free(data->instanceId);
|
||||
delete data;
|
||||
}
|
||||
|
@ -429,6 +431,12 @@ void Universe::pullNetwork() {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case client_packet_type::MESSAGE: {
|
||||
const auto ref = PacketReader(packet).remaning();
|
||||
broadcastMessage("Player" + std::to_string(Server::GetPeerData<net_client>(peer)->instanceId.index)
|
||||
+ ": " + std::string(static_cast<const char *>(ref.data()), ref.size()));
|
||||
break;
|
||||
}
|
||||
case client_packet_type::MISSING_CHUNKS: {
|
||||
if (auto player = findEntity(PLAYER_ENTITY_ID,Server::GetPeerData<net_client>(peer)->instanceId )) {
|
||||
const auto pos = player->pos.as_voxel();
|
||||
|
@ -457,7 +465,7 @@ void Universe::pullNetwork() {
|
|||
default:
|
||||
LOG_T("Bad packet from " << peer->address);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
void Universe::broadcastAreas() {
|
||||
|
@ -484,6 +492,9 @@ net::packet_t* Universe::serializeChunk(const robin_hood::pair<area_<chunk_pos>,
|
|||
assert(packet.isFull());
|
||||
return packet.get();
|
||||
}
|
||||
void Universe::broadcastMessage(const std::string& text) {
|
||||
host.broadcast(net::server_packet_type::MESSAGE, text.data(), text.size(), net::channel_type::RELIABLE);
|
||||
}
|
||||
|
||||
void Universe::updateChunk(area_map::iterator &, world::ChunkContainer::iterator &, chunk_pos, float deltaTime) {}
|
||||
void Universe::loadChunk(area_<chunk_pos>, chunk_pos, const world::ChunkContainer &) {}
|
||||
|
|
|
@ -69,6 +69,7 @@ namespace world::server {
|
|||
void pullNetwork();
|
||||
void broadcastAreas();
|
||||
net::packet_t* serializeChunk(const robin_hood::pair<area_<chunk_pos>, std::shared_ptr<Chunk>> &);
|
||||
virtual void broadcastMessage(const std::string &);
|
||||
|
||||
using area_map = robin_hood::unordered_map<area_id, std::shared_ptr<Area>>;
|
||||
|
||||
|
|
Loading…
Reference in New Issue