Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions backend/app/api/v1/endpoints/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ async def upload_map(file: UploadFile = File(...), user=Depends(get_current_user
raise HTTPException(status_code=500, detail=str(e))


@router.post("/open")
async def open_map(file: UploadFile = File(...)):
try:
content = await file.read()
chkt = get_chkt(BytesIO(content))
chk = CHK(chkt)
dat = DAT()

raw_map = get_map(chk, dat)
return {"file_name": file.filename, "raw_map": raw_map}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.get("/test_map")
async def get_test_map():
with open("./example/various_units.scx", "rb") as f:
Expand Down
11 changes: 7 additions & 4 deletions backend/app/services/rawdata/chk.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,11 +413,14 @@ def sprites(self) -> list[chk_types.Sprite]:

@cached_property
def strings(self) -> list[chk_types.String]:
str_bytes = self.chkt.getsection("STRx")
string_count = struct.unpack("I", str_bytes[0:4])[0]
str_section = "STRx" if b"STRx" in self.chkt.sections else "STR "
size = 2 if str_section == "STR " else 4
format = "H" if str_section == "STR " else "I"
str_bytes = self.chkt.getsection(str_section)
string_count = struct.unpack(format, str_bytes[0:size])[0]
offsets = [
struct.unpack("I", str_bytes[i : i + 4])[0]
for i in range(4, 4 + 4 * string_count, 4)
struct.unpack(format, str_bytes[i : i + size])[0]
for i in range(size, size + size * string_count, size)
]

result: list[chk_types.String] = []
Expand Down
Binary file added backend/example/A_Tale_of_Two_Cities.scx
Binary file not shown.
Binary file added backend/example/goe.scx
Binary file not shown.
60 changes: 56 additions & 4 deletions frontend/components/layout/app-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
} from "../ui/menubar";
import { useUsemapStore } from "@/components/pages/editor-page";
import api from "@/lib/api";
import { UsemapActions } from "@/store/mapStore";
import { Usemap } from "@/types/schemas/project/Usemap";
import { useRef } from "react";

async function onClickBuild(usemap: Usemap | null) {
if (!usemap) return;
Expand All @@ -35,21 +37,63 @@ async function onClickBuild(usemap: Usemap | null) {
}
}

async function onClickOpenUsemap(
file: File,
openUsemap: UsemapActions["openUsemap"],
) {
try {
openUsemap(file);
} catch (error) {
console.error("Failed to upload map:", error);
alert("Failed to load map file. Please try again.");
}
}

async function onClickSave(usemap: Usemap) {
const a = document.createElement("a");
const blob = new Blob([JSON.stringify(usemap)], { type: "application/json" });
const url = window.URL.createObjectURL(blob);
a.download = `${usemap.scenario_property.name.content}.wproject`;
a.href = url;

document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}

export function AppMenu() {
const rawMap = useUsemapStore((state) => state.usemap);
const openUsemap = useUsemapStore((state) => state.openUsemap);

const usemapInputRef = useRef<HTMLInputElement>(null);

const handleFileSelect = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0];
if (file) {
await onClickOpenUsemap(file, openUsemap);
event.target.value = "";
}
};

return (
<Menubar>
<MenubarMenu>
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>New Project</MenubarItem>
<MenubarItem onClick={() => onClickBuild(rawMap)}>
Download
</MenubarItem>
<MenubarSeparator />
<MenubarItem>Open</MenubarItem>
<MenubarItem onClick={() => usemapInputRef.current?.click()}>
Open
</MenubarItem>
<MenubarItem>Open Recents</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={() => rawMap && onClickSave(rawMap)}>
Save
</MenubarItem>
<MenubarItem onClick={() => onClickBuild(rawMap)}>Build</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
Expand Down Expand Up @@ -80,6 +124,14 @@ export function AppMenu() {
<MenubarItem>Redo</MenubarItem>
</MenubarContent>
</MenubarMenu>

<input
ref={usemapInputRef}
type="file"
accept=".chk,.scx,.scm,.wproject"
className="hidden"
onChange={handleFileSelect}
/>
</Menubar>
);
}
1 change: 1 addition & 0 deletions frontend/components/ui/menubar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export function MenubarItem({
</li>
);
}

return (
<li className={twMerge("w-full whitespace-nowrap text-sm", className)}>
<Button {...props}></Button>
Expand Down
21 changes: 15 additions & 6 deletions frontend/hooks/useImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ export function useViewportImage(): ViewportImageBundle {

terrainImage.current = getTerrainImage(
usemap.terrain,
usemap.entities.filter((entity) => entity.data && entity.data.kind === "Tile").map(e => e.data) as Tile[],
usemap.entities
.filter((entity) => entity.data && entity.data.kind === "Tile")
.map((e) => e.data) as Tile[],
tileGroup,
tilesetData,
);
Expand All @@ -161,8 +163,12 @@ export function useViewportImage(): ViewportImageBundle {
const requiredImageIDs = useMemo(() => {
const result = new Set<number>();
if (usemap) {
const unitEntities = usemap.entities.filter((entity) => entity.data && entity.data.kind === "Unit").map(e => e.data) as Unit[];
const spriteEntities = usemap.entities.filter((entity) => entity.data && entity.data.kind === "Sprite").map(e => e.data) as Sprite[];
const unitEntities = usemap.entities
.filter((entity) => entity.data && entity.data.kind === "Unit")
.map((e) => e.data) as Unit[];
const spriteEntities = usemap.entities
.filter((entity) => entity.data && entity.data.kind === "Sprite")
.map((e) => e.data) as Sprite[];

unitEntities.forEach((unit) => {
result.add(unit.unit_definition.specification.graphics.sprite.image.id);
Expand All @@ -183,7 +189,8 @@ export function useViewportImage(): ViewportImageBundle {
const bmp = await getPlacedUnitImage(
usemap.terrain,
usemap.entities
.filter((entity) => entity.data && entity.data.kind === "Unit").map(e => e.data) as Unit[],
.filter((entity) => entity.data && entity.data.kind === "Unit")
.map((e) => e.data) as Unit[],
imagesData,
);
setUnitImage(bmp);
Expand All @@ -197,7 +204,8 @@ export function useViewportImage(): ViewportImageBundle {
const bmp = await getPlacedSpriteImages(
usemap.terrain,
usemap.entities
.filter((entity) => entity.data && entity.data.kind === "Sprite").map(e => e.data) as Sprite[],
.filter((entity) => entity.data && entity.data.kind === "Sprite")
.map((e) => e.data) as Sprite[],
imagesData,
);
setSpriteImage(bmp);
Expand All @@ -209,7 +217,8 @@ export function useViewportImage(): ViewportImageBundle {
locationImage.current = getLocationImage(
usemap.terrain,
usemap.entities
.filter((entity) => entity.data && entity.data.kind === "Location").map(e => e.data) as Location[],
.filter((entity) => entity.data && entity.data.kind === "Location")
.map((e) => e.data) as Location[],
);
}, [usemap?.entities]);

Expand Down
168 changes: 104 additions & 64 deletions frontend/store/mapStore.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { Usemap } from "@/types/schemas/project/Usemap";
import { Usemap, UsemapSchema } from "@/types/schemas/project/Usemap";
import { createStore } from "zustand";
import { immer } from "zustand/middleware/immer";
import { produce } from "immer";
import { Entity } from "@/types/schemas/entities/Entity";
import { Asset } from "@/types/asset";
Expand Down Expand Up @@ -29,9 +30,11 @@ export type UsemapState = {
};

export type UsemapActions = {
fetchUsemap: (mapName: string) => Promise<void>;
setUsemap: (usemap: Usemap) => void;
openUsemap: (file: File) => Promise<void>;
addEntity: (entity: Asset<Entity>) => void;
deleteEntity: (entity: Asset<Entity>) => void;
fetchUsemap: (mapName: string) => Promise<void>;
updateEntityAssetName: (id: number, name: string) => void;
updateEntity: (id: number, path: string[], value: any) => void;
addAsset: (asset: Asset) => void;
Expand All @@ -51,66 +54,103 @@ export const createUsemapStore = () => {
return obj;
};

return createStore<UsemapStore>()((set) => ({
usemap: null,
fetchUsemap: async (mapName: string) => {
try {
const res = await api.get<Usemap>(`/api/v1/maps/${mapName}`);
set({ usemap: res.data });
} catch (error) {
console.error("Failed to fetch usemap:", error);
}
},
addEntity: (entity: Asset<Entity>) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.entities.push(entity);
}),
})),
deleteEntity: (entity: Asset<Entity>) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.entities = draft.entities.filter(
(e: Asset<Entity>) => e.id !== entity.id,
);
// maybe can replaced with delete draft.entities[entity.id]?
}),
})),
updateEntityAssetName: (id: number, name: string) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.entities = draft.entities.map((e) =>
e.id === id ? { ...e, name: name } : e,
);
}),
})),
updateEntity: (id: number, path: string[], value: any) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.entities = draft.entities.map((e) =>
e.id === id ? { ...e, data: update(e.data, path, value) } : e,
);
}),
})),
addAsset: (asset: Asset) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.assets.push(asset);
}),
})),
deleteAsset: (asset: Asset) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.assets = draft.assets.filter((a) => a.id !== asset.id);
}),
})),
updateAsset: (id: number, path: string[], value: any) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.assets = draft.assets.map((a) =>
a.id === id ? { ...a, data: update(a.data, path, value) } : a,
);
}),
})),
}));
return createStore<UsemapStore>()(
immer((set) => ({
usemap: null,
fetchUsemap: async (mapName: string) => {
try {
const res = await api.get<Usemap>(`/api/v1/maps/${mapName}`);
set({ usemap: res.data });
} catch (error) {
console.error("Failed to fetch usemap:", error);
}
},
openUsemap: async (file: File) => {
try {
const extension = file.name.split(".").pop()?.toLowerCase();
if (extension === "wproject") {
const jsonData = JSON.parse(await file.text());

// 검증만 하고 원본 데이터 사용
const validation = UsemapSchema.safeParse(jsonData);
if (!validation.success) {
console.warn("Schema validation failed:", validation.error);
// 경고만 하고 계속 진행하거나 에러 처리
}

set({ usemap: jsonData }); // 원본 데이터 사용
} else if (["chk", "scx", "scm"].includes(extension || "")) {
const formData = new FormData();
formData.append("file", file);

const response = await api.post("/api/v1/maps/open/", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});

const { raw_map } = response.data;
if (raw_map) {
set({ usemap: raw_map });
}
}
} catch (error) {
console.error("Failed to upload usemap:", error);
throw error; // 에러를 다시 던져서 컴포넌트에서 처리할 수 있도록
}
},
setUsemap: (usemap: Usemap) => set({ usemap: usemap }),
addEntity: (entity: Asset<Entity>) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.entities.push(entity);
}),
})),
deleteEntity: (entity: Asset<Entity>) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.entities = draft.entities.filter(
(e: Asset<Entity>) => e.id !== entity.id,
);
// maybe can replaced with delete draft.entities[entity.id]?
}),
})),
updateEntityAssetName: (id: number, name: string) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.entities = draft.entities.map((e) =>
e.id === id ? { ...e, name: name } : e,
);
}),
})),
updateEntity: (id: number, path: string[], value: any) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.entities = draft.entities.map((e) =>
e.id === id ? { ...e, data: update(e.data, path, value) } : e,
);
}),
})),
addAsset: (asset: Asset) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.assets.push(asset);
}),
})),
deleteAsset: (asset: Asset) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.assets = draft.assets.filter((a) => a.id !== asset.id);
}),
})),
updateAsset: (id: number, path: string[], value: any) =>
set((state) => ({
usemap: produce(state.usemap, (draft: Usemap) => {
draft.assets = draft.assets.map((a) =>
a.id === id ? { ...a, data: update(a.data, path, value) } : a,
);
}),
})),
})),
);
};
Loading