Forward-port v31 → main (#4087)

**Add approved & assigned issue number here:**

Resolves #(issue number)

> Forward-port of changes already merged on the **v31** live-release
branch into **main** (next / v32). Source PRs: #3917, #3907, #4012, plus
the "clan tab on crazy games" change. Strait of Malacca (#3914) was
already on main and is excluded. No new feature work — please confirm
the tracking issue number above.

## Description:

Carries over v31 changes that never made it into main:

- **ToS / legal-docs update (#3917)** — updates
`resources/privacy-policy.html` and `resources/terms-of-service.html`,
plus the "OpenFront LLC → OpenFront Inc." entity rename and 2026
copyright bumps across `LICENSE-ASSETS`, `LICENSING.md`, and
`proprietary/LICENSE`.
- **Don't show clan tab on crazy games** — hides the clan tab on the
CrazyGames platform.
- **Show bonus amount on currency packs (#3907)** — displays the bonus
amount on currency packs; `en.json` conflict resolved by keeping both
sibling keys.
- **Per-recipient cooldown for QuickChatExecution (#4012)** — adapted to
main's refactored config (main folded `DefaultConfig.ts` into a single
`class Config`), adding `quickChatCooldown()` directly to the class.

Verification: `tsc --noEmit` clean, ESLint clean on changed files, full
suite passes (1319 + 65 tests).

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

jish

---------

Co-authored-by: iamlewis <lewismmmm@gmail.com>
Co-authored-by: evanpelle <evanpelle@gmail.com>
This commit is contained in:
Josh Harris
2026-05-31 15:39:37 +01:00
committed by GitHub
15 changed files with 168 additions and 43 deletions
+3 -3
View File
@@ -10,7 +10,7 @@ Assets in the `/resources` directory are licensed under Creative Commons BY-SA 4
- Font files
- Level/map data files
Attribution required: "OpenFront" or "OpenFront LLC"
Attribution required: "OpenFront" or "OpenFront Inc."
To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/
or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
@@ -18,7 +18,7 @@ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
## Proprietary Assets (/proprietary) - All Rights Reserved
Assets in the `/proprietary` directory are:
- Copyright © 2024-2025 OpenFront LLC and Contributors
- Copyright © 2024-2026 OpenFront Inc. and Contributors
- All Rights Reserved
- NOT licensed for use, modification, or redistribution outside of OpenFront
- May only be used as part of the complete OpenFront software package
@@ -34,7 +34,7 @@ The following assets are NOT included in this repository and are NOT covered by
- Sound effects and music accessed via API
- Any assets not explicitly included in this repository
These external assets are proprietary property of OpenFront LLC.
These external assets are proprietary property of OpenFront Inc.
## Important Legal Notice
+8 -8
View File
@@ -12,7 +12,7 @@ This document provides comprehensive licensing information for the OpenFront.io
- **Date Range:** 2024 - March 25, 2025
- **License:** MIT License only
- **Applies to:** Entire project
- **Copyright Holders:** WarFront.io Team, OpenFront LLC (and contributors)
- **Copyright Holders:** WarFront.io Team, OpenFront Inc. (and contributors)
- **Note:** OpenFront.io extensively modified/replaced almost all original code
### Phase 2: Mixed MIT/GPL
@@ -22,7 +22,7 @@ This document provides comprehensive licensing information for the OpenFront.io
- **Licenses:**
- GPL v3.0 for `src/client` directory
- MIT for all other directories
- **Copyright Holders:** OpenFront LLC (and contributors)
- **Copyright Holders:** OpenFront Inc. (and contributors)
- **Note:** Created split licensing structure with copyleft for client code
### Phase 3: AGPL + Creative Commons
@@ -32,7 +32,7 @@ This document provides comprehensive licensing information for the OpenFront.io
- **Licenses:**
- **Code:** Licensed under AGPL v3.0 (entire codebase)
- **Assets:** Creative Commons BY-SA 4.0 (all non-code assets)
- **Copyright Holders:** OpenFront LLC (and contributors)
- **Copyright Holders:** OpenFront Inc. (and contributors)
- **Changes:**
- Unified licensing approach (no more directory-specific licenses)
- Upgraded from GPL v3 to AGPL v3 for stronger network copyleft
@@ -47,13 +47,13 @@ This document provides comprehensive licensing information for the OpenFront.io
- **Code:** AGPL v3.0 (unchanged from Phase 3)
- **Open Assets (/resources):** Creative Commons BY-SA 4.0 (unchanged from Phase 3)
- **Proprietary Assets (/proprietary):** All Rights Reserved
- **Copyright Holders:** OpenFront LLC and Contributors
- **Copyright Holders:** OpenFront Inc. and Contributors
- **Changes:**
- Added `/proprietary` directory for copyrighted assets
- Established dual-track asset licensing structure
- Open assets in `/resources` remain CC BY-SA 4.0
- Proprietary assets override open assets during build process
- Contributors retain copyright while granting usage rights to OpenFront LLC
- Contributors retain copyright while granting usage rights to OpenFront Inc.
### Phase 5: Update AGPL Additional Attribution Terms
@@ -98,7 +98,7 @@ All assets included in this repository (graphics, sounds, music, models) are lic
These external assets are:
- Copyright © 2024-2025 OpenFront LLC
- Copyright © 2024-2026 OpenFront Inc.
- All Rights Reserved
- NOT licensed for use, modification, or redistribution
- Proprietary and may not be extracted, copied, or used outside of the official OpenFront.io service
@@ -113,12 +113,12 @@ The open source license (AGPL) applies ONLY to code. The CC BY-SA license applie
See CONTRIBUTING.md for current licensing requirements. All code contributions are licensed under AGPL v3 and assets under CC BY-SA 4.0.
For questions, contact OpenFront LLC.
For questions, contact OpenFront Inc.
## Historical MIT License
Portions Copyright (c) 2024 WarFront.io Team
Portions Copyright (c) 2024-2025 OpenFront LLC (and contributors)
Portions Copyright (c) 2024-2025 OpenFront Inc. (and contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+2 -2
View File
@@ -1,4 +1,4 @@
Copyright (c) 2025 OpenFront LLC and Contributors
Copyright (c) 2025-2026 OpenFront Inc. and Contributors
All Rights Reserved.
PROPRIETARY ASSET LICENSE
@@ -23,7 +23,7 @@ You may NOT:
CONTRIBUTIONS:
By contributing assets to this directory, you:
- Retain copyright to your original work
- Grant OpenFront LLC a perpetual, worldwide, non-exclusive, royalty-free license to use,
- Grant OpenFront Inc. a perpetual, worldwide, non-exclusive, royalty-free license to use,
modify, and distribute your contributions as part of OpenFront
- Agree that your contributions will be distributed under this restrictive license
- Warrant that you have the right to grant these permissions
+2 -1
View File
@@ -1271,7 +1271,8 @@
"adfree": "ad-free for life!",
"hard": "Plutonium",
"soft": "Caps",
"per_day": "/day"
"per_day": "/day",
"free": "+{numFree} BONUS!"
},
"flag_input": {
"title": "Select Flag",
+15 -15
View File
@@ -72,13 +72,13 @@
</head>
<body>
<h1>Privacy Policy</h1>
<p class="updated-date"><strong>Last Updated: 5/11/2026</strong></p>
<p class="updated-date"><strong>Last Updated: 5/13/2026</strong></p>
<p>
This Privacy Policy explains how OpenFront ("we," "us," "our") collects,
uses, shares, and protects your personal information when you use our
Service. It also sets out your rights in relation to your personal
information and how to exercise them.
This Privacy Policy explains how OpenFront Inc., a Delaware corporation
("OpenFront," "we," "us," "our"), collects, uses, shares, and protects
your personal information when you use our Service. It also sets out your
rights in relation to your personal information and how to exercise them.
</p>
<p>
By using our Service, you agree to the collection and use of information
@@ -90,10 +90,10 @@
<h2>1. Who We Are</h2>
<p>The data controller responsible for your personal information is:</p>
<p>
<strong>OpenFront LLC</strong><br />
c/o Northwest Registered Agent, Inc.<br />
2108 N Street, Suite N<br />
Sacramento, CA 95816, United States<br />
<strong>OpenFront Inc.</strong>, a Delaware corporation<br />
c/o Paracorp Incorporated<br />
2140 South Dupont Hwy<br />
Camden, Kent County, DE 19934, United States<br />
Email: <a href="mailto:legal@openfront.io">legal@openfront.io</a>
</p>
<p>
@@ -535,8 +535,8 @@
<h2>7. International Data Transfers</h2>
<p>
OpenFront LLC is based in the United States. Some of our service providers
are also based outside your country of residence. Your personal
OpenFront Inc. is based in the United States. Some of our service
providers are also based outside your country of residence. Your personal
information may therefore be transferred to, and processed in, countries
other than the country in which you are located, including countries that
may not provide the same level of data protection as your home country.
@@ -764,10 +764,10 @@
Legal enquiries:
<a href="mailto:legal@openfront.io">legal@openfront.io</a>
<br /><br />
OpenFront LLC<br />
c/o Northwest Registered Agent, Inc.<br />
2108 N Street, Suite N<br />
Sacramento, CA 95816, United States
OpenFront Inc.<br />
c/o Paracorp Incorporated<br />
2140 South Dupont Hwy<br />
Camden, Kent County, DE 19934, United States
</p>
<div class="footer">
+12 -11
View File
@@ -56,15 +56,16 @@
</head>
<body>
<h1>Terms of Service</h1>
<p class="updated-date"><strong>Last Updated: 5/11/2026</strong></p>
<p class="updated-date"><strong>Last Updated: 5/13/2026</strong></p>
<h2>1. Introduction</h2>
<p>
Welcome to OpenFront ("we," "our," "us"). These Terms of Service ("Terms")
govern your access to and use of our website at https://openfront.io, our
game clients distributed via Steam, Crazy Games, and other platforms, and
our Discord bot (collectively, the "Service"). By accessing or using our
Service, you agree to be bound by these Terms.
Welcome to OpenFront. These Terms of Service ("Terms") govern your access
to and use of our website at https://openfront.io, our game clients
distributed via Steam, Crazy Games, and other platforms, and our Discord
bot (collectively, the "Service"). The Service is provided by OpenFront
Inc., a Delaware corporation ("OpenFront," "we," "our," "us"). By
accessing or using our Service, you agree to be bound by these Terms.
</p>
<h2>2. Definitions</h2>
@@ -446,7 +447,7 @@
<h2>17. Governing Law</h2>
<p>
These Terms shall be governed and construed in accordance with the laws of
California, without regard to its conflict of law provisions.
the State of Delaware, without regard to its conflict of law provisions.
</p>
<h2>18. Severability</h2>
@@ -477,10 +478,10 @@
<br />
support@openfront.io
<br /><br />
OpenFront LLC<br />
c/o Northwest Registered Agent, Inc.<br />
2108 N Street, Suite N<br />
Sacramento, CA 95816, United States
OpenFront Inc.<br />
c/o Paracorp Incorporated<br />
2140 South Dupont Hwy<br />
Camden, Kent County, DE 19934, United States
</p>
<div class="footer">
+10 -1
View File
@@ -112,7 +112,7 @@ export class CosmeticButton extends LitElement {
const colorClass = isHard ? "text-green-400" : "text-amber-700";
const currencyKey = isHard ? "cosmetics.hard" : "cosmetics.soft";
return html`<div
class="flex flex-col items-center justify-end h-full w-full text-center gap-1 pb-1"
class="relative flex flex-col items-center justify-end h-full w-full text-center gap-1 pb-1"
>
${icon}
<span class="text-lg font-black ${colorClass}"
@@ -121,6 +121,15 @@ export class CosmeticButton extends LitElement {
<span class="text-[10px] font-bold text-white/50 uppercase"
>${translateText(currencyKey)}</span
>
${pack.bonusAmount > 0
? html`<div
class="absolute top-3 -right-8 bg-green-500 text-white text-[10px] font-black px-8 py-0.5 rotate-45 shadow-md uppercase tracking-wide pointer-events-none"
>
${translateText("cosmetics.free", {
numFree: pack.bonusAmount.toLocaleString(),
})}
</div>`
: nothing}
</div>`;
}
+1 -1
View File
@@ -124,7 +124,7 @@ export class DesktopNavBar extends LitElement {
data-i18n="main.leaderboard"
></button>
<button
class="nav-menu-item text-white/70 hover:text-blue-500 font-medium tracking-wider uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
class="no-crazygames nav-menu-item text-white/70 hover:text-blue-500 font-medium tracking-wider uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
data-page="page-clan"
data-i18n="main.clans"
></button>
+1 -1
View File
@@ -115,7 +115,7 @@ export class MobileNavBar extends LitElement {
data-i18n="main.leaderboard"
></button>
<button
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
class="no-crazygames nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
data-page="page-clan"
data-i18n="main.clans"
></button>
+1
View File
@@ -89,6 +89,7 @@ export const PackSchema = CosmeticSchema.extend({
displayName: z.string(),
currency: z.enum(["hard", "soft"]),
amount: z.number().int().positive(),
bonusAmount: z.number().int().nonnegative(),
});
export const SubscriptionSchema = CosmeticSchema.extend({
+3
View File
@@ -513,6 +513,9 @@ export class Config {
emojiMessageCooldown(): Tick {
return 5 * 10;
}
quickChatCooldown(): Tick {
return 3 * 10;
}
targetDuration(): Tick {
return 10 * 10;
}
+7
View File
@@ -27,8 +27,15 @@ export class QuickChatExecution implements Execution {
}
tick(ticks: number): void {
if (!this.sender.canSendQuickChat(this.recipient)) {
this.active = false;
return;
}
const message = this.getMessageFromKey(this.quickChatKey);
this.sender.recordQuickChat(this.recipient);
this.mg.displayChat(
message[1],
message[0],
+2
View File
@@ -808,6 +808,8 @@ export interface Player {
canSendEmoji(recipient: Player | typeof AllPlayers): boolean;
outgoingEmojis(): EmojiMessage[];
sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void;
canSendQuickChat(recipient: Player): boolean;
recordQuickChat(recipient: Player): void;
// Donation
canDonateGold(recipient: Player): boolean;
+16
View File
@@ -89,6 +89,7 @@ export class PlayerImpl implements Player {
private targets_: Target[] = [];
private outgoingEmojis_: EmojiMessage[] = [];
private outgoingQuickChats_ = new Map<number, Tick>();
private sentDonations: Donation[] = [];
@@ -759,6 +760,21 @@ export class PlayerImpl implements Player {
return true;
}
canSendQuickChat(recipient: Player): boolean {
if (recipient === this) {
return false;
}
const lastSentAt = this.outgoingQuickChats_.get(recipient.smallID());
return (
lastSentAt === undefined ||
this.mg.ticks() - lastSentAt >= this.mg.config().quickChatCooldown()
);
}
recordQuickChat(recipient: Player): void {
this.outgoingQuickChats_.set(recipient.smallID(), this.mg.ticks());
}
canDonateGold(recipient: Player): boolean {
if (
!this.isAlive() ||
+85
View File
@@ -0,0 +1,85 @@
import { QuickChatExecution } from "../src/core/execution/QuickChatExecution";
import { Game, Player, PlayerType } from "../src/core/game/Game";
import { playerInfo, setup } from "./util/Setup";
let game: Game;
let player1: Player;
let player2: Player;
let player3: Player;
describe("QuickChat cooldown", () => {
beforeEach(async () => {
game = await setup("plains", {}, [
playerInfo("player1", PlayerType.Human),
playerInfo("player2", PlayerType.Human),
playerInfo("player3", PlayerType.Human),
]);
player1 = game.player("player1");
player1.conquer(game.ref(0, 0));
player2 = game.player("player2");
player2.conquer(game.ref(0, 1));
player3 = game.player("player3");
player3.conquer(game.ref(0, 2));
while (game.inSpawnPhase()) {
game.executeNextTick();
}
});
// Helper: add an execution and advance two ticks so tick() actually runs.
// (addExecution → unInitExecs; first tick: init(); second tick: tick())
function sendQuickChat(sender: Player, recipient: Player) {
game.addExecution(
new QuickChatExecution(sender, recipient.id(), "greet.hello", undefined),
);
game.executeNextTick(); // init
game.executeNextTick(); // tick
}
test("first quick chat is sent", () => {
expect(player1.canSendQuickChat(player2)).toBe(true);
sendQuickChat(player1, player2);
expect(player1.canSendQuickChat(player2)).toBe(false);
});
test("second quick chat within cooldown is blocked", () => {
sendQuickChat(player1, player2);
expect(player1.canSendQuickChat(player2)).toBe(false);
// Even after the second attempt, cooldown persists
sendQuickChat(player1, player2);
expect(player1.canSendQuickChat(player2)).toBe(false);
});
test("quick chat is allowed again after cooldown expires", () => {
sendQuickChat(player1, player2);
expect(player1.canSendQuickChat(player2)).toBe(false);
// Advance past the cooldown (3 * 10 = 30 ticks)
const cooldown = game.config().quickChatCooldown();
for (let i = 0; i < cooldown; i++) {
game.executeNextTick();
}
expect(player1.canSendQuickChat(player2)).toBe(true);
});
test("cooldown is per-sender — different sender is not affected", () => {
sendQuickChat(player1, player2);
expect(player1.canSendQuickChat(player2)).toBe(false);
// player2 sending to player1 is independent
expect(player2.canSendQuickChat(player1)).toBe(true);
});
test("cooldown is per-recipient — same sender can still chat with a different recipient", () => {
sendQuickChat(player1, player2);
expect(player1.canSendQuickChat(player2)).toBe(false);
// player1 is on cooldown for player2 but not for player3
expect(player1.canSendQuickChat(player3)).toBe(true);
});
});