Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new message types and handlers, refactor client and server code #2264

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
FROM mcr.microsoft.com/devcontainers/javascript-node:22

RUN apt-get update \
&& apt-get install --no-install-recommends -y \
clang-format-15 \
# Clean cache
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
&& apt-get clean -y && rm -rf /var/lib/apt/lists/* \
&& mv /usr/bin/clang-format-15 /usr/bin/clang-format
RUN \
curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor > /usr/share/keyrings/yarnkey.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" > /etc/apt/sources.list.d/yarn.list \
&& curl -fsSL https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor - > /usr/share/keyrings/kitware-archive-keyring.gpg \
&& echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ jammy main' > /etc/apt/sources.list.d/kitware.list \
&& apt-get update \
&& apt-get install -y \
nodejs \
yarn \
libicu-dev \
git \
cmake \
curl \
unzip \
tar \
make \
zip \
pkg-config \
cmake \
clang-15 \
clang-format-15 \
ninja-build \
&& rm -rf /var/lib/apt/lists/* \
&& mv /usr/bin/clang-format-15 /usr/bin/clang-format
1 change: 1 addition & 0 deletions libespm/src/WRLD.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "libespm/WRLD.h"
#include "libespm/RecordHeaderAccess.h"
#include <algorithm>

namespace espm {

Expand Down
160 changes: 136 additions & 24 deletions misc/git-hooks/clang-format-hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ const simpleGit = require("simple-git");
const fs = require("fs");
const path = require("path");

const extensions = [".cpp", ".h", ".hpp", ".cxx", ".cc"];

/**
* Utility: Recursively find all files in a directory.
*/
const findFiles = (dir, fileList = []) => {
const files = fs.readdirSync(dir);
files.forEach((file) => {
Expand All @@ -18,48 +19,159 @@ const findFiles = (dir, fileList = []) => {
return fileList;
};

const formatFiles = (files) => {
const filesToFormat = files.filter((file) =>
extensions.some((ext) => file.endsWith(ext))
/**
* Check Registry: Define custom checks here.
* Each check should implement `lint` and `fix` methods.
*/
const checks = [
{
name: "Clang Format",
appliesTo: (file) => [".cpp", ".h", ".hpp", ".cxx", ".cc"].some((ext) => file.endsWith(ext)),
lint: (file) => {
// Example: Use clang-format to lint
const lintCommand = `clang-format --dry-run --Werror ${file}`;
try {
execSync(lintCommand, { stdio: "inherit" });
console.log(`[PASS] ${file}`);
return true;
} catch (error) {
console.error(`[FAIL] ${file}`);
return false;
}
},
fix: (file) => {
// Use clang-format to autofix
const fixCommand = `clang-format -i ${file}`;
execSync(fixCommand, { stdio: "inherit" });
console.log(`[FIXED] ${file}`);
},
},
{
name: "Header/TypeScript Pair Check",
// Applies to files that reside in the specified parent directories
appliesTo: (file) => {
const serverDir = "skymp5-server/cpp/messages";
const clientDir = "skymp5-client/src/services/messages";
const validDirs = [serverDir, clientDir];

// Check if the file belongs to one of the valid parent directories
return validDirs.some((dir) => file.includes(path.sep + dir + path.sep))
&& !file.endsWith(path.sep + "anyMessage.ts")
&& !file.endsWith(path.sep + "refrIdMessageBase.ts")
&& !file.endsWith(path.sep + "MessageBase.h")
&& !file.endsWith(path.sep + "MessageSerializerFactory.cpp")
&& !file.endsWith(path.sep + "MessageSerializerFactory.h")
&& !file.endsWith(path.sep + "Messages.h")
&& !file.endsWith(path.sep + "MinPacketId.h")
&& !file.endsWith(path.sep + "MsgType.h");
},
lint: (file) => {
const serverDir = "skymp5-server/cpp/messages";
const clientDir = "skymp5-client/src/services/messages";
const ext = path.extname(file);
const baseName = path.basename(file, ext);

// Determine the pair file's extension and directory
const pairExt = ext === ".h" ? ".ts" : ".h";
const pairDir = file.includes(path.sep + serverDir + path.sep)
? clientDir
: serverDir;

const pairFiles = fs.readdirSync(pairDir);

// Find a case-insensitive match
const pairFile = pairFiles.find(
(candidate) => candidate.toLowerCase() === `${baseName}${pairExt}`.toLowerCase()
);

if (!pairFile) {
console.error(`[FAIL] Pair file not found for ${file}: ${pairFile}`);
return false;
} else {
console.log(`[PASS] Pair file found for ${file}: ${pairFile}`);
return true;
}
},
fix() { }
}
];

/**
* Core: Run checks (lint or fix) on given files.
*/
const runChecks = (files, { lintOnly = false }) => {
const filesToCheck = files.filter((file) =>
checks.some((check) => check.appliesTo(file))
);

if (filesToFormat.length === 0) {
console.log("No files to format.");
if (filesToCheck.length === 0) {
console.log("No matching files found for checks.");
return;
}

console.log("Formatting files:");
filesToFormat.forEach((file) => {
console.log(` - ${file}`);
execSync(`clang-format -i ${file}`, { stdio: "inherit" });
console.log(`${lintOnly ? "Linting" : "Fixing"} files:`);

let fail = false;

filesToCheck.forEach((file) => {
checks.forEach((check) => {
if (check.appliesTo(file)) {
try {
const res = lintOnly ? check.lint(file) : check.fix(file);
if (res === false) {
fail = true;
}
} catch (err) {
if (lintOnly) {
console.error(`Error in ${check.name}:`, err);
process.exit(1);
}
else {
throw err;
}
}
}
});
});

if (fail) {
process.exit(1);
}

console.log(`${lintOnly ? "Linting" : "Fixing"} completed.`);
};

/**
* CLI Entry Point
*/
(async () => {
const args = process.argv.slice(2);
const lintOnly = args.includes("--lint");
const allFiles = args.includes("--all");

try {
if (args.includes("--all")) {
console.log("Formatting all files in the repository...");
const allFiles = findFiles(process.cwd());
formatFiles(allFiles);
let files = [];

if (allFiles) {
console.log("Processing all files in the repository...");
files = findFiles(process.cwd());
} else {
console.log("Formatting staged files...");
console.log("Processing staged files...");
const git = simpleGit();
const changedFiles = await git.diff(["--name-only", "--cached"]);

const filesToFormat = changedFiles
files = changedFiles
.split("\n")
.filter((file) => file.trim() !== "")
.filter((file) => fs.existsSync(file)); // Do not try validate deleted files
formatFiles(filesToFormat);

filesToFormat.forEach((file) => execSync(`git add ${file}`));
.filter((file) => fs.existsSync(file)); // Exclude deleted files
}

console.log("Formatting completed.");
runChecks(files, { lintOnly });

if (!lintOnly && !allFiles) {
files.forEach((file) => execSync(`git add ${file}`));
}
} catch (err) {
console.error("Error during formatting:", err.message);
console.error("Error during processing:", err.message);
process.exit(1);
}
})();
38 changes: 38 additions & 0 deletions serialization/include/archives/BitStreamInputArchive.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <slikenet/BitStream.h>
#include <stdexcept>
#include <string>
#include <variant>
#include <vector>

#include "../impl/BitStreamUtil.h"
Expand Down Expand Up @@ -96,6 +97,43 @@ class BitStreamInputArchive
return *this;
}

template <typename... Types>
BitStreamInputArchive& Serialize(const char* key,
std::variant<Types...>& value)
{
uint32_t typeIndex = 0;

Serialize("typeIndex", typeIndex);

if (typeIndex >= sizeof...(Types)) {
throw std::runtime_error(
"Invalid type index for std::variant deserialization");
}

// Helper lambda to visit and deserialize the correct type
auto deserializeVisitor = [this](auto indexTag,
std::variant<Types...>& variant) {
using SelectedType =
typename std::variant_alternative<decltype(indexTag)::value,
std::variant<Types...>>::type;
SelectedType value;
Serialize("value", value);
variant = std::move(value);
};

// Visit the type corresponding to the typeIndex
[&]<std::size_t... Is>(std::index_sequence<Is...>)
{
((typeIndex == Is ? deserializeVisitor(
std::integral_constant<std::size_t, Is>{}, value)
: void()),
...);
}
(std::make_index_sequence<sizeof...(Types)>{});

return *this;
}

template <NoneOfTheAbove T>
BitStreamInputArchive& Serialize(const char* key, T& value)
{
Expand Down
16 changes: 16 additions & 0 deletions serialization/include/archives/BitStreamOutputArchive.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once
#include "concepts/Concepts.h"
#include <algorithm>
#include <optional>
#include <slikenet/BitStream.h>
#include <stdexcept>
Expand Down Expand Up @@ -84,6 +85,21 @@ class BitStreamOutputArchive
return *this;
}

template <typename... Types>
BitStreamOutputArchive& Serialize(const char* key,
std::variant<Types...>& value)
{
uint32_t typeIndex = static_cast<uint32_t>(value.index());

Serialize("typeIndex", typeIndex);

auto serializeVisitor = [&](auto& v) { Serialize("value", v); };

std::visit(serializeVisitor, value);

return *this;
}

template <NoneOfTheAbove T>
BitStreamOutputArchive& Serialize(const char* key, T& value)
{
Expand Down
49 changes: 49 additions & 0 deletions serialization/include/archives/JsonInputArchive.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <optional>
#include <stdexcept>
#include <string>
#include <variant>
#include <vector>

class JsonInputArchive
Expand Down Expand Up @@ -86,6 +87,54 @@ class JsonInputArchive
return *this;
}

template <typename... Types>
JsonInputArchive& Serialize(const char* key, std::variant<Types...>& value)
{
const auto& jsonValue = j.at(key);

// Helper lambda to attempt deserialization for a specific type
auto tryDeserialize = [&](auto typeTag) -> bool {
using SelectedType = decltype(typeTag);

nlohmann::json childArchiveInput = nlohmann::json::object();
childArchiveInput["candidate"] = jsonValue;
SelectedType deserializedValue;
JsonInputArchive childArchive(childArchiveInput);

bool deserializationSuccessful = false;
try {
childArchive.Serialize("candidate", deserializedValue);
deserializationSuccessful = true;
} catch (const std::exception&) {
deserializationSuccessful = false;
}

if (deserializationSuccessful) {
value = std::move(deserializedValue);
}

return deserializationSuccessful;
};

// Iterate through the variant types and attempt deserialization
bool success = false;
[&]<std::size_t... Is>(std::index_sequence<Is...>)
{
((success = success ||
tryDeserialize(std::declval<typename std::variant_alternative<
Is, std::variant<Types...>>::type>())),
...);
}
(std::make_index_sequence<sizeof...(Types)>{});

if (!success) {
throw std::runtime_error(
"Unable to deserialize JSON into any variant type");
}

return *this;
}

template <NoneOfTheAbove T>
JsonInputArchive& Serialize(const char* key, T& value)
{
Expand Down
Loading
Loading