fix: preserve user figures in avatar imager
CI / check (push) Successful in 26s
CI / release (push) Skipped
CI / deploy (push) Successful in 44s

This commit is contained in:
Simo committed 2026-08-09 21:48:43 +02:00
1 parent 49185dd7a9
commit 4a6fcd050e
5 files changed
+12 -251

No files matched your search

-89
View File
@@ -1,89 +0,0 @@
import { describe, expect, it } from "vitest";
import {
convertFigurePart,
convertFigureString,
getNewFormatFigure,
} from "./figure-convert";
describe("convertFigurePart", () => {
it("converts head part", () => {
expect(convertFigurePart("hr-11782-40-40")).toBe("hd-180-1");
expect(convertFigurePart("hr-11782-41-41")).toBe("hd-180-2");
});
it("converts hair part", () => {
expect(convertFigurePart("hd-180-7-14")).toBe("hd-180-1");
expect(convertFigurePart("hd-180-8-14")).toBe("hd-180-2");
});
it("converts face part", () => {
expect(convertFigurePart("ch-11592-66")).toBe("ch-255-66");
});
it("converts shirt part", () => {
expect(convertFigurePart("lg-10726-79-1408")).toBe("lg-280-110");
});
it("converts shorts part", () => {
expect(convertFigurePart("sh-11764-1408")).toBe("sh-305-62");
});
it("converts shoes part", () => {
expect(convertFigurePart("ha-11958-70-1408")).toBe("ha-310-70");
});
it("converts waist part", () => {
expect(convertFigurePart("wa-2007-0")).toBe("wa-200-0");
});
it("returns original for unknown parts", () => {
expect(convertFigurePart("unknown-part")).toBe("unknown-part");
expect(convertFigurePart("hd-999-99")).toBe("hd-999-99");
});
});
describe("convertFigureString", () => {
it("converts full figure string from old to new format (with defaults: exclude shoes, waist, dedupe head)", () => {
const oldFigure =
"hr-11782-40-40.hd-180-7-14.ch-11592-66.lg-10726-79-1408.sh-11764-1408.ha-11958-70-1408.wa-2007-0";
const newFigure = "hd-180-1.ch-255-66.lg-280-110.sh-305-62";
expect(convertFigureString(oldFigure)).toBe(newFigure);
});
it("includes all parts when options disabled", () => {
const oldFigure =
"hr-11782-40-40.hd-180-7-14.ch-11592-66.lg-10726-79-1408.sh-11764-1408.ha-11958-70-1408.wa-2007-0";
const newFigure =
"hd-180-1.hd-180-1.ch-255-66.lg-280-110.sh-305-62.ha-310-70.wa-200-0";
expect(
convertFigureString(oldFigure, {
excludeShoes: false,
excludeWaist: false,
dedupeHead: false,
}),
).toBe(newFigure);
});
it("handles empty string", () => {
expect(convertFigureString("")).toBe("");
});
it("handles already converted figure", () => {
const newFigure = "hd-180-1.ch-255-66.lg-280-110";
expect(convertFigureString(newFigure)).toBe(newFigure);
});
});
describe("getNewFormatFigure", () => {
it("converts figure using getNewFormatFigure with defaults", () => {
const oldFigure = "hr-11782-40-40.hd-180-7-14.ch-11592-66";
expect(getNewFormatFigure(oldFigure)).toBe("hd-180-1.ch-255-66");
});
it("allows custom options", () => {
const oldFigure = "hr-11782-40-40.hd-180-7-14.ch-11592-66.ha-11958-70-1408";
expect(getNewFormatFigure(oldFigure, { excludeShoes: false })).toBe(
"hd-180-1.ch-255-66.ha-310-70",
);
});
});
File diff suppressed because it is too large. Load diff
+9
View File
@@ -37,6 +37,15 @@ describe("avatarImageUrl", () => {
expect(url.startsWith("https://img.example.com?")).toBe(true); expect(url.startsWith("https://img.example.com?")).toBe(true);
}); });
it("preserves the exact user look instead of replacing it with a default figure", () => {
const look =
"hr-11782-40-40.hd-180-7-14.ch-11592-66.lg-10726-79-1408.sh-11764-1408.ha-11958-70-1408.wa-2007-0";
const url = new URL(avatarImageUrl(look));
expect(url.searchParams.get("figure")).toBe(look);
});
it("uses & when the base already has a query string", () => { it("uses & when the base already has a query string", () => {
// This test is no longer applicable since avatarImageUrl doesn't accept a base parameter // This test is no longer applicable since avatarImageUrl doesn't accept a base parameter
// The base URL is now resolved from env // The base URL is now resolved from env
+1 -3
View File
@@ -1,11 +1,9 @@
import { getNewFormatFigure } from "@/lib/figure-convert";
import { getAvatarUrl } from "@/lib/imager"; import { getAvatarUrl } from "@/lib/imager";
import type { AvatarOptions } from "@/types/admin"; import type { AvatarOptions } from "@/types/admin";
/** Build a Habbo avatar-imager URL for a figure string. */ /** Build a Habbo avatar-imager URL for a figure string. */
export function avatarImageUrl(look: string, opts: AvatarOptions = {}): string { export function avatarImageUrl(look: string, opts: AvatarOptions = {}): string {
const figure = getNewFormatFigure(look); return getAvatarUrl(look, opts);
return getAvatarUrl(figure, opts);
} }
/** URL-safe slug from a title (lowercase, ascii, hyphenated). */ /** URL-safe slug from a title (lowercase, ascii, hyphenated). */
+2 -4
View File
@@ -8,7 +8,6 @@
export type { AvatarOptions } from "@/types/admin"; export type { AvatarOptions } from "@/types/admin";
import { env } from "@/env"; import { env } from "@/env";
import { getNewFormatFigure } from "@/lib/figure-convert";
import type { AvatarOptions } from "@/types/admin"; import type { AvatarOptions } from "@/types/admin";
const DEFAULT_IMAGER_URL = "https://epicnabbo.nl/imaging/avatarimage"; const DEFAULT_IMAGER_URL = "https://epicnabbo.nl/imaging/avatarimage";
@@ -28,14 +27,13 @@ export function resolveImagerBase(): string {
/** /**
* Build an avatar image URL using the configured imager. * Build an avatar image URL using the configured imager.
* Uses effect=14 and img_format=apng by default for epicnabbo.nl compatibility. * Uses effect=14 and img_format=apng by default for epicnabbo.nl compatibility.
* Automatically converts old Habbo figure format to new epicnabbo.nl format. * Preserves the user's stored figure so the rendered avatar matches the account.
* Omits default parameters (direction=2, head_direction=3, size=m) for cleaner URLs. * Omits default parameters (direction=2, head_direction=3, size=m) for cleaner URLs.
*/ */
export function getAvatarUrl( export function getAvatarUrl(
figure: string, figure: string,
options: AvatarOptions = {}, options: AvatarOptions = {},
): string { ): string {
const convertedFigure = getNewFormatFigure(figure);
const { const {
size = "m", size = "m",
direction = 2, direction = 2,
@@ -48,7 +46,7 @@ export function getAvatarUrl(
} = options; } = options;
const params = new URLSearchParams({ const params = new URLSearchParams({
figure: convertedFigure, figure,
img_format, img_format,
effect: String(effect), effect: String(effect),
}); });