diff --git a/Makefile b/Makefile index 30cd661..01d6e91 100644 --- a/Makefile +++ b/Makefile @@ -25,5 +25,5 @@ clean: all: SilverMUDClient SilverMUDServer -debug: CFLAGS += -Wall -ggdb +debug: CFLAGS += -Wall -ggdb -Wextra debug: SilverMUDClientDebug SilverMUDServerDebug diff --git a/SilverMUD.org b/SilverMUD.org index 30aa5c8..070bfe8 100644 --- a/SilverMUD.org +++ b/SilverMUD.org @@ -4,5 +4,25 @@ world through the internet. It's designed to give a gamemaster the same power to improvise that they have at the table, through simple programming and easy-to-understand structures. ** Player's Guide +*** The Basic Commands +SilverMUD is played through a set of very simple commands. To use a command, +type a forward-slash (/) followed immediately by the command name. The command +can be upper or lower-case. + +| Command | Arguments | Effect | +|---------+---------------------------------------------------+--------------------------------------------------------------------| +| JOIN | Takes a character name | Logs you into the server with the given character name. | +| MOVE | Takes a path name or a path number | Moves you down the given path. | +| LOOK | None | Gives you a description of what's around you, and what you can do. | +| STAT | None | Displays your current status and character sheet. | +| SPEC | Core stat name | Allows you to apply spec points to a given stat. | +| TRY | Core stat name or skill name and an object number | Attempt to use the given stat or skill on the object. | + ** Gamemaster's Guide +*** Running the Server: + ** Developer's Guide +*** Build Prerequisites: +SilverMUD has the following dependencies: +- GnuTLS +- ncurses diff --git a/include/gamelogic.h b/include/gamelogic.h deleted file mode 100644 index be391ae..0000000 --- a/include/gamelogic.h +++ /dev/null @@ -1,26 +0,0 @@ -// gamelogic.h: Header file contatning function prototypes and datastructures -// for dealing with the game's logic. -// Barry Kane, 2022. -#ifndef GAMELOGIC_H -#define GAMELOGIC_H -#include "constants.h" -#include "playerdata.h" -#include "inputoutput.h" - -// ======================= -// -=[ Main Game Loop ]=-: -// ======================= - -// A datastructure containing the needed parameters for a main game loop: -typedef struct gameLogicParameters -{ - int * playerCount; - playerInfo * connectedPlayers; - inputMessageQueue * inputQueue; - outputMessageQueue * outputQueue; -} gameLogicParameters; - -// Thread function which runs the main game loop, given the needed parameters: -void * gameLogicLoop(void * parameters); - -#endif diff --git a/include/playerdata.h b/include/playerdata.h deleted file mode 100644 index 9966544..0000000 --- a/include/playerdata.h +++ /dev/null @@ -1,38 +0,0 @@ -// playerdata.h: Header file containing data structures for player data and function -// prototypes for interacting with said data. -#ifndef PLAYERDATA_H -#define PLAYERDATA_H -#include - -typedef struct playerPath playerPath; -typedef struct playerArea playerArea; - -struct playerPath -{ - char pathName[32]; - playerArea * areaToJoin; -}; - -struct playerArea -{ - char areaName[32]; - char areaDescription[256]; - playerPath * areaExits[16]; -}; - -typedef struct playerInfo -{ - char playerName[32]; - playerArea * currentArea; -} playerInfo; - -// Move a player to a different area given a path in the area: -int movePlayerToArea(playerInfo * player, char * requestedPath); - -// Create an area given a name and description: -playerArea * createArea(char * nameString, char * descriptionString); - -// Create a path between two areas given two areas and two strings: -int createPath(playerArea * fromArea, playerArea * toArea, char * fromDescription, char * toDescription); - -#endif diff --git a/src/client/SilverMUDClient.c b/src/client/SilverMUDClient.c index 119baed..57c57a1 100644 --- a/src/client/SilverMUDClient.c +++ b/src/client/SilverMUDClient.c @@ -13,12 +13,12 @@ #include #include #include -#include "../../include/constants.h" -#include "../../include/playerdata.h" -#include "../../include/texteffects.h" -#include "../../include/inputoutput.h" +#include "../constants.h" +#include "../playerdata.h" +#include "../texteffects.h" +#include "../inputoutput.h" -// A struct for passing arguments to our threads containing a file descriptor and a window pointer: +// A struct for bundling all needed paramaters for a thread so we can pass them using a void pointer: typedef struct threadparameters { gnutls_session_t tlsSession; @@ -72,26 +72,37 @@ void * messageSender(void * parameters) void * messageReceiver(void * parameters) { struct threadparameters *threadParameters = parameters; + bool serverMessage = false; userMessage receiveBuffer; - + int screenWidth = getmaxx(threadParameters->window); + // Repeatedly take messages from the server and print them to the chat log window: while (!shouldExit) { messageReceive(threadParameters->tlsSession, &receiveBuffer); if (receiveBuffer.senderName[0] == '\0') { + wrapString(receiveBuffer.messageContent, + strlen(receiveBuffer.messageContent) - 1, screenWidth); if (receiveBuffer.messageContent[0] == '\0') { shouldExit = true; pthread_exit(NULL); } - slowPrintNcurses("\n --====<", 8000, threadParameters->window, true); - slowPrintNcurses(">====-- \n", 8000, threadParameters->window, true); - slowPrintNcurses(receiveBuffer.messageContent, 8000, threadParameters->window, false); - slowPrintNcurses("\n --====<>====-- \n", 8000, threadParameters->window, true); + if(serverMessage == false) + { + slowPrintNcurses("\n --====<>====--", 4000, threadParameters->window, true); + serverMessage = true; + } + slowPrintNcurses("\n", 4000, threadParameters->window, true); + slowPrintNcurses(receiveBuffer.messageContent, 4000, threadParameters->window, false); + slowPrintNcurses("\n", 4000, threadParameters->window, true); } else { + wrapString(receiveBuffer.messageContent, + strlen(receiveBuffer.messageContent) - 1, + screenWidth - strlen(receiveBuffer.senderName) - 2); if (threadParameters->loggingFlag == true) { fputs(receiveBuffer.senderName, threadParameters->loggingStream); @@ -99,9 +110,14 @@ void * messageReceiver(void * parameters) fputs(receiveBuffer.messageContent, threadParameters->loggingStream); fflush(threadParameters->loggingStream); } - slowPrintNcurses(receiveBuffer.senderName, 8000, threadParameters->window, true); - slowPrintNcurses(": ", 8000, threadParameters->window, true); - slowPrintNcurses(receiveBuffer.messageContent, 8000, threadParameters->window, false); + if(serverMessage == true) + { + slowPrintNcurses("\n --====<>====-- \n", 4000, threadParameters->window, true); + serverMessage = false; + } + slowPrintNcurses(receiveBuffer.senderName, 4000, threadParameters->window, true); + slowPrintNcurses(": ", 4000, threadParameters->window, true); + slowPrintNcurses(receiveBuffer.messageContent, 4000, threadParameters->window, false); } } pthread_exit(NULL); @@ -115,7 +131,7 @@ int main(int argc, char **argv) pthread_t receivingThread; int port = 5000; int currentopt = 0; - int characterDelay = 8000; + int characterDelay = 4000; char chatLogPath[PATH_MAX + 1]; char gameLogPath[PATH_MAX + 1]; char ipAddress[32] = "127.0.0.1"; @@ -248,7 +264,7 @@ int main(int argc, char **argv) logArea = malloc(sizeof(*logArea)); messageArea = malloc(sizeof(*messageArea)); - + // Make the windows for the structs, and pass the socket descriptor: logArea->window = newwin(LINES - 5, COLS - 2, 1, 1); logArea->tlsSession = tlsSession; @@ -257,7 +273,7 @@ int main(int argc, char **argv) { logArea->loggingStream = chatLog; } - messageArea->window = newwin(3, COLS, LINES - 3, 0); + messageArea->window = newwin(3, COLS - 2, LINES - 4, 1); messageArea->tlsSession = tlsSession; messageArea->loggingFlag = gameLogging; if (gameLog != NULL) @@ -269,7 +285,7 @@ int main(int argc, char **argv) scrollok(logArea->window, true); scrollok(messageArea->window, true); - // Run a thread to send messages, and use main to recieve: + // Run a thread to send messages, and use another to recieve: pthread_create(&sendingThread, NULL, messageSender, messageArea); pthread_create(&receivingThread, NULL, messageReceiver, logArea); diff --git a/include/constants.h b/src/constants.h similarity index 100% rename from include/constants.h rename to src/constants.h diff --git a/src/gamelogic.c b/src/gamelogic.c index 0229d0b..3161d86 100644 --- a/src/gamelogic.c +++ b/src/gamelogic.c @@ -1,11 +1,13 @@ // gamelogic.c: Contains function definitons for dealing with the game's logic. // Barry Kane, 2022. #include +#include #include -#include "../include/constants.h" -#include "../include/gamelogic.h" -#include "../include/playerdata.h" -#include "../include/inputoutput.h" +#include +#include "constants.h" +#include "gamelogic.h" +#include "playerdata.h" +#include "inputoutput.h" // ======================= // -=[ Main Game Loop ]=-: @@ -14,103 +16,34 @@ // Thread function which runs the main game loop, given the needed parameters: void * gameLogicLoop(void * parameters) { - char formattedString[64]; gameLogicParameters * threadParameters = parameters; inputMessage * currentInput = NULL; bool keepRunning = true; + commandQueue * commandQueue = createCommandQueue(); while(keepRunning) { + // Evaluate remaining commands: + if(commandQueue->currentLength != 0) + { + evaluateNextCommand(threadParameters, commandQueue); + } // Check for new messages and pop them off the queue: if(threadParameters->inputQueue->currentLength != 0) { - while(threadParameters->inputQueue->lock == true) - { - threadParameters->inputQueue->lock = true; - } + while(threadParameters->inputQueue->lock == true); + threadParameters->inputQueue->lock = true; currentInput = peekInputMessage(threadParameters->inputQueue); userInputSanatize(currentInput->content->messageContent, MAX); // A slash as the first character means the message is a user command: if(currentInput->content->messageContent[0] == '/') { - // TODO: Implement Command Queue. - // For now, basic intepretation will do. - - // Exit command: Sends an "empty" exit message to disconnect a client: - if(strncmp(¤tInput->content->messageContent[1], "EXIT", 4) == 0 || - strncmp(¤tInput->content->messageContent[1], "exit", 4) == 0) - { - userMessage * exitMessage = malloc(sizeof(userMessage)); - exitMessage->senderName[0] = '\0'; - exitMessage->messageContent[0] = '\0'; - queueTargetedOutputMessage(threadParameters->outputQueue, exitMessage, ¤tInput->sender, 1); - free(exitMessage); - } - - // Move command: Moves the current player down a path in their current area, given a pathname or number: - if(strncmp(¤tInput->content->messageContent[1], "MOVE", 4) == 0 || - strncmp(¤tInput->content->messageContent[1], "move", 4) == 0) - { - userMessage * moveMessage = malloc(sizeof(userMessage)); - bzero(moveMessage, sizeof(userMessage)); - char requestedPath[32]; - strncpy(requestedPath, ¤tInput->content->messageContent[6], 32); - userInputSanatize(requestedPath, 32); - // Remove newlines: - for (int index = 0; index < 32; index++) - { - if (requestedPath[index] == '\n') - { - requestedPath[index] = '\0'; - } - } - requestedPath[31] = '\0'; - if(movePlayerToArea(currentInput->sender, requestedPath) == 0) - { - moveMessage->senderName[0] = '\0'; - strncat(moveMessage->messageContent, currentInput->sender->currentArea->areaName, 33); - strncat(moveMessage->messageContent, "\n", 2); - strncat(moveMessage->messageContent, currentInput->sender->currentArea->areaDescription, 256); - strncat(moveMessage->messageContent, "\nYou can go:", 13); - for(int index = 0; index < 16; index++) - { - if(currentInput->sender->currentArea->areaExits[index] != NULL) - { - snprintf(formattedString, 64, "\n\t%d. %s", index + 1, currentInput->sender->currentArea->areaExits[index]->pathName); - strncat(moveMessage->messageContent, formattedString, 64); - } - } - queueTargetedOutputMessage(threadParameters->outputQueue, moveMessage, ¤tInput->sender, 1); - } - free(moveMessage); - } - - // Look command: Sends the current area's name, description, and - if(strncmp(¤tInput->content->messageContent[1], "LOOK", 4) == 0 || - strncmp(¤tInput->content->messageContent[1], "look", 4) == 0) - { - userMessage * lookMessage = malloc(sizeof(userMessage)); - strncat(lookMessage->messageContent, currentInput->sender->currentArea->areaName, 33); - strncat(lookMessage->messageContent, "\n", 2); - strncat(lookMessage->messageContent, currentInput->sender->currentArea->areaDescription, 256); - strncat(lookMessage->messageContent, "\nYou can go:", 13); - for(int index = 0; index < 16; index++) - { - if(currentInput->sender->currentArea->areaExits[index] != NULL) - { - snprintf(formattedString, 64, "\n\t%d. %s", index + 1, currentInput->sender->currentArea->areaExits[index]->pathName); - strncat(lookMessage->messageContent, formattedString, 64); - } - } - queueTargetedOutputMessage(threadParameters->outputQueue, lookMessage, ¤tInput->sender, 1); - free(lookMessage); - } - - // Name command: Checks if the name is isn't used and is valid, then changes the player's name: - if(strncmp(¤tInput->content->messageContent[1], "NAME", 4) == 0 || - strncmp(¤tInput->content->messageContent[1], "name", 4) == 0) - { - - } + queueMessagedCommand(commandQueue, currentInput); + } + else if(currentInput->sender->currentArea == getAreaFromList(threadParameters->areaList, 0)) + { + currentInput = NULL; + threadParameters->inputQueue->lock = false; + dequeueInputMessage(threadParameters->inputQueue); } else { @@ -143,3 +76,635 @@ void * gameLogicLoop(void * parameters) } return NULL; } + +// Create a commandQueue: +commandQueue * createCommandQueue(void) +{ + commandQueue * newCommandQueue = calloc(1, sizeof(commandQueue)); + newCommandQueue->back = NULL; + newCommandQueue->front = NULL; + newCommandQueue->lock = false; + newCommandQueue->paused = false; + newCommandQueue->currentLength = 0; + return newCommandQueue; +} + +// Return the front commandEvent from a commandQueue: +commandEvent * peekCommand(commandQueue * queue) +{ + // Do nothing until the command queue is unlocked. + while(queue->lock); + + // Return the front item. + return queue->front; +} + +// Enqueue a messaged command to a commandQueue: +int queueMessagedCommand(commandQueue * queue, inputMessage * messageToQueue) +{ + // Prepare the new commandEvent: + commandEvent * newCommand = calloc(1, sizeof(commandEvent)); + newCommand->command = calloc(16, sizeof(char)); + newCommand->arguments = calloc(MAX, sizeof(char)); + newCommand->caller = messageToQueue->sender; + + // Seperate the command from it's arguments: + strtok(messageToQueue->content->messageContent, " "); + + // Copy the command and arguments to the new commandEvent: + strncpy(newCommand->command, &messageToQueue->content->messageContent[1], 16); + strncpy(newCommand->arguments, &messageToQueue->content->messageContent[strlen(newCommand->command) + 2], + MAX - (strlen(newCommand->command) + 2)); + + // Ensure the arguments are safe to parse, without adding newlines: + userNameSanatize(newCommand->command, 16); + userNameSanatize(newCommand->arguments, MAX); + + // Lowercase the command for easier comparison: + for (char * character = newCommand->command; *character; ++character) + { + *character = tolower(*character); + } + + // Wait for the queue to unlock: + while (queue->lock); + + // Check that we're not overflowing the queue: + if ((queue->currentLength + 1) > MAXQUEUELENGTH) + { + // Unlock the queue: + queue->lock = false; + return -1; + } + else + { + // If the queue is empty, set the first commandEvent as both the front and back of the queue: + if(queue->front == NULL) + { + queue->front = newCommand; + queue->back = newCommand; + queue->currentLength++; + + // Unlock the queue: + queue->lock = false; + + return 0; + } + else + { + queue->back->next = newCommand; + queue->back = newCommand; + queue->currentLength++; + + // Unlock the queue: + queue->lock = false; + + return 0; + } + } +} + +// Enqueue a command to a commandQueue: +int queueCommand(commandQueue * queue, char * command, char * arguments, int commandLength, int argumentsLength, playerInfo * callingPlayer) +{ + // Prepare the new commandEvent: + commandEvent * newCommand = calloc(1, sizeof(commandEvent)); + newCommand->command = calloc(16, sizeof(char)); + newCommand->arguments = calloc(MAX, sizeof(char)); + newCommand->caller = callingPlayer; + + // Copy the command and arguments: + strncpy(newCommand->command, command, commandLength); + strncpy(newCommand->arguments, arguments, argumentsLength); + + // Ensure the arguments are safe to parse, without adding newlines: + userNameSanatize(newCommand->command, 16); + + // Wait for the queue to unlock: + while (queue->lock); + + // Check that we're not overflowing the queue: + if ((queue->currentLength + 1) > MAXQUEUELENGTH) + { + // Unlock the queue: + queue->lock = false; + return -1; + } + else + { + // If the queue is empty, set the first commandEvent as both the front and back of the queue: + if(queue->front == NULL) + { + queue->front = newCommand; + queue->back = newCommand; + queue->currentLength++; + + // Unlock the queue: + queue->lock = false; + + return 0; + } + else + { + queue->back->next = newCommand; + queue->back = newCommand; + queue->currentLength++; + + // Unlock the queue: + queue->lock = false; + + return 0; + } + } +} + +// Dequeue the front commandEvent from a commandQueue: +int dequeueCommand(commandQueue * queue) +{ + // Wait for the queue to unlock: + while (queue->lock); + + // Lock the queue: + queue->lock = true; + + // Check the list isn't empty: + if(queue->front == NULL) + { + queue->lock = false; + return -1; + } + + // If there is only one item in the queue: + else if(queue->front == queue->back) + { + free(queue->front->command); + free(queue->front->arguments); + free(queue->front); + queue->front = NULL; + queue->back = NULL; + queue->currentLength--; + queue->lock = false; + return 0; + } + + // Remove the front item: + else + { + commandEvent * commandToDelete = queue->front; + queue->front = queue->front->next; + free(commandToDelete->command); + free(commandToDelete->arguments); + free(commandToDelete); + queue->currentLength--; + queue->lock = false; + return 0; + } +} + +// Evaluate the next commandEvent: +int evaluateNextCommand(gameLogicParameters * parameters, commandQueue * queue) +{ + commandEvent * currentCommand = peekCommand(queue); + while(queue->lock); + queue->lock = true; + if(currentCommand == NULL) + { + return -1; + } + // Try command: Attempt to use a stat or skill on an object: + if(strncmp(currentCommand->command, "try", 3) == 0) + { + userMessage * tryMessage = malloc(sizeof(userMessage)); + tryMessage->senderName[0] = '\0'; + switch (getCoreStatFromString(currentCommand->arguments, 9)) + { + case STRENGTH: + { + switch (statCheck(currentCommand->caller, 20, STRENGTH)) + { + case CRITICAL_FAILURE: + { + strcpy(tryMessage->messageContent, "You weak, puny shit. Bet you don't even lift, bro.\n"); + break; + } + case FAILURE: + { + strcpy(tryMessage->messageContent, "Come on, bro, you should be able to get this set done.\n"); + break; + } + case SUCCESS: + { + strcpy(tryMessage->messageContent, "Nice set, bro. Keep it up.\n"); + break; + } + case CRITICAL_SUCCESS: + { + strcpy(tryMessage->messageContent, "HOLY SHIT, BRO! THAT'S SOME MAD REPS RIGHT THERE!\n"); + break; + } + default: + { + strcpy(tryMessage->messageContent, "I don't even, bro.\n"); + } + } + break; + } + default: + { + strcpy(tryMessage->messageContent, "Not at the moment, mate.\n"); + break; + } + } + queueTargetedOutputMessage(parameters->outputQueue, tryMessage, ¤tCommand->caller, 1); + free(tryMessage); + } + // Exit command: Sends an "empty" exit message to disconnect a client: + if(strncmp(currentCommand->command, "exit", 4) == 0) + { + userMessage * exitMessage = malloc(sizeof(userMessage)); + exitMessage->senderName[0] = '\0'; + exitMessage->messageContent[0] = '\0'; + queueTargetedOutputMessage(parameters->outputQueue, exitMessage, ¤tCommand->caller, 1); + free(exitMessage); + } + + // Move command: Moves the caller to a different area given a path name or number: + if(strncmp(currentCommand->command, "move", 4) == 0) + { + char requestedPath[32]; + if(strlen(currentCommand->arguments) > 0 && currentCommand->caller->currentArea != getAreaFromList(parameters->areaList, 0)) + { + strncpy(requestedPath, currentCommand->arguments, 32); + userNameSanatize(requestedPath, 32); + requestedPath[31] = '\0'; + if(movePlayerToArea(currentCommand->caller, requestedPath) == 0) + { + // Call the look command after moving. It's fine to unlock, because the loop won't + // continue until the command is queued: + queue->lock = false; + queueCommand(queue, "look", "", 5, 0, currentCommand->caller); + queue->lock = true; + } + } + } + + // Look command: Returns the description of the current area and paths: + if(strncmp(currentCommand->command, "look", 4) == 0) + { + char formattedString[64]; + userMessage * lookMessage = calloc(1, sizeof(userMessage)); + lookMessage->senderName[0] = '\0'; + strncat(lookMessage->messageContent, currentCommand->caller->currentArea->areaName, 33); + strncat(lookMessage->messageContent, "\n", 2); + strncat(lookMessage->messageContent, currentCommand->caller->currentArea->areaDescription, MAX - 35); + queueTargetedOutputMessage(parameters->outputQueue, lookMessage, ¤tCommand->caller, 1); + bzero(lookMessage, sizeof(userMessage)); + if(currentCommand->caller->currentArea->areaExits[0] != NULL) + { + strncat(lookMessage->messageContent, "You can go:", 13); + for(int index = 0; index < 16; index++) + { + if(currentCommand->caller->currentArea->areaExits[index] != NULL) + { + snprintf(formattedString, 64, "\n\t%d. %s", index + 1, currentCommand->caller->currentArea->areaExits[index]->pathName); + strncat(lookMessage->messageContent, formattedString, 64); + } + } + queueTargetedOutputMessage(parameters->outputQueue, lookMessage, ¤tCommand->caller, 1); + } + free(lookMessage); + } + // Join command: Allows the player to join the game given a name: + // TODO: Implement login/character creation. Will be a while: + if(strncmp(currentCommand->command, "join", 4) == 0) + { + if(currentCommand->caller->currentArea == getAreaFromList(parameters->areaList, 0)) + { + bool validName = true; + for(int index = 0; index < *parameters->playerCount; index++) + { + if(currentCommand->arguments[0] == '\0') + { + validName = false; + } + if(strncmp(currentCommand->arguments, parameters->connectedPlayers[index].playerName, 16) == 0) + { + validName = false; + } + } + if(validName) + { + strncpy(currentCommand->caller->playerName, currentCommand->arguments, 16); + currentCommand->caller->currentArea = getAreaFromList(parameters->areaList, 1); + // Call the look command after joining. It's fine to unlock, because the loop won't + // continue until the command is queued: + queue->lock = false; + queueCommand(queue, "look", "", 5, 0, currentCommand->caller); + queue->lock = true; + } + } + } + // Talk command: Allows the player to begin a chat session with another player: + if(strncmp(currentCommand->command, "talk", 4) == 0) + { + // TODO: Implement. + } + if(strncmp(currentCommand->command, "skillissue", 10) == 0) + { + userMessage * statMessage = calloc(1, sizeof(userMessage)); + statMessage->senderName[0] = '\0'; + strcpy(statMessage->messageContent, "Have you tried getting good?"); + queueTargetedOutputMessage(parameters->outputQueue, statMessage, ¤tCommand->caller, 1); + free(statMessage); + } + // Stat command: Displays the current character's sheet. + if(strncmp(currentCommand->command, "stat", 4) == 0) + { + char * formattedString = calloc(121, sizeof(char)); + userMessage * statMessage = calloc(1, sizeof(userMessage)); + statMessage->senderName[0] = '\0'; + // Basic status: Name, level, location. + snprintf(formattedString, 120, "%s, Level %d | %s\n", currentCommand->caller->playerName, + currentCommand->caller->stats->level, currentCommand->caller->currentArea->areaName); + strncat(statMessage->messageContent, formattedString, 120); + + // Current stats: Health and WISED. + snprintf(formattedString, 120, + "Health: %d/%d\nStats:\n\tWits: %2d | Intellect: %2d | Strength: %2d | Endurance: %2d | Dexerity: %2d \n", + currentCommand->caller->stats->currentHealth, currentCommand->caller->stats->maxHealth, + currentCommand->caller->stats->wits, currentCommand->caller->stats->intellect, + currentCommand->caller->stats->strength, currentCommand->caller->stats->endurance, + currentCommand->caller->stats->dexerity); + strncat(statMessage->messageContent, formattedString, 120); + + // Levelling stats: Current XP, and spec points. + if(currentCommand->caller->stats->specPoints > 0 || currentCommand->caller->stats->skillPoints > 0) + { + snprintf(formattedString, 120, "Current Experience: %ld | Spec Points Available: %d | Skill Points Available: %d", + currentCommand->caller->stats->experience, currentCommand->caller->stats->specPoints, currentCommand->caller->stats->skillPoints); + } + else + { + snprintf(formattedString, 120, "Current Experience: %ld", currentCommand->caller->stats->experience); + } + strncat(statMessage->messageContent, formattedString, 120); + + queueTargetedOutputMessage(parameters->outputQueue, statMessage, ¤tCommand->caller, 1); + bzero(statMessage->messageContent, sizeof(char) * MAX); + if(currentCommand->caller->skills->head != NULL) + { + skillNode * currentSkill = currentCommand->caller->skills->head; + int charCount = 0; + bool addNewline = false; + while(currentSkill != NULL) + { + snprintf(formattedString, 120, "| %2d | %31s ", + currentSkill->skill->skillPoints, currentSkill->skill->skillName); + charCount += 43; + strncat(statMessage->messageContent, formattedString, 120); + if((charCount + 43) >= MAX) + { + strncat(statMessage->messageContent, "\n", 2); + queueTargetedOutputMessage(parameters->outputQueue, statMessage, ¤tCommand->caller, 1); + bzero(statMessage, sizeof(userMessage)); + charCount = 0; + break; + } + else if(addNewline) + { + strncat(statMessage->messageContent, "|\n", 3); + charCount++; + addNewline = false; + } + else + { + addNewline = true; + } + currentSkill = currentSkill->next; + + } + queueTargetedOutputMessage(parameters->outputQueue, statMessage, ¤tCommand->caller, 1); + } + free(statMessage); + free(formattedString); + } + + // Spec command: Assign spec points to stats: + if(strncmp(currentCommand->command, "spec", 4) == 0) + { + userMessage * specMessage = calloc(1, sizeof(userMessage)); + specMessage->senderName[0] = '\0'; + char * formattedString = calloc(121, sizeof(char)); + if(currentCommand->caller->stats->specPoints > 0) + { + int selectedAmount = 0; + strtok(currentCommand->arguments, " "); + selectedAmount = atoi(¤tCommand->arguments[strlen(currentCommand->arguments) + 1]); + coreStat selectedStat = getCoreStatFromString(currentCommand->arguments, 16); + if(selectedAmount > 0 && (currentCommand->caller->stats->specPoints - selectedAmount) >= 0) + { + switch (selectedStat) + { + case WITS: + { + currentCommand->caller->stats->wits += selectedAmount; + strncat(specMessage->messageContent, "Increased wits.", 16); + currentCommand->caller->stats->specPoints -= selectedAmount; + break; + } + case INTELLECT: + { + currentCommand->caller->stats->intellect += selectedAmount; + strncat(specMessage->messageContent, "Increased intellect.", 21); + currentCommand->caller->stats->specPoints -= selectedAmount; + break; + } + case STRENGTH: + { + currentCommand->caller->stats->strength += selectedAmount; + strncat(specMessage->messageContent, "Increased strength.", 20); + currentCommand->caller->stats->specPoints -= selectedAmount; + break; + } + case ENDURANCE: + { + currentCommand->caller->stats->endurance += selectedAmount; + strncat(specMessage->messageContent, "Increased endurance.", 21); + currentCommand->caller->stats->specPoints -= selectedAmount; + break; + } + case DEXERITY: + { + currentCommand->caller->stats->dexerity += selectedAmount; + strncat(specMessage->messageContent, "Increased dexerity.", 21); + currentCommand->caller->stats->specPoints -= selectedAmount; + break; + } + case INVALID: + { + strncat(specMessage->messageContent, "Invalid stat.", 21); + } + } + } + else + { + strncat(specMessage->messageContent, "You have entered an invalid amount of spec points.", 51); + } + } + else + { + strncat(specMessage->messageContent, "You have no spec points available.", 35); + } + + // Send the message: + queueTargetedOutputMessage(parameters->outputQueue, specMessage, ¤tCommand->caller, 1); + + // Show the new stat sheet: + queue->lock = false; + queueCommand(queue, "stat", "", 5, 0, currentCommand->caller); + queue->lock = true; + + // Free the finished message: + free(specMessage); + free(formattedString); + } + if(strncmp(currentCommand->command, "skill", 5) == 0) + { + userMessage * skillMessage = calloc(1, sizeof(userMessage)); + skillMessage->senderName[0] = '\0'; + if((currentCommand->caller->stats->skillPoints - 1) >= 0) + { + int returnValue = takeSkill(parameters->globalSkillList, currentCommand->arguments, + strlen(currentCommand->arguments), currentCommand->caller); + switch(returnValue) + { + case -1: + { + strcpy(skillMessage->messageContent, "Not a valid skill."); + break; + } + case 0: + { + strcpy(skillMessage->messageContent, "Took "); + strcat(skillMessage->messageContent, currentCommand->arguments); + strcat(skillMessage->messageContent, "."); + currentCommand->caller->stats->skillPoints--; + break; + } + } + } + else + { + strcpy(skillMessage->messageContent, "You don't have enough skill points to take this skill.\n"); + } + queueTargetedOutputMessage(parameters->outputQueue, skillMessage, ¤tCommand->caller, 1); + free(skillMessage); + } + if(strncmp(currentCommand->command, "listskills", 10) == 0) + { + skillNode * currentSkill = parameters->globalSkillList->head; + userMessage * listMessage = calloc(1, sizeof(userMessage)); + char * formattedString = calloc(121, sizeof(char)); + int charCount = 0; + bool addNewline = false; + while(currentSkill != NULL) + { + snprintf(formattedString, 120, "| %-31s ", currentSkill->skill->skillName); + charCount += 43; + strncat(listMessage->messageContent, formattedString, 120); + if((charCount + 46) >= MAX) + { + queueTargetedOutputMessage(parameters->outputQueue, listMessage, ¤tCommand->caller, 1); + bzero(listMessage, sizeof(userMessage)); + charCount = 0; + addNewline = false; + } + else if(addNewline) + { + strncat(listMessage->messageContent, "|\n", 3); + charCount++; + addNewline = false; + } + else + { + addNewline = true; + } + currentSkill = currentSkill->next; + } + queueTargetedOutputMessage(parameters->outputQueue, listMessage, ¤tCommand->caller, 1); + free(listMessage); + free(formattedString); + } + // Remove the current command and unlock the queue: + currentCommand = NULL; + queue->lock = false; + dequeueCommand(queue); + return 0; +} + +// Run a stat check: +outcome statCheck(playerInfo * player, int chance, coreStat statToCheck) +{ + if(chance > 100 || chance < 0) + { + return ERROR; + } + chance = 100 - chance; + int modifier = 0; + switch(statToCheck) + { + case WITS: + { + modifier = player->stats->wits * 4; + break; + } + case INTELLECT: + { + modifier = player->stats->intellect * 4; + break; + } + case STRENGTH: + { + modifier = player->stats->strength * 4; + break; + } + case ENDURANCE: + { + modifier = player->stats->endurance * 4; + break; + } + case DEXERITY: + { + modifier = player->stats->dexerity * 4; + break; + } + default: + { + return ERROR; + } + } + int attempt = (random() % 100) + modifier; + if(attempt >= chance) + { + if(attempt >= 98) + { + return CRITICAL_SUCCESS; + } + else + { + return SUCCESS; + } + } + else + { + if(attempt <= 2) + { + return CRITICAL_FAILURE; + } + else + { + return FAILURE; + } + } +} + diff --git a/src/gamelogic.h b/src/gamelogic.h new file mode 100644 index 0000000..6db3980 --- /dev/null +++ b/src/gamelogic.h @@ -0,0 +1,89 @@ +// gamelogic.h: Header file contatning function prototypes and datastructures +// for dealing with the game's logic. +// Barry Kane, 2022. +#ifndef GAMELOGIC_H +#define GAMELOGIC_H +#include "lists.h" +#include "constants.h" +#include "playerdata.h" +#include "inputoutput.h" + +// ======================= +// -=[ Main Game Loop ]=-: +// ======================= + +// A datastructure containing the needed parameters for a main game loop: +typedef struct gameLogicParameters +{ + int * playerCount; + areaNode * areaList; + playerInfo * connectedPlayers; + inputMessageQueue * inputQueue; + outputMessageQueue * outputQueue; + skillList * globalSkillList; +} gameLogicParameters; + +// Thread function which runs the main game loop, given the needed parameters: +void * gameLogicLoop(void * parameters); + +// ====================== +// -=[ Command Queue ]=-: +// ====================== +typedef struct commandEvent commandEvent; +typedef struct commandEvent +{ + playerInfo * caller; + commandEvent * next; + char * command; + char * arguments; +} commandEvent; + +// A first-in first-out queue for message input from players: +typedef struct commandQueue +{ + bool lock; + bool paused; + int currentLength; + commandEvent * back; + commandEvent * front; +} commandQueue; + +// Create a commandQueue: +commandQueue * createCommandQueue(void); + +// Enqueue a command to a commandQueue: +int queueCommand(commandQueue * queue, char * command, char * arguments, + int commandLength, int argumentsLength , playerInfo * callingPlayer); + +// Enqueue a messaged command to a commandQueue: +int queueMessagedCommand(commandQueue * queue, inputMessage * messageToQueue); + +// Dequeue the front commandEvent from a commandQueue: +int dequeueCommand(commandQueue * queue); + +// Return the front commandEvent from a commandQueue: +commandEvent * peekCommand(commandQueue * queue); + +// Evaluate the next commandEvent: +int evaluateNextCommand(gameLogicParameters * parameters, commandQueue * queue); + +/* // Evaluate the given commandEvent: */ +/* int evaluateCommand(gameLogicParameters * parameters, commandEvent * command); */ + +// ============================ +// -=[ Gameplay Primitives ]=-: +// ============================ + +typedef enum outcome +{ + CRITICAL_FAILURE, + FAILURE, + SUCCESS, + CRITICAL_SUCCESS, + ERROR +} outcome; + +// Run a stat check: +outcome statCheck(playerInfo * player, int chance, coreStat statToCheck); + +#endif diff --git a/src/inputoutput.c b/src/inputoutput.c index f2caa6a..24882b1 100644 --- a/src/inputoutput.c +++ b/src/inputoutput.c @@ -6,9 +6,9 @@ #include #include #include -#include "../include/constants.h" -#include "../include/playerdata.h" -#include "../include/inputoutput.h" +#include "constants.h" +#include "playerdata.h" +#include "inputoutput.h" // Sends a message to a given TLS session, wraps the calls to gnutls_write: int messageSend(gnutls_session_t receivingSession, userMessage * messageToSend) @@ -34,7 +34,7 @@ int messageReceive(gnutls_session_t receiveFromSession, userMessage * receiveToM do { returnValue = gnutls_record_recv(receiveFromSession, receiveToMessage->senderName, - sizeof(((userMessage*)0)->senderName)); + sizeof(((userMessage*)0)->senderName)); } while (returnValue == GNUTLS_E_AGAIN || returnValue == GNUTLS_E_INTERRUPTED); do { @@ -313,7 +313,7 @@ int queueInputMessage(inputMessageQueue * queue, userMessage messageToQueue, pla queue->back = inputMessage; queue->currentLength++; - // Unlock the queue: + // Unlock the queue: queue->lock = false; return 0; @@ -335,6 +335,19 @@ void userInputSanatize(char * inputString, int length) inputString[length - 1] = '\0'; } +void userNameSanatize(char * inputString, int length) +{ + for(int index = 0; index <= length; index++) + { + if(!isprint(inputString[index])) + { + inputString[index] = '\0'; + break; + } + } + inputString[length - 1] = '\0'; +} + // Return the front inputMessage from an inputMessageQueue: inputMessage * peekInputMessage(inputMessageQueue * queue) { diff --git a/include/inputoutput.h b/src/inputoutput.h similarity index 96% rename from include/inputoutput.h rename to src/inputoutput.h index 00a05a9..177fac0 100644 --- a/include/inputoutput.h +++ b/src/inputoutput.h @@ -12,7 +12,7 @@ // A message datastructure containing a user/character name and the content: typedef struct userMessage -{ +{ char senderName[32]; char messageContent[MAX]; } userMessage; @@ -100,5 +100,8 @@ inputMessage * peekInputMessage(inputMessageQueue * queue); // Sanatize user input to ensure it's okay to send to the server: void userInputSanatize(char * inputString, int length); +// Sanatize user names so they display correctly; +void userNameSanatize(char * inputString, int length); + #endif diff --git a/src/lists.c b/src/lists.c index a7379d0..e0c79b6 100644 --- a/src/lists.c +++ b/src/lists.c @@ -1,7 +1,7 @@ // Implementation of lists library for SilverMUD. // Barry Kane, 2021 -#include "../include/lists.h" -#include "../include/playerdata.h" +#include "lists.h" +#include "playerdata.h" areaNode * createAreaList(playerArea * initialArea) { @@ -33,7 +33,7 @@ int addAreaNodeToList(areaNode * toList, playerArea * areaToAdd) current->next = malloc(sizeof(areaNode)); current->next->prev = current; current->next->data = areaToAdd; - current->next->next = NULL; + current->next->next = NULL; } int deleteAreaNodeFromList(areaNode * fromList, playerArea * areaToDelete) @@ -61,7 +61,7 @@ int addPathNodeToList(pathNode * toList, playerPath * pathToAdd) current = toList; while(current->next != NULL) { - current = current->next; + current = current->next; } current->next = malloc(sizeof(pathNode)); current->next->prev = current; diff --git a/include/lists.h b/src/lists.h similarity index 100% rename from include/lists.h rename to src/lists.h diff --git a/src/playerdata.c b/src/playerdata.c index b4f52c7..9d7ae58 100644 --- a/src/playerdata.c +++ b/src/playerdata.c @@ -1,18 +1,21 @@ // playerdata.c: Contains functions definitions for working with player data. // Barry Kane, 2021 +#include #include #include #include -#include "../include/playerdata.h" +#include +#include "constants.h" +#include "playerdata.h" // Move a player to a different area given a path in the area: -int movePlayerToArea(playerInfo * player, char * requestedPath) + int movePlayerToArea(playerInfo * player, char * requestedPath) { // Check if a number was given first: int selected = atoi(requestedPath); if(selected != 0) { - if(player->currentArea->areaExits[selected -1]->areaToJoin != NULL) + if(player->currentArea->areaExits[selected -1] != NULL && player->currentArea->areaExits[selected -1]->areaToJoin != NULL) { player->currentArea = player->currentArea->areaExits[selected - 1]->areaToJoin; return 0; @@ -42,13 +45,24 @@ int movePlayerToArea(playerInfo * player, char * requestedPath) // Create an area given a name and description: playerArea * createArea(char * nameString, char * descriptionString) { + // Allocate and zero memory for the new area: playerArea * createdArea = calloc(1, sizeof(playerArea)); - strncpy(createdArea->areaName, nameString, 32); - strncpy(createdArea->areaDescription, descriptionString, 256); + + // Copy the strings into the newly created area: + strncpy(createdArea->areaName, nameString, 32 - 1); + strncpy(createdArea->areaDescription, descriptionString, MAX - 35); + + // Properly null-terminate the strings: + createdArea->areaName[31] = '\0'; + createdArea->areaDescription[MAX] = '\0'; + + // Ensure that all the paths are set to NULL: for(int index = 0; index < 16; index++) { createdArea->areaExits[index] = NULL; } + + // Return the pointer: return createdArea; } @@ -78,13 +92,301 @@ int createPath(playerArea * fromArea, playerArea * toArea, char * fromDescriptio return 2; } } - playerPath * fromPath = calloc(1, sizeof(playerPath)); - playerPath * toPath = calloc(1, sizeof(playerPath)); + playerPath * fromPath = malloc(sizeof(playerPath)); + playerPath * toPath = malloc(sizeof(playerPath)); fromArea->areaExits[fromAreaSlot] = fromPath; toArea->areaExits[toAreaSlot] = toPath; - strncpy(fromArea->areaExits[fromAreaSlot]->pathName, fromDescription, 32); - strncpy(toArea->areaExits[toAreaSlot]->pathName, toDescription, 32); + strncpy(fromPath->pathName, fromDescription, 32 - 1); + fromPath->pathName[31] = '\0'; + strncpy(toPath->pathName, toDescription, 32 - 1); + toPath->pathName[31] = '\0'; fromArea->areaExits[fromAreaSlot]->areaToJoin = toArea; toArea->areaExits[toAreaSlot]->areaToJoin = fromArea; + return 0; } +// Create a new skill and add it to the global skill list: +int createSkill(skillList * globalSkillList, char * skillName, int skillNameLength, bool trainedSkill) +{ + if(skillNameLength >= 32) + { + fprintf(stderr, "Skill name is too long. Please shorten the name and try again.\n"); + return -1; + } + playerSkill * newSkill = malloc(sizeof(playerSkill)); + + strncpy(newSkill->skillName, skillName, 31); + newSkill->skillName[31] = '\0'; + + newSkill->skillPoints = 0; + newSkill->skillModifier = 0; + newSkill->trainedSkill = trainedSkill; + + // Add the skill to a node in the list: + return(addSkillNode(globalSkillList, newSkill)); +} + +// Add a skill node to a skill list: +int addSkillNode(skillList * skillList, playerSkill * skill) +{ + if(skillList->head == NULL) + { + skillList->head = malloc(sizeof(skillNode)); + skillList->head->skill = skill; + skillList->head->next = NULL; + skillList->skillCount = 1; + return 0; + } + else + { + skillNode * currentNode = skillList->head; + while(currentNode->next != NULL) + { + currentNode = currentNode->next; + } + currentNode->next = malloc(sizeof(skillNode)); + currentNode->next->skill = skill; + currentNode->next->next = NULL; + skillList->skillCount++; + return skillList->skillCount; + } +} + +// Remove a skill node from a skill list: +int removeSkillNode(skillList * skillList, playerSkill * skill) +{ + if(skillList->head->skill == skill) + { + free(skillList->head->skill); + if(skillList->head->next != NULL) + { + skillNode * deletedNode = skillList->head; + skillList->head = skillList->head->next; + free(deletedNode); + return 0; + } + else + { + skillNode * currentNode = skillList->head; + skillNode * previousNode = skillList->head; + while(currentNode->skill != skill) + { + if(currentNode->next == NULL) + { + return -1; + } + previousNode = currentNode; + currentNode = currentNode->next; + } + free(currentNode->skill); + previousNode->next = currentNode->next; + free(currentNode); + return 0; + } + } + return -1; +} + +// Take a skill and add it to the player's skill list: +int takeSkill(skillList * globalSkillList, char * skillName, int skillNameLength, playerInfo * targetPlayer) +{ + + skillNode * currentNode = globalSkillList->head; + while(strncmp(skillName, currentNode->skill->skillName, 32) != 0) + { + if(currentNode->next == NULL) + { + fprintf(stderr, "Skill doesn't exist in skill list.\n"); + return -1; + } + currentNode = currentNode->next; + } + + bool playerHasSkill = false; + skillNode * currentPlayerNode = targetPlayer->skills->head; + while(currentPlayerNode != NULL) + { + if(strncmp(skillName, currentPlayerNode->skill->skillName, skillNameLength) == 0) + { + playerHasSkill = true; + break; + } + currentPlayerNode = currentPlayerNode->next; + } + if(playerHasSkill) + { + currentPlayerNode->skill->skillPoints++; + } + else + { + addSkillNode(targetPlayer->skills, currentNode->skill); + currentPlayerNode = targetPlayer->skills->head; + while(currentPlayerNode->next != NULL) + { + currentPlayerNode = currentPlayerNode->next; + } + currentPlayerNode->skill->skillPoints = 1; + } + return 0; +} + + +// Take a string containing a core stat name and return the core stat: +coreStat getCoreStatFromString(char * inputString, int stringLength) +{ + // Check we've got a long enough string to fit a stat: + if(stringLength < 4) + { + return INVALID; + } + + // Lowercase the string: + char * string = malloc(sizeof(char) * stringLength); + for(int index = 0; index < stringLength; index++) + { + string[index] = tolower(inputString[index]); + } + + // If we have a string that's at most just the stat name plus a null character, or + // a dirtier string, we can check in a better order and ignore impossibilites: + if(stringLength < 9) + { + if(stringLength <= 4) + { + if(strncmp(string, "wits", 4) == 0) + { + free(string); + return WITS; + } + else + { + free(string); + return INVALID; + } + } + // Hopefully one of the seven letter long ones: + else if(stringLength <= 7) + { + if(strncmp(string, "strength", 7) == 0) + { + free(string); + return STRENGTH; + } + else if(strncmp(string, "dexerity", 7) == 0) + { + free(string); + return DEXERITY; + } + if(strncmp(string, "wits", 4) == 0) + { + free(string); + return WITS; + } + else + { + free(string); + return INVALID; + } + } + // Hopefully one of the 8 letter long stats: + else + { + if(strncmp(string, "intellect", 8) == 0) + { + free(string); + return INTELLECT; + } + else if(strncmp(string, "endurance", 8) == 0) + { + free(string); + return ENDURANCE; + } + else if(strncmp(string, "strength", 7) == 0) + { + free(string); + return STRENGTH; + } + else if(strncmp(string, "dexerity", 7) == 0) + { + free(string); + return DEXERITY; + } + if(strncmp(string, "wits", 4) == 0) + { + free(string); + return WITS; + } + else + { + free(string); + return INVALID; + } + } + } + // Worst case, it's definitely a dirty string, compare them all: + else + { + if(strncmp(string, "wits", 4) == 0) + { + free(string); + return WITS; + } + else if(strncmp(string, "intellect", 8) == 0) + { + free(string); + return INTELLECT; + } + else if(strncmp(string, "strength", 7) == 0) + { + free(string); + return STRENGTH; + } + else if(strncmp(string, "endurance", 8) == 0) + { + free(string); + return ENDURANCE; + } + else if(strncmp(string, "dexerity", 7) == 0) + { + free(string); + return DEXERITY; + } + else + { + free(string); + return INVALID; + } + } +} + +int deallocatePlayer(playerInfo * playerToDeallocate) +{ + // Deallocate the skill list: + if(playerToDeallocate->skills->skillCount > 0) + { + // Allocate enough pointers: + skillNode * nodesToDeallocate[playerToDeallocate->skills->skillCount]; + skillNode * currentSkillNode = playerToDeallocate->skills->head; + + // Get a list of all the nodes together: + for(int index = 0; index < playerToDeallocate->skills->skillCount; index++) + { + nodesToDeallocate[index] = currentSkillNode; + currentSkillNode = currentSkillNode->next; + } + + // Deallocate all the nodes: + for(int index = 0; index < playerToDeallocate->skills->skillCount; index++) + { + free(nodesToDeallocate[index]); + } + } + + // Deallocate the stat block: + free(playerToDeallocate->stats); + + // Deallocate the player: + free(playerToDeallocate); + + return 0; +} diff --git a/src/playerdata.h b/src/playerdata.h new file mode 100644 index 0000000..db51b04 --- /dev/null +++ b/src/playerdata.h @@ -0,0 +1,114 @@ +// playerdata.h: Header file containing data structures for player data and function +// prototypes for interacting with said data. +#ifndef PLAYERDATA_H +#define PLAYERDATA_H +#include +#include +#include "constants.h" + +typedef struct playerPath playerPath; +typedef struct playerArea playerArea; + +struct playerPath +{ + char pathName[32]; + playerArea * areaToJoin; +}; + +struct playerArea +{ + char areaName[32]; + char areaDescription[MAX - 35]; + playerPath * areaExits[16]; +}; + +typedef struct statBlock +{ + // Levelling: + int level; + long experience; + + // Health: + int currentHealth; + int maxHealth; + + // Core Stats: + int wits; + int intellect; + int strength; + int endurance; + int dexerity; + + // Character Building: + int specPoints; + int skillPoints; +} statBlock; + +typedef struct playerSkill +{ + char skillName[32]; + int skillPoints; + int skillModifier; + bool trainedSkill; +} playerSkill; + +typedef struct skillNode skillNode; +struct skillNode +{ + playerSkill * skill; + skillNode * next; +}; + +typedef struct skillList +{ + skillNode * head; + int skillCount; +} skillList; + +typedef struct playerInfo +{ + char playerName[32]; + playerArea * currentArea; + statBlock * stats; + skillList * skills; +} playerInfo; + +typedef enum coreStat +{ + WITS, + INTELLECT, + STRENGTH, + ENDURANCE, + DEXERITY, + INVALID +} coreStat; + +// Move a player to a different area given a path in the area: +int movePlayerToArea(playerInfo * player, char * requestedPath); + +// Create an area given a name and description: +playerArea * createArea(char * nameString, char * descriptionString); + +// Create a path between two areas given two areas and two strings: +int createPath(playerArea * fromArea, playerArea * toArea, char * fromDescription, char * toDescription); + +// Create a new skill and add it to the global skill list: +int createSkill(skillList * globalSkillList, char * skillName, int skillNameLength, bool trainedSkill); + +// Add a skill node to a skill list: +int addSkillNode(skillList * skillList, playerSkill * skill); + +// Remove a skill node from a skill list: +int removeSkillNode(skillList * skillList, playerSkill * skill); +int removeSkillByID(skillList * skillList, playerSkill * skill); + +// Take a skill and add it to the player's skill list: +int takeSkill(skillList * globalSkillList, char * skillName, int skillNameLength, playerInfo * targetPlayer); +int takeSkillbyID(skillList * globalSkillList, int skillID, playerInfo * targetPlayer); + +// Take a string containing a core stat name and return the core stat: +coreStat getCoreStatFromString(char * string, int stringLength); + +// Deallocate a player: +int deallocatePlayer(playerInfo * playerToDeallocate); +#endif diff --git a/src/server/SilverMUDServer.c b/src/server/SilverMUDServer.c index be97ab7..1e68c64 100644 --- a/src/server/SilverMUDServer.c +++ b/src/server/SilverMUDServer.c @@ -1,6 +1,7 @@ // Silverkin Industries Comm-Link Server, Engineering Sample Alpha 0.3. // PROJECT CODENAME: WHAT DO I PAY YOU FOR? | Level-3 Clearance. // Barry Kane, 2021 +#include #include #include #include @@ -14,17 +15,19 @@ #include #include #include -#include "../../include/lists.h" -#include "../../include/gamelogic.h" -#include "../../include/constants.h" -#include "../../include/playerdata.h" -#include "../../include/texteffects.h" -#include "../../include/inputoutput.h" + +#include "../lists.h" +#include "../gamelogic.h" +#include "../constants.h" +#include "../playerdata.h" +#include "../texteffects.h" +#include "../inputoutput.h" typedef struct sockaddr sockaddr; int main() { + time_t currentTime; bool keepRunning = true; int socketFileDesc, connectionFileDesc, length, clientsAmount, socketCheck, activityCheck, returnVal; @@ -38,25 +41,61 @@ int main() inputMessageQueue * inputQueue = createInputMessageQueue(); outputMessageQueue * outputQueue = createOutputMessageQueue(); + // -==[ TEST GAME-STATE INITIALIZATION ]==- // Initialize test areas: - areaNode * areas = createAreaList(createArea("Spawn - North", "A large area, mostly empty, as if the designer hadn't bothered to put anything in it, just yet.")); - addAreaNodeToList(areas, createArea("Spawn - South", "A strange, white void. You feel rather uncomfortable.")); - addAreaNodeToList(areas, createArea("Temple of Emacs", "A beautifully ornate statue of GNU is above you on a pedestal. Inscribed into the pillar, over and over, is the phrase \"M-x exalt\", in delicate gold letters. You can't help but be awestruck.")); - createPath(getAreaFromList(areas, 0), getAreaFromList(areas, 1), "To South Spawn", "To North Spawn"); - createPath(getAreaFromList(areas, 2), getAreaFromList(areas, 1), "Back to South Spawn", "Path to Enlightenment."); + areaNode * areas = createAreaList(createArea("Login Area", "Please login with the /join command.")); + addAreaNodeToList(areas, createArea("Temple Entrance", + "You are standing outside a large, elaborate temple, of white marble and delicate construction. " + "Etched onto the left pillar next to the large opening is the same symbol, over and over again, a gentle curve with it's ends pointing to the right. " + "A similar symbol is on the right pillar, but it's ends are pointing to the left. ")); + + addAreaNodeToList(areas, createArea("The Hall of Documentation", + "Just past the threshold of the entrance lies a large hall, with bookshelves lining the walls, ceiling to floor. " + "The shelves are filled to the brim with finely-bound books, each with titles in silver lettering on the spine. " + "There are countless books, but you notice a large lectern in the center of the room, and a path leading upwards at the back. ")); + + addAreaNodeToList(areas, createArea("Monument to GNU", + "A beautifully ornate statue of GNU is above you on a pedestal. " + "Inscribed into the pillar, over and over, is the phrase \"M-x exalt\", in delicate gold letters. " + "You can't help but be awestruck.")); + // Initialize test paths: + createPath(getAreaFromList(areas, 1), getAreaFromList(areas, 2), "Go inside the temple.", "Leave the temple."); + createPath(getAreaFromList(areas, 3), getAreaFromList(areas, 2), "Back to the Hall of Documentation.", "Path to Enlightenment."); + skillList * globalSkillList = malloc(sizeof(skillList)); + globalSkillList->head = NULL; + + // Create a few basic skills: + createSkill(globalSkillList, "Medicine", 8, true); + createSkill(globalSkillList, "Lockpicking", 12, true); + createSkill(globalSkillList, "Programming", 12, true); + createSkill(globalSkillList, "Sensor Reading", 14, false); + createSkill(globalSkillList, "Starship Piloting", 17, true); + createSkill(globalSkillList, "Mechanical Repair", 17, true); // Initialize playerdata: for (int index = 0; index < PLAYERCOUNT; index++) { sprintf(testString, "UNNAMED %d", index); + // OH NO IT'S NOT MEMORY SAFE BETTER REWRITE IT IN RUST + // But wait, we know the string won't be too big, so it's fine. strcpy(connectedPlayers[index].playerName, testString); connectedPlayers[index].currentArea = getAreaFromList(areas, 0); + connectedPlayers[index].stats = calloc(1, sizeof(statBlock)); + connectedPlayers[index].stats->specPoints = 30; + connectedPlayers[index].stats->skillPoints = 30; + connectedPlayers[index].skills = calloc(1, sizeof(skillList)); + connectedPlayers[index].skills->head = NULL; } + + // -==[ TEST GAME-STATE INITIALIZATION END ]==- // Give an intro: Display the Silverkin Industries logo and splash text. slowPrint(logostring, 3000); slowPrint("\n--==== \033[33;40mSILVERKIN INDUSTRIES\033[0m COMM-LINK SERVER ====--\nVersion Alpha 0.3\n", 5000); - + + // Seed random number generator from the current time: + srandom((unsigned) time(¤tTime)); + // Initialize the sockets to 0, so we don't crash. for (int index = 0; index < PLAYERCOUNT; index++) { @@ -67,7 +106,7 @@ int main() socketFileDesc = socket(AF_INET, SOCK_STREAM, 0); if (socketFileDesc == -1) { - perror("\tSocket Creation is:\t\033[33;40mRED.\033[0m Aborting launch.\n"); + fprintf(stderr, "\tSocket Creation is:\t\033[33;40mRED.\033[0m Aborting launch.\n"); exit(0); } @@ -75,7 +114,7 @@ int main() { slowPrint("\tSocket Creation is:\t\033[32;40mGREEN.\033[0m\n", 5000); } - + bzero(&serverAddress, sizeof(serverAddress)); // Assign IP and port: @@ -86,7 +125,7 @@ int main() // Binding newly created socket to given IP, and checking it works: if ((bind(socketFileDesc, (sockaddr*)&serverAddress, sizeof(serverAddress))) != 0) { - perror("\tSocket Binding is:\t\033[33;40mRED.\033[0m Aborting launch.\n"); + fprintf(stderr, "\tSocket Binding is:\t\033[33;40mRED.\033[0m Aborting launch.\n"); exit(0); } @@ -98,48 +137,53 @@ int main() // Let's start listening: if ((listen(socketFileDesc, PLAYERCOUNT)) != 0) { - perror("\tServer Listening is:\t\033[33;40mRED.\033[0m Aborting launch.\n"); + fprintf(stderr, "\tServer Listener is:\t\033[33;40mRED.\033[0m Aborting launch.\n"); exit(EXIT_FAILURE); } else { - slowPrint("\tServer Listening is:\t\033[32;40mGREEN.\033[0m\n", 5000); + slowPrint("\tServer Listener is:\t\033[32;40mGREEN.\033[0m\n", 5000); } length = sizeof(clientAddress); + // Declare the needed variables for TLS sessions: gnutls_session_t tlssessions[PLAYERCOUNT]; gnutls_anon_server_credentials_t serverkey = NULL; gnutls_anon_allocate_server_credentials(&serverkey); gnutls_anon_set_server_known_dh_params(serverkey, GNUTLS_SEC_PARAM_MEDIUM); - // Initialize all the TLS Sessions to NULL: We use this to check if it's an "empty connection." + // Initialize all the TLS sessions to NULL: We use this to check if it's an "empty connection." for (int index = 0; index < PLAYERCOUNT; index++) { tlssessions[index] = NULL; if (gnutls_init(&tlssessions[index], GNUTLS_SERVER) < 0) { - perror("\tTLS Sessions Initialization is:\t\033[33;40mRED.\033[0m Aborting launch.\n"); + fprintf(stderr, "\tTLS Sessions Initialization is:\t\033[33;40mRED.\033[0m Aborting launch.\n"); exit(EXIT_FAILURE); } gnutls_priority_set_direct(tlssessions[index], "NORMAL:+ANON-ECDH:+ANON-DH", NULL); gnutls_credentials_set(tlssessions[index], GNUTLS_CRD_ANON, &serverkey); gnutls_handshake_set_timeout(tlssessions[index], GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT); - } - slowPrint("\tTLS Sessions Initialization is:\t\033[32;40mGREEN.\033[0m\n", 5000); + } + slowPrint("\tTLS Preparation is:\t\033[32;40mGREEN.\033[0m\n", 5000); // Prepare the game logic thread: gameLogicParameters * gameLogicThreadParameters = malloc(sizeof(gameLogicParameters)); gameLogicThreadParameters->connectedPlayers = connectedPlayers; gameLogicThreadParameters->playerCount = &clientsAmount; + gameLogicThreadParameters->globalSkillList = globalSkillList; gameLogicThreadParameters->outputQueue = outputQueue; gameLogicThreadParameters->inputQueue = inputQueue; + gameLogicThreadParameters->areaList = areas; pthread_create(&gameLogicThread, NULL, &gameLogicLoop, gameLogicThreadParameters); + slowPrint("\tEvent Thread is:\t\033[32;40mGREEN.\033[0m\n", 5000); + slowPrint("=====\n", 5000); struct timeval timeout = {0, 500}; while(keepRunning) { - // Clear the set of file descriptors and add the master socket: + // Clear the set of file descriptors angad add the master socket: FD_ZERO(&connectedClients); FD_SET(socketFileDesc, &connectedClients); clientsAmount = socketFileDesc; @@ -168,7 +212,7 @@ int main() // Check if select() worked: if ((activityCheck < 0) && (errno != EINTR)) { - perror("Error in select(), retrying.\n"); + fprintf(stderr, "Error in select(), retrying.\n"); } // If it's the master socket selected, there is a new connection: @@ -176,7 +220,7 @@ int main() { if ((connectionFileDesc = accept(socketFileDesc, (struct sockaddr *)&clientAddress, (socklen_t*)&length)) < 0) { - perror("Failed to accept connection. Aborting.\n"); + fprintf(stderr, "Failed to accept connection. Aborting.\n"); exit(EXIT_FAILURE); } // See if we can put in the client: @@ -193,9 +237,13 @@ int main() returnVal = gnutls_handshake(tlssessions[index]); } while (returnVal < 0 && gnutls_error_is_fatal(returnVal) == 0); + + // Send a greeting message: strcpy(sendBuffer.senderName, ""); strcpy(sendBuffer.messageContent, "Welcome to the server!"); messageSend(tlssessions[index], &sendBuffer); + strcpy(receiveBuffer.messageContent, "/look"); + queueInputMessage(inputQueue, receiveBuffer, &connectedPlayers[index]); break; } } @@ -210,45 +258,46 @@ int main() if(FD_ISSET(socketCheck, &connectedClients)) { int returnVal = messageReceive(tlssessions[index], &receiveBuffer); + // If player has disconnected: if(returnVal == -10 || returnVal == 0) { + // Close the session: gnutls_bye(tlssessions[index], GNUTLS_SHUT_WR); gnutls_deinit(tlssessions[index]); shutdown(clientSockets[index], 2); close(clientSockets[index]); clientSockets[index] = 0; tlssessions[index] = NULL; + + // Clear out the old player state so that a new one may join: + sprintf(testString, "UNNAMED %d", index); + strcpy(connectedPlayers[index].playerName, testString); + connectedPlayers[index].currentArea = getAreaFromList(areas, 0); + + // Prepare a fresh SSL session for the next new player: gnutls_init(&tlssessions[index], GNUTLS_SERVER); gnutls_priority_set_direct(tlssessions[index], "NORMAL:+ANON-ECDH:+ANON-DH", NULL); gnutls_credentials_set(tlssessions[index], GNUTLS_CRD_ANON, &serverkey); gnutls_handshake_set_timeout(tlssessions[index], GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT); - } - else + } + // Otherwise, they've sent a message: + else { queueInputMessage(inputQueue, receiveBuffer, &connectedPlayers[index]); } } } } - // TEMPORARY: MOVE INPUT MESSAGES TO OUTPUT MESSAGES: - /* while(inputQueue->currentLength > 0) */ - /* { */ - /* inputMessage * message = peekInputMessage(inputQueue); */ - /* strncpy(message->content->senderName, message->sender->playerName, 32); */ - /* userInputSanatize(message->content->messageContent, MAX); */ - /* if(message->content->messageContent[0] != '\n') */ - /* { */ - /* queueOutputMessage(outputQueue, *message->content); */ - /* } */ - /* dequeueInputMessage(inputQueue); */ - /* } */ + // Run through the output queue and send all unused messages: while(outputQueue->currentLength != 0) { while(outputQueue->lock); outputQueue->lock = true; outputMessage * message = peekOutputMessage(outputQueue); outputQueue->lock = false; + + // If the first target is set to NULL, it's intended for all connected: if(message->targets[0] == NULL) { for (int index = 0; index < PLAYERCOUNT; index++) diff --git a/src/texteffects.c b/src/texteffects.c index 5920be3..11860ad 100644 --- a/src/texteffects.c +++ b/src/texteffects.c @@ -1,6 +1,7 @@ // texteffects.c: Implementation of text effect library for SilverMUD. // Barry Kane, 2021. #include +#include #include #include @@ -38,3 +39,60 @@ void slowPrintNcurses(char * stringToPrint, int delay, WINDOW * window, bool bol } wrefresh(window); } + +void bruteForcePrintNcurses(char * stringToPrint, int delay, WINDOW * window, bool bolded) +{ + int characterIndex = 0; + if(bolded) + { + wattron(window, A_BOLD); + } + while(stringToPrint[characterIndex] != '\0') + { + for(char currentCharacter = 32; currentCharacter <= stringToPrint[characterIndex]; currentCharacter++) + { + waddch(window, currentCharacter); + wrefresh(window); + usleep(delay); + waddch(window, 8); + wrefresh(window); + } + waddch(window, stringToPrint[characterIndex]); + characterIndex++; + } + if(bolded) + { + wattroff(window, A_BOLD); + } + wrefresh(window); +} + +void wrapString(char * stringToWrap, int stringLength, int screenWidth) +{ + int characterCount = 0; + for(int index = 0; index < stringLength; index++) + { + if(stringToWrap[index] == '\n') + { + characterCount = 0; + } + else + { + characterCount++; + } + if(characterCount == screenWidth) + { + while(!isspace(stringToWrap[index]) && index > 0) + { + index--; + } + if(index == 0) + { + return; + } + stringToWrap[index] = '\n'; + index++; + characterCount = 0; + } + } +} diff --git a/include/texteffects.h b/src/texteffects.h similarity index 95% rename from include/texteffects.h rename to src/texteffects.h index 38e5177..abcf5c7 100644 --- a/include/texteffects.h +++ b/src/texteffects.h @@ -14,4 +14,5 @@ void slowPrintNcurses(char * stringToPrint, int delay, WINDOW * window, bool bol // A string containing an ASCII art version of the Silverkin Industries logo. char * logostring = " ///////\n //////////////////////////////////////////\n ///////////////////////////////////////////////////////////\n ////////// ////////////////////////////\n ### # # # # ##### ### # # # # # /////////////////\n ### # # # # ## # # ## # ## # //////////////\n ## # # # # # ### # # # # # # /////////\n #### # ### # ##### # # # # # # ## ///////\n # ## # ##### # # ### ### ### # ##### ### ////// \n # # # # # # # # ## # # # # ## ## ////\n # # # # # # # # ## # ### # # ## //\n # # ### ##### ##### ### # # # # #### ### //\n"; +void wrapString(char * stringToWrap, int stringLength, int screenWidth); #endif diff --git a/tests/corestat-from-string.c b/tests/corestat-from-string.c new file mode 100644 index 0000000..ab70d82 --- /dev/null +++ b/tests/corestat-from-string.c @@ -0,0 +1,11 @@ +// Hopefully optimized corestat to string: +#include +#include +#include +#include +#include "../src/playerdata.h" + +void main(int argc, char ** argv) +{ + getCoreStatFromString(argv[1], strlen(argv[1])); +} diff --git a/tests/list-test.c b/tests/list-test.c index e522514..9fbeb6a 100644 --- a/tests/list-test.c +++ b/tests/list-test.c @@ -1,5 +1,5 @@ -#include "../misc/lists.h" -#include "../misc/playerdata.h" +#include "../src/lists.h" +#include "../src/playerdata.h" #include void main() diff --git a/tests/outputQueue-test.c b/tests/outputQueue-test.c index 9cc894d..a36fd43 100644 --- a/tests/outputQueue-test.c +++ b/tests/outputQueue-test.c @@ -1,4 +1,4 @@ -#include "../misc/inputoutput.h" +#include "../src/inputoutput.h" #include #include int main()