feat: synthesize size 32 (catalog icon) frame for all furniture .nitro bundles
AddSize32ToBundle downscales the largest existing sprite (usually size 64) to generate a 32 visualization, so every bundle — including the source-provided gamedata furniture — carries a 32 frame without needing the original SWF. AddSize32AllNitros rewrites every existing .nitro across all nitro directories (incl. /var/www/Gamedata/bundled/furniture) and is idempotent (bundles that already have 32 are skipped). Expose it via a Studio button and the background admin endpoint POST /api/admin/import/furni/add-size32.
This commit is contained in:
1 parent
2996ae3ab0
commit
ac7c427c3d
4 files changed
+668
-1
No files matched your search
@@ -12,6 +12,7 @@ import {
|
|||||||
FileWarning,
|
FileWarning,
|
||||||
FolderSync,
|
FolderSync,
|
||||||
Globe2,
|
Globe2,
|
||||||
|
ImageDown,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
List,
|
List,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -147,6 +148,7 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) {
|
|||||||
const [recentResyncResult, setRecentResyncResult] =
|
const [recentResyncResult, setRecentResyncResult] =
|
||||||
useState<RecentFurniResyncResult | null>(null);
|
useState<RecentFurniResyncResult | null>(null);
|
||||||
const [verifyingInteractions, setVerifyingInteractions] = useState(false);
|
const [verifyingInteractions, setVerifyingInteractions] = useState(false);
|
||||||
|
const [addingSize32, setAddingSize32] = useState(false);
|
||||||
const [verifyResult, setVerifyResult] =
|
const [verifyResult, setVerifyResult] =
|
||||||
useState<VerifyInteractionsResult | null>(null);
|
useState<VerifyInteractionsResult | null>(null);
|
||||||
const searchRef = useRef<HTMLInputElement>(null);
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -775,6 +777,38 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addSize32All() {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
"Add a size-32 visualization to every existing .nitro that lacks one? This rewrites the live furniture bundles (incl. /var/www/Gamedata) and can take a while.",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAddingSize32(true);
|
||||||
|
try {
|
||||||
|
const res = await adminFetch("/api/admin/import/furni/add-size32", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
const d = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
toast.success(
|
||||||
|
d.alreadyRunning
|
||||||
|
? "Size-32 conversion is already running in the background"
|
||||||
|
: "Started size-32 conversion in the background — watch the server log for progress",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error(d.error || "Failed to start size-32 conversion");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "Size-32 conversion failed",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setAddingSize32(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function applyReorganize() {
|
async function applyReorganize() {
|
||||||
setReorgPreview(null);
|
setReorgPreview(null);
|
||||||
setReorganizing(true);
|
setReorganizing(true);
|
||||||
@@ -1551,6 +1585,10 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) {
|
|||||||
<FolderSync className="w-4 h-4 mr-2" />
|
<FolderSync className="w-4 h-4 mr-2" />
|
||||||
{reorganizing ? "Reorganizing..." : "Reorganize Catalog"}
|
{reorganizing ? "Reorganizing..." : "Reorganize Catalog"}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={addSize32All} disabled={addingSize32}>
|
||||||
|
<ImageDown className="w-4 h-4 mr-2" />
|
||||||
|
{addingSize32 ? "Adding size 32..." : "Add size 32 to all .nitro"}
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={verifyAllInteractions}
|
onClick={verifyAllInteractions}
|
||||||
disabled={verifyingInteractions}
|
disabled={verifyingInteractions}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { PERMS } from "@/lib/permissions";
|
||||||
|
import { logAudit } from "@/lib/services/audit";
|
||||||
|
import { addSize32AllNitros } from "@/lib/services/repair-nitros";
|
||||||
|
|
||||||
|
interface Size32Status {
|
||||||
|
running: boolean;
|
||||||
|
startedAt: number | null;
|
||||||
|
finishedAt: number | null;
|
||||||
|
succeeded: number;
|
||||||
|
failed: number;
|
||||||
|
skipped: number;
|
||||||
|
existing: number;
|
||||||
|
lastMessage: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status: Size32Status = {
|
||||||
|
running: false,
|
||||||
|
startedAt: null,
|
||||||
|
finishedAt: null,
|
||||||
|
succeeded: 0,
|
||||||
|
failed: 0,
|
||||||
|
skipped: 0,
|
||||||
|
existing: 0,
|
||||||
|
lastMessage: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
function runJob(userId: number) {
|
||||||
|
status.running = true;
|
||||||
|
status.startedAt = Date.now();
|
||||||
|
status.finishedAt = null;
|
||||||
|
status.succeeded = 0;
|
||||||
|
status.failed = 0;
|
||||||
|
status.skipped = 0;
|
||||||
|
status.existing = 0;
|
||||||
|
status.lastMessage = "started";
|
||||||
|
|
||||||
|
addSize32AllNitros((evt) => {
|
||||||
|
if (evt.type === "progress") {
|
||||||
|
if (evt.status === "done") status.succeeded++;
|
||||||
|
else if (evt.status === "convert-failed" || evt.status === "write-failed")
|
||||||
|
status.failed++;
|
||||||
|
else status.skipped++;
|
||||||
|
if (evt.message)
|
||||||
|
status.lastMessage = `${evt.classname}: ${evt.status} ${evt.message}`;
|
||||||
|
} else if (evt.type === "batch_complete") {
|
||||||
|
status.succeeded = evt.succeeded ?? 0;
|
||||||
|
status.failed = evt.failed ?? 0;
|
||||||
|
status.skipped = evt.skipped ?? 0;
|
||||||
|
status.existing = evt.existing ?? 0;
|
||||||
|
status.lastMessage = "done";
|
||||||
|
} else if (evt.type === "error") {
|
||||||
|
status.lastMessage = evt.message ?? null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
status.succeeded = result.succeeded;
|
||||||
|
status.failed = result.failed;
|
||||||
|
status.skipped = result.skipped;
|
||||||
|
status.existing = result.existing;
|
||||||
|
status.lastMessage = "done";
|
||||||
|
status.running = false;
|
||||||
|
status.finishedAt = Date.now();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
status.lastMessage = (err as Error).message;
|
||||||
|
status.running = false;
|
||||||
|
status.finishedAt = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
logAudit({
|
||||||
|
userId,
|
||||||
|
action: "furni_add_size32",
|
||||||
|
target: "FurniNitro",
|
||||||
|
targetId: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST = withAdmin(
|
||||||
|
{ permission: PERMS.ASSETS_IMPORT },
|
||||||
|
async (_request, ctx) => {
|
||||||
|
if (status.running) {
|
||||||
|
return apiOk({ ...status, alreadyRunning: true });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
runJob(ctx.session.user.id);
|
||||||
|
return apiOk({ ...status, started: true });
|
||||||
|
} catch (err) {
|
||||||
|
return apiError((err as Error).message, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const GET = withAdmin({ permission: PERMS.ASSETS_IMPORT }, async () => {
|
||||||
|
return apiOk({ ...status });
|
||||||
|
});
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user