fix: audio on GNU/Linux

This commit is contained in:
neoapps-dev
2026-04-25 20:24:19 +03:00
parent 9d80eea7d7
commit 87e841f565
2 changed files with 797 additions and 243 deletions
+381 -120
View File
@@ -13,10 +13,21 @@ export default function PckEditorView() {
const [openedPath, setOpenedPath] = useState<string | null>(null); const [openedPath, setOpenedPath] = useState<string | null>(null);
const [selectedAssetId, setSelectedAssetId] = useState<string | null>(null); const [selectedAssetId, setSelectedAssetId] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [isEditingProperty, setIsEditingProperty] = useState<{ idx: number, key: string, val: string } | null>(null); const [isEditingProperty, setIsEditingProperty] = useState<{
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set()); idx: number;
const [notification, setNotification] = useState<{ message: string, type: "success" | "error" } | null>(null); key: string;
const [showTypeModal, setShowTypeModal] = useState<{ file: { name: string, data: Uint8Array } } | null>(null); val: string;
} | null>(null);
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(
new Set(),
);
const [notification, setNotification] = useState<{
message: string;
type: "success" | "error";
} | null>(null);
const [showTypeModal, setShowTypeModal] = useState<{
file: { name: string; data: Uint8Array };
} | null>(null);
const [isChangingType, setIsChangingType] = useState(false); const [isChangingType, setIsChangingType] = useState(false);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const replaceInputRef = useRef<HTMLInputElement>(null); const replaceInputRef = useRef<HTMLInputElement>(null);
@@ -32,8 +43,12 @@ export default function PckEditorView() {
} }
const root: Record<string, TempNode> = {}; const root: Record<string, TempNode> = {};
pck.files.forEach(asset => { pck.files.forEach((asset) => {
if (searchTerm && !asset.path.toLowerCase().includes(searchTerm.toLowerCase())) return; if (
searchTerm &&
!asset.path.toLowerCase().includes(searchTerm.toLowerCase())
)
return;
const parts = asset.path.split("/"); const parts = asset.path.split("/");
let currentLevel = root; let currentLevel = root;
let currentPath = ""; let currentPath = "";
@@ -46,7 +61,7 @@ export default function PckEditorView() {
path: currentPath, path: currentPath,
isFolder: !isLast, isFolder: !isLast,
asset: isLast ? asset : undefined, asset: isLast ? asset : undefined,
children: {} children: {},
}; };
} }
currentLevel = currentLevel[part].children; currentLevel = currentLevel[part].children;
@@ -60,9 +75,9 @@ export default function PckEditorView() {
if (!a.isFolder && b.isFolder) return 1; if (!a.isFolder && b.isFolder) return 1;
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}) })
.map(node => ({ .map((node) => ({
...node, ...node,
children: convert(node.children) children: convert(node.children),
})); }));
}; };
@@ -70,13 +85,24 @@ export default function PckEditorView() {
}, [pck, searchTerm]); }, [pck, searchTerm]);
const selectedAsset = useMemo(() => { const selectedAsset = useMemo(() => {
return pck?.files.find(f => f.id === selectedAssetId) || null; return pck?.files.find((f) => f.id === selectedAssetId) || null;
}, [pck, selectedAssetId]); }, [pck, selectedAssetId]);
const [assetPreview, setAssetPreview] = useState<{ id: string, url: string } | null>(null); const [assetPreview, setAssetPreview] = useState<{
id: string;
url: string;
} | null>(null);
useEffect(() => { useEffect(() => {
if (!selectedAsset || ![PCKAssetType.SKIN, PCKAssetType.CAPE, PCKAssetType.TEXTURE, PCKAssetType.SKIN_DATA].includes(selectedAsset.type)) { if (
!selectedAsset ||
![
PCKAssetType.SKIN,
PCKAssetType.CAPE,
PCKAssetType.TEXTURE,
PCKAssetType.SKIN_DATA,
].includes(selectedAsset.type)
) {
setAssetPreview(null); setAssetPreview(null);
return; return;
} }
@@ -90,7 +116,10 @@ export default function PckEditorView() {
}; };
}, [selectedAsset]); }, [selectedAsset]);
const assetPreviewUrl = (assetPreview && selectedAsset && assetPreview.id === selectedAsset.id) ? assetPreview.url : null; const assetPreviewUrl =
assetPreview && selectedAsset && assetPreview.id === selectedAsset.id
? assetPreview.url
: null;
const toggleFolder = (path: string) => { const toggleFolder = (path: string) => {
const next = new Set(expandedFolders); const next = new Set(expandedFolders);
@@ -100,7 +129,7 @@ export default function PckEditorView() {
}; };
const renderTree = (nodes: any[], depth = 0) => { const renderTree = (nodes: any[], depth = 0) => {
return nodes.map(node => { return nodes.map((node) => {
const isExpanded = expandedFolders.has(node.path) || !!searchTerm; const isExpanded = expandedFolders.has(node.path) || !!searchTerm;
const isSelected = selectedAssetId === node.asset?.id; const isSelected = selectedAssetId === node.asset?.id;
return ( return (
@@ -115,14 +144,19 @@ export default function PckEditorView() {
} }
}} }}
style={{ paddingLeft: `${depth * 16 + 12}px` }} style={{ paddingLeft: `${depth * 16 + 12}px` }}
className={`flex items-center gap-2 p-2 cursor-pointer transition-all border-l-2 ${isSelected className={`flex items-center gap-2 p-2 cursor-pointer transition-all border-l-2 ${
? "bg-[#FFFF55]/10 border-[#FFFF55] text-[#FFFF55]" isSelected
: "border-transparent hover:bg-white/5 text-white" ? "bg-[#FFFF55]/10 border-[#FFFF55] text-[#FFFF55]"
} ${node.isFolder ? "font-bold" : ""}`} : "border-transparent hover:bg-white/5 text-white"
} ${node.isFolder ? "font-bold" : ""}`}
> >
{node.isFolder ? ( {node.isFolder ? (
<img <img
src={isExpanded ? "/images/Settings_Arrow_Down.png" : "/images/Settings_Arrow_Right.png"} src={
isExpanded
? "/images/Settings_Arrow_Down.png"
: "/images/Settings_Arrow_Right.png"
}
className="w-3 h-3 object-contain opacity-80" className="w-3 h-3 object-contain opacity-80"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
@@ -130,7 +164,11 @@ export default function PckEditorView() {
<div className="w-3" /> <div className="w-3" />
)} )}
<img <img
src={node.isFolder ? "/images/Folder_Icon.png" : "/images/tools/pck.png"} src={
node.isFolder
? "/images/Folder_Icon.png"
: "/images/tools/pck.png"
}
className={`w-4 h-4 object-contain ${isSelected ? "" : "grayscale opacity-60"}`} className={`w-4 h-4 object-contain ${isSelected ? "" : "grayscale opacity-60"}`}
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
@@ -179,7 +217,7 @@ export default function PckEditorView() {
endianness: "little", endianness: "little",
xmlSupport: false, xmlSupport: false,
properties: ["ANIM", "BOX"], properties: ["ANIM", "BOX"],
files: [] files: [],
}; };
setPck(newPck); setPck(newPck);
setOpenedPath(null); setOpenedPath(null);
@@ -188,7 +226,10 @@ export default function PckEditorView() {
showNotification("New PCK Created"); showNotification("New PCK Created");
}; };
const showNotification = (message: string, type: "success" | "error" = "success") => { const showNotification = (
message: string,
type: "success" | "error" = "success",
) => {
setNotification({ message, type }); setNotification({ message, type });
setTimeout(() => setNotification(null), 3000); setTimeout(() => setNotification(null), 3000);
}; };
@@ -196,7 +237,11 @@ export default function PckEditorView() {
const handleExportAsset = async (asset: PCKAsset) => { const handleExportAsset = async (asset: PCKAsset) => {
try { try {
const fileName = asset.path.split("/").pop() || "asset"; const fileName = asset.path.split("/").pop() || "asset";
const path = await TauriService.saveFileDialog("Export Asset", fileName, []); const path = await TauriService.saveFileDialog(
"Export Asset",
fileName,
[],
);
if (!path) return; if (!path) return;
playPressSound(); playPressSound();
await TauriService.writeBinaryFile(path, asset.data); await TauriService.writeBinaryFile(path, asset.data);
@@ -209,8 +254,8 @@ export default function PckEditorView() {
const handleDeleteAsset = (id: string) => { const handleDeleteAsset = (id: string) => {
if (!pck) return; if (!pck) return;
playBackSound(); playBackSound();
const newFiles = pck.files.filter(f => f.id !== id); const newFiles = pck.files.filter((f) => f.id !== id);
const assetPath = pck.files.find(f => f.id === id)?.path; const assetPath = pck.files.find((f) => f.id === id)?.path;
setPck({ ...pck, files: newFiles }); setPck({ ...pck, files: newFiles });
if (selectedAssetId === id) setSelectedAssetId(newFiles[0]?.id || null); if (selectedAssetId === id) setSelectedAssetId(newFiles[0]?.id || null);
showNotification(`Deleted: ${assetPath?.split("/").pop()}`); showNotification(`Deleted: ${assetPath?.split("/").pop()}`);
@@ -225,7 +270,9 @@ export default function PckEditorView() {
const buffer = await file.arrayBuffer(); const buffer = await file.arrayBuffer();
const data = new Uint8Array(buffer); const data = new Uint8Array(buffer);
const newFiles = pck.files.map(f => f.id === selectedAssetId ? { ...f, data, size: data.length } : f); const newFiles = pck.files.map((f) =>
f.id === selectedAssetId ? { ...f, data, size: data.length } : f,
);
setPck({ ...pck, files: newFiles }); setPck({ ...pck, files: newFiles });
e.target.value = ""; e.target.value = "";
showNotification("Asset Replaced"); showNotification("Asset Replaced");
@@ -251,7 +298,7 @@ export default function PckEditorView() {
type, type,
size: file.data.length, size: file.data.length,
data: file.data, data: file.data,
properties: [] properties: [],
}; };
if (type === PCKAssetType.SKIN || type === PCKAssetType.CAPE) { if (type === PCKAssetType.SKIN || type === PCKAssetType.CAPE) {
@@ -266,7 +313,7 @@ export default function PckEditorView() {
const handlePropertyEdit = (idx: number, newVal: string, isKey = false) => { const handlePropertyEdit = (idx: number, newVal: string, isKey = false) => {
if (!pck || !selectedAssetId) return; if (!pck || !selectedAssetId) return;
const newFiles = pck.files.map(f => { const newFiles = pck.files.map((f) => {
if (f.id === selectedAssetId) { if (f.id === selectedAssetId) {
const newProps = [...f.properties]; const newProps = [...f.properties];
if (isKey) { if (isKey) {
@@ -287,11 +334,11 @@ export default function PckEditorView() {
const handleAddProperty = () => { const handleAddProperty = () => {
if (!pck || !selectedAssetId) return; if (!pck || !selectedAssetId) return;
playPressSound(); playPressSound();
const newFiles = pck.files.map(f => { const newFiles = pck.files.map((f) => {
if (f.id === selectedAssetId) { if (f.id === selectedAssetId) {
return { return {
...f, ...f,
properties: [...f.properties, { key: "NEW_PROPERTY", value: "0" }] properties: [...f.properties, { key: "NEW_PROPERTY", value: "0" }],
}; };
} }
return f; return f;
@@ -302,7 +349,7 @@ export default function PckEditorView() {
const handleRemoveProperty = (idx: number) => { const handleRemoveProperty = (idx: number) => {
if (!pck || !selectedAssetId) return; if (!pck || !selectedAssetId) return;
playBackSound(); playBackSound();
const newFiles = pck.files.map(f => { const newFiles = pck.files.map((f) => {
if (f.id === selectedAssetId) { if (f.id === selectedAssetId) {
const newProps = [...f.properties]; const newProps = [...f.properties];
newProps.splice(idx, 1); newProps.splice(idx, 1);
@@ -316,7 +363,7 @@ export default function PckEditorView() {
const handleTypeChange = (newType: PCKAssetType) => { const handleTypeChange = (newType: PCKAssetType) => {
if (!pck || !selectedAssetId) return; if (!pck || !selectedAssetId) return;
playPressSound(); playPressSound();
const newFiles = pck.files.map(f => { const newFiles = pck.files.map((f) => {
if (f.id === selectedAssetId) { if (f.id === selectedAssetId) {
return { ...f, type: newType }; return { ...f, type: newType };
} }
@@ -325,11 +372,11 @@ export default function PckEditorView() {
setPck({ ...pck, files: newFiles }); setPck({ ...pck, files: newFiles });
}; };
const handleMoveAsset = (direction: 'up' | 'down') => { const handleMoveAsset = (direction: "up" | "down") => {
if (!pck || !selectedAssetId) return; if (!pck || !selectedAssetId) return;
const idx = pck.files.findIndex(f => f.id === selectedAssetId); const idx = pck.files.findIndex((f) => f.id === selectedAssetId);
if (idx === -1) return; if (idx === -1) return;
const newIdx = direction === 'up' ? idx - 1 : idx + 1; const newIdx = direction === "up" ? idx - 1 : idx + 1;
if (newIdx < 0 || newIdx >= pck.files.length) return; if (newIdx < 0 || newIdx >= pck.files.length) return;
playPressSound(); playPressSound();
const newFiles = [...pck.files]; const newFiles = [...pck.files];
@@ -340,7 +387,7 @@ export default function PckEditorView() {
const handleRenameAsset = (id: string, newPath: string) => { const handleRenameAsset = (id: string, newPath: string) => {
if (!pck) return; if (!pck) return;
playPressSound(); playPressSound();
const newFiles = pck.files.map(f => { const newFiles = pck.files.map((f) => {
if (f.id === id) { if (f.id === id) {
return { ...f, path: newPath }; return { ...f, path: newPath };
} }
@@ -359,9 +406,11 @@ export default function PckEditorView() {
showNotification("Exporting all assets..."); showNotification("Exporting all assets...");
for (const asset of pck.files) { for (const asset of pck.files) {
const parts = asset.path.split("/"); const parts = asset.path.split("/");
let currentPath = baseFolder;
const fileName = parts.join("_"); const fileName = parts.join("_");
await TauriService.writeBinaryFile(`${baseFolder}/${fileName}`, asset.data); await TauriService.writeBinaryFile(
`${baseFolder}/${fileName}`,
asset.data,
);
} }
showNotification("All Assets Exported"); showNotification("All Assets Exported");
} catch (err: any) { } catch (err: any) {
@@ -377,7 +426,11 @@ export default function PckEditorView() {
try { try {
let targetPath = openedPath; let targetPath = openedPath;
if (!targetPath) { if (!targetPath) {
targetPath = await TauriService.saveFileDialog("Save PCK", pck.files.length > 0 ? "new.pck" : "empty.pck", ["pck"]); targetPath = await TauriService.saveFileDialog(
"Save PCK",
pck.files.length > 0 ? "new.pck" : "empty.pck",
["pck"],
);
} }
if (targetPath) { if (targetPath) {
@@ -409,15 +462,24 @@ export default function PckEditorView() {
const getTypeColor = (type: PCKAssetType) => { const getTypeColor = (type: PCKAssetType) => {
switch (type) { switch (type) {
case PCKAssetType.SKIN: return "#FFFF55"; case PCKAssetType.SKIN:
case PCKAssetType.SKIN_DATA: return "#FFFF55"; return "#FFFF55";
case PCKAssetType.CAPE: return "#AA00AA"; case PCKAssetType.SKIN_DATA:
case PCKAssetType.TEXTURE: return "#55FFFF"; return "#FFFF55";
case PCKAssetType.AUDIO_DATA: return "#55FF55"; case PCKAssetType.CAPE:
case PCKAssetType.UI_DATA: return "#FFAA00"; return "#AA00AA";
case PCKAssetType.LOCALISATION: return "#FF55FF"; case PCKAssetType.TEXTURE:
case PCKAssetType.MODELS: return "#5555FF"; return "#55FFFF";
default: return "#AAAAAA"; case PCKAssetType.AUDIO_DATA:
return "#55FF55";
case PCKAssetType.UI_DATA:
return "#FFAA00";
case PCKAssetType.LOCALISATION:
return "#FF55FF";
case PCKAssetType.MODELS:
return "#5555FF";
default:
return "#AAAAAA";
} }
}; };
@@ -440,22 +502,31 @@ export default function PckEditorView() {
<button <button
onClick={handleFileLoad} onClick={handleFileLoad}
className="px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none" className="px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{
backgroundImage: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
}}
> >
Open PCK Open PCK
</button> </button>
<button <button
onClick={handleNewPCK} onClick={handleNewPCK}
className="px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none" className="px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{
backgroundImage: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
}}
> >
New PCK New PCK
</button> </button>
<button <button
onClick={handleExportAll} onClick={handleExportAll}
disabled={!pck || pck.files.length === 0} disabled={!pck || pck.files.length === 0}
className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${(!pck || pck.files.length === 0) ? "opacity-50 grayscale" : ""}`} className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${!pck || pck.files.length === 0 ? "opacity-50 grayscale" : ""}`}
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{
backgroundImage: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
}}
> >
Export All Export All
</button> </button>
@@ -463,7 +534,10 @@ export default function PckEditorView() {
onClick={handleSavePCK} onClick={handleSavePCK}
disabled={!pck} disabled={!pck}
className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${!pck ? "opacity-50 grayscale" : ""}`} className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${!pck ? "opacity-50 grayscale" : ""}`}
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{
backgroundImage: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
}}
> >
Save PCK Save PCK
</button> </button>
@@ -474,42 +548,86 @@ export default function PckEditorView() {
<div className="w-full flex gap-4 px-8 mb-4"> <div className="w-full flex gap-4 px-8 mb-4">
<div className="flex gap-6 bg-black/40 border-2 border-[#373737] p-3 w-full"> <div className="flex gap-6 bg-black/40 border-2 border-[#373737] p-3 w-full">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-white/40 text-xs uppercase font-bold tracking-widest">Endianness:</span> <span className="text-white/40 text-xs uppercase font-bold tracking-widest">
Endianness:
</span>
<button <button
onClick={() => { playPressSound(); setPck({ ...pck, endianness: pck.endianness === "little" ? "big" : "little" }); }} onClick={() => {
playPressSound();
setPck({
...pck,
endianness: pck.endianness === "little" ? "big" : "little",
});
}}
className="text-[#FFFF55] text-sm uppercase hover:underline" className="text-[#FFFF55] text-sm uppercase hover:underline"
> >
{pck.endianness} {pck.endianness}
</button> </button>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-white/40 text-xs uppercase font-bold tracking-widest">XML Support:</span> <span className="text-white/40 text-xs uppercase font-bold tracking-widest">
XML Support:
</span>
<button <button
onClick={() => { playPressSound(); setPck({ ...pck, xmlSupport: !pck.xmlSupport }); }} onClick={() => {
playPressSound();
setPck({ ...pck, xmlSupport: !pck.xmlSupport });
}}
className={`${pck.xmlSupport ? "text-[#FFFF55]" : "text-white/20"} text-sm uppercase hover:underline`} className={`${pck.xmlSupport ? "text-[#FFFF55]" : "text-white/20"} text-sm uppercase hover:underline`}
> >
{pck.xmlSupport ? "Enabled" : "Disabled"} {pck.xmlSupport ? "Enabled" : "Disabled"}
</button> </button>
</div> </div>
<div className="flex items-center gap-2 ml-auto"> <div className="flex items-center gap-2 ml-auto">
<span className="text-white/40 text-xs uppercase font-bold tracking-widest">Version:</span> <span className="text-white/40 text-xs uppercase font-bold tracking-widest">
Version:
</span>
<span className="text-white text-sm">{pck.version}</span> <span className="text-white text-sm">{pck.version}</span>
</div> </div>
</div> </div>
</div> </div>
)} )}
<input type="file" ref={replaceInputRef} onChange={handleReplaceAsset} className="hidden" /> <input
<input type="file" ref={addAssetInputRef} onChange={handleAddAsset} className="hidden" /> type="file"
ref={replaceInputRef}
onChange={handleReplaceAsset}
className="hidden"
/>
<input
type="file"
ref={addAssetInputRef}
onChange={handleAddAsset}
className="hidden"
/>
{!pck ? ( {!pck ? (
<div className="flex-1 w-full flex flex-col items-center justify-center p-12" <div
style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> className="flex-1 w-full flex flex-col items-center justify-center p-12"
<img src="/images/tools/pck.png" className="w-32 h-32 mb-8 opacity-20 grayscale" style={{ imageRendering: "pixelated" }} /> style={{
<h3 className="text-2xl text-white/40 mc-text-shadow italic">Open a PCK file to begin editing</h3> backgroundImage: "url('/images/frame_background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
<img
src="/images/tools/pck.png"
className="w-32 h-32 mb-8 opacity-20 grayscale"
style={{ imageRendering: "pixelated" }}
/>
<h3 className="text-2xl text-white/40 mc-text-shadow italic">
Open a PCK file to begin editing
</h3>
</div> </div>
) : ( ) : (
<div className="flex-1 w-full flex gap-4 overflow-hidden"> <div className="flex-1 w-full flex gap-4 overflow-hidden">
<div className="w-2/3 flex flex-col p-4" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> <div
className="w-2/3 flex flex-col p-4"
style={{
backgroundImage: "url('/images/frame_background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
<div className="mb-4 flex gap-4"> <div className="mb-4 flex gap-4">
<input <input
type="text" type="text"
@@ -521,7 +639,10 @@ export default function PckEditorView() {
<button <button
onClick={() => addAssetInputRef.current?.click()} onClick={() => addAssetInputRef.current?.click()}
className="px-4 py-2 text-white mc-text-shadow text-sm shrink-0" className="px-4 py-2 text-white mc-text-shadow text-sm shrink-0"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{
backgroundImage: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
}}
> >
Add Asset Add Asset
</button> </button>
@@ -530,11 +651,22 @@ export default function PckEditorView() {
{renderTree(treeData)} {renderTree(treeData)}
</div> </div>
</div> </div>
<div className="w-1/3 flex flex-col p-6 overflow-y-auto" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> <div
className="w-1/3 flex flex-col p-6 overflow-y-auto"
style={{
backgroundImage: "url('/images/frame_background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{!selectedAsset ? ( {!selectedAsset ? (
<div className="flex-1 flex flex-col items-center justify-center text-white/20 italic gap-4"> <div className="flex-1 flex flex-col items-center justify-center text-white/20 italic gap-4">
<img src="/images/tools/pck.png" className="w-16 h-16 opacity-10 grayscale" style={{ imageRendering: "pixelated" }} /> <img
src="/images/tools/pck.png"
className="w-16 h-16 opacity-10 grayscale"
style={{ imageRendering: "pixelated" }}
/>
<span>Select an asset to view details</span> <span>Select an asset to view details</span>
</div> </div>
) : ( ) : (
@@ -554,8 +686,14 @@ export default function PckEditorView() {
onClick={() => setIsChangingType(!isChangingType)} onClick={() => setIsChangingType(!isChangingType)}
className="flex items-center gap-1.5 px-2 py-0.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-sm transition-all group/type" className="flex items-center gap-1.5 px-2 py-0.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-sm transition-all group/type"
> >
<span className="text-[10px] uppercase tracking-widest mc-text-shadow font-bold" style={{ color: getTypeColor(selectedAsset.type) }}> <span
{PCKAssetType[selectedAsset.type].replace(/_/g, " ")} className="text-[10px] uppercase tracking-widest mc-text-shadow font-bold"
style={{ color: getTypeColor(selectedAsset.type) }}
>
{PCKAssetType[selectedAsset.type].replace(
/_/g,
" ",
)}
</span> </span>
<img <img
src="/images/Settings_Arrow_Down.png" src="/images/Settings_Arrow_Down.png"
@@ -580,16 +718,21 @@ export default function PckEditorView() {
exit={{ opacity: 0, scale: 0.95, y: -10 }} exit={{ opacity: 0, scale: 0.95, y: -10 }}
className="absolute top-full left-0 mt-2 z-[130] p-2 min-w-[180px] grid grid-cols-1 gap-1 shadow-2xl" className="absolute top-full left-0 mt-2 z-[130] p-2 min-w-[180px] grid grid-cols-1 gap-1 shadow-2xl"
style={{ style={{
backgroundImage: "url('/images/frame_background.png')", backgroundImage:
"url('/images/frame_background.png')",
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
imageRendering: "pixelated" imageRendering: "pixelated",
}} }}
> >
{Object.keys(PCKAssetType) {Object.keys(PCKAssetType)
.filter(k => isNaN(Number(k))) .filter((k) => isNaN(Number(k)))
.map((typeName) => { .map((typeName) => {
const typeVal = PCKAssetType[typeName as keyof typeof PCKAssetType]; const typeVal =
const isActive = selectedAsset.type === typeVal; PCKAssetType[
typeName as keyof typeof PCKAssetType
];
const isActive =
selectedAsset.type === typeVal;
return ( return (
<button <button
key={typeName} key={typeName}
@@ -597,13 +740,20 @@ export default function PckEditorView() {
handleTypeChange(typeVal); handleTypeChange(typeVal);
setIsChangingType(false); setIsChangingType(false);
}} }}
className={`w-full text-left px-3 py-2 text-[10px] uppercase tracking-widest transition-all border-l-2 ${isActive className={`w-full text-left px-3 py-2 text-[10px] uppercase tracking-widest transition-all border-l-2 ${
? "bg-white/10 border-[#FFFF55] text-white" isActive
: "border-transparent text-white/40 hover:text-white/80 hover:bg-white/5" ? "bg-white/10 border-[#FFFF55] text-white"
}`} : "border-transparent text-white/40 hover:text-white/80 hover:bg-white/5"
}`}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: getTypeColor(typeVal) }} /> <div
className="w-1.5 h-1.5 rounded-full"
style={{
backgroundColor:
getTypeColor(typeVal),
}}
/>
{typeName.replace(/_/g, " ")} {typeName.replace(/_/g, " ")}
</div> </div>
</button> </button>
@@ -616,24 +766,53 @@ export default function PckEditorView() {
</div> </div>
</div> </div>
<div className="flex gap-2 shrink-0"> <div className="flex gap-2 shrink-0">
<button onClick={() => handleMoveAsset('up')} className="hover:scale-110 active:scale-95 transition-transform"> <button
<img src="/images/Settings_Arrow_Up.png" className="w-4 h-4 object-contain" style={{ imageRendering: "pixelated" }} /> onClick={() => handleMoveAsset("up")}
className="hover:scale-110 active:scale-95 transition-transform"
>
<img
src="/images/Settings_Arrow_Up.png"
className="w-4 h-4 object-contain"
style={{ imageRendering: "pixelated" }}
/>
</button> </button>
<button onClick={() => handleMoveAsset('down')} className="hover:scale-110 active:scale-95 transition-transform"> <button
<img src="/images/Settings_Arrow_Down.png" className="w-4 h-4 object-contain" style={{ imageRendering: "pixelated" }} /> onClick={() => handleMoveAsset("down")}
className="hover:scale-110 active:scale-95 transition-transform"
>
<img
src="/images/Settings_Arrow_Down.png"
className="w-4 h-4 object-contain"
style={{ imageRendering: "pixelated" }}
/>
</button> </button>
</div> </div>
</div> </div>
{assetPreviewUrl && ( {assetPreviewUrl && (
<div className="w-full h-[550px] bg-black/40 border-2 border-[#373737] mb-6 flex items-center justify-center overflow-hidden relative group"> <div className="w-full h-[550px] bg-black/40 border-2 border-[#373737] mb-6 flex items-center justify-center overflow-hidden relative group">
{(selectedAsset.type === PCKAssetType.SKIN || selectedAsset.type === PCKAssetType.CAPE || selectedAsset.type === PCKAssetType.SKIN_DATA) ? ( {selectedAsset.type === PCKAssetType.SKIN ||
<SkinPreview3D key={selectedAsset.id} asset={selectedAsset} previewUrl={assetPreviewUrl || undefined} className="w-full h-full" /> selectedAsset.type === PCKAssetType.CAPE ||
selectedAsset.type === PCKAssetType.SKIN_DATA ? (
<SkinPreview3D
key={selectedAsset.id}
asset={selectedAsset}
previewUrl={assetPreviewUrl || undefined}
className="w-full h-full"
/>
) : ( ) : (
<img src={assetPreviewUrl} className="max-w-full max-h-full object-contain" style={{ imageRendering: "pixelated" }} /> <img
src={assetPreviewUrl}
className="max-w-full max-h-full object-contain"
style={{ imageRendering: "pixelated" }}
/>
)} )}
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-black/60 px-2 py-1 rounded text-[10px] text-white/60 pointer-events-none uppercase tracking-widest"> <div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-black/60 px-2 py-1 rounded text-[10px] text-white/60 pointer-events-none uppercase tracking-widest">
{selectedAsset.type === PCKAssetType.SKIN ? "3D Skin View" : selectedAsset.type === PCKAssetType.CAPE ? "3D Cape View" : "Texture Preview"} {selectedAsset.type === PCKAssetType.SKIN
? "3D Skin View"
: selectedAsset.type === PCKAssetType.CAPE
? "3D Cape View"
: "Texture Preview"}
</div> </div>
</div> </div>
)} )}
@@ -641,7 +820,9 @@ export default function PckEditorView() {
<div className="space-y-6 flex-1"> <div className="space-y-6 flex-1">
<div> <div>
<div className="flex justify-between items-end mb-2 px-1"> <div className="flex justify-between items-end mb-2 px-1">
<div className="text-white/40 text-[10px] uppercase tracking-widest text-[#FFFF55]/60">Metadata Properties</div> <div className="text-white/40 text-[10px] uppercase tracking-widest text-[#FFFF55]/60">
Metadata Properties
</div>
<button <button
onClick={handleAddProperty} onClick={handleAddProperty}
className="text-[#FFFF55] text-[10px] uppercase hover:underline" className="text-[#FFFF55] text-[10px] uppercase hover:underline"
@@ -651,12 +832,17 @@ export default function PckEditorView() {
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{selectedAsset.properties.map((prop, idx) => ( {selectedAsset.properties.map((prop, idx) => (
<div key={idx} className="flex flex-col gap-1 group/prop"> <div
key={idx}
className="flex flex-col gap-1 group/prop"
>
<div className="flex justify-between items-center px-1"> <div className="flex justify-between items-center px-1">
<input <input
type="text" type="text"
value={prop.key} value={prop.key}
onChange={(e) => handlePropertyEdit(idx, e.target.value, true)} onChange={(e) =>
handlePropertyEdit(idx, e.target.value, true)
}
className="bg-transparent text-white/40 text-[10px] outline-none hover:text-white/60 focus:text-[#FFFF55] w-2/3" className="bg-transparent text-white/40 text-[10px] outline-none hover:text-white/60 focus:text-[#FFFF55] w-2/3"
/> />
<button <button
@@ -670,7 +856,9 @@ export default function PckEditorView() {
<input <input
type="text" type="text"
value={prop.value} value={prop.value}
onChange={(e) => handlePropertyEdit(idx, e.target.value)} onChange={(e) =>
handlePropertyEdit(idx, e.target.value)
}
className="w-full bg-black/40 p-2 text-white border border-[#373737] text-sm focus:border-[#FFFF55] outline-none transition-colors" className="w-full bg-black/40 p-2 text-white border border-[#373737] text-sm focus:border-[#FFFF55] outline-none transition-colors"
/> />
</div> </div>
@@ -694,7 +882,10 @@ export default function PckEditorView() {
{ label: "04: No Body", flag: 0x2000 }, { label: "04: No Body", flag: 0x2000 },
{ label: "04: No R. Leg", flag: 0x4000 }, { label: "04: No R. Leg", flag: 0x4000 },
{ label: "04: No L. Leg", flag: 0x8000 }, { label: "04: No L. Leg", flag: 0x8000 },
{ label: "05: No Head Overlay", flag: 0x10000 }, {
label: "05: No Head Overlay",
flag: 0x10000,
},
{ label: "05: Back Crouch", flag: 0x20000 }, { label: "05: Back Crouch", flag: 0x20000 },
{ label: "05: Modern Skin", flag: 0x40000 }, { label: "05: Modern Skin", flag: 0x40000 },
{ label: "05: Slim Skin", flag: 0x80000 }, { label: "05: Slim Skin", flag: 0x80000 },
@@ -703,18 +894,40 @@ export default function PckEditorView() {
{ label: "06: No L. Pant", flag: 0x400000 }, { label: "06: No L. Pant", flag: 0x400000 },
{ label: "06: No R. Pant", flag: 0x800000 }, { label: "06: No R. Pant", flag: 0x800000 },
{ label: "07: No Jacket", flag: 0x1000000 }, { label: "07: No Jacket", flag: 0x1000000 },
{ label: "07: Rend Head Arm", flag: 0x2000000 }, {
{ label: "07: Rend R.Arm Arm", flag: 0x4000000 }, label: "07: Rend Head Arm",
{ label: "07: Rend L.Arm Arm", flag: 0x8000000 }, flag: 0x2000000,
{ label: "08: Rend Body Arm", flag: 0x10000000 }, },
{ label: "08: Rend R.Leg Arm", flag: 0x20000000 }, {
{ label: "08: Rend L.Leg Arm", flag: 0x40000000 }, label: "07: Rend R.Arm Arm",
flag: 0x4000000,
},
{
label: "07: Rend L.Arm Arm",
flag: 0x8000000,
},
{
label: "08: Rend Body Arm",
flag: 0x10000000,
},
{
label: "08: Rend R.Leg Arm",
flag: 0x20000000,
},
{
label: "08: Rend L.Leg Arm",
flag: 0x40000000,
},
{ label: "08: Dinnerbone", flag: 0x80000000 }, { label: "08: Dinnerbone", flag: 0x80000000 },
].map((item) => { ].map((item) => {
const currentVal = parseInt(prop.value) || 0; const currentVal = parseInt(prop.value) || 0;
const isChecked = (currentVal & item.flag) !== 0; const isChecked =
(currentVal & item.flag) !== 0;
return ( return (
<label key={item.label} className="flex items-center gap-2 cursor-pointer group/flag"> <label
key={item.label}
className="flex items-center gap-2 cursor-pointer group/flag"
>
<input <input
type="checkbox" type="checkbox"
checked={isChecked} checked={isChecked}
@@ -722,12 +935,19 @@ export default function PckEditorView() {
const newVal = e.target.checked const newVal = e.target.checked
? currentVal | item.flag ? currentVal | item.flag
: currentVal & ~item.flag; : currentVal & ~item.flag;
handlePropertyEdit(idx, `0x${newVal.toString(16).toUpperCase().padStart(8, "0")}`); handlePropertyEdit(
idx,
`0x${newVal.toString(16).toUpperCase().padStart(8, "0")}`,
);
}} }}
className="hidden" className="hidden"
/> />
<div className={`w-3 h-3 border transition-colors ${isChecked ? "bg-[#FFFF55] border-[#FFFF55]" : "border-white/20 group-hover/flag:border-white/40"}`} /> <div
<span className={`text-[9px] uppercase tracking-tight ${isChecked ? "text-[#FFFF55]" : "text-white/40 group-hover/flag:text-white/60"}`}> className={`w-3 h-3 border transition-colors ${isChecked ? "bg-[#FFFF55] border-[#FFFF55]" : "border-white/20 group-hover/flag:border-white/40"}`}
/>
<span
className={`text-[9px] uppercase tracking-tight ${isChecked ? "text-[#FFFF55]" : "text-white/40 group-hover/flag:text-white/60"}`}
>
{item.label} {item.label}
</span> </span>
</label> </label>
@@ -738,7 +958,9 @@ export default function PckEditorView() {
</div> </div>
))} ))}
{selectedAsset.properties.length === 0 && ( {selectedAsset.properties.length === 0 && (
<div className="text-white/20 italic text-sm px-1 py-4 border-2 border-dashed border-[#373737] text-center">No metadata properties</div> <div className="text-white/20 italic text-sm px-1 py-4 border-2 border-dashed border-[#373737] text-center">
No metadata properties
</div>
)} )}
</div> </div>
</div> </div>
@@ -748,14 +970,22 @@ export default function PckEditorView() {
<button <button
onClick={() => handleExportAsset(selectedAsset)} onClick={() => handleExportAsset(selectedAsset)}
className="py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]" className="py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{
backgroundImage:
"url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
}}
> >
Export Export
</button> </button>
<button <button
onClick={() => replaceInputRef.current?.click()} onClick={() => replaceInputRef.current?.click()}
className="py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]" className="py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{
backgroundImage:
"url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
}}
> >
Replace Replace
</button> </button>
@@ -763,14 +993,22 @@ export default function PckEditorView() {
<button <button
onClick={() => setIsRenamingAsset(selectedAsset.id)} onClick={() => setIsRenamingAsset(selectedAsset.id)}
className="w-full py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]" className="w-full py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{
backgroundImage:
"url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
}}
> >
Rename Asset (Path) Rename Asset (Path)
</button> </button>
<button <button
onClick={() => handleDeleteAsset(selectedAsset.id)} onClick={() => handleDeleteAsset(selectedAsset.id)}
className="w-full py-2 text-red-500/80 mc-text-shadow text-sm transition-all hover:text-red-500 hover:scale-[1.02]" className="w-full py-2 text-red-500/80 mc-text-shadow text-sm transition-all hover:text-red-500 hover:scale-[1.02]"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{
backgroundImage:
"url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
}}
> >
Delete This Asset Delete This Asset
</button> </button>
@@ -808,7 +1046,7 @@ export default function PckEditorView() {
style={{ style={{
backgroundImage: "url('/images/frame_background.png')", backgroundImage: "url('/images/frame_background.png')",
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
imageRendering: "pixelated" imageRendering: "pixelated",
}} }}
> >
<span className="text-white text-lg mc-text-shadow font-bold tracking-widest uppercase"> <span className="text-white text-lg mc-text-shadow font-bold tracking-widest uppercase">
@@ -836,17 +1074,23 @@ export default function PckEditorView() {
style={{ style={{
backgroundImage: "url('/images/frame_background.png')", backgroundImage: "url('/images/frame_background.png')",
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
imageRendering: "pixelated" imageRendering: "pixelated",
}} }}
> >
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase">Select Asset Type</h3> <h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase">
Select Asset Type
</h3>
<div className="grid grid-cols-2 gap-4 w-full"> <div className="grid grid-cols-2 gap-4 w-full">
{Object.keys(PCKAssetType) {Object.keys(PCKAssetType)
.filter(k => isNaN(Number(k))) .filter((k) => isNaN(Number(k)))
.map((typeName) => ( .map((typeName) => (
<button <button
key={typeName} key={typeName}
onClick={() => confirmAddAsset(PCKAssetType[typeName as keyof typeof PCKAssetType])} onClick={() =>
confirmAddAsset(
PCKAssetType[typeName as keyof typeof PCKAssetType],
)
}
className="py-3 px-4 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55] border-2 border-transparent hover:border-[#FFFF55]/30 bg-black/40" className="py-3 px-4 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55] border-2 border-transparent hover:border-[#FFFF55]/30 bg-black/40"
> >
{typeName.replace(/_/g, " ")} {typeName.replace(/_/g, " ")}
@@ -866,7 +1110,9 @@ export default function PckEditorView() {
<AnimatePresence> <AnimatePresence>
{isRenamingAsset && ( {isRenamingAsset && (
<RenameAssetModal <RenameAssetModal
initialPath={pck?.files.find(f => f.id === isRenamingAsset)?.path || ""} initialPath={
pck?.files.find((f) => f.id === isRenamingAsset)?.path || ""
}
onClose={() => setIsRenamingAsset(null)} onClose={() => setIsRenamingAsset(null)}
onConfirm={(newPath) => { onConfirm={(newPath) => {
handleRenameAsset(isRenamingAsset, newPath); handleRenameAsset(isRenamingAsset, newPath);
@@ -879,7 +1125,15 @@ export default function PckEditorView() {
); );
} }
function RenameAssetModal({ initialPath, onClose, onConfirm }: { initialPath: string, onClose: () => void, onConfirm: (path: string) => void }) { function RenameAssetModal({
initialPath,
onClose,
onConfirm,
}: {
initialPath: string;
onClose: () => void;
onConfirm: (path: string) => void;
}) {
const [path, setPath] = useState(initialPath); const [path, setPath] = useState(initialPath);
return ( return (
<div className="fixed inset-0 z-[150] flex items-center justify-center p-4"> <div className="fixed inset-0 z-[150] flex items-center justify-center p-4">
@@ -898,13 +1152,17 @@ function RenameAssetModal({ initialPath, onClose, onConfirm }: { initialPath: st
style={{ style={{
backgroundImage: "url('/images/frame_background.png')", backgroundImage: "url('/images/frame_background.png')",
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
imageRendering: "pixelated" imageRendering: "pixelated",
}} }}
> >
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase">Rename Asset</h3> <h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase">
Rename Asset
</h3>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div> <div>
<label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block">New Asset Path</label> <label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block">
New Asset Path
</label>
<input <input
type="text" type="text"
value={path} value={path}
@@ -923,7 +1181,10 @@ function RenameAssetModal({ initialPath, onClose, onConfirm }: { initialPath: st
<button <button
onClick={() => onConfirm(path)} onClick={() => onConfirm(path)}
className="px-8 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none" className="px-8 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{
backgroundImage: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
}}
> >
Rename Rename
</button> </button>
+416 -123
View File
@@ -1,50 +1,157 @@
import { useState, useEffect, useRef, useCallback } from "react"; import { useState, useEffect, useRef, useCallback } from "react";
const TRACKS = [ const TRACKS = [
"/music/Blind Spots.ogg", "music/Blind Spots.ogg",
"/music/Key.ogg", "music/Key.ogg",
"/music/Living Mice.ogg", "music/Living Mice.ogg",
"/music/Oxygene.ogg", "music/Oxygene.ogg",
"/music/Subwoofer Lullaby.ogg", "music/Subwoofer Lullaby.ogg",
]; ];
const resolveAudioUrl = (path: string) => {
const relativePath = path.startsWith("/") ? path.slice(1) : path;
return new URL(relativePath, window.location.origin).href;
};
const SPLASHES = [ const SPLASHES = [
"Legacy is back!", "Pixelated goodness!", "Console Edition vibe!", "100% Not Microsoft!", "Legacy is back!",
"Symmetry is key!", "Does anyone even read these?", "Task failed successfully.", "Pixelated goodness!",
"Hardware accelerated!", "It's a feature, not a bug.", "Look behind you.", "Console Edition vibe!",
"Works on my machine.", "Now gluten-free!", "Mom, get the camera!", "Batteries not included.", "100% Not Microsoft!",
"May contain nuts.", "Press Alt+F4 for diamonds!", "Downloading more RAM...", "Symmetry is key!",
"Reinventing the wheel!", "The cake is a lie.", "Powered by copious amounts of coffee.", "Does anyone even read these?",
"I'm running out of ideas.", "That's no moon...", "Now with 100% more nostalgia!", "Task failed successfully.",
"Legacy is the new modern.", "No microtransactions!", "As seen on TV!", "Ironic, isn't it?", "Hardware accelerated!",
"Creeper? Aww man.", "Technoblade never dies!", "is smartcmd dead ?", "NO BUILT IN MS AUTH !", "It's a feature, not a bug.",
"Mr_Anilex wasn't here!", "Who's Jack ?", "This text is blue!", "Bonjour!", "Hola!", "Look behind you.",
"Salam!", "Hi!", "Reverse engineering WiiU version", "Try Terraria", "Don't try Valorant", "Works on my machine.",
"This could never be a sad place!", "Made without microslop", "Thank you C418", "Bread is pain", "Now gluten-free!",
"From the star!", "Never gonna give you up!", "9+10=21", ".party() was successful", "Not Kogama", "Mom, get the camera!",
"You can be proud of you!", "Let's drink Orange Joe", "Kirater is a great singer!", "Batteries not included.",
"Mirkette My beloved", "Started in Bordeaux", "Oui Oui Baguette", "Milk In The Microwave", "May contain nuts.",
"8-3: DISINTEGRATION LOOP", "Turn the light OFF", "Not written by Mr_Anilex", "Press Alt+F4 for diamonds!",
"The One Who's Running the Show!", "Playing Forever", "The World looks cubic!", "huh?", "Downloading more RAM...",
"Sybau", "Available on Toaster", "Try ArchLinux", "67% Accurate", "A molecule of meow", "Reinventing the wheel!",
"http://localhost:3000", "uuhhhh...", "Oyasumi", "XDDCC", "I don't want to set the world on fire", "The cake is a lie.",
"Directed by Michael Bay", "We see you, Opal!", "A Cool Cat in Town", "pikmin", "Powered by copious amounts of coffee.",
"Not BrainRotted!", "Farting is Natural -Leon", "93/100 on metacritic", "Not (anymore) on Steam", "I'm running out of ideas.",
"Sudo apt install EmeraldLauncher", "Sudo pacman -S EmeraldLauncher", "Kay-Chan my beloved! <3", "That's no moon...",
"Peak!", "OpenSource!", "made by human with bone and flesh", "Made with hate against microslop", "Now with 100% more nostalgia!",
"Steelorse :fire:", "It's Minecraft but i'm not sure", "Look at you!", "You're beautiful", "Legacy is the new modern.",
"Mr_Anilex has a big ego", "Traduis-moi !", "May contains Mr_Anilex", "Neoapps didn't write this splash", "No microtransactions!",
"Where's Kinger ?", "KayJann, Breakcore and code", "Hey Goku!", "Vegeta is a DZ mashallah", "As seen on TV!",
"Bogos Binted? Vorp", "YOU SHALL NOT PASS !", "Bready, Steady, GO !", "Not-so-Empty-house", "Ironic, isn't it?",
"We'll Meet Again", "idk", "wdym", "Not making sense", "Dw!", "i forgor", "Remember to be patient!", "Creeper? Aww man.",
"NOW'S YOUR CHANCE TO BE A.", "BIG SHOT", "A burning memory", "FREE MONEY!", "Technoblade never dies!",
"is smartcmd dead ?",
"NO BUILT IN MS AUTH !",
"Mr_Anilex wasn't here!",
"Who's Jack ?",
"This text is blue!",
"Bonjour!",
"Salam!",
"Reverse engineering Wii U version",
"Don't try Valorant!",
"This could never be a sad place!",
"Made without microslop",
"Thank you C418",
"Bread is pain",
"From the star!",
"Never gonna give you up!",
"9+10=21",
".party() was successful",
"Not Kogama",
"You can be proud of you!",
"Let's drink Orange Joe",
"Kirater is a great singer!",
"Mirkette My beloved",
"Started in Bordeaux",
"Oui Oui Baguette",
"Milk In The Microwave",
"8-3: DISINTEGRATION LOOP",
"Turn the light OFF",
"Not written by Mr_Anilex",
"The One Who's Running the Show!",
"Playing Forever",
"The World looks cubic!",
"huh?",
"Sybau",
"Available on Toaster",
"Try ArchLinux",
"69% Accurate",
"A molecule of meow",
"http://localhost:3000",
"uuhhhh...",
"Oyasumi",
"I don't want to set the world on fire",
"Directed by Michael Bay",
"We see you, Opal!",
"A Cool Cat in Town",
"Not BrainRotted!",
"Farting is Natural -Leon",
"93/100 on metacritic",
"Not (anymore) on Steam",
"Sudo apt install EmeraldLauncher",
"Sudo pacman -S EmeraldLauncher",
"Kay-Chan my beloved! <3",
"Peak!",
"OpenSource!",
"made by human with bone and flesh",
"Made with hate against microslop",
"Steelorse :fire:",
"It's Minecraft but i'm not sure",
"Look at you!",
"You're beautiful",
"Mr_Anilex has a big ego",
"Traduis-moi !",
"May contains Mr_Anilex",
"Neoapps didn't write this splash",
"Where's Kinger ?",
"KayJann, Breakcore and code",
"Hey Goku!",
"Vegeta is a DZ mashallah",
"Bogos Binted? Vorp",
"YOU SHALL NOT PASS !",
"Bready, Steady, GO !",
"Not-so-Empty-house",
"We'll Meet Again",
"idk",
"wdym",
"Not making sense",
"Dw!",
"i forgor",
"Remember to be patient!",
"NOW'S YOUR CHANCE TO BE A.",
"BIG SHOT",
"A burning memory",
"FREE MONEY!",
"Can You Really Call This A Hotel. I didn't Reveive A Mint On My Pillow Or Anything", "Can You Really Call This A Hotel. I didn't Reveive A Mint On My Pillow Or Anything",
"Try Indie Game", "SHARK WITH LEGS!", "it's a seal!", "Shrimp.", "Limited edition!", "Try Indie Game",
"Fat free!", "GOTY!", "Water proof!", "LALALA-LAVA", "CHICHICHI-CHICKEN", "Tasty ah hell", "SHARK WITH LEGS!",
"1% sugar!", "150% hyperbole!", "Hotter than the sun!", "Woo, reddit!", "it's a seal!",
"J'ai fait une blague à un poisson il m'a dit que c'était trop marin !", "Shrimp.",
"piebot was here!", "Legacy in an evolved manner.", "neoapps is cool!", "Also try neoLegacy!", "Limited edition!",
"$20 is $20", "no RenderDragon!", "Iggy Jiggy", "Arch, btw!", "Bedrock bad!" "Fat free!",
"GOTY!",
"Water proof!",
"LALALA-LAVA",
"CHICHICHI-CHICKEN",
"Tasty ah hell",
"1% sugar!",
"150% hyperbole!",
"Hotter than the sun!",
"Woo, reddit!",
"piebot was here!",
"Legacy in an evolved manner.",
"neoapps is cool!",
"Also try neoLegacy!",
"$20 is $20",
"no RenderDragon!",
"Iggy Jiggy",
"Arch, btw!",
"Bedrock bad!",
"Also try Terraria!",
"Also try LC Launcher!",
]; ];
interface AudioControllerProps { interface AudioControllerProps {
@@ -55,68 +162,215 @@ interface AudioControllerProps {
isWindowVisible: boolean; isWindowVisible: boolean;
} }
export function useAudioController({ musicVol, sfxVol, showIntro, isGameRunning, isWindowVisible }: AudioControllerProps) { export function useAudioController({
musicVol,
sfxVol,
showIntro,
isGameRunning,
isWindowVisible,
}: AudioControllerProps) {
const [currentTrack, setCurrentTrack] = useState(0); const [currentTrack, setCurrentTrack] = useState(0);
const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(null);
const [splashIndex, setSplashIndex] = useState(-1); const [splashIndex, setSplashIndex] = useState(-1);
const audioContextRef = useRef<AudioContext | null>(null);
const musicSourceRef = useRef<AudioBufferSourceNode | null>(null);
const musicGainRef = useRef<GainNode | null>(null);
const trackBuffersRef = useRef<Map<number, AudioBuffer>>(new Map());
const sfxBufferCacheRef = useRef<Map<string, AudioBuffer>>(new Map());
const musicPausedRef = useRef<{ at: number; track: number } | null>(null); const musicPausedRef = useRef<{ at: number; track: number } | null>(null);
const fadeIntervalRef = useRef<any>(null); const fadeIntervalRef = useRef<number | null>(null);
const playSfx = useCallback((file: string) => { const targetVolumeRef = useRef(musicVol / 100);
const a = new Audio(`/sounds/${file}`); const getAudioContext = useCallback(() => {
a.volume = sfxVol / 100; if (!audioContextRef.current) {
a.play().catch(() => { }); audioContextRef.current = new AudioContext();
}, [sfxVol]); }
if (audioContextRef.current.state === "suspended") {
audioContextRef.current.resume();
}
return audioContextRef.current;
}, []);
const loadAudioBuffer = useCallback(
async (url: string): Promise<AudioBuffer | undefined> => {
try {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const ctx = getAudioContext();
return await ctx.decodeAudioData(arrayBuffer);
} catch (error) {
console.error("Failed to load audio:", url, error);
return undefined;
}
},
[getAudioContext],
);
const playSfx = useCallback(
async (file: string) => {
const url = resolveAudioUrl(`/sounds/${file}`);
let buffer = sfxBufferCacheRef.current.get(file);
if (!buffer) {
buffer = await loadAudioBuffer(url);
if (buffer) {
sfxBufferCacheRef.current.set(file, buffer);
}
}
if (buffer) {
const ctx = getAudioContext();
const source = ctx.createBufferSource();
const gainNode = ctx.createGain();
source.buffer = buffer;
gainNode.gain.value = sfxVol / 100;
source.connect(gainNode);
gainNode.connect(ctx.destination);
source.start();
}
},
[sfxVol, loadAudioBuffer, getAudioContext],
);
const playPressSound = useCallback(() => playSfx("press.wav"), [playSfx]); const playPressSound = useCallback(() => playSfx("press.wav"), [playSfx]);
const playBackSound = useCallback(() => playSfx("back.ogg"), [playSfx]); const playBackSound = useCallback(() => playSfx("back.ogg"), [playSfx]);
const playSplashSound = useCallback(() => playSfx("orb.ogg"), [playSfx]); const playSplashSound = useCallback(() => playSfx("orb.ogg"), [playSfx]);
const fadeOut = useCallback((audio: HTMLAudioElement, duration: number = 500) => {
return new Promise<void>((resolve) => { const stopMusic = useCallback(() => {
if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current); if (musicSourceRef.current) {
const initialVolume = audio.volume; try {
const steps = 5; musicSourceRef.current.stop();
const stepDuration = duration / steps; } catch (e) {}
let currentStep = 0; musicSourceRef.current.disconnect();
fadeIntervalRef.current = setInterval(() => { musicSourceRef.current = null;
currentStep++; }
const progress = currentStep / steps; if (fadeIntervalRef.current) {
audio.volume = initialVolume * (1 - progress); clearInterval(fadeIntervalRef.current);
if (currentStep >= steps) { fadeIntervalRef.current = null;
if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current); }
fadeIntervalRef.current = null;
audio.pause();
audio.volume = initialVolume;
resolve();
}
}, stepDuration);
});
}, []); }, []);
const fadeIn = useCallback((audio: HTMLAudioElement, targetVolume: number, duration: number = 500) => { const playMusicBuffer = useCallback(
return new Promise<void>((resolve) => { (buffer: AudioBuffer, startTime: number = 0) => {
if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current); stopMusic();
audio.volume = 0; const ctx = getAudioContext();
const playPromise = audio.play(); const source = ctx.createBufferSource();
if (playPromise !== undefined) { const gainNode = ctx.createGain();
playPromise.catch(() => { });
} source.buffer = buffer;
gainNode.gain.value = 0;
source.connect(gainNode);
gainNode.connect(ctx.destination);
const offset = startTime % buffer.duration;
source.start(0, offset);
musicSourceRef.current = source;
musicGainRef.current = gainNode;
const steps = 5;
const stepDuration = 100;
let currentStep = 0;
fadeIntervalRef.current = window.setInterval(() => {
currentStep++;
const progress = currentStep / steps;
if (musicGainRef.current) {
musicGainRef.current.gain.value = targetVolumeRef.current * progress;
}
if (currentStep >= steps) {
clearInterval(fadeIntervalRef.current || undefined);
fadeIntervalRef.current = null;
if (musicGainRef.current) {
musicGainRef.current.gain.value = targetVolumeRef.current;
}
}
}, stepDuration);
source.onended = () => {
if (musicSourceRef.current) {
setCurrentTrack((prev) => (prev + 1) % TRACKS.length);
}
};
},
[stopMusic, getAudioContext],
);
const fadeOutMusic = useCallback(
(duration: number = 500): Promise<void> => {
return new Promise((resolve) => {
if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current);
if (!musicGainRef.current) {
resolve();
return;
}
const steps = 5;
const stepDuration = duration / steps;
const initialVolume = musicGainRef.current.gain.value;
let currentStep = 0;
fadeIntervalRef.current = window.setInterval(() => {
currentStep++;
const progress = currentStep / steps;
if (musicGainRef.current) {
musicGainRef.current.gain.value = initialVolume * (1 - progress);
}
if (currentStep >= steps) {
clearInterval(fadeIntervalRef.current || undefined);
fadeIntervalRef.current = null;
stopMusic();
if (musicGainRef.current) {
musicGainRef.current.gain.value = initialVolume;
}
resolve();
}
}, stepDuration);
});
},
[stopMusic],
);
const fadeInMusic = useCallback(
(buffer: AudioBuffer, targetVolume: number, duration: number = 500) => {
stopMusic();
const ctx = getAudioContext();
const source = ctx.createBufferSource();
const gainNode = ctx.createGain();
source.buffer = buffer;
gainNode.gain.value = 0;
source.connect(gainNode);
gainNode.connect(ctx.destination);
source.start();
musicSourceRef.current = source;
musicGainRef.current = gainNode;
targetVolumeRef.current = targetVolume;
const steps = 5; const steps = 5;
const stepDuration = duration / steps; const stepDuration = duration / steps;
let currentStep = 0; let currentStep = 0;
fadeIntervalRef.current = setInterval(() => {
fadeIntervalRef.current = window.setInterval(() => {
currentStep++; currentStep++;
const progress = currentStep / steps; const progress = currentStep / steps;
audio.volume = targetVolume * progress; if (musicGainRef.current) {
musicGainRef.current.gain.value = targetVolume * progress;
}
if (currentStep >= steps) { if (currentStep >= steps) {
if (fadeIntervalRef.current) clearInterval(fadeIntervalRef.current); clearInterval(fadeIntervalRef.current || undefined);
fadeIntervalRef.current = null; fadeIntervalRef.current = null;
audio.volume = targetVolume; if (musicGainRef.current) {
resolve(); musicGainRef.current.gain.value = targetVolume;
}
} }
}, stepDuration); }, stepDuration);
});
}, []); source.onended = () => {
if (musicSourceRef.current) {
setCurrentTrack((prev) => (prev + 1) % TRACKS.length);
}
};
},
[stopMusic, getAudioContext],
);
const cycleSplash = useCallback(() => { const cycleSplash = useCallback(() => {
playSplashSound(); playSplashSound();
@@ -129,66 +383,105 @@ export function useAudioController({ musicVol, sfxVol, showIntro, isGameRunning,
useEffect(() => { useEffect(() => {
if (showIntro) return; if (showIntro) return;
if (audioElement) return;
const audio = new Audio(TRACKS[currentTrack]);
audio.volume = musicVol / 100;
const handleEnded = () => setCurrentTrack((prev) => (prev + 1) % TRACKS.length);
audio.addEventListener("ended", handleEnded);
const playPromise = audio.play();
if (playPromise !== undefined) {
playPromise.catch(() => {
const startMusic = () => {
audio.play().catch(() => { });
document.removeEventListener("click", startMusic);
document.removeEventListener("keydown", startMusic);
};
document.addEventListener("click", startMusic, { once: true });
document.addEventListener("keydown", startMusic, { once: true });
});
}
setAudioElement(audio); const loadAndPlay = async () => {
return () => { let buffer = trackBuffersRef.current.get(currentTrack);
audio.removeEventListener("ended", handleEnded); if (!buffer) {
audio.pause(); buffer = await loadAudioBuffer(resolveAudioUrl(TRACKS[currentTrack]));
if (buffer) {
trackBuffersRef.current.set(currentTrack, buffer);
}
}
if (buffer) {
playMusicBuffer(buffer);
}
}; };
}, [showIntro, audioElement, currentTrack, musicVol]);
loadAndPlay();
return () => {
stopMusic();
};
}, [showIntro, currentTrack, loadAudioBuffer, playMusicBuffer, stopMusic]);
useEffect(() => { useEffect(() => {
if (!audioElement) return; if (!audioContextRef.current || showIntro) return;
audioElement.src = TRACKS[currentTrack];
audioElement.play().catch(() => { }); const loadAndPlay = async () => {
}, [currentTrack]); let buffer = trackBuffersRef.current.get(currentTrack);
if (!buffer) {
buffer = await loadAudioBuffer(resolveAudioUrl(TRACKS[currentTrack]));
if (buffer) {
trackBuffersRef.current.set(currentTrack, buffer);
}
}
if (buffer) {
fadeInMusic(buffer, musicVol / 100, 500);
}
};
loadAndPlay();
}, [currentTrack, showIntro, musicVol, loadAudioBuffer, fadeInMusic]);
useEffect(() => { useEffect(() => {
if (!audioElement) return;
const shouldPause = isGameRunning || !isWindowVisible; const shouldPause = isGameRunning || !isWindowVisible;
if (shouldPause) { if (shouldPause) {
if (!audioElement.paused || fadeIntervalRef.current) { if (musicSourceRef.current || fadeIntervalRef.current) {
if (!musicPausedRef.current) { if (!musicPausedRef.current) {
musicPausedRef.current = { const ctx = getAudioContext();
at: audioElement.currentTime, if (musicGainRef.current) {
track: currentTrack, musicPausedRef.current = {
}; at: ctx.currentTime,
track: currentTrack,
};
} else {
musicPausedRef.current = { at: 0, track: currentTrack };
}
} }
fadeOut(audioElement, 500); fadeOutMusic(500);
} }
} else if (musicPausedRef.current) { } else if (musicPausedRef.current) {
const { at, track } = musicPausedRef.current; const { track } = musicPausedRef.current;
musicPausedRef.current = null; musicPausedRef.current = null;
targetVolumeRef.current = musicVol / 100;
const playWithPos = async () => {
let buffer = trackBuffersRef.current.get(currentTrack);
if (!buffer) {
buffer = await loadAudioBuffer(resolveAudioUrl(TRACKS[currentTrack]));
if (buffer) {
trackBuffersRef.current.set(currentTrack, buffer);
}
}
if (buffer) {
fadeInMusic(buffer, musicVol / 100, 500);
}
};
if (track === currentTrack) { if (track === currentTrack) {
audioElement.currentTime = at; playWithPos();
} else {
setCurrentTrack(track);
} }
fadeIn(audioElement, musicVol / 100, 500);
} }
}, [isGameRunning, isWindowVisible, audioElement, currentTrack, musicVol, fadeOut, fadeIn]); }, [
isGameRunning,
isWindowVisible,
currentTrack,
musicVol,
fadeOutMusic,
fadeInMusic,
loadAudioBuffer,
getAudioContext,
]);
useEffect(() => { useEffect(() => {
if (audioElement) { targetVolumeRef.current = musicVol / 100;
audioElement.volume = musicVol / 100; if (musicGainRef.current && !fadeIntervalRef.current) {
musicGainRef.current.gain.value = musicVol / 100;
} }
}, [musicVol, audioElement]); }, [musicVol]);
return { return {
currentTrack, currentTrack,