commit 8e9a80caf0f5d3d5c8c8bcc75b2882384c5d0e8f Author: Safariminer Date: Sat Jul 26 16:27:54 2025 -0400 initial commit where rendering still works diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd6e8dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +gameenv/data/maps +gameenv/data/textures +deps +.vs +x64 +*/x64 +x86 +*/x86 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e25e886 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# mpfw +## focusing on complete id control + +## Supported file types +- BSP29 (Quake maps) \ No newline at end of file diff --git a/gameenv/data/cfg/startup.cfg b/gameenv/data/cfg/startup.cfg new file mode 100644 index 0000000..471eec4 --- /dev/null +++ b/gameenv/data/cfg/startup.cfg @@ -0,0 +1,5 @@ +echo "MPFW Quake" +echo "Test Startup Script" + +mode 1 +map "data/maps/fullquake/start.bsp" diff --git a/gameenv/data/fonts/RobotoMono/LICENSE.txt b/gameenv/data/fonts/RobotoMono/LICENSE.txt new file mode 100644 index 0000000..38d9750 --- /dev/null +++ b/gameenv/data/fonts/RobotoMono/LICENSE.txt @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-Bold.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-Bold.ttf new file mode 100644 index 0000000..bef439f Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-Bold.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-BoldItalic.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-BoldItalic.ttf new file mode 100644 index 0000000..642dd05 Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-BoldItalic.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-ExtraLight.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-ExtraLight.ttf new file mode 100644 index 0000000..8cc41ca Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-ExtraLight.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-ExtraLightItalic.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-ExtraLightItalic.ttf new file mode 100644 index 0000000..47e4a4f Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-ExtraLightItalic.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-Italic.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-Italic.ttf new file mode 100644 index 0000000..781eff8 Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-Italic.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-Light.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-Light.ttf new file mode 100644 index 0000000..b6fb475 Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-Light.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-LightItalic.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-LightItalic.ttf new file mode 100644 index 0000000..48fec00 Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-LightItalic.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-Medium.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-Medium.ttf new file mode 100644 index 0000000..53fdd40 Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-Medium.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-MediumItalic.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-MediumItalic.ttf new file mode 100644 index 0000000..6916e76 Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-MediumItalic.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-Regular.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-Regular.ttf new file mode 100644 index 0000000..3806bfb Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-Regular.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-SemiBold.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-SemiBold.ttf new file mode 100644 index 0000000..b828c3a Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-SemiBold.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-SemiBoldItalic.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-SemiBoldItalic.ttf new file mode 100644 index 0000000..06032b5 Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-SemiBoldItalic.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-Thin.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-Thin.ttf new file mode 100644 index 0000000..71f1a46 Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-Thin.ttf differ diff --git a/gameenv/data/fonts/RobotoMono/RobotoMono-ThinItalic.ttf b/gameenv/data/fonts/RobotoMono/RobotoMono-ThinItalic.ttf new file mode 100644 index 0000000..a1b2e5a Binary files /dev/null and b/gameenv/data/fonts/RobotoMono/RobotoMono-ThinItalic.ttf differ diff --git a/gameenv/data/palette.lmp b/gameenv/data/palette.lmp new file mode 100644 index 0000000..7eefda1 Binary files /dev/null and b/gameenv/data/palette.lmp differ diff --git a/mpfw.sln b/mpfw.sln new file mode 100644 index 0000000..a4ba410 --- /dev/null +++ b/mpfw.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35527.113 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mpfw", "mpfw\mpfw.vcxproj", "{FB36E71A-46AB-4ECE-9438-E683C4208FB1}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "mpfw_server", "mpfw_server\mpfw_server.vcxproj", "{67CEAC9D-B0F3-4AB3-B3FF-0A9ADAD98E45}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FB36E71A-46AB-4ECE-9438-E683C4208FB1}.Debug|x64.ActiveCfg = Debug|x64 + {FB36E71A-46AB-4ECE-9438-E683C4208FB1}.Debug|x64.Build.0 = Debug|x64 + {FB36E71A-46AB-4ECE-9438-E683C4208FB1}.Debug|x86.ActiveCfg = Debug|Win32 + {FB36E71A-46AB-4ECE-9438-E683C4208FB1}.Debug|x86.Build.0 = Debug|Win32 + {FB36E71A-46AB-4ECE-9438-E683C4208FB1}.Release|x64.ActiveCfg = Release|x64 + {FB36E71A-46AB-4ECE-9438-E683C4208FB1}.Release|x64.Build.0 = Release|x64 + {FB36E71A-46AB-4ECE-9438-E683C4208FB1}.Release|x86.ActiveCfg = Release|Win32 + {FB36E71A-46AB-4ECE-9438-E683C4208FB1}.Release|x86.Build.0 = Release|Win32 + {67CEAC9D-B0F3-4AB3-B3FF-0A9ADAD98E45}.Debug|x64.ActiveCfg = Debug|x64 + {67CEAC9D-B0F3-4AB3-B3FF-0A9ADAD98E45}.Debug|x64.Build.0 = Debug|x64 + {67CEAC9D-B0F3-4AB3-B3FF-0A9ADAD98E45}.Debug|x86.ActiveCfg = Debug|Win32 + {67CEAC9D-B0F3-4AB3-B3FF-0A9ADAD98E45}.Debug|x86.Build.0 = Debug|Win32 + {67CEAC9D-B0F3-4AB3-B3FF-0A9ADAD98E45}.Release|x64.ActiveCfg = Release|x64 + {67CEAC9D-B0F3-4AB3-B3FF-0A9ADAD98E45}.Release|x64.Build.0 = Release|x64 + {67CEAC9D-B0F3-4AB3-B3FF-0A9ADAD98E45}.Release|x86.ActiveCfg = Release|Win32 + {67CEAC9D-B0F3-4AB3-B3FF-0A9ADAD98E45}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D438E333-35BA-4815-92C4-C0C289E3CD9B} + EndGlobalSection +EndGlobal diff --git a/mpfw/MPFW_CVars.cpp b/mpfw/MPFW_CVars.cpp new file mode 100644 index 0000000..b663ae6 --- /dev/null +++ b/mpfw/MPFW_CVars.cpp @@ -0,0 +1,16 @@ +#include "MPFW_CVars.h" + + +#include + + +std::variant MPFW::CVar::get() { + try { + int retval = std::stoi(value); + return retval; + } + catch(...){ + return value; + } +} + diff --git a/mpfw/MPFW_CVars.h b/mpfw/MPFW_CVars.h new file mode 100644 index 0000000..34f861b --- /dev/null +++ b/mpfw/MPFW_CVars.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include + +namespace MPFW { + class CVar { + public: + std::string value; + std::variant get(); + }; + + + using CVarMap = std::map; + +} + diff --git a/mpfw/MPFW_Console.cpp b/mpfw/MPFW_Console.cpp new file mode 100644 index 0000000..a1f5094 --- /dev/null +++ b/mpfw/MPFW_Console.cpp @@ -0,0 +1,175 @@ +#include "MPFW_Console.h" +#include +#include +#include + + +MPFW_ConsoleCommand(echo) { + for (int i = 0; i < cmd.size(); i++) { + *logStr += std::format("{} ", cmd[i]); + } + *logStr += "\n"; +} + +MPFW_ConsoleCommand(quit) { + exit(0); // some day there'll be proper unloading :3 +} + +MPFW_ConsoleCommand(mode) { + if (cmd.size() != 1) { + *logStr += "'mode' command usage:\n mode <0/1>\n\nModes:\n 0 - MPFW (default)\n 1 - Quake\n"; + } + else { + try { + int newMode = std::stoi(cmd[0]); + if (newMode > 1 || newMode < 0) throw std::out_of_range("not a mode"); + ctx->mode = (MPFW::Console::OperationMode)newMode; + *logStr += "Mode is set to '" + std::to_string(newMode) + "'\n"; + } + catch(...){ + *logStr += "'mode' command usage:\n mode <0/1>\n\nModes:\n 0 - MPFW (default)\n 1 - Quake\n"; + } + } +} + +MPFW_ConsoleCommand(map_quake) { + + if (cmd.size() != 1) { + *logStr += "'map' command usage:\n map \n"; + } + else { + try { + ctx->mapQuake->LoadBSPMap(cmd[0]); + *logStr += std::format("{}", ctx->mapQuake->data); + } + catch (...) { + *logStr += "Couldn't load map '" + cmd[0] + "'\n"; + } + } +} + +MPFW_ConsoleCommand(map_mpfw) { + + *logStr += "Mode MPFW has no \"map\" command because it has no available map format.\n\nUse one of these modes instead:\n - 'mode 1' : QUAKE\n\n"; +} + + +MPFW_ConsoleCommand(set) { + +} + + + +MPFW::Console::CommandHandler::CommandHandler(CommandHandlerResources* c) +{ + + chr = c; + +#pragma region + + + // System commands are defined manually à la Jason Thor Hall™ + // but that's to make absolutely f*cking sure that these + // elemental commands work. less important commands will be + // defined with a definition instantiator or some random shit + + CommandID mpfwEcho = { MPFW, "echo" }; + CommandID quakeEcho = { QUAKE, "echo" }; + functionMap[mpfwEcho] = echo; + functionMap[quakeEcho] = echo; + + + CommandID mpfwQuit = { MPFW, "quit" }; + CommandID quakeQuit = { QUAKE, "quit" }; + functionMap[mpfwQuit] = quit; + functionMap[quakeQuit] = quit; + + + CommandID mpfwMode = { MPFW, "mode" }; + CommandID quakeMode = { QUAKE, "mode" }; + functionMap[mpfwMode] = mode; + functionMap[quakeMode] = mode; + + CommandID mpfwMap = { MPFW, "map" }; + functionMap[mpfwMap] = map_mpfw; + + CommandID quakeMap = { QUAKE, "map" }; + functionMap[quakeMap] = map_quake; + +#pragma endregion System Commands +} + +std::vector MPFW::Console::CommandHandler::parseCommand(std::string s) +{ + + + bool inQString = false; + + + std::vector retval; + + std::string cstr; // current string + for (int i = 0; i < s.size(); i++) { + if (s[i] == '"') { + inQString = !inQString; + if (!inQString) { + retval.push_back(cstr); + cstr = ""; + } + } + else { + if (!inQString && s[i] == ' ') { + retval.push_back(cstr); + cstr = ""; + } + else cstr += s[i]; + } + } + + if(cstr != "") retval.push_back(cstr); + + return retval; + +} + +void MPFW::Console::CommandHandler::Run(std::string command, bool verbose) +{ + if(verbose) logStr += "\n" + std::string(chr->mode == QUAKE ? "QUAKE" : "MPFW") + "] " + command + "\n"; + std::vector cmdParseResult = parseCommand(command); + CommandID id; + if (cmdParseResult.size() > 0){ + id.n = cmdParseResult[0]; + cmdParseResult.erase(cmdParseResult.begin(), cmdParseResult.begin() + 1); + + id.m = chr->mode; + + if (functionMap.find(id) != functionMap.end()) { + functionMap[id](cmdParseResult, chr, &logStr); + } + else { + logStr += "Unknown command " + id.n + " in mode " + (chr->mode == QUAKE ? "QUAKE" : "MPFW") + "\n"; + } + + } + + +} + +void MPFW::Console::CommandHandler::RunScript(std::string path) +{ + std::ifstream i(path); + if (i.good() && i.is_open()) { + std::string line; + + while (std::getline(i, line)) { + Run(line, false); + } + } + else { + throw std::runtime_error("Can't run called script " + path); + } +} + +MPFW::Console::CommandHandler::~CommandHandler() +{ +} diff --git a/mpfw/MPFW_Console.h b/mpfw/MPFW_Console.h new file mode 100644 index 0000000..a82f3ea --- /dev/null +++ b/mpfw/MPFW_Console.h @@ -0,0 +1,84 @@ +#pragma once +#include +#include +#include "MPFW_Quake.h" +#include +#include + +#define MPFW_ConsoleCommand(x) void x(std::vector cmd, MPFW::Console::CommandHandlerResources* ctx, std::string* logStr) + +namespace MPFW { + namespace Console { + + enum OperationMode { + MPFW = 0, + QUAKE = 1 + }; + + struct CommandHandlerResources { + Quake::Maps::MapFile* mapQuake; + OperationMode mode = MPFW; + }; + + + + struct CommandID { + OperationMode m; + std::string n; + }; + inline bool operator==(const CommandID& left, const CommandID& right) { + return left.m == right.m && left.n == right.n; + } + + } +} + + +namespace std +{ + template<> + struct hash + { + typedef MPFW::Console::CommandID argument_type; + typedef std::size_t result_type; + result_type operator()(argument_type const& in) const + { + std::string t; + t = in.n + (in.m == MPFW::Console::QUAKE ? "QUAKE" : "MPFW"); + std::hash h; + + return h(t); + + } + }; +} + + +namespace MPFW{ + namespace Console{ + + class CommandHandler { + CommandHandlerResources* chr; + std::unordered_map,CommandHandlerResources*, std::string*)>> functionMap; + public: + + [[deprecated("Never initialize an empty command handler")]] + CommandHandler() { throw; } + + CommandHandler(CommandHandlerResources* chr); + + std::vector parseCommand(std::string s); + + std::string logStr; + void Run(std::string command, bool verbose = true); + void RunScript(std::string path); + + + + ~CommandHandler(); + }; + } +} + + + diff --git a/mpfw/MPFW_HL.cpp b/mpfw/MPFW_HL.cpp new file mode 100644 index 0000000..976085d --- /dev/null +++ b/mpfw/MPFW_HL.cpp @@ -0,0 +1,180 @@ +#include "MPFW_HL.h" +#include "MPFW_Utils.h" +#include +#include +#include +#include +#include + +MPFW::HL::WAD::WAD() +{ + std::cout << "Warning: Empty WAD instance created.\n"; +} + +MPFW::HL::WAD::WAD(std::string path) +{ + LoadWAD(path); +} + +void MPFW::HL::WAD::LoadWAD(std::string path) +{ + loaded = false; + lumps.clear(); + lumpCount = 0; + std::cout << "Loading file " << path << "...\n"; + std::ifstream file(path, std::ios::binary); + std::string filedata; + std::stringstream filestream; filestream << file.rdbuf(); + filedata = filestream.str();/* + if ( + + + MPFW::Utils::Strings::IterativeComp(filedata, 0, "IWAD")) { + wadType = IWAD; + std::cout << "File is an IWAD\n"; + } + else if (MPFW::Utils::Strings::IterativeComp(filedata, 0, "PWAD")) { + wadType = PWAD; + std::cout << "File is a PWAD\n"; + } + else { + throw std::runtime_error("Invalid WAD file: Error at header start(IWAD/PWAD)\n"); + }*/ + lumpCount = MPFW::Utils::Strings::StringIndexToInteger_4b_le(filedata, 4); + + std::cout << "File has " << lumpCount << " lumps\n"; + int directoryPointer = MPFW::Utils::Strings::StringIndexToInteger_4b_le(filedata, 8); + + std::string directory = MPFW::Utils::Strings::IterativeStringExcerpt(filedata, directoryPointer, 16 * lumpCount); + + for (int i = 0; i < lumpCount; i++) { + Lump lump; + std::string lumpIndex = MPFW::Utils::Strings::IterativeStringExcerpt(directory, i * 16, 16); + int lumpPointer = MPFW::Utils::Strings::StringIndexToInteger_4b_le(lumpIndex, 0); + int lumpSize = MPFW::Utils::Strings::StringIndexToInteger_4b_le(lumpIndex, 4); + lump.name = "", lump.data = ""; + for (int j = 0; j < 8; j++) { + if (lumpIndex.at(8 + j) != 0x0) lump.name += lumpIndex.at(8 + j); + } + + lump.data = MPFW::Utils::Strings::IterativeStringExcerpt(filedata, lumpPointer, lumpSize); + + lumps.push_back(lump); + + std::cout << "(" << i << "/" << lumpCount << ")" << "Loaded lump \"" << lump.name << "\" of size " << lumpSize << "\n"; + } + loaded = true; +} + +MPFW::HL::WAD::~WAD() +{ +} + +MPFW::HL::Map::Map() +{ +} + +MPFW::HL::Map::Map(std::string path) +{ + Load(path); +} + +void MPFW::HL::Map::Load(std::string path) +{ + std::ifstream in(path); + + if (!in.is_open()) throw std::runtime_error("Cannot open file " + path + "\n"); + + int scope = 0; + + + std::string line; + GenericUnknownClass* parsedClass = new GenericUnknownClass(); + WorldSpawnClass* worldSpawn = new WorldSpawnClass(); + bool inWorldSpawn = false; + while (std::getline(in, line)) { + + if (line == "{") { + scope++; + std::cout << "scope: " << scope << std::endl; + } + else if (line == "}") { + scope--; + std::cout << "scope: " << scope << std::endl; + if (scope == 0) { + if (inWorldSpawn) { + inWorldSpawn = false; + mapdata.push_back(worldSpawn); + } + else { + mapdata.push_back(parsedClass); + parsedClass = new GenericUnknownClass(); + } + } + } + else { + if (line != "") { + if (scope == 1) { + std::string key = MPFW::Utils::Strings::IterativeStringExcerpt_delim(line, 1, '"'); + std::string val = MPFW::Utils::Strings::IterativeStringExcerpt_delim(line, 4 + key.size(), '"'); + std::cout << key << " : " << val << "\n"; + if (key == "classname") { + if (val == "worldspawn") { + inWorldSpawn = true; + std::cout << "in world spawn\n"; + worldSpawn->classname = "worldspawn"; + } + else { + parsedClass->classname = val; + } + } + else if (!inWorldSpawn) parsedClass->fields[key] = val; + else { + if (key == "defaultteam") worldSpawn->defaultteam = std::stoi(val); + if (key == "newunit") worldSpawn->newunit = std::stoi(val); + if (key == "gametitle") worldSpawn->gametitle = std::stoi(val); + if (key == "startdark") worldSpawn->startdark = std::stoi(val); + if (key == "MaxRange") worldSpawn->maxrange = std::stoi(val); + if (key == "sounds") worldSpawn->sounds = std::stoi(val); + if (key == "mapversion") worldSpawn->mapversion = std::stoi(val); + if (key == "wad") worldSpawn->wad = val; + } + + } + else if (scope == 2) { + std::stringstream stream(line); + Plane plane; + std::string temp; + stream >> + temp >> plane.a.x >> plane.a.y >> plane.a.z >> temp >> + temp >> plane.b.x >> plane.b.y >> plane.b.z >> temp >> + temp >> plane.c.x >> plane.c.y >> plane.c.z >> temp >> + plane.texturename >> + temp >> plane.u.x >> plane.u.y >> plane.u.z >> plane.uOffset >> temp >> + temp >> plane.v.x >> plane.v.y >> plane.v.z >> plane.vOffset >> temp >> + plane.rotation >> plane.uScale >> plane.vScale; + + worldSpawn->planes.push_back(plane); + + } + else { + throw std::out_of_range("Out of scope content"); + } + } + } + } + + if (scope != 0) { + if (scope > 1) throw std::length_error("File " + path + " is visibly incomplete by scope arithmetics.\n"); + else { + std::cerr << "[WARNING] File " + path + " is missing a final closing bracket.\n"; + } + } + + in.close(); + +} + +MPFW::HL::Map::~Map() +{ +} diff --git a/mpfw/MPFW_HL.h b/mpfw/MPFW_HL.h new file mode 100644 index 0000000..a7d38ee --- /dev/null +++ b/mpfw/MPFW_HL.h @@ -0,0 +1,85 @@ +#pragma once +#include +#include +#include +#include + +namespace MPFW { + namespace HL { + struct Lump { + std::string name, data; + }; + + class WAD { // WAD3 file format + std::vector lumps; + public: + bool loaded = false; + int lumpCount; + WAD(); + WAD(std::string path); + void LoadWAD(std::string path); + std::vector getLumps() { + return lumps; + } + ~WAD(); + }; + + struct Plane { + Vector3 a, b, c, u, v; + std::string texturename; + float rotation, uOffset, uScale, vOffset, vScale; + + }; + + struct Brush { + std::vector planes; + }; + + class GenericClass { + public: + std::string classname; + virtual void onCreate() = 0; + virtual void onUpdate() = 0; + virtual void onDestroy() = 0; + }; + + class GenericOriginedClass : public GenericClass { + public: + Vector3 origin; + virtual void onCreate() = 0; + virtual void onUpdate() = 0; + virtual void onDestroy() = 0; + }; + + class GenericUnknownClass : public GenericClass{ + public: + std::map fields; + void onCreate() {} + void onUpdate() {} + void onDestroy() {} + }; + + class WorldSpawnClass : public GenericClass { + public: + std::string wad; + int defaultteam, newunit, gametitle, startdark, maxrange, sounds, mapversion; + std::vector planes; + void onCreate() {} + void onUpdate() {} + void onDestroy() {} + }; + + struct __HLParseKeyVal { + std::string key, val; + }; + + class Map { + public: + std::vector mapdata; + Map(); + Map(std::string path); + void Load(std::string path); + ~Map(); + }; + } +} \ No newline at end of file diff --git a/mpfw/MPFW_MPFWMF.h b/mpfw/MPFW_MPFWMF.h new file mode 100644 index 0000000..7c3ccc3 --- /dev/null +++ b/mpfw/MPFW_MPFWMF.h @@ -0,0 +1,43 @@ +#pragma once + +#include // we shalln't worry ourselves with full raylib if we're going to end up networking this bad bitch +#include // listening to le colibri nécrophile made me decide otherwise +#include +#include +#include + +namespace MPFW { + namespace MPFW { + namespace MF { + + typedef enum { + UNKNOWN, + PLAYERSTART, + DECORATION + } EntityType; + + class BaseEntity { + public: + EntityType entityType; + Vector3 origin; + virtual void Start(Map* map, int entityNum) = 0; + virtual void Update(Map* map, int entityNum) = 0; + virtual void Stop(Map* map, int entityNum) = 0; + virtual void Destroy(Map* map, int entityNum) = 0; + }; + + + class Map { + public: + + Model terrain; + + + Map(); + + + ~Map(); + }; + } + } +} \ No newline at end of file diff --git a/mpfw/MPFW_Quake.cpp b/mpfw/MPFW_Quake.cpp new file mode 100644 index 0000000..7461a26 --- /dev/null +++ b/mpfw/MPFW_Quake.cpp @@ -0,0 +1,461 @@ + +#include +#include "MPFW_Quake.h" +#include "MPFW_Utils.h" +#include +#include +#include +#include +#include + +#include + + +void MPFW::Quake::Maps::MapFile::LoadBSPMap(std::string path) +{ + std::ifstream fileHandle(path, std::ios::binary); // open the file as bin + + std::string buffer; + ds = Debug::NONE; + if (fileHandle.is_open() && fileHandle.good()) { + std::stringstream sstr; // get out of working with streams + sstr << fileHandle.rdbuf(); // as fast as possible + buffer = sstr.str(); // 3 LINES [WR SPEEDRUN] + + // operating on files as a string or a char array is simpler + // than writing some fancy << fuckery (even though it is nice + // to see some << fuckery work without any moving parts) + + } + else { + fileHandle.close(); + std::print("Bad file handle.\n"); + ds = Debug::DONE; + throw std::runtime_error("Bad file handle in Quake BSP parsing.\n"); + } + + fileHandle.close(); // close it as soon as we can get file loaded + // as to not disturb other programs + + + // we can now start the parsing + // hehehe >:3 + + + + // if file is smaller than header + if (buffer.size() < 124) { + std::print("Bad file content size.\n"); + throw std::runtime_error("File is smaller than header. Can't parse."); + } + + ds = Debug::HEADER; + data.header.version = 0; // initialize version for addition + + data.header.version += buffer[0]; + data.header.version += buffer[1] << 8; + data.header.version += buffer[2] << 16; + data.header.version += buffer[3] << 24; + + + + + + std::vector vecdir; + + + + for (int i = 4; i < 123; i += 8) { + + DirectoryEntry nd{0,0}; // new directory; + + nd.offset = Utils::Strings::StringIndexToInteger_4b_le(buffer, i); + nd.size = Utils::Strings::StringIndexToInteger_4b_le(buffer, i + 4); + + + vecdir.push_back(nd); + } + + + + // redistribution of entries + data.header.entities = vecdir[0]; + data.header.planes = vecdir[1]; + data.header.miptex = vecdir[2]; + data.header.vertices = vecdir[3]; + data.header.visilist = vecdir[4]; + data.header.nodes = vecdir[5]; + data.header.texinfo = vecdir[6]; + data.header.faces = vecdir[7]; + data.header.lightmaps = vecdir[8]; + data.header.clipnodes = vecdir[9]; + data.header.leaves = vecdir[10]; + data.header.lface = vecdir[11]; + data.header.edges = vecdir[12]; + data.header.ledges = vecdir[13]; + data.header.models = vecdir[14]; + + + data.models.clear(); + data.edges.clear(); + data.faces.clear(); + data.ledges.clear(); + data.texInfo.clear(); + data.vertices.clear(); + + + + // Loading Models + // we're loading models first because that's + // what the spec does <3 + + ds = Debug::MODELS; + +#ifdef MPFW_QUAKE_SLOW_LOADING + data.models.resize(data.header.models.size / 64); + + modelsCDBG = 0; + for (int i = 0; i < data.header.models.size / 64; i++) { + int base = data.header.models.offset + 64 * i; + + + qModel qm; + + + qm.bound.min.x = Utils::Strings::StringIndexToFloat_4b_le(buffer, base); + qm.bound.min.y = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 4); + qm.bound.min.z = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 8); + qm.bound.max.x = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 12); + qm.bound.max.y = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 16); + qm.bound.max.z = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 20); + + + qm.origin.x = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 24); + qm.origin.y = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 28); + qm.origin.z = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 32); + + qm.node_id0 = Utils::Strings::StringIndexToInteger_4b_le(buffer, base + 36); + qm.node_id1 = Utils::Strings::StringIndexToInteger_4b_le(buffer, base + 40); + qm.node_id2 = Utils::Strings::StringIndexToInteger_4b_le(buffer, base + 44); + qm.node_id3 = Utils::Strings::StringIndexToInteger_4b_le(buffer, base + 48); + + + qm.numleafs = Utils::Strings::StringIndexToInteger_4b_le(buffer, base + 52); + qm.face_id = Utils::Strings::StringIndexToInteger_4b_le(buffer, base + 56); + qm.face_num = Utils::Strings::StringIndexToInteger_4b_le(buffer, base + 60); + + + data.models[i] = qm; + modelsCDBG++; + + + // std::print("Model {}/{}\n", i, data.header.models.size / 64); + + } +#else + std::string modelsStr = Utils::Strings::IterativeStringExcerpt(buffer, data.header.models.offset, data.header.models.size); + data.models.resize(data.header.models.size / 64); + std::memcpy(data.models.data(), modelsStr.data(), data.header.models.size); +#endif + + + // parsing vertices + + ds = Debug::VERTICES; +#ifdef MPFW_QUAKE_SLOW_LOADING + verticesCDBG = 0; + data.vertices.resize(data.header.vertices.size / 12); + + for (int i = 0; i < data.header.vertices.size / 12; i++) { + int base = data.header.vertices.offset + (12 * i); + + Vector3 v; + + v.x = Utils::Strings::StringIndexToFloat_4b_le(buffer, base); + v.y = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 4); + v.z = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 8); + + data.vertices[i] = v; + + // std::print("Vertex {}/{}\n", i, data.header.vertices.size / 12); + verticesCDBG++; + } + +#else + + std::string verticesStr = Utils::Strings::IterativeStringExcerpt(buffer, data.header.vertices.offset, data.header.vertices.size); + data.vertices.resize(data.header.vertices.size / 12); + std::memcpy(data.vertices.data(), verticesStr.data(), data.header.vertices.size); +#endif + // parsing edges + + ds = Debug::EDGES; + +#ifdef MPFW_QUAKE_SLOW_LOADING + edgesCDBG = 0; + data.edges.resize(data.header.edges.size / 4); + + for (int i = 0; i < data.header.edges.size / 4; i++) { + int base = data.header.edges.offset + (4 * i); + Edge e; + e.vertex0 = Utils::Strings::StringIndexToInteger_2b_le(buffer, base); + e.vertex1 = Utils::Strings::StringIndexToInteger_2b_le(buffer, base+2); + data.edges[i] = e; + edgesCDBG++; + } +#else + + std::string edgeStr = Utils::Strings::IterativeStringExcerpt(buffer, data.header.edges.offset, data.header.edges.size); + data.edges.resize(data.header.edges.size / 4); + std::memcpy(data.edges.data(), edgeStr.data(), data.header.edges.size); +#endif + + ds = Debug::TEXINFO; + +#ifdef MPFW_QUAKE_SLOW_LOADING + texInfoCDBG = 0; + data.texInfo.resize(data.header.texinfo.size / 40); + for (int i = 0; i < data.header.texinfo.size / 40; i++) { + int base = data.header.texinfo.offset + (40 * i); + Surface s; + + s.vectorS.x = Utils::Strings::StringIndexToFloat_4b_le(buffer, base); + s.vectorS.y = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 4); + s.vectorS.z = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 8); + s.distS = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 12); + s.vectorT.x = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 16); + s.vectorT.y = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 20); + s.vectorT.z = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 24); + s.distT = Utils::Strings::StringIndexToFloat_4b_le(buffer, base + 28); + s.textureId = Utils::Strings::StringIndexToInteger_4b_le(buffer, base + 32); + s.animated = Utils::Strings::StringIndexToInteger_4b_le(buffer, base + 36); + + data.texInfo[i] = s; + texInfoCDBG++; + } +#else + + std::string texInfoStr = Utils::Strings::IterativeStringExcerpt(buffer, data.header.texinfo.offset, data.header.texinfo.size); + data.texInfo.resize(data.header.texinfo.size / 40); + std::memcpy(data.texInfo.data(), texInfoStr.data(), data.header.texinfo.size); +#endif + + ds = Debug::LEDGES; +#ifdef MPFW_QUAKE_SLOW_LOADING + for (int i = 0; i < data.header.ledges.size / 2; i++) { + int base = data.header.ledges.offset + (2 * i); + + + data.ledges.push_back(Utils::Strings::StringIndexToInteger_2b_le(buffer, base)); + + } +#else + + + std::string ledgesStr = Utils::Strings::IterativeStringExcerpt(buffer, data.header.ledges.offset, data.header.ledges.size); + data.ledges.resize(data.header.ledges.size / 4); + std::memcpy(data.ledges.data(), ledgesStr.data(), data.header.ledges.size); + + +#endif + + + // parsing faces + + ds = Debug::FACES; + + +#ifdef MPFW_QUAKE_SLOW_LOADING + + for (int i = 0; i < data.header.faces.size / 20; i++) { + int base = data.header.faces.offset + (20 * i); + + Face f; + + f.planeId = Utils::Strings::StringIndexToInteger_2b_le(buffer, base); + f.side = Utils::Strings::StringIndexToInteger_2b_le(buffer, base + 2); + + f.ledgeId = (signed long)(Utils::Strings::StringIndexToInteger_4b_le(buffer, base + 4)); + + f.ledgeNum = Utils::Strings::StringIndexToInteger_2b_le(buffer, base + 8); + + + f.texinfoId = Utils::Strings::StringIndexToInteger_2b_le(buffer, base + 10); + + f.typelight = buffer[base + 12]; + f.baselight = buffer[base + 13]; + f.light[0] = buffer[base + 14]; + f.light[1] = buffer[base + 15]; + f.lightmap = Utils::Strings::StringIndexToInteger_4b_le(buffer, base + 16); + + + data.faces.push_back(f); + + } + + +#else + + std::string facesStr = Utils::Strings::IterativeStringExcerpt( + buffer, + data.header.faces.offset, + data.header.faces.size + ); + data.faces.resize(data.header.faces.size / 20); + std::memcpy(data.faces.data(), facesStr.data(), data.header.faces.size); +#endif + + + + std::print("Loading mip textures manually for now\n"); + + + for (int i = 0; i < data.textures.size(); i++) { + rlUnloadTexture(data.textures[i].glTextureID); + } + cMipHeader cmh; + + cmh.numtex = Utils::Strings::StringIndexToInteger_4b_le(buffer, data.header.miptex.offset); + std::vector texOffsets; + // std::memcpy(texOffsets.data(), Utils::Strings::IterativeStringExcerpt(buffer, data.header.miptex.offset + 4, data.header.miptex.size - 4).data(), data.header.miptex.size - 4); + for (int i = 0; i < cmh.numtex; i++) { + texOffsets.push_back(Utils::Strings::StringIndexToInteger_4b_le(buffer, data.header.miptex.offset + 4 + 4*i)); + } + + + for (int i = 0; i < cmh.numtex; i++) { + unsigned int base = static_cast(texOffsets[i]) + (unsigned)data.header.miptex.offset; + + // this shit is ¤ undocumented! ¤ + // fuck the unofficial quake specs + if (texOffsets[i] == -1) { + // is a texoffset of 0xFFFFFFFF a reference to the fact that + // it starts right after the mipheader? probably not, but it + // makes the texture loading code happy so... :shrug: + base = (unsigned)data.header.miptex.offset + 4 + texOffsets.size() * 4; + } + + cMiptex t; + std::memcpy(&t, Utils::Strings::IterativeStringExcerpt(buffer, base, 40).data(), 40); + + Image img = GenImageColor(t.width, t.height, BLACK); + for (int y = 0; y < t.height; y++) { + for (int x = 0; x < t.width; x++) { + ImageDrawPixel(&img, x, y, + { + pal->data[(unsigned char)buffer[base + t.offset1 + (t.width * y + x)]].r, + pal->data[(unsigned char)buffer[base + t.offset1 + (t.width * y + x)]].g, + pal->data[(unsigned char)buffer[base + t.offset1 + (t.width * y + x)]].b, + 255 + } + ); + } + } + + Texture2D temp = LoadTextureFromImage(img); + + rlMipTex mt; + mt.glTextureID = temp.id; + mt.height = temp.height; + mt.width = temp.width; + mt.type = temp.format; + + UnloadImage(img); + data.textures.push_back(mt); + + } + + data.renderFaces.clear(); + + std::print("Precalculating faces and texture coordinates\n"); + + for (int i = 0; i < data.faces.size(); i++) { + CalculatedFace cface; + + for (int k = 0; k < data.faces[i].ledgeNum; k++) { + if (data.ledges[(unsigned int)(data.faces[i].ledgeId) + k] < 0) { + cface.vertices.push_back(MPFW::Quake::Maps::qVec2RLVec( + data.vertices[ + data.edges[ + abs( + data.ledges[ + data.faces[i].ledgeId + k + ] + ) + ].vertex0 + ] + )); + } + else { + cface.vertices.push_back(MPFW::Quake::Maps::qVec2RLVec( + data.vertices[ + data.edges[ + abs( + data.ledges[ + data.faces[i].ledgeId + k + ] + ) + ].vertex1 + ] + )); + } + + } + + + cface.glTextureId = data.textures[data.texInfo[data.faces[i].texinfoId].textureId].glTextureID; + cface.glTextureWidth = data.textures[data.texInfo[data.faces[i].texinfoId].textureId].width; + cface.glTextureHeight = data.textures[data.texInfo[data.faces[i].texinfoId].textureId].height; + + + for (int j = cface.vertices.size() - 1; j > 0; j--) { + + + cface.texCoords.push_back(Vector2( + (Vector3DotProduct(MPFW::Quake::Maps::RLVec2qVec(cface.vertices[0]), data.texInfo[data.faces[i].texinfoId].vectorS) + data.texInfo[data.faces[i].texinfoId].distS) / data.textures[data.texInfo[data.faces[i].texinfoId].textureId].width, + (Vector3DotProduct(MPFW::Quake::Maps::RLVec2qVec(cface.vertices[0]), data.texInfo[data.faces[i].texinfoId].vectorT) + data.texInfo[data.faces[i].texinfoId].distT) / data.textures[data.texInfo[data.faces[i].texinfoId].textureId].height + )); + cface.texCoords.push_back(Vector2( + (Vector3DotProduct(MPFW::Quake::Maps::RLVec2qVec(cface.vertices[j]), data.texInfo[data.faces[i].texinfoId].vectorS) + data.texInfo[data.faces[i].texinfoId].distS) / data.textures[data.texInfo[data.faces[i].texinfoId].textureId].width, + (Vector3DotProduct(MPFW::Quake::Maps::RLVec2qVec(cface.vertices[j]), data.texInfo[data.faces[i].texinfoId].vectorT) + data.texInfo[data.faces[i].texinfoId].distT) / data.textures[data.texInfo[data.faces[i].texinfoId].textureId].height + )); + cface.texCoords.push_back(Vector2( + (Vector3DotProduct(MPFW::Quake::Maps::RLVec2qVec(cface.vertices[j - 1]), data.texInfo[data.faces[i].texinfoId].vectorS) + data.texInfo[data.faces[i].texinfoId].distS) / data.textures[data.texInfo[data.faces[i].texinfoId].textureId].width, + (Vector3DotProduct(MPFW::Quake::Maps::RLVec2qVec(cface.vertices[j - 1]), data.texInfo[data.faces[i].texinfoId].vectorT) + data.texInfo[data.faces[i].texinfoId].distT) / data.textures[data.texInfo[data.faces[i].texinfoId].textureId].height + )); + } + + + data.renderFaces.push_back(cface); + + + } + + ds = Debug::DONE; + + + + +} +Vector3 MPFW::Quake::Maps::qVec2RLVec(Vector3 q) { + return { q.y / 2, q.z / 2, q.x / 2 }; +} +Vector3 MPFW::Quake::Maps::RLVec2qVec(Vector3 q) { + return { q.z * 2, q.x * 2, q.y * 2 }; +} + +void MPFW::Quake::Maps::Palette::LoadPalette(std::string path) +{ + std::ifstream i(path, std::ios::binary); + if (i.is_open() && i.good()) { + data.clear(); + data.resize(256); + std::stringstream s; + s << i.rdbuf(); + std::string d = s.str(); + + std::memcpy(data.data(), d.data(), 256 * 3); + + } + else throw std::runtime_error("can't open palette file " + path); +} diff --git a/mpfw/MPFW_Quake.h b/mpfw/MPFW_Quake.h new file mode 100644 index 0000000..9bba5b6 --- /dev/null +++ b/mpfw/MPFW_Quake.h @@ -0,0 +1,235 @@ +#pragma once +// based off of https://www.gamers.org/dEngine/quake/spec/quake-spec34/qkspec_4.htm +// (mostly) + +#include +#include +#include +#include + +namespace MPFW { + namespace Quake { + namespace Maps{ + + + namespace Debug { + enum DebugState { + NONE, + HEADER, + MODELS, + VERTICES, + EDGES, + TEXINFO, + LEDGES, + FACES, + DONE + }; + } + + + Vector3 qVec2RLVec(Vector3 q); + Vector3 RLVec2qVec(Vector3 q); + + struct DirectoryEntry { + long offset; + long size; + }; + + + + + struct BSPHeader { + long version; // must be 0x17 + DirectoryEntry entities; // list of entities + DirectoryEntry planes; // planes + DirectoryEntry miptex; // wall textures + DirectoryEntry vertices; // map vertices + DirectoryEntry visilist; // leaves visibility lists + DirectoryEntry nodes; // bsp nodes + DirectoryEntry texinfo; // texture info for faces + DirectoryEntry faces; // faces of each surface + DirectoryEntry lightmaps; // wall light maps + DirectoryEntry clipnodes; // clip nodes + DirectoryEntry leaves; // bsp leaves + DirectoryEntry lface; // list of faces + DirectoryEntry edges; // edges of faces + DirectoryEntry ledges; // list of edges + DirectoryEntry models; // list of models + }; + + struct qBoundingBox { + Vector3 min,max; + }; + + struct qBoundingBoxShort { + short minX, minY, minZ, maxX, maxY, maxZ; + }; + + struct qModel { + qBoundingBox bound; + Vector3 origin; + long node_id0, // first bsp node + node_id1, // first clip node + node_id2, // second clip node + node_id3; // usually zero + long numleafs; + long face_id, + face_num; + }; + struct Edge { + unsigned short vertex0, vertex1; + }; + struct Surface { + Vector3 vectorS; + float distS; + Vector3 vectorT; + float distT; + unsigned long textureId; + unsigned long animated; + }; + struct Face { + unsigned short planeId; + unsigned short side; + long ledgeId; + unsigned short ledgeNum; + unsigned short texinfoId; + + unsigned char typelight, baselight; + unsigned char light[2]; + long lightmap; + }; + + + struct CalculatedFace { + std::vector vertices; + std::vector texCoords; + int glTextureId, glTextureWidth, glTextureHeight; + }; + + + struct cMipHeader { + long numtex; + void *offset; + }; + + using MipHeader = std::vector; + + struct cMiptex{ + char name[16]; + unsigned long width, height, + offset1, offset2, offset4, offset8; + }; + + struct rlMipTex { + char name[16]; + unsigned long width, height; + unsigned long glTextureID; + int type; + }; + + + struct Node { + long planeId; + unsigned short front, back; + qBoundingBoxShort box; + unsigned short faceId, faceNum; + }; + struct Leaf { + long type, vislist; + qBoundingBoxShort bound; + unsigned short lfaceId, lfaceNum; + unsigned char sndWater, sndSky, sndSlime, sndLava; + }; + + + struct qColor { + unsigned char r, g, b; + }; + + + class Palette { + public: + std::vector data; + Palette() { data = std::vector(256); } + Palette(std::string path) { LoadPalette(path); } + + void LoadPalette(std::string path); + ~Palette() {} + }; + + struct MapData { + BSPHeader header; + + + std::vector vertices; + std::vector edges; + std::vector models; + + std::vector texInfo; + std::vector faces; + std::vector ledges; + std::vector textures; + + + + std::vector renderFaces; + + + }; + + class MapFile { + public: + Debug::DebugState ds = Debug::NONE; + MapData data; + Palette* pal; + + int modelsCDBG = 0; + int verticesCDBG = 0; + int edgesCDBG = 0; + int texInfoCDBG = 0; + int facesCDBG = 0; + + MapFile(){} + MapFile(std::string path) { + LoadBSPMap(path); + } + void LoadBSPMap(std::string path); + ~MapFile(){} + }; + } + } +} + + + +template<> +struct std::formatter { + + + constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } + auto format(const MPFW::Quake::Maps::DirectoryEntry& in, std::format_context& ctx) const { + return std::format_to(ctx.out(), "([MPFW::Quake::Maps::DirectoryEntry]: at pos {}, {}B in size)", in.offset, in.size); + } + +}; + + +template<> +struct std::formatter { + + + constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } + auto format(const MPFW::Quake::Maps::MapData& in, std::format_context& ctx) const { + return std::format_to(ctx.out(), + "([MPFW::Quake::Maps::MapFile]:\n" + " Vertices: {}\n" + " Edges: {}\n" + " Models: {}\n" + " TexInfo: {}\n" + " Faces: {}\n" + " Ledges: {}\n" + ")", in.vertices.size(), in.edges.size(), in.models.size(), in.texInfo.size(), in.faces.size(), in.ledges.size()); + } + +}; + diff --git a/mpfw/MPFW_UI.h b/mpfw/MPFW_UI.h new file mode 100644 index 0000000..d48fa96 --- /dev/null +++ b/mpfw/MPFW_UI.h @@ -0,0 +1,5 @@ +#pragma once + +namespace MPFW { + +} \ No newline at end of file diff --git a/mpfw/MPFW_Utils.cpp b/mpfw/MPFW_Utils.cpp new file mode 100644 index 0000000..e35026c --- /dev/null +++ b/mpfw/MPFW_Utils.cpp @@ -0,0 +1,153 @@ +#include "MPFW_Utils.h" +#include +#include +#include +bool MPFW::Utils::Strings::IterativeComp(std::string originalString, int startPointer, std::string toCompare) +{ + bool retVal = true; + for (int i = 0; i < toCompare.length(); i++) { + retVal = originalString.at(startPointer + i) == toCompare.at(i); + if (retVal == false) break; + } + return retVal; +} + +std::string MPFW::Utils::Strings::IterativeStringExcerpt(std::string originalString, int startPointer, int length) +{ + std::string retval = ""; + if (startPointer == -1) return retval; + for (int i = 0; i < length; i++) { + retval += originalString[startPointer + i]; + } + + return retval; +} + +std::string MPFW::Utils::Strings::IterativeStringExcerpt_delim(std::string originalString, int startPointer, char delimiter) +{ + std::string retval = ""; + if (startPointer == -1) return retval; + for (int i = startPointer; i < originalString.size(); i++) { + if (originalString[i] == delimiter) break; + retval += originalString[i]; + } + + return retval; +} + +int32_t MPFW::Utils::Strings::StringIndexToInteger_4b_le(std::string originalString, int startPointer) +{ + int32_t retval; + + retval = (unsigned char)originalString.at(startPointer + 3) << 24; + retval += (unsigned char)originalString.at(startPointer + 2) << 16; + retval += (unsigned char)originalString.at(startPointer + 1) << 8; + retval += (unsigned char)originalString.at(startPointer); + + + return retval; +} + +int32_t MPFW::Utils::Strings::StringIndexToInteger_2b_le(std::string originalString, int startPointer) +{ + int32_t retval; + + retval = (unsigned char)originalString.at(startPointer + 1) << 8; + retval += (unsigned char)originalString.at(startPointer); + + + return retval; +} + +std::tuple MPFW::Utils::Strings::ParseCLineIntoKeyVal(std::string originalString) +{ + std::tuple retval; + + int equPos = -1, valstartpos = -1, valendpos = -1; + bool inQuotes = false; + + for (int i = 0; i < originalString.size(); i++) { + if (originalString[i] == '=' and equPos == -1) { + equPos = i; + valstartpos = i + 1; + } + if (originalString[i] == '"') { + if (inQuotes) { + valendpos = i - 1; + break; + } + if (valstartpos == -1) { + valstartpos = i + 1; + inQuotes = true; + } + } + if (originalString[i] == ';' and not inQuotes) { + valendpos = i - 1; + } + + } + + try { + retval = std::make_tuple(IterativeStringExcerpt(originalString, 0, equPos), IterativeStringExcerpt(originalString, valstartpos, valendpos - valstartpos + 1)); + } + catch(...){ + retval = std::make_tuple("", ""); + } + return retval; +} + +std::string MPFW::Utils::Strings::SingleWordTrimmer(std::string originalString) +{ + for (int i = 0; i < originalString.size(); i++) { + if (originalString[i] == ' ' or originalString[i] == '/') return IterativeStringExcerpt(originalString, 0, i); + } + + return originalString; +} + +float MPFW::Utils::Strings::StringIndexToFloat_4b_le(std::string originalString, int startPointer) +{ + + + unsigned char buf[4]; + + buf[0] = originalString[startPointer]; + buf[1] = originalString[startPointer + 1]; + buf[2] = originalString[startPointer + 2]; + buf[3] = originalString[startPointer + 3]; + + + float retval = (*(float*)buf); + + return retval; + +} + +std::basic_string MPFW::Utils::Geometry::TesselateVertexString(VertexString vs) +{ + + std::basic_string retval; + + /*GLUtesselator* tess = gluNewTess(); + + + gluTessBeginPolygon(tess, 0); + + for (int i = 0; i < vs.size(); i++) { + GLdouble* coords; + coords[0] = static_cast(vs[i].x); + coords[1] = 0; + coords[2] = static_cast(vs[i].y); + gluTessVertex(tess, coords, coords); + } + gluTessEndPolygon(tess); + + + + gluDeleteTess(tess); + + */ + throw; + + return retval; +} diff --git a/mpfw/MPFW_Utils.h b/mpfw/MPFW_Utils.h new file mode 100644 index 0000000..49fe1c2 --- /dev/null +++ b/mpfw/MPFW_Utils.h @@ -0,0 +1,28 @@ +#pragma once +#include + +namespace MPFW { + namespace Utils { + namespace Geometry { + struct Vertex2D { + float x, y; + }; + struct Triangle2D { + float a, b, c; + }; + using VertexString = std::basic_string; + + std::basic_string TesselateVertexString(VertexString vs); + } + namespace Strings { + bool IterativeComp(std::string originalString, int startPointer, std::string toCompare); + std::string IterativeStringExcerpt(std::string originalString, int startPointer, int length); + std::string IterativeStringExcerpt_delim(std::string originalString, int startPointer, char delimiter); + int32_t StringIndexToInteger_4b_le(std::string originalString, int startPointer); + int32_t StringIndexToInteger_2b_le(std::string originalString, int startPointer); + std::tuple ParseCLineIntoKeyVal(std::string originalString); + std::string SingleWordTrimmer(std::string originalString); + float StringIndexToFloat_4b_le(std::string originalString, int startPointer); + } + } +} \ No newline at end of file diff --git a/mpfw/main.cpp b/mpfw/main.cpp new file mode 100644 index 0000000..34f6a84 --- /dev/null +++ b/mpfw/main.cpp @@ -0,0 +1,530 @@ +#include +#include + +#include +#include +#include +#include +#include "MPFW_Quake.h" +#include "MPFW_Console.h" +#include +#include +#include + + +// turn quake miptex into rl texture +Texture2D RLMT_QUAKE(MPFW::Quake::Maps::rlMipTex rlmt) { + Texture2D retval; + + + retval.id = rlmt.glTextureID; + retval.width = rlmt.width; + retval.height = rlmt.height; + retval.format = rlmt.type; + + return retval; +} + + +Camera camera = { 0 }; + +Vector3 rotation = { 1,0,0 }; +Vector3 hRotation = { 1,0,0 }; +Vector3 velocity = { 0,0,0 }; +float accelFactor = 300.0f; + +struct ConsoleSettings { + int height; +}; + + // this is grossly imprecise because of a lack of direct interfaces + // with the loop, but it's simple and should be performant in a +// thread +//static void __DebugCounter_Quake(MPFW::Quake::Maps::MapFile* mapFile) { +// while (mapFile->ds != MPFW::Quake::Maps::Debug::DONE) { +// switch (mapFile->ds) { +// case MPFW::Quake::Maps::Debug::HEADER: +// std::print("Reading header\n"); +// break; +// case MPFW::Quake::Maps::Debug::MODELS: +// std::print("Model {}/{}\n", mapFile->modelsCDBG, mapFile->data.header.models.size / 64); +// break; +// case MPFW::Quake::Maps::Debug::VERTICES: +// std::print("Vertex {}/{}\n", mapFile->verticesCDBG, mapFile->data.header.vertices.size / 12); +// break; +// case MPFW::Quake::Maps::Debug::EDGES: +// std::print("Edge {}/{}\n", mapFile->edgesCDBG, mapFile->data.header.edges.size / 4); +// break; +// case MPFW::Quake::Maps::Debug::TEXINFO: +// std::print("Surface {}/{}\n", mapFile->texInfoCDBG, mapFile->data.header.texinfo.size / 40); +// break; +// case MPFW::Quake::Maps::Debug::LEDGES: +// std::print("Ledge {}/{}\n", mapFile->data.ledges.size(), mapFile->data.header.ledges.size / 2); +// break; +// case MPFW::Quake::Maps::Debug::FACES: +// std::print("Face {}/{}\n", mapFile->data.faces.size(), mapFile->data.header.faces.size / 20); +// break; +// } +//#pragma warning(suppress : 4996) +// _sleep(250); +// } +//} + + + + +void Look() { + + hRotation = Vector3RotateByAxisAngle(hRotation, { 0,1,0 }, -GetMouseDelta().x * GetFrameTime()); + rotation = Vector3RotateByAxisAngle(rotation, { 0,1,0 }, -GetMouseDelta().x * GetFrameTime()); + rotation = Vector3Normalize({ rotation.x, Clamp(rotation.y - GetMouseDelta().y * GetFrameTime(), -0.99, 0.99), rotation.z }); +} + +void AirAccelerate(bool inAir) { + if (IsKeyDown(KEY_W)) { + velocity += hRotation * GetFrameTime() * accelFactor * (IsKeyDown(KEY_LEFT_SHIFT) ? 2 : 1); + } + if (IsKeyDown(KEY_S)) { + velocity -= hRotation * GetFrameTime() * accelFactor; + } + + Vector3 sideRotation = Vector3RotateByAxisAngle(hRotation, { 0,1,0 }, DEG2RAD * 90); + + if (IsKeyDown(KEY_A)) { + velocity += sideRotation * GetFrameTime() * accelFactor * (IsKeyDown(KEY_LEFT_SHIFT) ? 2 : 1); + } + if (IsKeyDown(KEY_D)) { + velocity -= sideRotation * GetFrameTime() * accelFactor * (IsKeyDown(KEY_LEFT_SHIFT) ? 2 : 1); + } + if (inAir)velocity.y -= GetFrameTime(); + velocity.x *= 16 * GetFrameTime(); + velocity.z *= 16 * GetFrameTime(); +} + +void Accelerate() { + if (IsKeyDown(KEY_SPACE))velocity.y += accelFactor*0.01; + AirAccelerate(false); + AirAccelerate(false); + + if (velocity.y < 0) velocity.y = 0; + velocity *= 16*GetFrameTime(); +} + + + +#ifdef _DEBUG + +template +void ___test___(T a, T b) { + if (a != b) throw; +} + +#endif + +int main() { + + +#ifdef _DEBUG + + std::print("COMPILED IN DEBUG MODE\n----------------------\n\n"); + + std::print("Unit testing\n\n\n"); + + { // UT01 : formatting a quake map directory entry + std::print("UT01 : formatting a quake map directory entry\n"); + MPFW::Quake::Maps::DirectoryEntry dirent; + + dirent.offset = 1000; + dirent.size = 2000; + + std::string retval = std::format("{}", dirent); + std::print("{}", retval); + ___test___(retval, std::string("([MPFW::Quake::Maps::DirectoryEntry]: at pos 1000, 2000B in size)")); + + std::println(); + std::print("UT01 Success\n"); + + } + + std::print("\nEnd of unit testing\n\n\n"); +#endif + + MPFW::Quake::Maps::MapFile map; + // std::thread t(__DebugCounter_Quake, &map); + + // t.detach(); + + std::vector colors; + + for (int i = 0; i < 3000000; i++) { + colors.push_back({ (unsigned char)GetRandomValue(0, 255), (unsigned char)GetRandomValue(0, 255), (unsigned char)GetRandomValue(0, 255), 255 }); + } + + std::print("MAP DATA:\n---------\n\n"); + std::print("Version: {}\n", map.data.header.version); + std::print("\n---\nDIRENTS\n-------\n"); + std::print("Entities: {}\n", map.data.header.entities); + std::print("Planes: {}\n", map.data.header.planes); + std::print("Wall Textures (miptex): {}\n", map.data.header.miptex); + std::print("Map Vertices: {}\n", map.data.header.vertices); + std::print("Leaves Visibility Lists: {}\n", map.data.header.visilist); + std::print("BSP Nodes: {}\n", map.data.header.nodes); + std::print("Texture Info for Faces: {}\n", map.data.header.texinfo); + std::print("Faces of each surface: {}\n", map.data.header.faces); + std::print("Wall Lightmaps: {}\n", map.data.header.lightmaps); + std::print("Clip Nodes: {}\n", map.data.header.clipnodes); + std::print("BSP Leaves: {}\n", map.data.header.leaves); + std::print("List of Faces: {}\n", map.data.header.lface); + std::print("Edges of Faces: {}\n", map.data.header.edges); + std::print("List of Edges: {}\n", map.data.header.ledges); + std::print("Models: {}\n", map.data.header.models); + + std::print("---\n\n"); + + std::print("Vertex count: {} (on {} in header)\n", map.data.vertices.size(), map.data.header.vertices.size / sizeof(Vector3)); + std::print("Edge count: {} (on {} in header)\n", map.data.edges.size(), map.data.header.edges.size / 4); + + std::print("Model count: {} (on {} in header)\n", map.data.models.size(), map.data.header.models.size / sizeof(MPFW::Quake::Maps::qModel)); + for (int i = 0; i < map.data.models.size(); i++) { + if (map.data.models[i].node_id3 != 0) { + std::print("node 3 on {} isn't 0 ({})\n", i, map.data.models[i].node_id3); + } + } + + map.pal = new MPFW::Quake::Maps::Palette("data/palette.lmp"); + + SetConfigFlags(FLAG_WINDOW_RESIZABLE); + + InitWindow(1280, 720, TextFormat("mpfw")); + SetTargetFPS(60); + SetExitKey(0); + DisableCursor(); + + + MPFW::Console::CommandHandlerResources chr; + chr.mapQuake = ↦ + + MPFW::Console::CommandHandler cmdH(&chr); + + Font robotoMonoRegular = LoadFontEx("data/fonts/RobotoMono/RobotoMono-Regular.ttf", 30, NULL, 6000); + int robotoMonoHeight = MeasureTextEx(robotoMonoRegular, "X", 30, 0).y; + + + cmdH.RunScript("data/cfg/startup.cfg"); + + + + // rlSetClipPlanes(1, INFINITY); + camera.position = { 0,5,0 }; + camera.target = { 1,0,0 }; + camera.fovy = 120; + camera.up = { 0,1,0 }; + camera.projection = CAMERA_PERSPECTIVE; + + bool indivFaceMode = false; + int indivFace = 0; + bool consoleOn = false; + std::string cmdBuf = ""; + while (!WindowShouldClose()) { + if (IsKeyPressed(KEY_APOSTROPHE)) { + consoleOn = !consoleOn; + } + if(!consoleOn){ + Look(); + if (camera.position.y > 6) AirAccelerate(true); + else Accelerate(); + + + if (IsKeyPressed(KEY_LEFT) && indivFace > 0) indivFace--; + if (IsKeyPressed(KEY_RIGHT) && indivFace < map.data.faces.size() - 1) indivFace++; + if (IsKeyPressed(KEY_F)) indivFaceMode = !indivFaceMode; + + } + + camera.position += velocity; + camera.target = camera.position + rotation; + BeginDrawing(); + ClearBackground(BLACK); + DrawRectangleGradientV(0, 0, GetScreenWidth(), GetScreenHeight(), WHITE, LIGHTGRAY); + + BeginMode3D(camera); + // DrawGrid(1000, 10); + if (!indivFaceMode) { + for (int i = 0; i < map.data.vertices.size(); i++) { + // DrawPoint3D(MPFW::Quake::Maps::qVec2RLVec(map.data.vertices[i]), BLACK); + // if (i != 0) DrawLine3D(MPFW::Quake::Maps::qVec2RLVec(map.data.vertices[i]), MPFW::Quake::Maps::qVec2RLVec(map.data.vertices[i - 1]), PURPLE); + } + for (int i = 0; i < map.data.edges.size(); i++) { + // DrawLine3D(MPFW::Quake::Maps::qVec2RLVec(map.data.vertices[map.data.edges[i].vertex0]), MPFW::Quake::Maps::qVec2RLVec(map.data.vertices[map.data.edges[i].vertex1]), PURPLE); + } + + for (int i = 0; i < map.data.faces.size(); i++) { + + std::vector vertices; + + + + + + // Vector3 a = MPFW::Quake::Maps::qVec2RLVec(map.data.vertices[map.data.edges[abs(map.data.ledges[map.data.faces[i].ledgeId])].vertex0]); + + + for (int k = 0; k < map.data.faces[i].ledgeNum; k++) { + if (map.data.ledges[(unsigned int)(map.data.faces[i].ledgeId) + k] < 0) { + vertices.push_back(MPFW::Quake::Maps::qVec2RLVec( + map.data.vertices[ + map.data.edges[ + abs( + map.data.ledges[ + map.data.faces[i].ledgeId + k + ] + ) + ].vertex0 + ] + )); + } + else { + vertices.push_back(MPFW::Quake::Maps::qVec2RLVec( + map.data.vertices[ + map.data.edges[ + abs( + map.data.ledges[ + map.data.faces[i].ledgeId + k + ] + ) + ].vertex1 + ] + )); + } + + + /*DrawLine3D(MPFW::Quake::Maps::qVec2RLVec( + map.data.vertices[ + map.data.edges[ + abs( + map.data.ledges[ + map.data.faces[i].ledgeId + k + ] + ) + ].vertex0 + ] + ), + + MPFW::Quake::Maps::qVec2RLVec( + map.data.vertices[ + map.data.edges[ + abs( + map.data.ledges[ + map.data.faces[i].ledgeId + k + ] + ) + ].vertex1 + ] + ), RED);*/ + + } + + for(int j = vertices.size() - 1; j > 0; j--){ + // rlColor4ub(colors[i].r, colors[i].g, colors[i].b, colors[i].a); + rlColor4ub(255, 255, 255, 255); + rlSetTexture(map.data.textures[map.data.texInfo[map.data.faces[i].texinfoId].textureId].glTextureID); + rlBegin(RL_QUADS); // textures don't work well with triangles in rlgl for some reason + + // rlTexCoord2f(0, 0); + rlTexCoord2f( + (Vector3DotProduct(MPFW::Quake::Maps::RLVec2qVec(vertices[0]), map.data.texInfo[map.data.faces[i].texinfoId].vectorS) + map.data.texInfo[map.data.faces[i].texinfoId].distS) / map.data.textures[map.data.texInfo[map.data.faces[i].texinfoId].textureId].width, + (Vector3DotProduct(MPFW::Quake::Maps::RLVec2qVec(vertices[0]), map.data.texInfo[map.data.faces[i].texinfoId].vectorT) + map.data.texInfo[map.data.faces[i].texinfoId].distT) / map.data.textures[map.data.texInfo[map.data.faces[i].texinfoId].textureId].height + ); + rlVertex3f(vertices[0].x, vertices[0].y, vertices[0].z); + // rlTexCoord2f(0, 1); + rlTexCoord2f( + (Vector3DotProduct(MPFW::Quake::Maps::RLVec2qVec(vertices[j]), map.data.texInfo[map.data.faces[i].texinfoId].vectorS) + map.data.texInfo[map.data.faces[i].texinfoId].distS) / map.data.textures[map.data.texInfo[map.data.faces[i].texinfoId].textureId].width, + (Vector3DotProduct(MPFW::Quake::Maps::RLVec2qVec(vertices[j]), map.data.texInfo[map.data.faces[i].texinfoId].vectorT) + map.data.texInfo[map.data.faces[i].texinfoId].distT) / map.data.textures[map.data.texInfo[map.data.faces[i].texinfoId].textureId].height + ); + rlVertex3f(vertices[j].x, vertices[j].y, vertices[j].z); + // rlTexCoord2f(1, 1); + rlTexCoord2f( + (Vector3DotProduct(MPFW::Quake::Maps::RLVec2qVec(vertices[j - 1]), map.data.texInfo[map.data.faces[i].texinfoId].vectorS) + map.data.texInfo[map.data.faces[i].texinfoId].distS) / map.data.textures[map.data.texInfo[map.data.faces[i].texinfoId].textureId].width, + (Vector3DotProduct(MPFW::Quake::Maps::RLVec2qVec(vertices[j - 1]), map.data.texInfo[map.data.faces[i].texinfoId].vectorT) + map.data.texInfo[map.data.faces[i].texinfoId].distT) / map.data.textures[map.data.texInfo[map.data.faces[i].texinfoId].textureId].height + ); + rlVertex3f(vertices[j - 1].x, vertices[j - 1].y, vertices[j - 1].z); + rlVertex3f(vertices[j - 1].x, vertices[j - 1].y, vertices[j - 1].z); + + rlEnd(); + // rlColor4ub(255, 255, 255, 255); + } + + + + + + } + } + else { + int i = indivFace; + + std::vector vertices; + + rlColor4ub(colors[i].r, colors[i].g, colors[i].b, colors[i].a); + rlBegin(RL_TRIANGLES); + + + + Vector3 a = MPFW::Quake::Maps::qVec2RLVec(map.data.vertices[map.data.edges[abs(map.data.ledges[map.data.faces[i].ledgeId])].vertex0]); + + + for (int k = 0; k < map.data.faces[i].ledgeNum; k++) { + if (map.data.ledges[map.data.faces[i].ledgeId + k] < 0) { + vertices.push_back(MPFW::Quake::Maps::qVec2RLVec( + map.data.vertices[ + map.data.edges[ + abs( + map.data.ledges[ + map.data.faces[i].ledgeId + k + ] + ) + ].vertex1 + ] + )); + } + else { + vertices.push_back(MPFW::Quake::Maps::qVec2RLVec( + map.data.vertices[ + map.data.edges[ + abs( + map.data.ledges[ + map.data.faces[i].ledgeId + k + ] + ) + ].vertex0 + ] + )); + } + + + DrawLine3D(MPFW::Quake::Maps::qVec2RLVec( + map.data.vertices[ + map.data.edges[ + abs( + map.data.ledges[ + map.data.faces[i].ledgeId + k + ] + ) + ].vertex0 + ] + ), + + MPFW::Quake::Maps::qVec2RLVec( + map.data.vertices[ + map.data.edges[ + abs( + map.data.ledges[ + map.data.faces[i].ledgeId + k + ] + ) + ].vertex1 + ] + ), RED); + + } + + + rlVertex3f(vertices[0].x, vertices[0].y, vertices[0].z); + + rlVertex3f(vertices[2].x, vertices[2].y, vertices[2].z); + rlVertex3f(vertices[1].x, vertices[1].y, vertices[1].z); + + + + + rlEnd(); + rlColor4f(255, 255, 255, 255); + } + EndMode3D(); + DrawFPS(0, 0); + if (indivFaceMode) { + + + std::vector vertices; + { + int i = indivFace; + + + rlColor4ub(colors[i].r, colors[i].g, colors[i].b, colors[i].a); + rlBegin(RL_TRIANGLES); + + + + Vector3 a = MPFW::Quake::Maps::qVec2RLVec(map.data.vertices[map.data.edges[abs(map.data.ledges[map.data.faces[i].ledgeId])].vertex0]); + + + for (int k = 0; k < map.data.faces[i].ledgeNum; k++) { + if (map.data.ledges[map.data.faces[i].ledgeId + k] < 0) { + vertices.push_back(MPFW::Quake::Maps::qVec2RLVec( + map.data.vertices[ + map.data.edges[ + abs( + map.data.ledges[ + map.data.faces[i].ledgeId + k + ] + ) + ].vertex1 + ] + )); + } + else { + vertices.push_back(MPFW::Quake::Maps::qVec2RLVec( + map.data.vertices[ + map.data.edges[ + abs( + map.data.ledges[ + map.data.faces[i].ledgeId + k + ] + ) + ].vertex0 + ] + )); + } + + + + } + + } + DrawText(TextFormat("Current face: %i", indivFace), 0, 30, 30, BLACK); + for (int i = 0; i < vertices.size(); i++) { + DrawText(TextFormat("Vertex %i : %f %f %f", i, vertices[i].x, vertices[i].y, vertices[i].z), 0, 60 + 10 * i, 10, BLACK); + } + } + + if (consoleOn) { + DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight() / 2, {0,0,0,200}); + DrawTextEx(robotoMonoRegular, cmdH.logStr.c_str(), { 0, GetScreenHeight() / 2 - MeasureTextEx(robotoMonoRegular, cmdH.logStr.c_str(), 20, 0).y }, 20, 0, WHITE); + + DrawRectangle(0, GetScreenHeight() / 2, GetScreenWidth(), 20, { 0,0,0,200 }); + DrawLine(0, GetScreenHeight() / 2, GetScreenWidth(), GetScreenHeight() / 2, GOLD); + DrawTextEx(robotoMonoRegular, cmdBuf.c_str(), { 0, (float)GetScreenHeight() / 2 }, 20, 0, GRAY); + + int key = GetCharPressed(); + while (key > 0) { + if ((key >= 32) && (key <= 126)) { + cmdBuf += (char)key; + + } + key = GetCharPressed(); + } + if (IsKeyPressed(KEY_BACKSPACE) && cmdBuf != "") cmdBuf.pop_back(); + + if (IsKeyPressed(KEY_ENTER)) { + cmdH.Run(cmdBuf); + cmdBuf = ""; + } + + + + } + EndDrawing(); + } + + CloseWindow(); +} diff --git a/mpfw/mpfw.vcxproj b/mpfw/mpfw.vcxproj new file mode 100644 index 0000000..f65ad46 --- /dev/null +++ b/mpfw/mpfw.vcxproj @@ -0,0 +1,159 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {fb36e71a-46ab-4ece-9438-e683c4208fb1} + mpfw + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + true + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpplatest + $(SOLUTIONDIR)deps/include + + + Console + true + $(SOLUTIONDIR)deps/lib + raylib.lib;winmm.lib;gdi32.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + MSVCRT + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpplatest + $(SOLUTIONDIR)deps/include + + + Console + true + true + true + $(SOLUTIONDIR)deps/lib + raylib.lib;winmm.lib;gdi32.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + MSVCRT + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mpfw/mpfw.vcxproj.filters b/mpfw/mpfw.vcxproj.filters new file mode 100644 index 0000000..2ff9031 --- /dev/null +++ b/mpfw/mpfw.vcxproj.filters @@ -0,0 +1,60 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/mpfw/mpfw.vcxproj.user b/mpfw/mpfw.vcxproj.user new file mode 100644 index 0000000..d011c1f --- /dev/null +++ b/mpfw/mpfw.vcxproj.user @@ -0,0 +1,11 @@ + + + + $(SolutionDir)/gameenv + WindowsLocalDebugger + + + $(SolutionDir)/gameenv + WindowsLocalDebugger + + \ No newline at end of file diff --git a/mpfw_server/Config.h b/mpfw_server/Config.h new file mode 100644 index 0000000..8a83b96 --- /dev/null +++ b/mpfw_server/Config.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace Config { + + + struct Config { + unsigned char maxPlayers = 16; // maximum players that can fit on the server. + // original quake specs say up to 16, glquake slist + // can show up to 255 (standard calls for uchar). + + std::string serverName = "MPFWTESTSRV"; // left column in slist + std::string serverBanner = "oh yeah this a mfing server!"; + std::string ip = "192.168.56.1"; + char unknownMagic[2] = {'x', 'D'}; // unknown short sent at the end of accepted connection request replies + + int udpControl = 26000; // port to which clients will connect for control + + }; + + +} \ No newline at end of file diff --git a/mpfw_server/NetQuake.cpp b/mpfw_server/NetQuake.cpp new file mode 100644 index 0000000..55304f1 --- /dev/null +++ b/mpfw_server/NetQuake.cpp @@ -0,0 +1,534 @@ +#include "NetQuake.h" +#include // basic io +#include // dynamic arrays +#include // keyval maps +#include +#include +#include +#include +#include +#include +#include + +NetQuake::SerializedGenericPacket NetQuake::Serialize(NetworkGenericPacket ngp) +{ + SerializedGenericPacket sgp; + + + unsigned short typeAsShort; + + typeAsShort = (ngp.type[0] << 8) + ngp.type[1]; + sgp.type = (PacketType)typeAsShort; + sgp.length = (ngp.length[0] << 8) + ngp.length[1]; + + sgp.remainder = ngp.remainder; + + return sgp; +} + +NetQuake::GenericGamePacket NetQuake::toGenericGamePacket(SerializedGenericPacket sgp) +{ + GenericGamePacket retval; + try { + + int currentPacketLength = 0; + int currentPacketAt = 0; + bool inString = false; + for (int i = 0; i < sgp.remainder.size(); i++) { + if (currentPacketLength == 0 && !inString) { + if (sgp.remainder[i] == CLIENT_MESSAGE_NOOP) { + ClientMessage msg; + msg.type = CLIENT_MESSAGE_NOOP; + retval.messageBlock.messages.push_back(msg); + } + if (sgp.remainder[i] == CLIENT_MESSAGE_KEEPALIVE) { + ClientMessage msg; + msg.type = CLIENT_MESSAGE_NOOP; + retval.messageBlock.messages.push_back(msg); + } + if (sgp.remainder[i] == CLIENT_MESSAGE_MOVEMENT) { + ClientMessage msg; + msg.type = CLIENT_MESSAGE_MOVEMENT; + retval.messageBlock.messages.push_back(msg); + currentPacketLength = 15; + } + if (sgp.remainder[i] == CLIENT_MESSAGE_CONSOLE) { + ClientMessage msg; + msg.type = CLIENT_MESSAGE_CONSOLE; + retval.messageBlock.messages.push_back(msg); + + } + } + else if (inString) { + if (sgp.remainder[i] == 0x00) inString = false; + else { + retval.messageBlock.messages[retval.messageBlock.messages.size() - 1].data += sgp.remainder[i]; + } + } + else { + retval.messageBlock.messages[retval.messageBlock.messages.size() - 1].data += sgp.remainder[i]; + currentPacketAt++; + if (currentPacketAt == currentPacketLength) { + currentPacketLength = 0; + currentPacketAt = 0; + } + } + } + + retval.valid = true; + } + catch (std::exception& e) { + std::println("Something went wrong in serialized generic packet to generic game packet conversion: {}", e.what()); + } + + return retval; +} + +std::string NetQuake::Serialize(ServerInfoReply sirp) +{ + std::string retval = ""; + + using namespace std::string_literals; + retval += CONTROL_SERVER_INFO_REPLY; + retval += sirp.address.ip + ":"; + retval += std::to_string(sirp.address.port) + "\0"s; + retval += sirp.hostname + "\0"s; + retval += sirp.levelname + "\0"s; + retval += sirp.currentPlayers; + retval += sirp.maxPlayers; + retval += sirp.netVersion; + + short retvalsize = retval.size() + 4; + unsigned char retvalsizechar[2]; + + retvalsizechar[0] = retvalsize & 0xff; + retvalsizechar[1] = (retvalsize >> 8) & 0xff; + + + std::string fretval = "\x80"; + fretval += "\0"s; + fretval += retvalsizechar[1]; + fretval += retvalsizechar[0]; + fretval += retval; + return fretval; +} + +std::string NetQuake::Serialize(ConnectionRequestReply crr) +{ + std::string retval = ""; + using namespace std::string_literals; + + + if (crr.reject) { + retval = "\x82" + crr.reason + "\0"s; + } + else { + + unsigned char portchar[2]; + portchar[1] = crr.port & 0xff; + portchar[0] = (crr.port >> 8) & 0xff; + retval = "\x81"s; + retval += portchar[1]; + retval += portchar[0]; + retval += (unsigned char)crr.unknown[0]; + retval += (unsigned char)crr.unknown[1]; + } + + + short retvalsize = retval.size() + 4; + unsigned char retvalsizechar[2]; + + retvalsizechar[0] = retvalsize & 0xff; + retvalsizechar[1] = (retvalsize >> 8) & 0xff; + + std::string fretval = "\x80"; + fretval += "\0"s; + fretval += retvalsizechar[1]; + fretval += retvalsizechar[0]; + fretval += retval; + return fretval; +} + +std::string NetQuake::AcknowledgePacket(int packet) +{ + std::string retval; + using namespace std::string_literals; + retval += reinterpret_cast(packet); + short retvalsize = retval.size() + 4; + unsigned char retvalsizechar[2]; + + retvalsizechar[0] = retvalsize & 0xff; + retvalsizechar[1] = (retvalsize >> 8) & 0xff; + + std::string fretval = "\0"s; + fretval += "\x10"s; + fretval += retvalsizechar[1]; + fretval += retvalsizechar[0]; + fretval += retval; + return fretval; +} + +void NetQuake::NetQuake_ControlServer(Internal::InternalRequestConsole* internalRequestConsole, Config::Config* config, bool* serverRunning) +{ + + asio::io_context io_context; + asio::ip::udp::socket socket(io_context, asio::ip::udp::endpoint(asio::ip::udp::v4(), config->udpControl)); + std::print("Started control server on {}\n", config->udpControl); + + NetQuake::ServerInfoReply sir; + sir.address.ip = config->ip; + sir.address.port = config->udpControl; + sir.currentPlayers = 0; + sir.maxPlayers = config->maxPlayers; + + sir.hostname = config->serverName; + sir.levelname = "start"; + sir.netVersion = NetQuake::QGOLD; + + + while (true) { + asio::ip::udp::endpoint remote_endpoint; + std::array recv_buf; + + + socket.receive_from(asio::buffer(recv_buf), remote_endpoint); + + NetQuake::NetworkGenericPacket gpac; + + gpac.type[0] = recv_buf[0]; + gpac.type[1] = recv_buf[1]; + gpac.length[0] = recv_buf[2]; + gpac.length[1] = recv_buf[3]; + for (int i = 4; i < recv_buf.size(); i++) { + gpac.remainder += recv_buf[i]; + } + + NetQuake::SerializedGenericPacket sgpac = NetQuake::Serialize(gpac); + + std::cout << "new packet\n"; + switch (sgpac.type) { + + case NetQuake::NQ_CONTROL_PACKET: + { + NetQuake::GenericControlPacket gcpac(sgpac); + + std::cout << "GenericControlPacket: " << gcpac.opcode << "\n"; + + // the 0x01 i had so much trouble getting through + if (gcpac.opcode == NetQuake::CONTROL_CONNECTION_REQUEST) { + std::cout << "Connection Request\n"; + + int endpointRequestId = internalRequestConsole->newEndpointRequest(remote_endpoint); + + while (!internalRequestConsole->getEndpointRequest(endpointRequestId).ready) { + // waiting for endpoint request to go through + } + + Internal::NewEndpointRequestReply nerr = internalRequestConsole->getEndpointRequest(endpointRequestId); + + NetQuake::ConnectionRequestReply crr; + + crr.reject = nerr.error; + crr.reason = nerr.reason; + crr.port = nerr.port; + + crr.unknown[0] = config->unknownMagic[0]; + crr.unknown[1] = config->unknownMagic[1]; + + asio::error_code e; + socket.send_to(asio::buffer(NetQuake::Serialize(crr)), remote_endpoint, 0, e); + /*NetQuake::ConnectionRequestReply crr; + + crr.reject = false; + crr.port = 26000; + crr.unknown[0] = config->unknownMagic[0]; + crr.unknown[1] = config->unknownMagic[1]; + + asio::error_code e; + socket.send_to(asio::buffer(NetQuake::Serialize(crr)), remote_endpoint, 0, e); + */ + } + if (gcpac.opcode == NetQuake::CONTROL_SERVER_INFO_REQUEST) { + NetQuake::ServerInfoRequest sirqpac(gcpac); + + std::cout << "ServerInfoRequest: server info request for game \"" << sirqpac.gameName << "\", netversion " << sirqpac.netVersion << "\n"; + + std::string serializedTemplate = NetQuake::Serialize(sir); + + std::ofstream templatestream("template.txt", std::ios::binary); + templatestream << serializedTemplate; + templatestream.close(); + asio::error_code e; + socket.send_to(asio::buffer(serializedTemplate), remote_endpoint, 0, e); + std::cout << e.message() << "\n"; + } + + } + break; + + default: + std::cout << "[WARNING] Unknown or unsupported packet came through.\n"; + break; + } + // std::cout << gcpac.opcode << "\n"; + + } + +} + +void NetQuake::NetQuake_EndpointServer(Config::Config* config, int port, asio::ip::udp::endpoint startRemote) +{ + asio::io_context io_context; + asio::error_code e; + + + asio::ip::udp::socket socket(io_context, asio::ip::udp::endpoint(asio::ip::udp::v4(), port)); + + socket.connect(startRemote); + + std::print("New endpoint server started on {}.\nClient Start Info: {} | {}\n", port, startRemote.address().to_string(), startRemote.port()); + + { // startup block + ServerMessageBlock smb; + smb.type = NQ_MESSAGE_BLOCK_END; + smb.order = 100; + ServerMessages::Print serverBanner; + + serverBanner.text = config->serverBanner; + smb.serverMessages.push_back(&serverBanner); + + + ServerMessages::ServerInfo serverInfo; + + serverInfo.mapName = "start"; + serverInfo.maxClients = config->maxPlayers; + serverInfo.multi = 1; + serverInfo.numModels = 0; + serverInfo.numSounds = 0; + serverInfo.serverVersion = 15; + + smb.serverMessages.push_back(&serverInfo); + + ServerMessages::SignOn signOn; + signOn.signon = 1; // prespawn(2=light,3=render) + smb.serverMessages.push_back(&signOn); + + std::print("Tried sending server banner to client.\n"); + + socket.send_to(asio::buffer(smb.Serialize()), startRemote, 0, e); + std::ofstream smbattempt("smb.txt"); + smbattempt << smb.Serialize(); + smbattempt.close(); + + + std::print("Server banner should be sent.\n"); + } + + while (true) { + asio::ip::udp::endpoint rec; + std::string recv_buf; + socket.receive_from(asio::buffer(recv_buf), rec); + std::cout << "packet on secondary line " << port << "\n"; + std::cout << "received packet size: " << recv_buf.size() << "\n"; + if(recv_buf.size() > 4) { + try { + NetQuake::NetworkGenericPacket ngp; + ngp.type[0] = recv_buf[0]; + ngp.type[1] = recv_buf[1]; + ngp.length[0] = recv_buf[2]; + ngp.length[1] = recv_buf[3]; + for (int i = 4; i < recv_buf.size(); i++) { + ngp.remainder += recv_buf[i]; + } + SerializedGenericPacket sgpac = Serialize(ngp); + GenericGamePacket ggp = toGenericGamePacket(sgpac); + + + if (ggp.type == NQ_MESSAGE_BLOCK_CHUNK || ggp.type == NQ_MESSAGE_BLOCK_END) { + socket.send_to(asio::buffer(AcknowledgePacket(ggp.packetNumber)), rec); + std::println("Server acknowledged packet {}", ggp.packetNumber); + } + } + catch (...) { + std::println("Error on packet on {}", port); + break; + } + } + else { + std::println("Weird packet received... :shrug:"); + + } + } +} + +NetQuake::GenericControlPacket::GenericControlPacket(SerializedGenericPacket sgp) +{ + + if (sgp.type != NQ_CONTROL_PACKET) { + throw std::runtime_error("Wrong packet type given to generic control packet constructor."); + } + + opcode = (Control_OpCode)sgp.remainder[0]; + + remainder = sgp.remainder; + remainder.erase(0, 1); + + +} + +NetQuake::ServerInfoRequest::ServerInfoRequest(GenericControlPacket gcp) +{ + if (gcp.opcode != CONTROL_SERVER_INFO_REQUEST) { + throw std::runtime_error("Wrong packet type given to server info request constructor."); + } + + std::string toParse = gcp.remainder; + + int i = 0; + for (; toParse[i] != '\0'; i++) { + gameName += toParse[i]; + } + netVersion = (NetProtocolVersion) toParse[i+1]; + +} + +NetQuake::ConnectionRequest::ConnectionRequest(GenericControlPacket gcp) +{ + if (gcp.opcode != CONTROL_SERVER_INFO_REQUEST) { + throw std::runtime_error("Wrong packet type given to server info request constructor."); + } + + std::string toParse = gcp.remainder; + + int i = 0; + for (; toParse[i] != '\0'; i++) { + gameName += toParse[i]; + } + netVersion = (NetProtocolVersion)toParse[i + 1]; + +} + +NetQuake::Internal::InternalRequestConsole::InternalRequestConsole() +{ + std::print("New Internal Request Console Created.\n"); +} + +int NetQuake::Internal::InternalRequestConsole::newEndpointRequest(asio::ip::udp::endpoint ep) +{ + NewEndpointRequestReply nerr; + nerr.ready = false; + nerr.startRemote = ep; + newEndpointRequests.push_back(nerr); + return newEndpointRequests.size() - 1; +} + +NetQuake::Internal::NewEndpointRequestReply NetQuake::Internal::InternalRequestConsole::getEndpointRequest(int id) +{ + return newEndpointRequests[id]; +} + +void NetQuake::Internal::InternalRequestConsole::Update(Config::Config *config, std::vector* gameEndpointsVector) +{ + + for (int i = lastEndpointRequestTreated; i < newEndpointRequests.size(); i++) { + if (!newEndpointRequests[i].ready) { + if(gameEndpointsVector->size() < config->maxPlayers){ + GameEndpoint* endpoint = new GameEndpoint(config, 1000+i, newEndpointRequests[i].startRemote); + newEndpointRequests[i].port = 1000 + i; + } + else { + newEndpointRequests[i].error = true; + newEndpointRequests[i].reason = "too many players"; + } + + newEndpointRequests[i].ready = true; + } + } + +} + +NetQuake::Internal::InternalRequestConsole::~InternalRequestConsole() +{ + newEndpointRequests.clear(); + std::print("Internal Request Console Destroyed.\n"); +} + +NetQuake::GameEndpoint::GameEndpoint() +{ + throw; // constructor musn't be empty +} + +NetQuake::GameEndpoint::GameEndpoint(Config::Config* config, int port, asio::ip::udp::endpoint startRemote) +{ + std::thread internalInternalThread(NetQuake_EndpointServer, config, port, startRemote); + internalThread.swap(internalInternalThread); +} + +NetQuake::GameEndpoint::~GameEndpoint() +{ + internalThread.~thread(); +} + +std::string NetQuake::ServerMessages::Print::Serialize() { + using namespace std::string_literals; + std::string retval = text; + retval += "\0"s; + return retval; +} + +std::string NetQuake::ServerMessages::ServerInfo::Serialize() { + std::string retval = ""; + using namespace std::string_literals; + retval += serverVersion; + retval += maxClients; + retval += mapName + "\0"s; + for (int i = 0; i < precacheModels.size(); i++) { + retval += precacheModels[i] + "\0"s; + } + retval += numModels; + for (int i = 0; i < precacheSounds.size(); i++) { + retval += precacheSounds[i] + "\0"s; + } + + retval += numSounds; + return retval; +} + + +std::string NetQuake::ServerMessages::SignOn::Serialize() { + std::string retval = ""; + + retval += signon; + + return retval; +} + +std::string NetQuake::ServerMessageBlock::Serialize() +{ + std::string retval; + + using namespace std::string_literals; + + for (int i = 0; i < serverMessages.size(); i++) { + retval += serverMessages[i]->type; + retval += serverMessages[i]->Serialize(); + } + + + + short retvalsize = retval.size() + 4; + unsigned char retvalsizechar[2]; + unsigned char typechar[2]; + + retvalsizechar[0] = retvalsize & 0xff; + retvalsizechar[1] = (retvalsize >> 8) & 0xff; + typechar[1] = this->type & 0xff; + typechar[0] = (this->type >> 8) & 0xff; + + std::string fretval = std::string({(char)typechar[0]}); + fretval += typechar[1]; + fretval += retvalsizechar[1]; + fretval += (char)retvalsizechar[0]; + fretval += retval; + return fretval; +} diff --git a/mpfw_server/NetQuake.h b/mpfw_server/NetQuake.h new file mode 100644 index 0000000..d10384f --- /dev/null +++ b/mpfw_server/NetQuake.h @@ -0,0 +1,259 @@ +#pragma once + + +#include +#include "Config.h" +#include +#include +#include +#include +#include + + +namespace NetQuake { + + + + struct NetworkGenericPacket { // generic packet straight from server + char type[2]; + char length[2]; + std::string remainder; + }; + + + enum PacketType { + NQ_CONTROL_PACKET = 0x8000, + NQ_MESSAGE_BLOCK_CHUNK = 0x0001, + NQ_MESSAGE_BLOCK_END = 0x0009, + NQ_ACK = 0x0002, + NQ_UNRELIABLE = 0x0010, + NQ_UNKNOWN_PACKET + }; + + struct SerializedGenericPacket { + PacketType type; + int length; + std::string remainder; + }; + + SerializedGenericPacket Serialize(NetworkGenericPacket ngp); + + + enum ClientMessageType { + CLIENT_MESSAGE_NOOP = 0x00, + CLIENT_MESSAGE_KEEPALIVE = 0x01, + CLIENT_MESSAGE_DISCONNECT = 0x02, + CLIENT_MESSAGE_MOVEMENT = 0x03, + CLIENT_MESSAGE_CONSOLE = 0x04, + CLIENT_MESSAGE_UNKNOWN + }; + + struct ClientMessage { + ClientMessageType type; + std::string data; + }; + + struct ClientMessageBlock { + std::vector messages; + }; + + struct GenericGamePacket { + PacketType type; + int length; + long packetNumber; + ClientMessageBlock messageBlock; + bool valid = false; + }; + GenericGamePacket toGenericGamePacket(SerializedGenericPacket sgp); + + enum DemoMessageType { + DEM_BAD = 0x00, + DEM_NOP = 0x01, + DEM_DISCONNECT = 0x02, + DEM_UPDATESTAT = 0x03, + DEM_VERSION = 0x04, + DEM_SETVIEW = 0x05, + DEM_SOUND = 0x06, + DEM_TIME = 0x07, + DEM_PRINT = 0x08, + DEM_STUFFTEXT = 0x09, + DEM_SETANGLE = 0x0A, + DEM_SERVERINFO = 0x0B, + DEM_LIGHTSTYLE = 0x0C, + DEM_UPDATENAME = 0x0D, + DEM_UPDATEFRAGS = 0x0E, + DEM_CLIENTDATA = 0x0F, + DEM_STOPSOUND = 0x10, + DEM_UPDATECOLORS = 0x11, + DEM_PARTICLE = 0x12, + DEM_DAMAGE = 0x13, + DEM_SPAWNSTATIC = 0x14, + DEM_SPAWNBINARY = 0x15, + DEM_SPAWNBASELINE = 0x16, + DEM_TEMP_ENTITY = 0x17, + DEM_SETPAUSE = 0x18, + DEM_SIGNONUM = 0x19, + DEM_CENTERPRINT = 0x1A, + DEM_KILLEDMONSTER = 0x1B, + DEM_FOUNDSECRET = 0x1C, + DEM_SPAWNSTATICSOUND = 0x1D, + DEM_INTERMISSION = 0x1E, + DEM_FINALE = 0x1F, + DEM_CDTRACK = 0x20, + DEM_SELLSCREEN = 0x21, + DEM_CUTSCENE = 0x22, + DEM_UPDATEENTITY = 0x80 + }; + + class GenericServerMessage { + public: + DemoMessageType type; + virtual std::string Serialize() = 0; + }; + + namespace ServerMessages { + class Print : public GenericServerMessage { // 0x08 + public: + std::string text = ""; + Print() { this->type = DEM_PRINT; } + std::string Serialize(); + }; + class ServerInfo : public GenericServerMessage { // 0x0B + public: + long serverVersion = 15; + long maxClients; + long multi; + std::string mapName; + std::vector precacheModels; + long numModels; + std::vector precacheSounds; + long numSounds; + ServerInfo() { + this->type = DEM_SERVERINFO; + precacheModels = std::vector(256); + precacheSounds = std::vector(256); + } + + + + + std::string Serialize(); + }; + + class SignOn : public GenericServerMessage { + public: + long signon; + SignOn() { type = DEM_SIGNONUM; } + std::string Serialize(); + }; + } + + class ServerMessageBlock{ + public: + PacketType type; + int order; + std::vector serverMessages; + std::string Serialize(); + }; + + + + enum Control_OpCode { + CONTROL_CONNECTION_REQUEST = 0x01, + CONTROL_SERVER_INFO_REQUEST = 0x02, + CONTROL_PLAYER_INFO_REQUEST = 0x03, + CONTROL_RULE_INFO_REQUEST = 0x04, + CONTROL_SERVER_INFO_REPLY = 0x83 + }; + + struct GenericControlPacket { + Control_OpCode opcode; + std::string remainder; + GenericControlPacket(SerializedGenericPacket sgp); + }; + + enum NetProtocolVersion { + QTEST1 = 0x01, + QUNKNOWN = 0x02, + QGOLD = 0x03 + }; + + struct ServerInfoRequest { + std::string gameName; + NetProtocolVersion netVersion; + ServerInfoRequest(GenericControlPacket gcp); + }; + + struct Address { + std::string ip; + int port; + }; + + struct ServerInfoReply { + Address address; + std::string hostname; + std::string levelname; + char currentPlayers; + char maxPlayers; + NetProtocolVersion netVersion; + }; + + std::string Serialize(ServerInfoReply sirp); + + struct ConnectionRequest { + std::string gameName; + NetProtocolVersion netVersion; + ConnectionRequest(GenericControlPacket gcp); + }; + + struct ConnectionRequestReply { + bool reject; + short port; // LITTLE-ENDIAN (!)(!)(!)(!)(!) + char unknown[2]; // [ have fun :) ] + + std::string reason; + }; + + ::std::string Serialize(ConnectionRequestReply crr); + + class GameEndpoint { + std::thread internalThread; + public: + GameEndpoint(); + GameEndpoint(Config::Config* config, int port, asio::ip::udp::endpoint startRemote); + + ~GameEndpoint(); + }; + + namespace Internal { + + struct NewEndpointRequestReply { + bool ready = false; + bool error = false; + std::string reason; + int port; + asio::ip::udp::endpoint startRemote; + }; + + class InternalRequestConsole { + std::vector newEndpointRequests; + int lastEndpointRequestTreated = 0; + public: + InternalRequestConsole(); + + int newEndpointRequest(asio::ip::udp::endpoint ep); + NewEndpointRequestReply getEndpointRequest(int id); + void Update(Config::Config *config, std::vector* gameEndpointsVector); + ~InternalRequestConsole(); + }; + } + + + std::string AcknowledgePacket(int packet); + + + void NetQuake_ControlServer(Internal::InternalRequestConsole* internalRequestConsole, Config::Config* config, bool* serverRunning); + void NetQuake_EndpointServer(Config::Config* config, int port, asio::ip::udp::endpoint startRemote); + + +} \ No newline at end of file diff --git a/mpfw_server/main.cpp b/mpfw_server/main.cpp new file mode 100644 index 0000000..cf0ed25 --- /dev/null +++ b/mpfw_server/main.cpp @@ -0,0 +1,85 @@ +// // MPFW SERVER +// // +// * * // +// * . * // implemented +// *I* // | +// * // v +// // (Net)Quake(World)-compatible game server +// // ^ +// // | +// // eventually? +// // +// // +//// github.com/safariminer +/// tilde.town/~safariminer +// + + // + // Notes relative to netquake + // - based on : https://www.gamers.org/dEngine/quake/QDP/qnp.html + // - Netquake = UDP + // - all numbers = big endian unless exception + // - don't blindly follow the spec: + // | game endpoints in quake don't have auth + // | do not use basic range, randomize ports throughout the available range + // | verify ip and port + // | even though the spec calls for it, don't display IP/port info in playerinfo control +// + +#ifdef _DEBUG +#define MPFW_SERVER_VERSION std::string(std::string("Debug Build ") + std::string(__DATE__)) +#else +#define MPFW_SERVER_VERSION "Release build" +#endif + +#include // basic io +#include // dynamic arrays +#include // keyval maps +#include +#include +#include +#include + +#include "Config.h" +#include "NetQuake.h" +#include + +#include +#include + + + +bool _serverRunning = true; + + +int rcp = 0; + +Config::Config *config = new Config::Config; + + +int main(int argc, char** argv) { + + + std::print("MPFW Server\nBuild Version: {}\n--------------------\n", MPFW_SERVER_VERSION); + + std::vector gameEndpoints; + NetQuake::Internal::InternalRequestConsole* internalRequestConsole = new NetQuake::Internal::InternalRequestConsole(); + + asio::io_context io_context; + + std::print("Starting control server thread...\n"); + + std::thread controlServer(NetQuake::NetQuake_ControlServer, internalRequestConsole, config, &_serverRunning); + + controlServer.detach(); + std::print("Started control server thread\n"); + + + + while (_serverRunning) { + internalRequestConsole->Update(config, &gameEndpoints); + } + + delete internalRequestConsole; + return 0; +} \ No newline at end of file diff --git a/mpfw_server/mpfw_server.vcxproj b/mpfw_server/mpfw_server.vcxproj new file mode 100644 index 0000000..a835461 --- /dev/null +++ b/mpfw_server/mpfw_server.vcxproj @@ -0,0 +1,144 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {67ceac9d-b0f3-4ab3-b3ff-0a9adad98e45} + mpfwserver + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + + + Console + true + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + $(SOLUTIONDIR)deps/include + stdcpplatest + + + Console + true + $(SOLUTIONDIR)deps/lib + winmm.lib;ws2_32.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + $(SOLUTIONDIR)deps/include + stdcpplatest + + + Console + true + $(SOLUTIONDIR)deps/lib + winmm.lib;ws2_32.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mpfw_server/mpfw_server.vcxproj.filters b/mpfw_server/mpfw_server.vcxproj.filters new file mode 100644 index 0000000..4faab7d --- /dev/null +++ b/mpfw_server/mpfw_server.vcxproj.filters @@ -0,0 +1,33 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/mpfw_server/mpfw_server.vcxproj.user b/mpfw_server/mpfw_server.vcxproj.user new file mode 100644 index 0000000..ce3a9d8 --- /dev/null +++ b/mpfw_server/mpfw_server.vcxproj.user @@ -0,0 +1,11 @@ + + + + $(SOLUTIONDIR)gameenv/ + WindowsLocalDebugger + + + $(SOLUTIONDIR)gameenv/ + WindowsLocalDebugger + + \ No newline at end of file