Break up Chat module into Chat and Commands modules

This commit is contained in:
UnknownShadow200 2023-12-03 10:49:33 +11:00
parent 27a583b521
commit e58642e566
7 changed files with 588 additions and 547 deletions

View File

@ -1,21 +1,13 @@
#include "Chat.h"
#include "Commands.h"
#include "String.h"
#include "Stream.h"
#include "Platform.h"
#include "Event.h"
#include "Game.h"
#include "Logger.h"
#include "Server.h"
#include "World.h"
#include "Inventory.h"
#include "Entity.h"
#include "Window.h"
#include "Graphics.h"
#include "Funcs.h"
#include "Block.h"
#include "EnvRenderer.h"
#include "Utils.h"
#include "TexturePack.h"
#include "Options.h"
#include "Drawer2D.h"
@ -252,510 +244,6 @@ void Chat_AddOf(const cc_string* text, int msgType) {
}
/*########################################################################################################################*
*---------------------------------------------------------Commands--------------------------------------------------------*
*#########################################################################################################################*/
#define COMMANDS_PREFIX "/client"
#define COMMANDS_PREFIX_SPACE "/client "
static struct ChatCommand* cmds_head;
static struct ChatCommand* cmds_tail;
static cc_bool Commands_IsCommandPrefix(const cc_string* str) {
static const cc_string prefixSpace = String_FromConst(COMMANDS_PREFIX_SPACE);
static const cc_string prefix = String_FromConst(COMMANDS_PREFIX);
if (!str->length) return false;
if (Server.IsSinglePlayer && str->buffer[0] == '/') return true;
return String_CaselessStarts(str, &prefixSpace)
|| String_CaselessEquals(str, &prefix);
}
void Commands_Register(struct ChatCommand* cmd) {
LinkedList_Append(cmd, cmds_head, cmds_tail);
}
static struct ChatCommand* Commands_FindMatch(const cc_string* cmdName) {
struct ChatCommand* match = NULL;
struct ChatCommand* cmd;
cc_string name;
for (cmd = cmds_head; cmd; cmd = cmd->next) {
name = String_FromReadonly(cmd->name);
if (String_CaselessEquals(&name, cmdName)) return cmd;
}
for (cmd = cmds_head; cmd; cmd = cmd->next) {
name = String_FromReadonly(cmd->name);
if (!String_CaselessStarts(&name, cmdName)) continue;
if (match) {
Chat_Add1("&e/client: Multiple commands found that start with: \"&f%s&e\".", cmdName);
return NULL;
}
match = cmd;
}
if (!match) {
Chat_Add1("&e/client: Unrecognised command: \"&f%s&e\".", cmdName);
Chat_AddRaw("&e/client: Type &a/client &efor a list of commands.");
}
return match;
}
static void Commands_PrintDefault(void) {
cc_string str; char strBuffer[STRING_SIZE];
struct ChatCommand* cmd;
cc_string name;
Chat_AddRaw("&eList of client commands:");
String_InitArray(str, strBuffer);
for (cmd = cmds_head; cmd; cmd = cmd->next) {
name = String_FromReadonly(cmd->name);
if ((str.length + name.length + 2) > str.capacity) {
Chat_Add(&str);
str.length = 0;
}
String_AppendString(&str, &name);
String_AppendConst(&str, ", ");
}
if (str.length) { Chat_Add(&str); }
Chat_AddRaw("&eTo see help for a command, type /client help [cmd name]");
}
static void Commands_Execute(const cc_string* input) {
static const cc_string prefixSpace = String_FromConst(COMMANDS_PREFIX_SPACE);
static const cc_string prefix = String_FromConst(COMMANDS_PREFIX);
cc_string text = *input;
struct ChatCommand* cmd;
int offset, count;
cc_string name, value;
cc_string args[50];
if (String_CaselessStarts(&text, &prefixSpace)) { /* /client command args */
offset = prefixSpace.length;
} else if (String_CaselessStarts(&text, &prefix)) { /* /clientcommand args */
offset = prefix.length;
} else { /* /command args */
offset = 1;
}
text = String_UNSAFE_SubstringAt(&text, offset);
/* Check for only / or /client */
if (!text.length) { Commands_PrintDefault(); return; }
String_UNSAFE_Separate(&text, ' ', &name, &value);
cmd = Commands_FindMatch(&name);
if (!cmd) return;
if ((cmd->flags & COMMAND_FLAG_SINGLEPLAYER_ONLY) && !Server.IsSinglePlayer) {
Chat_Add1("&e/client: \"&f%s&e\" can only be used in singleplayer.", &name);
return;
}
if (cmd->flags & COMMAND_FLAG_UNSPLIT_ARGS) {
/* argsCount = 0 if value.length is 0, 1 otherwise */
cmd->Execute(&value, value.length != 0);
} else {
count = String_UNSAFE_Split(&value, ' ', args, Array_Elems(args));
cmd->Execute(args, value.length ? count : 0);
}
}
/*########################################################################################################################*
*------------------------------------------------------Simple commands----------------------------------------------------*
*#########################################################################################################################*/
static void HelpCommand_Execute(const cc_string* args, int argsCount) {
struct ChatCommand* cmd;
int i;
if (!argsCount) { Commands_PrintDefault(); return; }
cmd = Commands_FindMatch(args);
if (!cmd) return;
for (i = 0; i < Array_Elems(cmd->help); i++) {
if (!cmd->help[i]) continue;
Chat_AddRaw(cmd->help[i]);
}
}
static struct ChatCommand HelpCommand = {
"Help", HelpCommand_Execute,
COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client help [command name]",
"&eDisplays the help for the given command.",
}
};
static void GpuInfoCommand_Execute(const cc_string* args, int argsCount) {
char buffer[7 * STRING_SIZE];
cc_string str, line;
String_InitArray(str, buffer);
Gfx_GetApiInfo(&str);
while (str.length) {
String_UNSAFE_SplitBy(&str, '\n', &line);
if (line.length) Chat_Add1("&a%s", &line);
}
}
static struct ChatCommand GpuInfoCommand = {
"GpuInfo", GpuInfoCommand_Execute,
COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client gpuinfo",
"&eDisplays information about your GPU.",
}
};
static void RenderTypeCommand_Execute(const cc_string* args, int argsCount) {
int flags;
if (!argsCount) {
Chat_AddRaw("&e/client: &cYou didn't specify a new render type."); return;
}
flags = EnvRenderer_CalcFlags(args);
if (flags >= 0) {
EnvRenderer_SetMode(flags);
Options_Set(OPT_RENDER_TYPE, args);
Chat_Add1("&e/client: &fRender type is now %s.", args);
} else {
Chat_Add1("&e/client: &cUnrecognised render type &f\"%s\"&c.", args);
}
}
static struct ChatCommand RenderTypeCommand = {
"RenderType", RenderTypeCommand_Execute,
COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client rendertype [normal/legacy/fast]",
"&bnormal: &eDefault render mode, with all environmental effects enabled",
"&blegacy: &eSame as normal mode, &cbut is usually slightly slower",
" &eIf you have issues with clouds and map edges disappearing randomly, use this mode",
"&bfast: &eSacrifices clouds, fog and overhead sky for faster performance",
}
};
static void ResolutionCommand_Execute(const cc_string* args, int argsCount) {
int width, height;
if (argsCount < 2) {
Chat_Add4("&e/client: &fCurrent resolution is %i@%f2 x %i@%f2",
&WindowInfo.Width, &DisplayInfo.ScaleX, &WindowInfo.Height, &DisplayInfo.ScaleY);
} else if (!Convert_ParseInt(&args[0], &width) || !Convert_ParseInt(&args[1], &height)) {
Chat_AddRaw("&e/client: &cWidth and height must be integers.");
} else if (width <= 0 || height <= 0) {
Chat_AddRaw("&e/client: &cWidth and height must be above 0.");
} else {
Window_SetSize(width, height);
/* Window_Create uses these, but scales by DPI. Hence DPI unscale them here. */
Options_SetInt(OPT_WINDOW_WIDTH, (int)(width / DisplayInfo.ScaleX));
Options_SetInt(OPT_WINDOW_HEIGHT, (int)(height / DisplayInfo.ScaleY));
}
}
static struct ChatCommand ResolutionCommand = {
"Resolution", ResolutionCommand_Execute,
0,
{
"&a/client resolution [width] [height]",
"&ePrecisely sets the size of the rendered window.",
}
};
static void ModelCommand_Execute(const cc_string* args, int argsCount) {
if (argsCount) {
Entity_SetModel(&LocalPlayer_Instance.Base, args);
} else {
Chat_AddRaw("&e/client model: &cYou didn't specify a model name.");
}
}
static struct ChatCommand ModelCommand = {
"Model", ModelCommand_Execute,
COMMAND_FLAG_SINGLEPLAYER_ONLY | COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client model [name]",
"&bnames: &echibi, chicken, creeper, human, pig, sheep",
"&e skeleton, spider, zombie, sit, <numerical block id>",
}
};
static void ClearDeniedCommand_Execute(const cc_string* args, int argsCount) {
int count = TextureCache_ClearDenied();
Chat_Add1("Removed &e%i &fdenied texture pack URLs.", &count);
}
static struct ChatCommand ClearDeniedCommand = {
"ClearDenied", ClearDeniedCommand_Execute,
COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client cleardenied",
"&eClears the list of texture pack URLs you have denied",
}
};
/*########################################################################################################################*
*-------------------------------------------------------CuboidCommand-----------------------------------------------------*
*#########################################################################################################################*/
static int cuboid_block = -1;
static IVec3 cuboid_mark1, cuboid_mark2;
static cc_bool cuboid_persist, cuboid_hooked, cuboid_hasMark1;
static const cc_string cuboid_msg = String_FromConst("&eCuboid: &fPlace or delete a block.");
static const cc_string yes_string = String_FromConst("yes");
static void CuboidCommand_DoCuboid(void) {
IVec3 min, max;
BlockID toPlace;
int x, y, z;
IVec3_Min(&min, &cuboid_mark1, &cuboid_mark2);
IVec3_Max(&max, &cuboid_mark1, &cuboid_mark2);
if (!World_Contains(min.X, min.Y, min.Z)) return;
if (!World_Contains(max.X, max.Y, max.Z)) return;
toPlace = (BlockID)cuboid_block;
if (cuboid_block == -1) toPlace = Inventory_SelectedBlock;
for (y = min.Y; y <= max.Y; y++) {
for (z = min.Z; z <= max.Z; z++) {
for (x = min.X; x <= max.X; x++) {
Game_ChangeBlock(x, y, z, toPlace);
}
}
}
}
static void CuboidCommand_BlockChanged(void* obj, IVec3 coords, BlockID old, BlockID now) {
cc_string msg; char msgBuffer[STRING_SIZE];
String_InitArray(msg, msgBuffer);
if (!cuboid_hasMark1) {
cuboid_mark1 = coords;
cuboid_hasMark1 = true;
Game_UpdateBlock(coords.X, coords.Y, coords.Z, old);
String_Format3(&msg, "&eCuboid: &fMark 1 placed at (%i, %i, %i), place mark 2.", &coords.X, &coords.Y, &coords.Z);
Chat_AddOf(&msg, MSG_TYPE_CLIENTSTATUS_1);
} else {
cuboid_mark2 = coords;
CuboidCommand_DoCuboid();
if (!cuboid_persist) {
Event_Unregister_(&UserEvents.BlockChanged, NULL, CuboidCommand_BlockChanged);
cuboid_hooked = false;
Chat_AddOf(&String_Empty, MSG_TYPE_CLIENTSTATUS_1);
} else {
cuboid_hasMark1 = false;
Chat_AddOf(&cuboid_msg, MSG_TYPE_CLIENTSTATUS_1);
}
}
}
static cc_bool CuboidCommand_ParseArgs(const cc_string* args) {
cc_string value = *args;
int block;
/* Check for /cuboid [block] yes */
if (String_CaselessEnds(&value, &yes_string)) {
cuboid_persist = true;
value.length -= 3;
String_UNSAFE_TrimEnd(&value);
/* special case "/cuboid yes" */
if (!value.length) return true;
}
block = Block_Parse(&value);
if (block == -1) {
Chat_Add1("&eCuboid: &c\"%s\" is not a valid block name or id.", &value); return false;
}
if (block > Game_Version.MaxCoreBlock && !Block_IsCustomDefined(block)) {
Chat_Add1("&eCuboid: &cThere is no block with id \"%s\".", &value); return false;
}
cuboid_block = block;
return true;
}
static void CuboidCommand_Execute(const cc_string* args, int argsCount) {
if (cuboid_hooked) {
Event_Unregister_(&UserEvents.BlockChanged, NULL, CuboidCommand_BlockChanged);
cuboid_hooked = false;
}
cuboid_block = -1;
cuboid_hasMark1 = false;
cuboid_persist = false;
if (argsCount && !CuboidCommand_ParseArgs(args)) return;
Chat_AddOf(&cuboid_msg, MSG_TYPE_CLIENTSTATUS_1);
Event_Register_(&UserEvents.BlockChanged, NULL, CuboidCommand_BlockChanged);
cuboid_hooked = true;
}
static struct ChatCommand CuboidCommand = {
"Cuboid", CuboidCommand_Execute,
COMMAND_FLAG_SINGLEPLAYER_ONLY | COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client cuboid [block] [persist]",
"&eFills the 3D rectangle between two points with [block].",
"&eIf no block is given, uses your currently held block.",
"&e If persist is given and is \"yes\", then the command",
"&e will repeatedly cuboid, without needing to be typed in again.",
}
};
/*########################################################################################################################*
*------------------------------------------------------TeleportCommand----------------------------------------------------*
*#########################################################################################################################*/
static void TeleportCommand_Execute(const cc_string* args, int argsCount) {
struct Entity* e = &LocalPlayer_Instance.Base;
struct LocationUpdate update;
Vec3 v;
if (argsCount != 3) {
Chat_AddRaw("&e/client teleport: &cYou didn't specify X, Y and Z coordinates.");
return;
}
if (!Convert_ParseFloat(&args[0], &v.X) || !Convert_ParseFloat(&args[1], &v.Y) || !Convert_ParseFloat(&args[2], &v.Z)) {
Chat_AddRaw("&e/client teleport: &cCoordinates must be decimals");
return;
}
update.flags = LU_HAS_POS;
update.pos = v;
e->VTABLE->SetLocation(e, &update);
}
static struct ChatCommand TeleportCommand = {
"TP", TeleportCommand_Execute,
COMMAND_FLAG_SINGLEPLAYER_ONLY,
{
"&a/client tp [x y z]",
"&eMoves you to the given coordinates.",
}
};
/*########################################################################################################################*
*------------------------------------------------------BlockEditCommand----------------------------------------------------*
*#########################################################################################################################*/
static cc_bool BlockEditCommand_GetInt(const cc_string* str, const char* name, int* value, int min, int max) {
if (!Convert_ParseInt(str, value)) {
Chat_Add1("&eBlockEdit: &e%c must be an integer", name);
return false;
}
if (*value < min || *value > max) {
Chat_Add3("&eBlockEdit: &e%c must be between %i and %i", name, &min, &max);
return false;
}
return true;
}
static cc_bool BlockEditCommand_GetTexture(const cc_string* str, int* tex) {
return BlockEditCommand_GetInt(str, "Texture", tex, 0, ATLAS1D_MAX_ATLASES - 1);
}
static void BlockEditCommand_Execute(const cc_string* args, int argsCount__) {
cc_string parts[3];
cc_string* prop;
cc_string* value;
int argsCount, block, v;
if (String_CaselessEqualsConst(args, "properties")) {
Chat_AddRaw("&eEditable block properties:");
Chat_AddRaw("&a name &e- Sets the name of the block");
Chat_AddRaw("&a all &e- Sets textures on all six sides of the block");
Chat_AddRaw("&a sides &e- Sets textures on four sides of the block");
Chat_AddRaw("&a left/right/front/back/top/bottom &e- Sets one texture");
Chat_AddRaw("&a collide &e- Sets collision mode of the block");
return;
}
argsCount = String_UNSAFE_Split(args, ' ', parts, 3);
if (argsCount < 3) {
Chat_AddRaw("&eBlockEdit: &eThree arguments required &e(See &a/client help blockedit&e)");
return;
}
block = Block_Parse(&parts[0]);
if (block == -1) {
Chat_Add1("&eBlockEdit: &c\"%s\" is not a valid block name or ID", &parts[0]);
return;
}
/* TODO: Redo as an array */
prop = &parts[1];
value = &parts[2];
if (String_CaselessEqualsConst(prop, "name")) {
Block_SetName(block, value);
} else if (String_CaselessEqualsConst(prop, "all")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_SetSide(v, block);
Block_Tex(block, FACE_YMAX) = v;
Block_Tex(block, FACE_YMIN) = v;
} else if (String_CaselessEqualsConst(prop, "sides")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_SetSide(v, block);
} else if (String_CaselessEqualsConst(prop, "left")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_Tex(block, FACE_XMIN) = v;
} else if (String_CaselessEqualsConst(prop, "right")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_Tex(block, FACE_XMAX) = v;
} else if (String_CaselessEqualsConst(prop, "bottom")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_Tex(block, FACE_YMIN) = v;
} else if (String_CaselessEqualsConst(prop, "top")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_Tex(block, FACE_YMAX) = v;
} else if (String_CaselessEqualsConst(prop, "front")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_Tex(block, FACE_ZMIN) = v;
} else if (String_CaselessEqualsConst(prop, "back")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_Tex(block, FACE_ZMAX) = v;
} else if (String_CaselessEqualsConst(prop, "collide")) {
if (!BlockEditCommand_GetInt(value, "Collide mode", &v, 0, COLLIDE_CLIMB)) return;
Blocks.Collide[block] = v;
} else {
Chat_Add1("&eBlockEdit: &eUnknown property %s &e(See &a/client help blockedit&e)", prop);
return;
}
Block_DefineCustom(block, false);
}
static struct ChatCommand BlockEditCommand = {
"BlockEdit", BlockEditCommand_Execute,
COMMAND_FLAG_SINGLEPLAYER_ONLY | COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client blockedit [block] [property] [value]",
"&eEdits the given property of the given block",
"&a/client blockedit properties",
"&eLists the editable block properties",
}
};
/*########################################################################################################################*
*-------------------------------------------------------Generic chat------------------------------------------------------*
*#########################################################################################################################*/
@ -775,24 +263,12 @@ void Chat_Send(const cc_string* text, cc_bool logUsage) {
Event_RaiseChat(&ChatEvents.ChatSending, text, 0);
if (logUsage) LogInputUsage(text);
if (Commands_IsCommandPrefix(text)) {
Commands_Execute(text);
} else {
if (!Commands_Execute(text)) {
Server.SendChat(text);
}
}
static void OnInit(void) {
Commands_Register(&GpuInfoCommand);
Commands_Register(&HelpCommand);
Commands_Register(&RenderTypeCommand);
Commands_Register(&ResolutionCommand);
Commands_Register(&ModelCommand);
Commands_Register(&CuboidCommand);
Commands_Register(&TeleportCommand);
Commands_Register(&ClearDeniedCommand);
Commands_Register(&BlockEditCommand);
#if defined CC_BUILD_MOBILE || defined CC_BUILD_WEB
/* Better to not log chat by default on mobile/web, */
/* since it's not easily visible to end users */
@ -823,7 +299,6 @@ static void OnReset(void) {
static void OnFree(void) {
CloseLogFile();
ClearCPEMessages();
cmds_head = NULL;
ClearChatLogs();
StringsBuffer_Clear(&Chat_InputLog);

View File

@ -40,30 +40,11 @@ extern cc_string Chat_Status[5], Chat_BottomRight[3], Chat_ClientStatus[2];
extern cc_string Chat_Announcement, Chat_BigAnnouncement, Chat_SmallAnnouncement;
/* All chat messages received */
extern struct StringsBuffer Chat_Log;
/* All chat entered by the user */
/* All chat input entered by the user */
extern struct StringsBuffer Chat_InputLog;
/* Whether chat messages are logged to disc */
extern cc_bool Chat_Logging;
/* This command is only available in singleplayer */
#define COMMAND_FLAG_SINGLEPLAYER_ONLY 0x01
/* args is passed as a single string instead of being split by spaces */
#define COMMAND_FLAG_UNSPLIT_ARGS 0x02
struct ChatCommand;
/* Represents a client-side command/action. */
struct ChatCommand {
const char* name; /* Full name of this command */
/* Function pointer for the actual action the command performs */
void (*Execute)(const cc_string* args, int argsCount);
cc_uint8 flags; /* Flags for handling this command (see COMMAND_FLAG defines) */
const char* help[5]; /* Messages to show when a player uses /help on this command */
struct ChatCommand* next; /* Next command in linked-list of client commands */
};
/* Registers a client-side command, allowing it to be used with /client [cmd name] */
CC_API void Commands_Register( struct ChatCommand* cmd);
typedef void (*FP_Commands_Register)(struct ChatCommand* cmd);
/* Sets the name of log file (no .txt, so e.g. just "singleplayer") */
/* NOTE: This can only be set once. */
void Chat_SetLogName(const cc_string* name);

View File

@ -373,6 +373,7 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="Animations.h" />
<ClInclude Include="Commands.h" />
<ClInclude Include="EntityRenderers.h" />
<ClInclude Include="LBackend.h" />
<ClInclude Include="Http.h" />
@ -447,6 +448,7 @@
</ItemGroup>
<ItemGroup>
<ClCompile Include="Animations.c" />
<ClCompile Include="Commands.c" />
<ClCompile Include="EntityRenderers.c" />
<ClCompile Include="GameVersion.c" />
<ClCompile Include="Graphics_3DS.c" />

View File

@ -345,6 +345,9 @@
<ClInclude Include="EntityRenderers.h">
<Filter>Header Files\Entities</Filter>
</ClInclude>
<ClInclude Include="Commands.h">
<Filter>Header Files\Game</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="String.c">
@ -698,6 +701,9 @@
<ClCompile Include="Graphics_PS2.c">
<Filter>Source Files\Graphics</Filter>
</ClCompile>
<ClCompile Include="Commands.c">
<Filter>Source Files\Game</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\misc\windows\CCicon.rc">

544
src/Commands.c Normal file
View File

@ -0,0 +1,544 @@
#include "Commands.h"
#include "Chat.h"
#include "String.h"
#include "Event.h"
#include "Game.h"
#include "Logger.h"
#include "Server.h"
#include "World.h"
#include "Inventory.h"
#include "Entity.h"
#include "Window.h"
#include "Graphics.h"
#include "Funcs.h"
#include "Block.h"
#include "EnvRenderer.h"
#include "Utils.h"
#include "TexturePack.h"
#include "Options.h"
#include "Drawer2D.h"
#define COMMANDS_PREFIX "/client"
#define COMMANDS_PREFIX_SPACE "/client "
static struct ChatCommand* cmds_head;
static struct ChatCommand* cmds_tail;
void Commands_Register(struct ChatCommand* cmd) {
LinkedList_Append(cmd, cmds_head, cmds_tail);
}
/*########################################################################################################################*
*------------------------------------------------------Command handling---------------------------------------------------*
*#########################################################################################################################*/
static struct ChatCommand* Commands_FindMatch(const cc_string* cmdName) {
struct ChatCommand* match = NULL;
struct ChatCommand* cmd;
cc_string name;
for (cmd = cmds_head; cmd; cmd = cmd->next) {
name = String_FromReadonly(cmd->name);
if (String_CaselessEquals(&name, cmdName)) return cmd;
}
for (cmd = cmds_head; cmd; cmd = cmd->next) {
name = String_FromReadonly(cmd->name);
if (!String_CaselessStarts(&name, cmdName)) continue;
if (match) {
Chat_Add1("&e/client: Multiple commands found that start with: \"&f%s&e\".", cmdName);
return NULL;
}
match = cmd;
}
if (!match) {
Chat_Add1("&e/client: Unrecognised command: \"&f%s&e\".", cmdName);
Chat_AddRaw("&e/client: Type &a/client &efor a list of commands.");
}
return match;
}
static void Commands_PrintDefault(void) {
cc_string str; char strBuffer[STRING_SIZE];
struct ChatCommand* cmd;
cc_string name;
Chat_AddRaw("&eList of client commands:");
String_InitArray(str, strBuffer);
for (cmd = cmds_head; cmd; cmd = cmd->next) {
name = String_FromReadonly(cmd->name);
if ((str.length + name.length + 2) > str.capacity) {
Chat_Add(&str);
str.length = 0;
}
String_AppendString(&str, &name);
String_AppendConst(&str, ", ");
}
if (str.length) { Chat_Add(&str); }
Chat_AddRaw("&eTo see help for a command, type /client help [cmd name]");
}
static cc_bool IsSingleplayerCommandPrefix(const cc_string* input) {
return input->length && Server.IsSinglePlayer && input->buffer[0] == '/';
}
cc_bool Commands_Execute(const cc_string* input) {
static const cc_string prefixSpace = String_FromConst(COMMANDS_PREFIX_SPACE);
static const cc_string prefix = String_FromConst(COMMANDS_PREFIX);
cc_string text;
struct ChatCommand* cmd;
int offset, count;
cc_string name, value;
cc_string args[50];
if (String_CaselessStarts(input, &prefixSpace)) { /* /client command args */
offset = prefixSpace.length;
} else if (String_CaselessStarts(input, &prefix)) { /* /clientcommand args */
offset = prefix.length;
} else if (IsSingleplayerCommandPrefix(input)) { /* /command args */
offset = 1;
} else {
return false;
}
text = String_UNSAFE_SubstringAt(input, offset);
/* Check for only / or /client */
if (!text.length) { Commands_PrintDefault(); return true; }
String_UNSAFE_Separate(&text, ' ', &name, &value);
cmd = Commands_FindMatch(&name);
if (!cmd) return true;
if ((cmd->flags & COMMAND_FLAG_SINGLEPLAYER_ONLY) && !Server.IsSinglePlayer) {
Chat_Add1("&e/client: \"&f%s&e\" can only be used in singleplayer.", &name);
return true;
}
if (cmd->flags & COMMAND_FLAG_UNSPLIT_ARGS) {
/* argsCount = 0 if value.length is 0, 1 otherwise */
cmd->Execute(&value, value.length != 0);
} else {
count = String_UNSAFE_Split(&value, ' ', args, Array_Elems(args));
cmd->Execute(args, value.length ? count : 0);
}
return true;
}
/*########################################################################################################################*
*------------------------------------------------------Simple commands----------------------------------------------------*
*#########################################################################################################################*/
static void HelpCommand_Execute(const cc_string* args, int argsCount) {
struct ChatCommand* cmd;
int i;
if (!argsCount) { Commands_PrintDefault(); return; }
cmd = Commands_FindMatch(args);
if (!cmd) return;
for (i = 0; i < Array_Elems(cmd->help); i++) {
if (!cmd->help[i]) continue;
Chat_AddRaw(cmd->help[i]);
}
}
static struct ChatCommand HelpCommand = {
"Help", HelpCommand_Execute,
COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client help [command name]",
"&eDisplays the help for the given command.",
}
};
static void GpuInfoCommand_Execute(const cc_string* args, int argsCount) {
char buffer[7 * STRING_SIZE];
cc_string str, line;
String_InitArray(str, buffer);
Gfx_GetApiInfo(&str);
while (str.length) {
String_UNSAFE_SplitBy(&str, '\n', &line);
if (line.length) Chat_Add1("&a%s", &line);
}
}
static struct ChatCommand GpuInfoCommand = {
"GpuInfo", GpuInfoCommand_Execute,
COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client gpuinfo",
"&eDisplays information about your GPU.",
}
};
static void RenderTypeCommand_Execute(const cc_string* args, int argsCount) {
int flags;
if (!argsCount) {
Chat_AddRaw("&e/client: &cYou didn't specify a new render type."); return;
}
flags = EnvRenderer_CalcFlags(args);
if (flags >= 0) {
EnvRenderer_SetMode(flags);
Options_Set(OPT_RENDER_TYPE, args);
Chat_Add1("&e/client: &fRender type is now %s.", args);
} else {
Chat_Add1("&e/client: &cUnrecognised render type &f\"%s\"&c.", args);
}
}
static struct ChatCommand RenderTypeCommand = {
"RenderType", RenderTypeCommand_Execute,
COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client rendertype [normal/legacy/fast]",
"&bnormal: &eDefault render mode, with all environmental effects enabled",
"&blegacy: &eSame as normal mode, &cbut is usually slightly slower",
" &eIf you have issues with clouds and map edges disappearing randomly, use this mode",
"&bfast: &eSacrifices clouds, fog and overhead sky for faster performance",
}
};
static void ResolutionCommand_Execute(const cc_string* args, int argsCount) {
int width, height;
if (argsCount < 2) {
Chat_Add4("&e/client: &fCurrent resolution is %i@%f2 x %i@%f2",
&WindowInfo.Width, &DisplayInfo.ScaleX, &WindowInfo.Height, &DisplayInfo.ScaleY);
} else if (!Convert_ParseInt(&args[0], &width) || !Convert_ParseInt(&args[1], &height)) {
Chat_AddRaw("&e/client: &cWidth and height must be integers.");
} else if (width <= 0 || height <= 0) {
Chat_AddRaw("&e/client: &cWidth and height must be above 0.");
} else {
Window_SetSize(width, height);
/* Window_Create uses these, but scales by DPI. Hence DPI unscale them here. */
Options_SetInt(OPT_WINDOW_WIDTH, (int)(width / DisplayInfo.ScaleX));
Options_SetInt(OPT_WINDOW_HEIGHT, (int)(height / DisplayInfo.ScaleY));
}
}
static struct ChatCommand ResolutionCommand = {
"Resolution", ResolutionCommand_Execute,
0,
{
"&a/client resolution [width] [height]",
"&ePrecisely sets the size of the rendered window.",
}
};
static void ModelCommand_Execute(const cc_string* args, int argsCount) {
if (argsCount) {
Entity_SetModel(&LocalPlayer_Instance.Base, args);
} else {
Chat_AddRaw("&e/client model: &cYou didn't specify a model name.");
}
}
static struct ChatCommand ModelCommand = {
"Model", ModelCommand_Execute,
COMMAND_FLAG_SINGLEPLAYER_ONLY | COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client model [name]",
"&bnames: &echibi, chicken, creeper, human, pig, sheep",
"&e skeleton, spider, zombie, sit, <numerical block id>",
}
};
static void ClearDeniedCommand_Execute(const cc_string* args, int argsCount) {
int count = TextureCache_ClearDenied();
Chat_Add1("Removed &e%i &fdenied texture pack URLs.", &count);
}
static struct ChatCommand ClearDeniedCommand = {
"ClearDenied", ClearDeniedCommand_Execute,
COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client cleardenied",
"&eClears the list of texture pack URLs you have denied",
}
};
/*########################################################################################################################*
*-------------------------------------------------------CuboidCommand-----------------------------------------------------*
*#########################################################################################################################*/
static int cuboid_block = -1;
static IVec3 cuboid_mark1, cuboid_mark2;
static cc_bool cuboid_persist, cuboid_hooked, cuboid_hasMark1;
static const cc_string cuboid_msg = String_FromConst("&eCuboid: &fPlace or delete a block.");
static const cc_string yes_string = String_FromConst("yes");
static void CuboidCommand_DoCuboid(void) {
IVec3 min, max;
BlockID toPlace;
int x, y, z;
IVec3_Min(&min, &cuboid_mark1, &cuboid_mark2);
IVec3_Max(&max, &cuboid_mark1, &cuboid_mark2);
if (!World_Contains(min.X, min.Y, min.Z)) return;
if (!World_Contains(max.X, max.Y, max.Z)) return;
toPlace = (BlockID)cuboid_block;
if (cuboid_block == -1) toPlace = Inventory_SelectedBlock;
for (y = min.Y; y <= max.Y; y++) {
for (z = min.Z; z <= max.Z; z++) {
for (x = min.X; x <= max.X; x++) {
Game_ChangeBlock(x, y, z, toPlace);
}
}
}
}
static void CuboidCommand_BlockChanged(void* obj, IVec3 coords, BlockID old, BlockID now) {
cc_string msg; char msgBuffer[STRING_SIZE];
String_InitArray(msg, msgBuffer);
if (!cuboid_hasMark1) {
cuboid_mark1 = coords;
cuboid_hasMark1 = true;
Game_UpdateBlock(coords.X, coords.Y, coords.Z, old);
String_Format3(&msg, "&eCuboid: &fMark 1 placed at (%i, %i, %i), place mark 2.", &coords.X, &coords.Y, &coords.Z);
Chat_AddOf(&msg, MSG_TYPE_CLIENTSTATUS_1);
} else {
cuboid_mark2 = coords;
CuboidCommand_DoCuboid();
if (!cuboid_persist) {
Event_Unregister_(&UserEvents.BlockChanged, NULL, CuboidCommand_BlockChanged);
cuboid_hooked = false;
Chat_AddOf(&String_Empty, MSG_TYPE_CLIENTSTATUS_1);
} else {
cuboid_hasMark1 = false;
Chat_AddOf(&cuboid_msg, MSG_TYPE_CLIENTSTATUS_1);
}
}
}
static cc_bool CuboidCommand_ParseArgs(const cc_string* args) {
cc_string value = *args;
int block;
/* Check for /cuboid [block] yes */
if (String_CaselessEnds(&value, &yes_string)) {
cuboid_persist = true;
value.length -= 3;
String_UNSAFE_TrimEnd(&value);
/* special case "/cuboid yes" */
if (!value.length) return true;
}
block = Block_Parse(&value);
if (block == -1) {
Chat_Add1("&eCuboid: &c\"%s\" is not a valid block name or id.", &value); return false;
}
if (block > Game_Version.MaxCoreBlock && !Block_IsCustomDefined(block)) {
Chat_Add1("&eCuboid: &cThere is no block with id \"%s\".", &value); return false;
}
cuboid_block = block;
return true;
}
static void CuboidCommand_Execute(const cc_string* args, int argsCount) {
if (cuboid_hooked) {
Event_Unregister_(&UserEvents.BlockChanged, NULL, CuboidCommand_BlockChanged);
cuboid_hooked = false;
}
cuboid_block = -1;
cuboid_hasMark1 = false;
cuboid_persist = false;
if (argsCount && !CuboidCommand_ParseArgs(args)) return;
Chat_AddOf(&cuboid_msg, MSG_TYPE_CLIENTSTATUS_1);
Event_Register_(&UserEvents.BlockChanged, NULL, CuboidCommand_BlockChanged);
cuboid_hooked = true;
}
static struct ChatCommand CuboidCommand = {
"Cuboid", CuboidCommand_Execute,
COMMAND_FLAG_SINGLEPLAYER_ONLY | COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client cuboid [block] [persist]",
"&eFills the 3D rectangle between two points with [block].",
"&eIf no block is given, uses your currently held block.",
"&e If persist is given and is \"yes\", then the command",
"&e will repeatedly cuboid, without needing to be typed in again.",
}
};
/*########################################################################################################################*
*------------------------------------------------------TeleportCommand----------------------------------------------------*
*#########################################################################################################################*/
static void TeleportCommand_Execute(const cc_string* args, int argsCount) {
struct Entity* e = &LocalPlayer_Instance.Base;
struct LocationUpdate update;
Vec3 v;
if (argsCount != 3) {
Chat_AddRaw("&e/client teleport: &cYou didn't specify X, Y and Z coordinates.");
return;
}
if (!Convert_ParseFloat(&args[0], &v.X) || !Convert_ParseFloat(&args[1], &v.Y) || !Convert_ParseFloat(&args[2], &v.Z)) {
Chat_AddRaw("&e/client teleport: &cCoordinates must be decimals");
return;
}
update.flags = LU_HAS_POS;
update.pos = v;
e->VTABLE->SetLocation(e, &update);
}
static struct ChatCommand TeleportCommand = {
"TP", TeleportCommand_Execute,
COMMAND_FLAG_SINGLEPLAYER_ONLY,
{
"&a/client tp [x y z]",
"&eMoves you to the given coordinates.",
}
};
/*########################################################################################################################*
*------------------------------------------------------BlockEditCommand----------------------------------------------------*
*#########################################################################################################################*/
static cc_bool BlockEditCommand_GetInt(const cc_string* str, const char* name, int* value, int min, int max) {
if (!Convert_ParseInt(str, value)) {
Chat_Add1("&eBlockEdit: &e%c must be an integer", name);
return false;
}
if (*value < min || *value > max) {
Chat_Add3("&eBlockEdit: &e%c must be between %i and %i", name, &min, &max);
return false;
}
return true;
}
static cc_bool BlockEditCommand_GetTexture(const cc_string* str, int* tex) {
return BlockEditCommand_GetInt(str, "Texture", tex, 0, ATLAS1D_MAX_ATLASES - 1);
}
static void BlockEditCommand_Execute(const cc_string* args, int argsCount__) {
cc_string parts[3];
cc_string* prop;
cc_string* value;
int argsCount, block, v;
if (String_CaselessEqualsConst(args, "properties")) {
Chat_AddRaw("&eEditable block properties:");
Chat_AddRaw("&a name &e- Sets the name of the block");
Chat_AddRaw("&a all &e- Sets textures on all six sides of the block");
Chat_AddRaw("&a sides &e- Sets textures on four sides of the block");
Chat_AddRaw("&a left/right/front/back/top/bottom &e- Sets one texture");
Chat_AddRaw("&a collide &e- Sets collision mode of the block");
return;
}
argsCount = String_UNSAFE_Split(args, ' ', parts, 3);
if (argsCount < 3) {
Chat_AddRaw("&eBlockEdit: &eThree arguments required &e(See &a/client help blockedit&e)");
return;
}
block = Block_Parse(&parts[0]);
if (block == -1) {
Chat_Add1("&eBlockEdit: &c\"%s\" is not a valid block name or ID", &parts[0]);
return;
}
/* TODO: Redo as an array */
prop = &parts[1];
value = &parts[2];
if (String_CaselessEqualsConst(prop, "name")) {
Block_SetName(block, value);
} else if (String_CaselessEqualsConst(prop, "all")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_SetSide(v, block);
Block_Tex(block, FACE_YMAX) = v;
Block_Tex(block, FACE_YMIN) = v;
} else if (String_CaselessEqualsConst(prop, "sides")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_SetSide(v, block);
} else if (String_CaselessEqualsConst(prop, "left")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_Tex(block, FACE_XMIN) = v;
} else if (String_CaselessEqualsConst(prop, "right")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_Tex(block, FACE_XMAX) = v;
} else if (String_CaselessEqualsConst(prop, "bottom")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_Tex(block, FACE_YMIN) = v;
} else if (String_CaselessEqualsConst(prop, "top")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_Tex(block, FACE_YMAX) = v;
} else if (String_CaselessEqualsConst(prop, "front")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_Tex(block, FACE_ZMIN) = v;
} else if (String_CaselessEqualsConst(prop, "back")) {
if (!BlockEditCommand_GetTexture(value, &v)) return;
Block_Tex(block, FACE_ZMAX) = v;
} else if (String_CaselessEqualsConst(prop, "collide")) {
if (!BlockEditCommand_GetInt(value, "Collide mode", &v, 0, COLLIDE_CLIMB)) return;
Blocks.Collide[block] = v;
} else {
Chat_Add1("&eBlockEdit: &eUnknown property %s &e(See &a/client help blockedit&e)", prop);
return;
}
Block_DefineCustom(block, false);
}
static struct ChatCommand BlockEditCommand = {
"BlockEdit", BlockEditCommand_Execute,
COMMAND_FLAG_SINGLEPLAYER_ONLY | COMMAND_FLAG_UNSPLIT_ARGS,
{
"&a/client blockedit [block] [property] [value]",
"&eEdits the given property of the given block",
"&a/client blockedit properties",
"&eLists the editable block properties",
}
};
/*########################################################################################################################*
*------------------------------------------------------Commands component-------------------------------------------------*
*#########################################################################################################################*/
static void OnInit(void) {
Commands_Register(&GpuInfoCommand);
Commands_Register(&HelpCommand);
Commands_Register(&RenderTypeCommand);
Commands_Register(&ResolutionCommand);
Commands_Register(&ModelCommand);
Commands_Register(&CuboidCommand);
Commands_Register(&TeleportCommand);
Commands_Register(&ClearDeniedCommand);
Commands_Register(&BlockEditCommand);
}
static void OnFree(void) {
cmds_head = NULL;
}
struct IGameComponent Commands_Component = {
OnInit, /* Init */
OnFree /* Free */
};

31
src/Commands.h Normal file
View File

@ -0,0 +1,31 @@
#ifndef CC_COMMANDS_H
#define CC_COMMANDS_H
#include "Core.h"
/* Executes actions in response to certain chat input
Copyright 2014-2023 ClassiCube | Licensed under BSD-3
*/
struct IGameComponent;
extern struct IGameComponent Commands_Component;
cc_bool Commands_Execute(const cc_string* input);
/* This command is only available in singleplayer */
#define COMMAND_FLAG_SINGLEPLAYER_ONLY 0x01
/* args is passed as a single string instead of being split by spaces */
#define COMMAND_FLAG_UNSPLIT_ARGS 0x02
struct ChatCommand;
/* Represents a client-side command/action */
struct ChatCommand {
const char* name; /* Full name of this command */
/* Function pointer for the actual action the command performs */
void (*Execute)(const cc_string* args, int argsCount);
cc_uint8 flags; /* Flags for handling this command (see COMMAND_FLAG defines) */
const char* help[5]; /* Messages to show when a player uses /help on this command */
struct ChatCommand* next; /* Next command in linked-list of client commands */
};
/* Registers a client-side command, allowing it to be used with /client [cmd name] */
CC_API void Commands_Register( struct ChatCommand* cmd);
typedef void (*FP_Commands_Register)(struct ChatCommand* cmd);
#endif

View File

@ -15,6 +15,7 @@
#include "Logger.h"
#include "Entity.h"
#include "Chat.h"
#include "Commands.h"
#include "Drawer2D.h"
#include "Model.h"
#include "Particle.h"
@ -410,6 +411,7 @@ static void Game_Load(void) {
Game_AddComponent(&SystemFonts_Component);
Game_AddComponent(&Chat_Component);
Game_AddComponent(&Commands_Component);
Game_AddComponent(&Particles_Component);
Game_AddComponent(&TabList_Component);
Game_AddComponent(&Models_Component);