MinecraftConsoles/Minecraft.Client/ServerConsole.cpp

767 lines
18 KiB
C++

#include "stdafx.h"
#ifdef _WINDOWS64
#include "ServerConsole.h"
#include "MinecraftServer.h"
#include "PlayerList.h"
#include "ServerPlayer.h"
#include "..\Minecraft.World\StringHelpers.h"
#include "..\Minecraft.World\EntityIO.h"
#include "..\Minecraft.World\Tile.h"
#include "..\Minecraft.World\MobEffect.h"
#include "..\Minecraft.World\LevelSettings.h"
#include <cstdarg>
#include <algorithm>
#include <sstream>
#include <cstdio>
ServerConsole* ServerConsole::s_instance = nullptr;
const char* ServerConsole::Color::Reset = "\033[0m";
const char* ServerConsole::Color::Red = "\033[31m";
const char* ServerConsole::Color::Green = "\033[32m";
const char* ServerConsole::Color::Yellow = "\033[33m";
const char* ServerConsole::Color::Blue = "\033[34m";
const char* ServerConsole::Color::Magenta = "\033[35m";
const char* ServerConsole::Color::Cyan = "\033[36m";
const char* ServerConsole::Color::White = "\033[37m";
const char* ServerConsole::Color::Gray = "\033[90m";
const char* ServerConsole::Color::BrightRed = "\033[91m";
const char* ServerConsole::Color::BrightGreen = "\033[92m";
const char* ServerConsole::Color::BrightYellow= "\033[93m";
const char* ServerConsole::Color::BrightBlue = "\033[94m";
const char* ServerConsole::Color::BrightMagenta="\033[95m";
const char* ServerConsole::Color::BrightCyan = "\033[96m";
const char* ServerConsole::Color::BrightWhite = "\033[97m";
// Known command names for completion and highlighting
static const char* s_commandNames[] = {
"help", "stop", "list", "say", "save-all",
"kill", "gamemode", "teleport", "tp", "give", "giveitem",
"enchant", "enchantitem", "time", "weather", "summon",
"setblock", "fill", "effect", "clear", "vanish", "op", "deop",
"kick", "ban", "unban", "pardon", "banlist",
nullptr
};
// Subcommand completions for specific commands
static const char* s_timeSubcmds[] = { "set", "add", "query", nullptr };
static const char* s_timeSetValues[] = { "day", "night", "noon", "midnight", nullptr };
static const char* s_weatherTypes[] = { "clear", "rain", "thunder", nullptr };
static const char* s_gamemodeNames[] = { "survival", "creative", "adventure", "0", "1", "2", "s", "c", "a", nullptr };
static const char* s_effectActions[] = { "give", "clear", nullptr };
ServerConsole::ServerConsole(MinecraftServer* server)
: m_server(server)
, m_cursorPos(0)
, m_historyIndex(-1)
, m_tabIndex(-1)
, m_running(true)
{
m_hConsoleIn = GetStdHandle(STD_INPUT_HANDLE);
m_hConsoleOut = GetStdHandle(STD_OUTPUT_HANDLE);
s_instance = this;
}
ServerConsole::~ServerConsole()
{
if (s_instance == this)
s_instance = nullptr;
}
void ServerConsole::enableVirtualTerminal()
{
DWORD dwMode = 0;
if (GetConsoleMode(m_hConsoleOut, &dwMode))
{
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(m_hConsoleOut, dwMode);
}
if (GetConsoleMode(m_hConsoleIn, &dwMode))
{
// Disable line input and echo so we can handle raw key events
dwMode &= ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT);
dwMode |= ENABLE_WINDOW_INPUT;
SetConsoleMode(m_hConsoleIn, dwMode);
}
}
void ServerConsole::logInfo(const char* fmt, ...)
{
ServerConsole* sc = getInstance();
if (sc) sc->clearInputLine();
char buf[2048];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
printf("%s[INFO]%s %s\n", Color::BrightCyan, Color::Reset, buf);
fflush(stdout);
if (sc) sc->redrawInputLine();
}
void ServerConsole::logWarn(const char* fmt, ...)
{
ServerConsole* sc = getInstance();
if (sc) sc->clearInputLine();
char buf[2048];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
printf("%s[WARN]%s %s%s%s\n", Color::BrightYellow, Color::Reset, Color::Yellow, buf, Color::Reset);
fflush(stdout);
if (sc) sc->redrawInputLine();
}
void ServerConsole::logError(const char* fmt, ...)
{
ServerConsole* sc = getInstance();
if (sc) sc->clearInputLine();
char buf[2048];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
printf("%s[ERROR]%s %s%s%s\n", Color::BrightRed, Color::Reset, Color::Red, buf, Color::Reset);
fflush(stdout);
if (sc) sc->redrawInputLine();
}
void ServerConsole::logSuccess(const char* fmt, ...)
{
ServerConsole* sc = getInstance();
if (sc) sc->clearInputLine();
char buf[2048];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
printf("%s[INFO]%s %s%s%s\n", Color::BrightGreen, Color::Reset, Color::Green, buf, Color::Reset);
fflush(stdout);
if (sc) sc->redrawInputLine();
}
void ServerConsole::logCommand(const char* label, const char* message)
{
ServerConsole* sc = getInstance();
if (sc) sc->clearInputLine();
printf("%s[%s]%s %s\n", Color::BrightMagenta, label, Color::Reset, message);
fflush(stdout);
if (sc) sc->redrawInputLine();
}
void ServerConsole::logInfo(const wchar_t* message)
{
ServerConsole* sc = getInstance();
if (sc) sc->clearInputLine();
printf("%s[INFO]%s %ls\n", Color::BrightCyan, Color::Reset, message);
fflush(stdout);
if (sc) sc->redrawInputLine();
}
void ServerConsole::logWarn(const wchar_t* message)
{
ServerConsole* sc = getInstance();
if (sc) sc->clearInputLine();
printf("%s[WARN]%s %s%ls%s\n", Color::BrightYellow, Color::Reset, Color::Yellow, message, Color::Reset);
fflush(stdout);
if (sc) sc->redrawInputLine();
}
void ServerConsole::logError(const wchar_t* message)
{
ServerConsole* sc = getInstance();
if (sc) sc->clearInputLine();
printf("%s[ERROR]%s %s%ls%s\n", Color::BrightRed, Color::Reset, Color::Red, message, Color::Reset);
fflush(stdout);
if (sc) sc->redrawInputLine();
}
void ServerConsole::printPrompt()
{
printf("%s> %s", Color::BrightGreen, Color::Reset);
fflush(stdout);
}
void ServerConsole::clearInputLine()
{
// Move to start of line, clear it
printf("\r\033[K");
fflush(stdout);
}
void ServerConsole::redrawInputLine()
{
printf("\r\033[K");
printPrompt();
if (!m_inputBuffer.empty())
{
std::string highlighted = highlightCommand(m_inputBuffer);
printf("%s", highlighted.c_str());
}
// Position cursor correctly
int promptLen = 2; // "> "
int targetCol = promptLen + m_cursorPos;
printf("\r\033[%dC", targetCol);
fflush(stdout);
}
std::string ServerConsole::highlightCommand(const std::string& input)
{
if (input.empty()) return input;
std::string result;
std::istringstream stream(input);
std::string token;
bool firstToken = true;
size_t pos = 0;
// Find the first word (command name)
size_t firstSpace = input.find(' ');
std::string cmdName = (firstSpace != std::string::npos) ? input.substr(0, firstSpace) : input;
std::string cmdLower = cmdName;
std::transform(cmdLower.begin(), cmdLower.end(), cmdLower.begin(), ::tolower);
// Check if it starts with /
size_t cmdStart = 0;
if (!cmdLower.empty() && cmdLower[0] == '/')
{
result += Color::Gray;
result += '/';
cmdLower = cmdLower.substr(1);
cmdName = cmdName.substr(1);
cmdStart = 1;
}
// Highlight command name
bool knownCmd = false;
for (int i = 0; s_commandNames[i] != nullptr; ++i)
{
if (cmdLower == s_commandNames[i])
{
knownCmd = true;
break;
}
}
if (knownCmd)
result += std::string(Color::BrightGreen) + cmdName + Color::Reset;
else
result += std::string(Color::BrightRed) + cmdName + Color::Reset;
// Highlight rest (arguments)
if (firstSpace != std::string::npos)
{
std::string rest = input.substr(firstSpace);
// Color numbers differently from strings
std::string argResult;
bool inNumber = false;
for (size_t i = 0; i < rest.size(); ++i)
{
char c = rest[i];
if (c == ' ')
{
if (inNumber) { argResult += Color::Reset; inNumber = false; }
argResult += c;
}
else if ((c >= '0' && c <= '9') || c == '-' || c == '.')
{
if (!inNumber) { argResult += Color::BrightYellow; inNumber = true; }
argResult += c;
}
else if (c == '~')
{
if (inNumber) { argResult += Color::Reset; inNumber = false; }
argResult += Color::BrightMagenta;
argResult += c;
argResult += Color::Reset;
}
else
{
if (inNumber) { argResult += Color::Reset; inNumber = false; }
argResult += Color::BrightWhite;
argResult += c;
argResult += Color::Reset;
}
}
if (inNumber) argResult += Color::Reset;
result += argResult;
}
return result;
}
static std::string toLowerStr(const std::string& s)
{
std::string r = s;
std::transform(r.begin(), r.end(), r.begin(), ::tolower);
return r;
}
std::vector<std::string> ServerConsole::getCompletions(const std::string& partial)
{
std::vector<std::string> results;
if (partial.empty()) return results;
// Parse the input to figure out what we're completing
std::string input = partial;
bool hasSlash = false;
if (!input.empty() && input[0] == '/')
{
hasSlash = true;
input = input.substr(1);
}
// Split into tokens
std::vector<std::string> tokens;
{
std::istringstream ss(input);
std::string tok;
while (ss >> tok) tokens.push_back(tok);
}
// If input ends with a space, we're completing the NEXT token
bool completingNext = (!partial.empty() && partial.back() == ' ');
if (completingNext) tokens.push_back("");
if (tokens.empty())
{
// Complete command names
for (int i = 0; s_commandNames[i] != nullptr; ++i)
{
std::string name = s_commandNames[i];
results.push_back(hasSlash ? ("/" + name) : name);
}
return results;
}
if (tokens.size() == 1)
{
// Complete command name
std::string prefix = toLowerStr(tokens[0]);
for (int i = 0; s_commandNames[i] != nullptr; ++i)
{
std::string name = s_commandNames[i];
if (name.find(prefix) == 0)
{
results.push_back(hasSlash ? ("/" + name) : name);
}
}
return results;
}
// Token 1+ : subcommand or argument completions
std::string cmdName = toLowerStr(tokens[0]);
std::string argPrefix = toLowerStr(tokens.back());
// Subcommand completions
const char** subCmds = nullptr;
if (tokens.size() == 2)
{
if (cmdName == "time") subCmds = s_timeSubcmds;
else if (cmdName == "weather") subCmds = s_weatherTypes;
else if (cmdName == "gamemode") subCmds = s_gamemodeNames;
else if (cmdName == "effect") subCmds = s_effectActions;
}
else if (tokens.size() == 3 && cmdName == "time" && toLowerStr(tokens[1]) == "set")
{
subCmds = s_timeSetValues;
}
if (subCmds != nullptr)
{
for (int i = 0; subCmds[i] != nullptr; ++i)
{
std::string sub = subCmds[i];
if (sub.find(argPrefix) == 0)
{
results.push_back(sub);
}
}
// Don't return yet - also try player names
}
// Player name completions for commands that take player names
bool wantsPlayerName = false;
if (cmdName == "kill" || cmdName == "tp" || cmdName == "teleport" ||
cmdName == "give" || cmdName == "giveitem" || cmdName == "enchant" || cmdName == "enchantitem" ||
cmdName == "gamemode" || cmdName == "effect" || cmdName == "clear" ||
cmdName == "vanish" || cmdName == "op" || cmdName == "deop" ||
cmdName == "kick" || cmdName == "ban")
{
// Most commands take a player name as the first or second arg
wantsPlayerName = true;
}
if (wantsPlayerName)
{
MinecraftServer* server = MinecraftServer::getInstance();
if (server != nullptr)
{
PlayerList* playerList = server->getPlayers();
if (playerList != nullptr)
{
for (size_t i = 0; i < playerList->players.size(); ++i)
{
if (playerList->players[i] != nullptr)
{
wstring wname = playerList->players[i]->getName();
// Convert to narrow string
std::string name(wname.begin(), wname.end());
std::string nameLower = toLowerStr(name);
if (nameLower.find(argPrefix) == 0)
{
results.push_back(name);
}
}
}
// Add @a, @p, @r, @s selectors
const char* selectors[] = { "@a", "@p", "@r", "@s", nullptr };
for (int i = 0; selectors[i] != nullptr; ++i)
{
std::string sel = selectors[i];
if (sel.find(argPrefix) == 0)
results.push_back(sel);
}
}
}
}
// Entity name completions for summon (use public idsSpawnableInCreative)
if (cmdName == "summon" && tokens.size() == 2)
{
for (auto& pair : EntityIO::idsSpawnableInCreative)
{
wstring wname = EntityIO::getEncodeId(pair.first);
if (!wname.empty())
{
std::string name(wname.begin(), wname.end());
std::string nameLower = toLowerStr(name);
if (nameLower.find(argPrefix) == 0)
{
results.push_back(name);
}
}
}
}
return results;
}
void ServerConsole::handleTabCompletion()
{
if (m_tabIndex == -1)
{
// Start new tab completion
m_tabPartial = m_inputBuffer;
m_tabCompletions = getCompletions(m_tabPartial);
if (m_tabCompletions.empty()) return;
m_tabIndex = 0;
}
else
{
m_tabIndex = (m_tabIndex + 1) % (int)m_tabCompletions.size();
}
if (m_tabCompletions.empty()) return;
// Figure out what portion to replace
// Find the last space in the partial to know what word we're completing
std::string completion = m_tabCompletions[m_tabIndex];
size_t lastSpace = m_tabPartial.rfind(' ');
if (lastSpace == std::string::npos)
{
// Replacing the whole input
m_inputBuffer = completion;
}
else
{
// Replacing the last word
m_inputBuffer = m_tabPartial.substr(0, lastSpace + 1) + completion;
}
m_cursorPos = (int)m_inputBuffer.size();
// If there are multiple completions, show them
if (m_tabCompletions.size() > 1)
{
clearInputLine();
printf("\r\n");
for (size_t i = 0; i < m_tabCompletions.size(); ++i)
{
if ((int)i == m_tabIndex)
printf("%s%s%s ", Color::BrightGreen, m_tabCompletions[i].c_str(), Color::Reset);
else
printf("%s ", m_tabCompletions[i].c_str());
}
printf("\r\n");
fflush(stdout);
}
redrawInputLine();
}
void ServerConsole::insertChar(char c)
{
if ((int)m_inputBuffer.size() >= MAX_INPUT_LENGTH) return;
m_inputBuffer.insert(m_inputBuffer.begin() + m_cursorPos, c);
m_cursorPos++;
m_tabIndex = -1; // Reset tab completion
redrawInputLine();
}
void ServerConsole::deleteCharBack()
{
if (m_cursorPos > 0)
{
m_inputBuffer.erase(m_cursorPos - 1, 1);
m_cursorPos--;
m_tabIndex = -1;
redrawInputLine();
}
}
void ServerConsole::deleteCharForward()
{
if (m_cursorPos < (int)m_inputBuffer.size())
{
m_inputBuffer.erase(m_cursorPos, 1);
m_tabIndex = -1;
redrawInputLine();
}
}
void ServerConsole::moveCursorLeft()
{
if (m_cursorPos > 0)
{
m_cursorPos--;
redrawInputLine();
}
}
void ServerConsole::moveCursorRight()
{
if (m_cursorPos < (int)m_inputBuffer.size())
{
m_cursorPos++;
redrawInputLine();
}
}
void ServerConsole::moveCursorHome()
{
m_cursorPos = 0;
redrawInputLine();
}
void ServerConsole::moveCursorEnd()
{
m_cursorPos = (int)m_inputBuffer.size();
redrawInputLine();
}
void ServerConsole::historyUp()
{
if (m_history.empty()) return;
if (m_historyIndex == -1)
{
m_savedInput = m_inputBuffer;
m_historyIndex = 0;
}
else if (m_historyIndex < (int)m_history.size() - 1)
{
m_historyIndex++;
}
else
{
return; // at oldest entry
}
m_inputBuffer = m_history[m_historyIndex];
m_cursorPos = (int)m_inputBuffer.size();
m_tabIndex = -1;
redrawInputLine();
}
void ServerConsole::historyDown()
{
if (m_historyIndex == -1) return;
m_historyIndex--;
if (m_historyIndex < 0)
{
m_historyIndex = -1;
m_inputBuffer = m_savedInput;
}
else
{
m_inputBuffer = m_history[m_historyIndex];
}
m_cursorPos = (int)m_inputBuffer.size();
m_tabIndex = -1;
redrawInputLine();
}
void ServerConsole::readInputLine(std::string& outLine)
{
m_inputBuffer.clear();
m_cursorPos = 0;
m_historyIndex = -1;
m_tabIndex = -1;
m_savedInput.clear();
printPrompt();
while (m_running)
{
INPUT_RECORD ir;
DWORD count = 0;
if (!ReadConsoleInputA(m_hConsoleIn, &ir, 1, &count) || count == 0)
{
Sleep(10);
continue;
}
if (ir.EventType != KEY_EVENT || !ir.Event.KeyEvent.bKeyDown)
continue;
KEY_EVENT_RECORD& key = ir.Event.KeyEvent;
WORD vk = key.wVirtualKeyCode;
char ch = key.uChar.AsciiChar;
if (vk == VK_RETURN)
{
printf("\r\n");
fflush(stdout);
outLine = m_inputBuffer;
return;
}
else if (vk == VK_TAB)
{
handleTabCompletion();
}
else if (vk == VK_BACK)
{
deleteCharBack();
}
else if (vk == VK_DELETE)
{
deleteCharForward();
}
else if (vk == VK_LEFT)
{
moveCursorLeft();
}
else if (vk == VK_RIGHT)
{
moveCursorRight();
}
else if (vk == VK_UP)
{
historyUp();
}
else if (vk == VK_DOWN)
{
historyDown();
}
else if (vk == VK_HOME)
{
moveCursorHome();
}
else if (vk == VK_END)
{
moveCursorEnd();
}
else if (vk == VK_ESCAPE)
{
m_inputBuffer.clear();
m_cursorPos = 0;
m_tabIndex = -1;
redrawInputLine();
}
else if (ch >= 32 && ch < 127)
{
insertChar(ch);
}
}
outLine.clear();
}
void ServerConsole::run()
{
enableVirtualTerminal();
printf("\n%s========================================%s\n", Color::BrightCyan, Color::Reset);
printf("%s Minecraft Legacy Console Server%s\n", Color::BrightWhite, Color::Reset);
printf("%s========================================%s\n", Color::BrightCyan, Color::Reset);
printf("Type %shelp%s for available commands.\n", Color::BrightGreen, Color::Reset);
printf("Use %sTab%s for auto-completion.\n\n", Color::BrightYellow, Color::Reset);
while (m_running && !MinecraftServer::serverHalted())
{
std::string line;
readInputLine(line);
if (!m_running || MinecraftServer::serverHalted()) break;
// Trim
size_t start = line.find_first_not_of(" \t");
size_t end = line.find_last_not_of(" \t");
if (start == std::string::npos) continue;
line = line.substr(start, end - start + 1);
if (line.empty()) continue;
// Add to history (avoid duplicates at front)
if (m_history.empty() || m_history.front() != line)
{
m_history.push_front(line);
if ((int)m_history.size() > MAX_HISTORY)
m_history.pop_back();
}
// Convert to wstring and send to server
wstring command = convStringToWstring(line);
if (m_server != nullptr)
{
m_server->handleConsoleInput(command, m_server);
}
}
}
#endif // _WINDOWS64