initial commit where rendering still works

This commit is contained in:
Safariminer 2025-07-26 16:27:54 -04:00
commit 8e9a80caf0
43 changed files with 3481 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
gameenv/data/maps
gameenv/data/textures
deps
.vs
x64
*/x64
x86
*/x86

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# mpfw
## focusing on complete id control
## Supported file types
- BSP29 (Quake maps)

View File

@ -0,0 +1,5 @@
echo "MPFW Quake"
echo "Test Startup Script"
mode 1
map "data/maps/fullquake/start.bsp"

View File

@ -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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
gameenv/data/palette.lmp Normal file

Binary file not shown.

41
mpfw.sln Normal file
View File

@ -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

16
mpfw/MPFW_CVars.cpp Normal file
View File

@ -0,0 +1,16 @@
#include "MPFW_CVars.h"
#include <string>
std::variant<std::string, int> MPFW::CVar::get() {
try {
int retval = std::stoi(value);
return retval;
}
catch(...){
return value;
}
}

17
mpfw/MPFW_CVars.h Normal file
View File

@ -0,0 +1,17 @@
#pragma once
#include <iostream>
#include <map>
#include <variant>
namespace MPFW {
class CVar {
public:
std::string value;
std::variant<std::string, int> get();
};
using CVarMap = std::map<std::string, CVar>;
}

175
mpfw/MPFW_Console.cpp Normal file
View File

@ -0,0 +1,175 @@
#include "MPFW_Console.h"
#include <string>
#include <fstream>
#include <sstream>
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 <path>\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<std::string> MPFW::Console::CommandHandler::parseCommand(std::string s)
{
bool inQString = false;
std::vector<std::string> 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<std::string> 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()
{
}

84
mpfw/MPFW_Console.h Normal file
View File

@ -0,0 +1,84 @@
#pragma once
#include <iostream>
#include <vector>
#include "MPFW_Quake.h"
#include <functional>
#include <map>
#define MPFW_ConsoleCommand(x) void x(std::vector<std::string> 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<MPFW::Console::CommandID>
{
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<std::string> h;
return h(t);
}
};
}
namespace MPFW{
namespace Console{
class CommandHandler {
CommandHandlerResources* chr;
std::unordered_map<CommandID, std::function<void(std::vector<std::string>,CommandHandlerResources*, std::string*)>> functionMap;
public:
[[deprecated("Never initialize an empty command handler")]]
CommandHandler() { throw; }
CommandHandler(CommandHandlerResources* chr);
std::vector<std::string> parseCommand(std::string s);
std::string logStr;
void Run(std::string command, bool verbose = true);
void RunScript(std::string path);
~CommandHandler();
};
}
}

180
mpfw/MPFW_HL.cpp Normal file
View File

@ -0,0 +1,180 @@
#include "MPFW_HL.h"
#include "MPFW_Utils.h"
#include <fstream>
#include <sstream>
#include <string>
#include <iostream>
#include <exception>
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()
{
}

85
mpfw/MPFW_HL.h Normal file
View File

@ -0,0 +1,85 @@
#pragma once
#include <iostream>
#include <vector>
#include <raylib.h>
#include <map>
namespace MPFW {
namespace HL {
struct Lump {
std::string name, data;
};
class WAD { // WAD3 file format
std::vector<Lump> lumps;
public:
bool loaded = false;
int lumpCount;
WAD();
WAD(std::string path);
void LoadWAD(std::string path);
std::vector<Lump> 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<Plane> 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<std::string, std::string> 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<Plane> planes;
void onCreate() {}
void onUpdate() {}
void onDestroy() {}
};
struct __HLParseKeyVal {
std::string key, val;
};
class Map {
public:
std::vector<GenericClass*> mapdata;
Map();
Map(std::string path);
void Load(std::string path);
~Map();
};
}
}

43
mpfw/MPFW_MPFWMF.h Normal file
View File

@ -0,0 +1,43 @@
#pragma once
#include <raymath.h> // we shalln't worry ourselves with full raylib if we're going to end up networking this bad bitch
#include <raylib.h> // listening to le colibri nécrophile made me decide otherwise
#include <iostream>
#include <functional>
#include <vector>
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();
};
}
}
}

461
mpfw/MPFW_Quake.cpp Normal file
View File

@ -0,0 +1,461 @@
#include <raylib.h>
#include "MPFW_Quake.h"
#include "MPFW_Utils.h"
#include <sstream>
#include <fstream>
#include <string>
#include <iostream>
#include <algorithm>
#include <rlgl.h>
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<DirectoryEntry> 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<long> 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<unsigned int>(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);
}

235
mpfw/MPFW_Quake.h Normal file
View File

@ -0,0 +1,235 @@
#pragma once
// based off of https://www.gamers.org/dEngine/quake/spec/quake-spec34/qkspec_4.htm
// (mostly)
#include <raymath.h>
#include <iostream>
#include <vector>
#include <print>
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<Vector3> vertices;
std::vector<Vector2> texCoords;
int glTextureId, glTextureWidth, glTextureHeight;
};
struct cMipHeader {
long numtex;
void *offset;
};
using MipHeader = std::vector<long>;
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<qColor> data;
Palette() { data = std::vector<qColor>(256); }
Palette(std::string path) { LoadPalette(path); }
void LoadPalette(std::string path);
~Palette() {}
};
struct MapData {
BSPHeader header;
std::vector<Vector3> vertices;
std::vector<Edge> edges;
std::vector<qModel> models;
std::vector<Surface> texInfo;
std::vector<Face> faces;
std::vector<int> ledges;
std::vector<rlMipTex> textures;
std::vector<CalculatedFace> 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<MPFW::Quake::Maps::DirectoryEntry> {
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<MPFW::Quake::Maps::MapData> {
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());
}
};

5
mpfw/MPFW_UI.h Normal file
View File

@ -0,0 +1,5 @@
#pragma once
namespace MPFW {
}

153
mpfw/MPFW_Utils.cpp Normal file
View File

@ -0,0 +1,153 @@
#include "MPFW_Utils.h"
#include <Windows.h>
#include <gl/GL.h>
#include <gl/GLU.h>
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<std::string, std::string> MPFW::Utils::Strings::ParseCLineIntoKeyVal(std::string originalString)
{
std::tuple<std::string, std::string> 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<std::string, std::string>(IterativeStringExcerpt(originalString, 0, equPos), IterativeStringExcerpt(originalString, valstartpos, valendpos - valstartpos + 1));
}
catch(...){
retval = std::make_tuple<std::string, std::string>("", "");
}
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::Triangle2D> MPFW::Utils::Geometry::TesselateVertexString(VertexString vs)
{
std::basic_string<Triangle2D> retval;
/*GLUtesselator* tess = gluNewTess();
gluTessBeginPolygon(tess, 0);
for (int i = 0; i < vs.size(); i++) {
GLdouble* coords;
coords[0] = static_cast<double>(vs[i].x);
coords[1] = 0;
coords[2] = static_cast<double>(vs[i].y);
gluTessVertex(tess, coords, coords);
}
gluTessEndPolygon(tess);
gluDeleteTess(tess);
*/
throw;
return retval;
}

28
mpfw/MPFW_Utils.h Normal file
View File

@ -0,0 +1,28 @@
#pragma once
#include <iostream>
namespace MPFW {
namespace Utils {
namespace Geometry {
struct Vertex2D {
float x, y;
};
struct Triangle2D {
float a, b, c;
};
using VertexString = std::basic_string<Vertex2D>;
std::basic_string<Triangle2D> 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<std::string, std::string> ParseCLineIntoKeyVal(std::string originalString);
std::string SingleWordTrimmer(std::string originalString);
float StringIndexToFloat_4b_le(std::string originalString, int startPointer);
}
}
}

530
mpfw/main.cpp Normal file
View File

@ -0,0 +1,530 @@
#include <iostream>
#include <raylib.h>
#include <raymath.h>
#include <fstream>
#include <sstream>
#include <string>
#include "MPFW_Quake.h"
#include "MPFW_Console.h"
#include <print>
#include <rlgl.h>
#include <thread>
// 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 <typename T>
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<Color> 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 = &map;
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<Vector3> 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<Vector3> 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<Vector3> 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();
}

159
mpfw/mpfw.vcxproj Normal file
View File

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{fb36e71a-46ab-4ece-9438-e683c4208fb1}</ProjectGuid>
<RootNamespace>mpfw</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalIncludeDirectories>$(SOLUTIONDIR)deps/include</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>$(SOLUTIONDIR)deps/lib</AdditionalLibraryDirectories>
<AdditionalDependencies>raylib.lib;winmm.lib;gdi32.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
<IgnoreSpecificDefaultLibraries>MSVCRT</IgnoreSpecificDefaultLibraries>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalIncludeDirectories>$(SOLUTIONDIR)deps/include</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>$(SOLUTIONDIR)deps/lib</AdditionalLibraryDirectories>
<AdditionalDependencies>raylib.lib;winmm.lib;gdi32.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
<IgnoreSpecificDefaultLibraries>MSVCRT</IgnoreSpecificDefaultLibraries>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="main.cpp" />
<ClCompile Include="MPFW_Console.cpp" />
<ClCompile Include="MPFW_CVars.cpp" />
<ClCompile Include="MPFW_HL.cpp" />
<ClCompile Include="MPFW_Quake.cpp" />
<ClCompile Include="MPFW_Utils.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="MPFW_Console.h" />
<ClInclude Include="MPFW_CVars.h" />
<ClInclude Include="MPFW_HL.h" />
<ClInclude Include="MPFW_MPFWMF.h" />
<ClInclude Include="MPFW_Quake.h" />
<ClInclude Include="MPFW_UI.h" />
<ClInclude Include="MPFW_Utils.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

60
mpfw/mpfw.vcxproj.filters Normal file
View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MPFW_Utils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MPFW_HL.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MPFW_Quake.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MPFW_Console.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MPFW_CVars.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="MPFW_HL.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MPFW_Utils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MPFW_MPFWMF.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MPFW_Quake.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MPFW_Console.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MPFW_CVars.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MPFW_UI.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

11
mpfw/mpfw.vcxproj.user Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LocalDebuggerWorkingDirectory>$(SolutionDir)/gameenv</LocalDebuggerWorkingDirectory>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LocalDebuggerWorkingDirectory>$(SolutionDir)/gameenv</LocalDebuggerWorkingDirectory>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
</Project>

23
mpfw_server/Config.h Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include <iostream>
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
};
}

534
mpfw_server/NetQuake.cpp Normal file
View File

@ -0,0 +1,534 @@
#include "NetQuake.h"
#include <iostream> // basic io
#include <vector> // dynamic arrays
#include <map> // keyval maps
#include <asio.hpp>
#include <asio/ts/buffer.hpp>
#include <asio/ts/internet.hpp>
#include <print>
#include <string>
#include <fstream>
#include <excpt.h>
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<char*>(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<char, 1024> 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<GameEndpoint>* 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;
}

259
mpfw_server/NetQuake.h Normal file
View File

@ -0,0 +1,259 @@
#pragma once
#include <iostream>
#include "Config.h"
#include <asio/ts/internet.hpp>
#include <asio.hpp>
#include <map>
#include <vector>
#include <thread>
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<ClientMessage> 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<std::string> precacheModels;
long numModels;
std::vector<std::string> precacheSounds;
long numSounds;
ServerInfo() {
this->type = DEM_SERVERINFO;
precacheModels = std::vector<std::string>(256);
precacheSounds = std::vector<std::string>(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<GenericServerMessage*> 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<NewEndpointRequestReply> newEndpointRequests;
int lastEndpointRequestTreated = 0;
public:
InternalRequestConsole();
int newEndpointRequest(asio::ip::udp::endpoint ep);
NewEndpointRequestReply getEndpointRequest(int id);
void Update(Config::Config *config, std::vector<GameEndpoint>* 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);
}

85
mpfw_server/main.cpp Normal file
View File

@ -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 <iostream> // basic io
#include <vector> // dynamic arrays
#include <map> // keyval maps
#include <asio.hpp>
#include <asio/ts/buffer.hpp>
#include <asio/ts/internet.hpp>
#include <print>
#include "Config.h"
#include "NetQuake.h"
#include <thread>
#include <fstream>
#include <string>
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<NetQuake::GameEndpoint> 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;
}

View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{67ceac9d-b0f3-4ab3-b3ff-0a9adad98e45}</ProjectGuid>
<RootNamespace>mpfwserver</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>$(SOLUTIONDIR)deps/include</AdditionalIncludeDirectories>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>$(SOLUTIONDIR)deps/lib</AdditionalLibraryDirectories>
<AdditionalDependencies>winmm.lib;ws2_32.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>$(SOLUTIONDIR)deps/include</AdditionalIncludeDirectories>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalLibraryDirectories>$(SOLUTIONDIR)deps/lib</AdditionalLibraryDirectories>
<AdditionalDependencies>winmm.lib;ws2_32.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="main.cpp" />
<ClCompile Include="NetQuake.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="Config.h" />
<ClInclude Include="NetQuake.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="NetQuake.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="NetQuake.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Config.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LocalDebuggerWorkingDirectory>$(SOLUTIONDIR)gameenv/</LocalDebuggerWorkingDirectory>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LocalDebuggerWorkingDirectory>$(SOLUTIONDIR)gameenv/</LocalDebuggerWorkingDirectory>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
</Project>