Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
42d32d1
Add files via upload
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 28, 2026
05d3788
Update index.ts
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 28, 2026
6c00bae
Update TankBody.ts
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 28, 2026
fdae111
Update TankBody.ts
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 28, 2026
847194d
Add files via upload
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 28, 2026
03f2a41
Add files via upload
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 29, 2026
3f5db7b
rework
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 29, 2026
dce180e
Update Achievements.ts
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 29, 2026
1b752bc
Comment
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 29, 2026
b3e1ce1
Update TankBody.ts
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 29, 2026
c9456db
Type fix
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 30, 2026
022237f
Fix ram achievements
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 30, 2026
117881b
fix crash
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 30, 2026
65e96e6
More tag support
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
7ffdefe
Achievements config var
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
ab49419
Achievements config var
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
446ab06
Achievements config var
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
52c780d
Achievements config var
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
371c572
Disable in sbx
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
440162c
Disable in sbx
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
c8d6dca
Disable in sbx
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
a90ba18
Disable in sbx
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
e017dab
Format
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
f49df0a
Format
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
d390720
Fix multiple tags case
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
0403bc8
Fix
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
3da884f
Death anim tweaks
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
be63486
More tags
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
5f170a1
Typo
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
baddc6d
Typo
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
6e9c682
Death anim tweaks
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
eacf37b
Physics
c86ec23b-fef1-4979-b2fa-b9adc351b8cc Aug 31, 2026
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
20 changes: 18 additions & 2 deletions src/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { CameraFlags, ClientBound, ArenaFlags, InputFlags, NameFlags, ServerBoun
import { AI, AIState, Inputs } from "./Entity/AI";
import AbstractBoss from "./Entity/Boss/AbstractBoss";
import { executeCommand } from "./Const/Commands";
import { sendAchievements } from "./Const/Achievements";
import { bannedClients } from ".";

/** XORed onto the tank id in the Tank Upgrade packet. */
Expand Down Expand Up @@ -109,6 +110,9 @@ export default class Client {
/** Wether or not the player is in godmode. */
public isInvulnerable: boolean = false;

/** Achievements unlocked this tick */
private pendingAchievements: string[] = [];

/** Returns a new writer stream connected to the socket. */
public write() {
return new WSWriterStream(this);
Expand Down Expand Up @@ -325,7 +329,7 @@ export default class Client {

player.destroy();
player.onDeath(player);
player.onKill(player);
player.onKill(player, player);
}
}

Expand Down Expand Up @@ -511,6 +515,10 @@ export default class Client {
this.write().u8(ClientBound.Notification).stringNT(text).u32(color).float(time).stringNT(id).send();
}

public giveAchievements(achievements: string[]) {
this.pendingAchievements.push(...achievements);
}

/** Bans the ip from all servers until restart. */
public ban() {
const ws = this.ws;
Expand Down Expand Up @@ -543,7 +551,6 @@ export default class Client {
const tank = camera.cameraData.player = camera.relationsData.owner = camera.relationsData.parent = new TankBody(this.game, camera, this.inputs);
tank.setTank(Tank.Basic);
tank.nameData.values.name = name;
this.game.arena.spawnPlayer(tank, this);
camera.setLevel(camera.cameraData.values.respawnLevel);

if (this.hasCheated()) this.setHasCheated(true);
Expand All @@ -553,6 +560,8 @@ export default class Client {
camera.spectatee = null;
this.inputs.isPossessing = false;
this.inputs.movement.magnitude = 0;

this.game.arena.spawnPlayer(tank, this); // Should be last
}

public tick(tick: number) {
Expand Down Expand Up @@ -592,9 +601,16 @@ export default class Client {
this.camera.cameraData.cameraX = this.camera.cameraData.cameraY = 0;
this.camera.cameraData.flags &= ~CameraFlags.showingDeathStats;
}

if (tick >= this.lastPingTick + 60 * config.tps) {
return this.terminate();
}

if (this.pendingAchievements.length) {
sendAchievements(this, this.pendingAchievements);

this.pendingAchievements.length = 0;
}
}
/** toString override from base Object. Adds debug info */
public toString(verbose: boolean = false): string {
Expand Down
268 changes: 268 additions & 0 deletions src/Const/Achievements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/*
DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import _Achievements from "./Achievements.json";
import Client from "../Client";
import Writer from "../Coder/Writer";
import { ClientBound, Tank } from "./Enums";
import { DevTank } from "./DevTankDefinitions";
import { enableAchievements } from "../config";

const NAME_SEED = 170;
const DESC_SEED = 221;

const OP_EQUALS = 0;
const OP_GTE = 1;
const OP_LTE = 2;

/** The event types achievements can have */
export type eventId = "kill" | "score" | "levelUp" | "statUpgraded" | "classChange" | "latency";

/** The types of achievements */
export type achievementType = "counter";

/**
* Format that the game stores achievements in its memory.
*/
export interface AchievementDefinition {
/** Achievement name */
name: string;
/** Achievement description */
desc: string;
/** Conditions needed to unlock */
conds: AchievementCondition[];
/** Achievement hash which is sent to clients */
hash: string;
}

export interface AchievementTags {
/** Current value */
"value"?: string | number;
/** Total value */
"total"?: string | number;
/** Value change */
"delta"?: string | number;
/** Tank ID */
"class"?: Tank | DevTank;
/** Tank level */
"level"?: number;
/** Stat ID */
"id"?: number;
/** Is max stat level */
"isMaxLevel"?: boolean;
/** Used for ramming case */
"weapon.isTank"?: boolean;
/** Was the victim a tank? */
"victim.isTank"?: boolean;
/** Was the victim a boss? */
"victim.isBoss"?: boolean;
/** Was the victim a shiny shape? */
"victim.isShiny"?: boolean;
/** Victim tank ID */
"victim.class"?: Tank | DevTank;
/** Victim mob ID */
"victim.arenaMobID"?: string | null;
}

export interface AchievementCondition {
event?: eventId;
type?: achievementType;
tags: AchievementTags;
threshold?: number;
/** Created during parsing */
op?: number | null;
}

/** From https://github.com/jtpio/murmurhash2 */
export const MurMurHash2 = (str: string, seed: number): number => {
const m = 0x5bd1e995;
const encoder = new TextEncoder();

const data = encoder.encode(str);
let len = data.length;
let h = seed ^ len;
let i = 0;

while (len >= 4) {
let k =
(data[i] & 0xff) |
((data[++i] & 0xff) << 8) |
((data[++i] & 0xff) << 16) |
((data[++i] & 0xff) << 24);

k = (k & 0xffff) * m + ((((k >>> 16) * m) & 0xffff) << 16);
k ^= k >>> 24;
k = (k & 0xffff) * m + ((((k >>> 16) * m) & 0xffff) << 16);

h = ((h & 0xffff) * m + ((((h >>> 16) * m) & 0xffff) << 16)) ^ k;

len -= 4;
++i;
}

switch (len) {
case 3:
h ^= (data[i + 2] & 0xff) << 16;
case 2:
h ^= (data[i + 1] & 0xff) << 8;
case 1:
h ^= data[i] & 0xff;
h = (h & 0xffff) * m + ((((h >>> 16) * m) & 0xffff) << 16);
}

h ^= h >>> 13;
h = (h & 0xffff) * m + ((((h >>> 16) * m) & 0xffff) << 16);
h ^= h >>> 15;

return h >>> 0;
}

export const createAchievementHash = (a: AchievementDefinition) => {
return `${MurMurHash2(a.name, NAME_SEED).toString(16)}${MurMurHash2(a.desc, DESC_SEED).toString(16)}_1`;
}

export const compileConds = (conds: AchievementCondition[]) => {
for (const c of conds) {
const tags = c.tags;

for (const key in tags) {
const value = tags[key as keyof AchievementTags] as string;

if (key === "total" || key === "value" || key === "delta") {
tags[key] = parseInt(value.slice(2));
const op = value.slice(0, 2);

switch (op) {
case "==":
c.op = OP_EQUALS;
break;
case ">=":
c.op = OP_GTE;
break;
case "<=":
c.op = OP_LTE;
break;
default: throw new Error(`Invalid operation: ${op}`);
}
}
}
}

return conds;
}

export const compileAchievement = (a: AchievementDefinition) => {
return {
...a,
hash: createAchievementHash(a),
conds: compileConds(a.conds)
}
}

const Achievements = _Achievements.map(a => compileAchievement(a as AchievementDefinition));
export default Achievements;

export const achievementEventMap = Achievements.reduce((map, a) => {
for (const c of a.conds) {
const byEvent = map.get(c.event);

if (!byEvent) {
map.set(c.event, [a]);
continue;
}

byEvent.push(a);
}

return map;
}, new Map());

export const sendAchievementEvent = (client: Client, event: eventId, data: AchievementTags) => {
if (!enableAchievements) return;

const completed = [];

const achievements = achievementEventMap.get(event);

for (const a of achievements) {
if (checkCondition(a, data)) {
completed.push(a.hash);
}
}

if (completed.length) {
client.giveAchievements(completed);
}
}

const checkCondition = (achievement: AchievementDefinition, data: AchievementTags) => {
const conds = achievement.conds;

return conds.every(condition => parseCondition(condition, data));
}

const parseCondition = (conds: AchievementCondition | null, data: AchievementTags): boolean => {
if (!conds) return true;

const tags = conds.tags
for (const key in tags) {
const value = tags[key as keyof AchievementTags]!;
const givenValue = data[key as keyof AchievementTags]!;

if (key === "total" || key === "value" || key === "delta") {
const op = conds.op;

switch (op) {
case OP_EQUALS: // ==
if (givenValue !== value) return false;
break;
case OP_GTE: // >=
if (givenValue < value) return false;
break;
case OP_LTE: // <=
if (givenValue > value) return false;
break;
}
} else {
if (givenValue !== value) {
return false;
}
}
}

return true;
}

export const sendAchievements = (client: Client, hashes: string[]) => {
if (client.terminated) return;

const w = client.write();

w.u8(ClientBound.Achievement);
w.vu(hashes.length);

for (let i = 0; i < hashes.length; ++i) {
w.stringNT(hashes[i]);
}

w.send();
}

export const getAchievementByName = (name: string): AchievementDefinition | null => {
return Achievements.find(a => a.name === name) || null;
}
19 changes: 17 additions & 2 deletions src/Const/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { Entity, EntityStateFlags } from "../Native/Entity";
import { saveToVLog } from "../util";
import { ClientBound, Stat, StatCount, PhysicsFlags, StyleFlags, Tank } from "./Enums";
import { getTankByName } from "./TankDefinitions";
import { sendAchievements, getAchievementByName } from "./Achievements";

const RELATIVE_POS_REGEX = new RegExp(/~(-?\d+)?/);

Expand All @@ -69,6 +70,7 @@ export const enum CommandID {
gameAnnounce = "game_announce",
gameGoldenName = "game_golden_name",
gameNeutral = "game_neutral",
gameAchievement = "game_achievement",
adminSummon = "admin_summon",
adminKillAll = "admin_kill_all",
adminKillEntity = "admin_kill_entity",
Expand Down Expand Up @@ -164,12 +166,19 @@ export const commandDefinitions = {
permissionLevel: AccessLevel.FullAccess,
isCheat: false
},
game_neutral: {
game_neutral: {
id: CommandID.gameNeutral,
description: "Sets your tank's team to the neutral team",
permissionLevel: AccessLevel.FullAccess,
isCheat: false
},
game_achievement: {
id: CommandID.gameAchievement,
usage: "[achievementName]",
description: "Increments the given achievement. Example usage: game_achievement \"Shiny!\"",
permissionLevel: AccessLevel.FullAccess,
isCheat: false
},
admin_summon: {
id: CommandID.adminSummon,
usage: "[entityName] [?count] [?x] [?y]",
Expand Down Expand Up @@ -313,7 +322,7 @@ export const commandCallbacks = {
.float(parseInt(time))
.stringNT(id).send();
},
game_golden_name: (client: Client, activeArg?: string) => {
game_golden_name: (client: Client) => {
client.setHasCheated(!client.hasCheated());
},
game_neutral: (client: Client) => {
Expand All @@ -325,6 +334,12 @@ export const commandCallbacks = {

TeamEntity.setTeam(team, player);
},
game_achievement: (client: Client, nameArg: string) => {
const achievement = getAchievementByName(nameArg);
if (!achievement) return;

sendAchievements(client, [achievement.hash]);
},
admin_summon: (client: Client, entityArg: string, countArg?: string, xArg?: string, yArg?: string) => {
const count = countArg ? parseInt(countArg) : 1;
let x = parseInt(xArg || "0", 10);
Expand Down
Loading
Loading