This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
package org.zaine.app.controller;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.zaine.app.dto.RpgSaveDTO;
|
||||||
|
import org.zaine.app.service.RpgSaveService;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/play/rpg")
|
||||||
|
@Tag(name = "RPG Saves", description = "Persistent save slots for browser RPGs")
|
||||||
|
public class RpgSaveController {
|
||||||
|
private final RpgSaveService rpgSaveService;
|
||||||
|
|
||||||
|
public RpgSaveController(RpgSaveService rpgSaveService) {
|
||||||
|
this.rpgSaveService = rpgSaveService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/save/{slot}")
|
||||||
|
public ResponseEntity<RpgSaveDTO> load(@PathVariable String slot) {
|
||||||
|
RpgSaveDTO save = rpgSaveService.load(slot);
|
||||||
|
if (save == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(save);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/save/{slot}")
|
||||||
|
public ResponseEntity<RpgSaveDTO> save(@PathVariable String slot, @RequestBody RpgSaveDTO dto) {
|
||||||
|
return ResponseEntity.ok(rpgSaveService.save(slot, dto.getPayload()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/save/{slot}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable String slot) {
|
||||||
|
rpgSaveService.delete(slot);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main/java/org/zaine/app/dto/RpgSaveDTO.java
Normal file
24
src/main/java/org/zaine/app/dto/RpgSaveDTO.java
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package org.zaine.app.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
public class RpgSaveDTO {
|
||||||
|
private String slot;
|
||||||
|
private JsonNode payload;
|
||||||
|
|
||||||
|
public String getSlot() {
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSlot(String slot) {
|
||||||
|
this.slot = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonNode getPayload() {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPayload(JsonNode payload) {
|
||||||
|
this.payload = payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
70
src/main/java/org/zaine/app/service/RpgSaveService.java
Normal file
70
src/main/java/org/zaine/app/service/RpgSaveService.java
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package org.zaine.app.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.zaine.app.dto.RpgSaveDTO;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class RpgSaveService {
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final Path saveDirectory;
|
||||||
|
|
||||||
|
public RpgSaveService(
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
@Value("${play.rpg.save-dir:${user.home}/.org-web-rpg-saves}") String saveDirectory) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.saveDirectory = Path.of(saveDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RpgSaveDTO load(String slot) {
|
||||||
|
Path file = savePath(slot);
|
||||||
|
if (!Files.exists(file)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(file.toFile(), RpgSaveDTO.class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to load RPG save", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RpgSaveDTO save(String slot, JsonNode payload) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(saveDirectory);
|
||||||
|
RpgSaveDTO dto = new RpgSaveDTO();
|
||||||
|
dto.setSlot(slot);
|
||||||
|
dto.setPayload(payload);
|
||||||
|
|
||||||
|
ObjectNode wrapped = objectMapper.createObjectNode();
|
||||||
|
wrapped.put("slot", slot);
|
||||||
|
wrapped.set("payload", payload);
|
||||||
|
objectMapper.writerWithDefaultPrettyPrinter().writeValue(savePath(slot).toFile(), wrapped);
|
||||||
|
return dto;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to save RPG progress", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(String slot) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(savePath(slot));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to delete RPG save", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path savePath(String slot) {
|
||||||
|
if (slot == null || !slot.matches("[A-Za-z0-9_-]{1,64}")) {
|
||||||
|
throw new IllegalArgumentException("Invalid RPG save slot");
|
||||||
|
}
|
||||||
|
return saveDirectory.resolve(slot + ".json").normalize();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user