Commit 9a095a09 authored by Shu's avatar Shu 🍠
Browse files

Chat

parent 686472aa
......@@ -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>;
}
\ No newline at end of file
......@@ -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>>;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment