10 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
e9e5ec2fd6 Avoid windows build-dir lock by using per-version build folders 2026-07-29 22:25:29 +00:00
copilot-swe-agent[bot]
0639bf2ae8 Fix windows build lock by disabling MSBuild node reuse 2026-07-29 22:18:45 +00:00
copilot-swe-agent[bot]
ed3bf6a952 Initial plan 2026-07-29 22:16:24 +00:00
Filip Georgiev
c7bccd62c6 Fix swampfire nano mission by by refactoring Mission.cpp (#322) 2026-07-29 15:12:40 -07:00
574d3111bc Switch to debian:trixie-slim image 2026-05-23 23:16:05 -07:00
424f83ed94 Add script to build in Docker 2026-05-23 23:09:18 -07:00
cd7caafec7 Docker fixes 2026-05-23 22:56:11 -07:00
ecc3e7731c Fix checks for trade item register/unregister 2026-05-23 21:15:40 -07:00
b2aa43b467 Add .clangd config for the clangd language server
PROTOCOL_VERSION is set to 1013 by default because that treats the
largest amount of code as active and subject to parsing and
verification.
2026-04-28 02:48:37 +02:00
04ebd46769 Fix potential for projectile leak 2026-04-18 14:35:07 -07:00
9 changed files with 104 additions and 81 deletions

2
.clangd Normal file
View File

@@ -0,0 +1,2 @@
CompileFlags:
Add: [-Wall, -Wno-unknown-pragmas, -std=c++17, -DPROTOCOL_VERSION=1013, -DGIT_VERSION="", -I./src, -I./vendor]

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
**/*.o

View File

@@ -84,11 +84,8 @@ jobs:
Invoke-Expression "vcpkg integrate install"
foreach ($version in $versions) {
if (Test-Path -LiteralPath "build") {
Remove-Item "build" -Recurse
Write-Output "Deleted existing build folder"
}
Invoke-Expression "cmake -B build -DPROTOCOL_VERSION=$version -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake"
$buildDir = "build-$version"
Invoke-Expression "cmake -B $buildDir -DPROTOCOL_VERSION=$version -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake"
if ($LASTEXITCODE -ne "0") {
Write-Error "cmake generation failed for version $version" -ErrorAction Stop
}
@@ -96,7 +93,7 @@ jobs:
foreach ($configuration in $configurations) {
Write-Output "Building version $version $configuration"
Invoke-Expression "msbuild build\OpenFusion.sln /maxcpucount:8 /p:BuildInParallel=true /p:CL_MPCount=8 /p:UseMultiToolTask=true /p:Configuration=$configuration"
Invoke-Expression "msbuild $buildDir\OpenFusion.sln /maxcpucount:8 /nodeReuse:false /p:BuildInParallel=true /p:CL_MPCount=8 /p:UseMultiToolTask=true /p:Configuration=$configuration"
if ($LASTEXITCODE -ne "0") {
Write-Error "msbuild build failed for version $version" -ErrorAction Stop
}

View File

@@ -1,41 +1,39 @@
# build
FROM alpine:3 as build
WORKDIR /usr/src/app
RUN apk update && apk upgrade && apk add \
linux-headers \
git \
clang18 \
make \
sqlite-dev
COPY src ./src
COPY vendor ./vendor
COPY .git ./.git
COPY Makefile CMakeLists.txt version.h.in ./
RUN sed -i 's/^CC=clang$/&-18/' Makefile
RUN sed -i 's/^CXX=clang++$/&-18/' Makefile
RUN make nosandbox -j$(nproc)
# prod
FROM alpine:3
WORKDIR /usr/src/app
RUN apk update && apk upgrade && apk add \
libstdc++ \
sqlite-dev
COPY --from=build /usr/src/app/bin/fusion /bin/fusion
COPY sql ./sql
CMD ["/bin/fusion"]
EXPOSE 23000/tcp
EXPOSE 23001/tcp
EXPOSE 8003/tcp
LABEL Name=openfusion Version=2.0.0
# build
FROM debian:trixie-slim as build
WORKDIR /usr/src/app
RUN apt update && apt upgrade -y && apt install -y \
git \
clang \
build-essential \
libsqlite3-dev
COPY src ./src
COPY vendor ./vendor
COPY .git ./.git
COPY Makefile CMakeLists.txt version.h.in ./
RUN make nosandbox -j$(nproc)
# export-only stage: `docker build --target=export --output=./bin .`
FROM scratch AS export
COPY --from=build /usr/src/app/bin/fusion /fusion
# prod
FROM debian:trixie-slim
WORKDIR /usr/src/app
RUN apt update && apt upgrade -y && apt install -y \
libsqlite3-dev
COPY --from=build /usr/src/app/bin/fusion /bin/fusion
CMD ["/bin/fusion"]
EXPOSE 23000/tcp
EXPOSE 23001/tcp
EXPOSE 8003/tcp
LABEL Name=openfusion Version=2.0.0

1
build_docker.sh Executable file
View File

@@ -0,0 +1 @@
docker build --target=export --output=./bin .

View File

@@ -8,6 +8,7 @@ services:
- ./config.ini:/usr/src/app/config.ini
- ./database.db:/usr/src/app/database.db
- ./tdata:/usr/src/app/tdata
- ./sql:/usr/src/app/sql
ports:
- "23000:23000"
- "23001:23001"

View File

@@ -804,8 +804,20 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT* pkt = (sP_CL2FE_REQ_PC_ROCKET_STYLE_HIT*)data->buf;
Player* plr = PlayerManager::getPlayer(sock);
if (plr == nullptr) {
return;
}
if (Bullets.find(plr->iID) == Bullets.end() || Bullets[plr->iID].find(pkt->iBulletID) == Bullets[plr->iID].end()) {
std::cout << "[WARN] projectileHit: bullet not found" << std::endl;
return;
}
// Remove the bullet immediately to prevent leaking it in early return paths
Bullet bullet = Bullets[plr->iID][pkt->iBulletID];
Bullets[plr->iID].erase(pkt->iBulletID);
if (pkt->iTargetCnt == 0) {
Bullets[plr->iID].erase(pkt->iBulletID);
// no targets hit, don't send response
return;
}
@@ -848,11 +860,6 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
sAttackResult* respdata = (sAttackResult*)(respbuf + sizeof(sP_FE2CL_PC_GRENADE_STYLE_HIT));
resp->iTargetCnt = pkt->iTargetCnt;
if (Bullets.find(plr->iID) == Bullets.end() || Bullets[plr->iID].find(pkt->iBulletID) == Bullets[plr->iID].end()) {
std::cout << "[WARN] projectileHit: bullet not found" << std::endl;
return;
}
Bullet* bullet = &Bullets[plr->iID][pkt->iBulletID];
for (int i = 0; i < pkt->iTargetCnt; i++) {
if (NPCManager::NPCs.find(pktdata[i]) == NPCManager::NPCs.end()) {
@@ -870,10 +877,10 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
Mob* mob = (Mob*)npc;
std::pair<int, int> damage;
damage.first = pkt->iTargetCnt > 1 ? bullet->groupDamage : bullet->pointDamage;
damage.first = pkt->iTargetCnt > 1 ? bullet.groupDamage : bullet.pointDamage;
int difficulty = (int)mob->data["m_iNpcLevel"];
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, bullet->weaponBoost, Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
damage = getDamage(damage.first, (int)mob->data["m_iProtection"], true, bullet.weaponBoost, Nanos::nanoStyle(plr->activeNano), (int)mob->data["m_iNpcStyle"], difficulty);
damage.first = mob->takeDamage(sock, damage.first);
@@ -885,11 +892,9 @@ static void projectileHit(CNSocket* sock, CNPacketData* data) {
resp->iPC_ID = plr->iID;
resp->iBulletID = pkt->iBulletID;
resp->Bullet.iID = bullet->bulletType;
resp->Bullet.iID = bullet.bulletType;
sock->sendPacket((void*)respbuf, P_FE2CL_PC_GRENADE_STYLE_HIT, resplen);
PlayerManager::sendToViewable(sock, (void*)respbuf, P_FE2CL_PC_GRENADE_STYLE_HIT, resplen);
Bullets[plr->iID].erase(resp->iBulletID);
}
static void playerTick(CNServer *serv, time_t currTime) {

View File

@@ -595,30 +595,33 @@ void Missions::mobKilled(CNSocket *sock, int mobid, std::map<int, int>& rolls) {
}
// drop quest item
if (task["m_iCSUItemNumNeeded"][j] != 0 && !isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j]) ) {
bool drop = rolls[plr->tasks[i]] % 100 < task["m_iSTItemDropRate"][j];
if (drop) {
dropQuestItem(sock, plr->tasks[i], 1, task["m_iCSUItemID"][j], mobid);
/*
* Workaround: The client has a bug where it only sends a TASK_END request
* for the first task of multiple that met their quest item requirements
* at the same time. We deal with this by sending TASK_END response packets
* proactively and then silently ignoring the extra TASK_END requests it
* sends afterwards.
*/
if (isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j])) {
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, end);
end.iTaskNum = plr->tasks[i];
if (!endTask(sock, plr->tasks[i]))
continue;
sock->sendPacket(end, P_FE2CL_REP_PC_TASK_END_SUCC);
if (task["m_iCSUItemNumNeeded"][j] != 0) {
if (!isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j])) {
bool drop = rolls[plr->tasks[i]] % 100 < task["m_iSTItemDropRate"][j];
if (drop) {
dropQuestItem(sock, plr->tasks[i], 1, task["m_iCSUItemID"][j], mobid);
}
} else {
// fail to drop (itemID == 0)
dropQuestItem(sock, plr->tasks[i], 1, 0, mobid);
else {
// fail to drop (itemID == 0)
dropQuestItem(sock, plr->tasks[i], 1, 0, mobid);
}
}
/*
* Workaround: The client has a bug where it only sends a TASK_END request
* for the first task of multiple that met their quest item requirements
* at the same time. We deal with this by sending TASK_END response packets
* proactively and then silently ignoring the extra TASK_END requests it
* sends afterwards.
*/
if (isQuestItemFull(sock, task["m_iCSUItemID"][j], task["m_iCSUItemNumNeeded"][j])) {
INITSTRUCT(sP_FE2CL_REP_PC_TASK_END_SUCC, end);
end.iTaskNum = plr->tasks[i];
if (!endTask(sock, plr->tasks[i]))
continue;
sock->sendPacket(end, P_FE2CL_REP_PC_TASK_END_SUCC);
}
}
}

View File

@@ -340,6 +340,9 @@ static void tradeConfirmCancel(CNSocket* sock, CNPacketData* data) {
static void tradeRegisterItem(CNSocket* sock, CNPacketData* data) {
sP_CL2FE_REQ_PC_TRADE_ITEM_REGISTER* pacdat = (sP_CL2FE_REQ_PC_TRADE_ITEM_REGISTER*)data->buf;
if (pacdat->Item.iInvenNum < 0 || pacdat->Item.iInvenNum >= AINVEN_COUNT)
return; // inventory bounds check
if (pacdat->Item.iSlotNum < 0 || pacdat->Item.iSlotNum > 4)
return; // sanity check, there are only 5 trade slots
@@ -353,7 +356,13 @@ static void tradeRegisterItem(CNSocket* sock, CNPacketData* data) {
return;
Player* plr = PlayerManager::getPlayer(sock);
if (!plr->isTrading)
return;
Player* plr2 = PlayerManager::getPlayer(otherSock);
if (!plr2->isTrading)
return;
plr->Trade[pacdat->Item.iSlotNum] = pacdat->Item;
plr->isTradeConfirm = false;
plr2->isTradeConfirm = false;
@@ -397,7 +406,13 @@ static void tradeUnregisterItem(CNSocket* sock, CNPacketData* data) {
return;
Player* plr = PlayerManager::getPlayer(sock);
if (!plr->isTrading)
return;
Player* plr2 = PlayerManager::getPlayer(otherSock);
if (!plr2->isTrading)
return;
plr->isTradeConfirm = false;
plr2->isTradeConfirm = false;