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 1 commit
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
153 changes: 129 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,152 @@ 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");
},
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);
}
})();
2 changes: 1 addition & 1 deletion skymp5-client/src/services/events/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { HostStartMessage } from "../messages/hostStartMessage";
import { HostStopMessage } from "../messages/hostStopMessage";
import { SetInventoryMessage } from "../messages/setInventoryMessage";
import { OpenContainerMessage } from "../messages/openContainerMessage";
import { ChangeValuesMessage } from "../messages/changeValues";
import { ChangeValuesMessage } from "../messages/changeValuesMessage";
import { CreateActorMessage } from "../messages/createActorMessage";
import { CustomPacketMessage2 } from "../messages/customPacketMessage2";
import { DestroyActorMessage } from "../messages/destroyActorMessage";
Expand Down
2 changes: 1 addition & 1 deletion skymp5-client/src/services/messages/anyMessage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import { ActivateMessage } from "./activateMessage";
import { ChangeValuesMessage } from "./changeValues";
import { ChangeValuesMessage } from "./changeValuesMessage";
import { ConsoleCommandMessage } from "./consoleCommandMessage";
import { CraftItemMessage } from "./craftItemMessage";
import { CreateActorMessage } from "./createActorMessage";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ export interface ConsoleCommandMessage {

interface ConsoleCommandMessageData {
commandName: string;
args: unknown[];
args: Array<number | string>;
}
2 changes: 1 addition & 1 deletion skymp5-client/src/services/messages/customEventMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import { MsgType } from "../../messages";

export interface CustomEventMessage {
t: MsgType.CustomEvent,
args: unknown[],
argsJsonDumps: string[],
eventName: string
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MsgType } from "../../messages";
import { ChangeValuesMessage } from "./changeValues";
import { ChangeValuesMessage } from "./changeValuesMessage";
import { TeleportMessage } from "./teleportMessage";
import { UpdatePropertyMessage } from "./updatePropertyMessage";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import { MsgType } from "../../messages"

export interface FinishSpSnippetMessage {
t: MsgType.FinishSpSnippet,
returnValue: unknown, // TODO: improve type: there should union of possible Papyrus values
returnValue?: boolean | number | string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The returnValue in FinishSpSnippetMessage is defined as boolean | number | string in TypeScript, but in C++ it's defined as std::optional<std::variant<bool, double, std::string>>. The use of double in C++ might lead to precision issues if the number is an integer in TypeScript. Consider aligning the types to avoid potential issues.

snippetIdx: number,
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class GamemodeEventSourceService extends ClientListener {
sendEvent: (...args: unknown[]) => {
const message: CustomEventMessage = {
t: MsgType.CustomEvent,
args,
argsJsonDumps: args.map(arg => JSON.stringify(arg)),
eventName
};
this.controller.emitter.emit("sendMessage", {
Expand Down
2 changes: 1 addition & 1 deletion skymp5-client/src/services/services/remoteServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { ModelApplyUtils } from '../../view/modelApplyUtils';
import { FormModel, WorldModel } from '../../view/model';
import { LoadGameService } from './loadGameService';
import { UpdateMovementMessage } from '../messages/updateMovementMessage';
import { ChangeValuesMessage } from '../messages/changeValues';
import { ChangeValuesMessage } from '../messages/changeValuesMessage';
import { UpdateAnimationMessage } from '../messages/updateAnimationMessage';
import { UpdateEquipmentMessage } from '../messages/updateEquipmentMessage';
import { RagdollService } from './ragdollService';
Expand Down
2 changes: 1 addition & 1 deletion skymp5-client/src/services/services/sendInputsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { nextHostAttempt } from "../../view/hostAttempts";
import { SkympClient } from "./skympClient";
import { MessageWithRefrId } from "../events/sendMessageWithRefrIdEvent";
import { UpdateMovementMessage } from "../messages/updateMovementMessage";
import { ChangeValuesMessage } from "../messages/changeValues";
import { ChangeValuesMessage } from "../messages/changeValuesMessage";
import { UpdateAnimationMessage } from "../messages/updateAnimationMessage";
import { UpdateEquipmentMessage } from "../messages/updateEquipmentMessage";
import { UpdateAppearanceMessage } from "../messages/updateAppearanceMessage";
Expand Down
24 changes: 18 additions & 6 deletions skymp5-client/src/services/services/spSnippetService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,23 @@ export class SpSnippetService extends ClientListener {
res = null;
}

const message: FinishSpSnippetMessage = {
if (res !== null
&& typeof res !== "number"
&& typeof res !== "string"
&& typeof res !== "boolean") {
logError(this, `Unsupported SpSnippet result type '${typeof res}'`)
return;
}

const message: FinishSpSnippetMessage = res === null ? {
t: MsgType.FinishSpSnippet,
returnValue: res,
snippetIdx: msg.snippetIdx,
snippetIdx: msg.snippetIdx
}
: {
t: MsgType.FinishSpSnippet,
returnValue: res,
snippetIdx: msg.snippetIdx,
}

this.controller.emitter.emit("sendMessage", {
message: message,
Expand All @@ -49,7 +61,7 @@ export class SpSnippetService extends ClientListener {
});
}

private async run(snippet: SpSnippetMessage): Promise<any> {
private async run(snippet: SpSnippetMessage): Promise<unknown> {
const functionLowerCase = snippet.function.toLowerCase();
const classLowerCase = snippet.class.toLowerCase();

Expand Down Expand Up @@ -164,7 +176,7 @@ export class SpSnippetService extends ClientListener {
return arg;
};

private async runMethod(snippet: SpSnippetMessage): Promise<any> {
private async runMethod(snippet: SpSnippetMessage): Promise<unknown> {
const selfId = remoteIdToLocalId(snippet.selfId);
const self = this.sp.Game.getFormEx(selfId);
if (!self)
Expand Down Expand Up @@ -196,7 +208,7 @@ export class SpSnippetService extends ClientListener {
);
};

private async runStatic(snippet: SpSnippetMessage): Promise<any> {
private async runStatic(snippet: SpSnippetMessage): Promise<unknown> {
const papyrusClass = this.spAny[snippet.class];
return await papyrusClass[snippet.function](
...snippet.arguments.map((arg) => this.deserializeArg(arg))
Expand Down
34 changes: 34 additions & 0 deletions skymp5-server/cpp/messages/ActivateMessage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#pragma once
#include "MessageBase.h"
#include "MsgType.h"
#include <optional>
#include <type_traits>

struct ActivateMessage : public MessageBase<ActivateMessage>
{
static constexpr auto kMsgType =
std::integral_constant<char, static_cast<char>(MsgType::Activate)>{};

struct Data
{
template <class Archive>
void Serialize(Archive& archive)
{
archive.Serialize("caster", caster)
.Serialize("target", target)
.Serialize("isSecondActivation", isSecondActivation);
}

uint64_t caster = 0;
uint64_t target = 0;
bool isSecondActivation = false;
};

template <class Archive>
void Serialize(Archive& archive)
{
archive.Serialize("t", kMsgType).Serialize("data", data);
}

Data data;
};
31 changes: 31 additions & 0 deletions skymp5-server/cpp/messages/ConsoleCommandMessage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#pragma once
#include "MessageBase.h"
#include "MsgType.h"
#include <optional>
#include <type_traits>

struct ConsoleCommandMessage : public MessageBase<ConsoleCommandMessage>
{
static constexpr auto kMsgType =
std::integral_constant<char, static_cast<char>(MsgType::ConsoleCommand)>{};

struct Data
{
template <class Archive>
void Serialize(Archive& archive)
{
archive.Serialize("commandName", commandName).Serialize("args", args);
}

std::string commandName;
std::vector<std::variant<int64_t, std::string>> args;
};

template <class Archive>
void Serialize(Archive& archive)
{
archive.Serialize("t", kMsgType).Serialize("data", data);
}

Data data;
};
Loading
Loading