7#include "absl/strings/str_format.h"
17namespace rom_svc = ::yaze::proto;
25std::string GenerateProposalId() {
26 auto now = std::chrono::system_clock::now();
27 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
28 now.time_since_epoch())
30 return absl::StrFormat(
"proposal_%lld", ms);
33std::string BuildRoomRawJson(
const zelda3::Room& room) {
34 nlohmann::json payload = nlohmann::json::object();
35 payload[
"room_id"] = room.id();
36 payload[
"layout"] = room.layout_id();
37 payload[
"floor1"] = room.floor1();
38 payload[
"floor2"] = room.floor2();
39 payload[
"blockset"] = room.blockset();
40 payload[
"spriteset"] = room.spriteset();
41 payload[
"palette"] = room.palette();
42 payload[
"effect"] =
static_cast<int>(room.effect());
43 payload[
"tag1"] =
static_cast<int>(room.tag1());
44 payload[
"tag2"] =
static_cast<int>(room.tag2());
46 auto objects = nlohmann::json::array();
47 for (
const auto& obj : room.GetTileObjects()) {
48 nlohmann::json entry = {
49 {
"id",
static_cast<int>(obj.id_)},
53 {
"layer", obj.GetLayerValue()},
54 {
"size_x_bits", obj.size_x_bits_},
55 {
"size_y_bits", obj.size_y_bits_},
57 objects.push_back(entry);
59 payload[
"objects"] = objects;
61 auto doors = nlohmann::json::array();
62 for (
const auto& door : room.GetDoors()) {
63 nlohmann::json entry = {
64 {
"byte1", door.byte1},
65 {
"byte2", door.byte2},
66 {
"position", door.position},
67 {
"direction",
static_cast<int>(door.direction)},
68 {
"type",
static_cast<int>(door.type)},
70 doors.push_back(entry);
72 payload[
"doors"] = doors;
74 auto sprites = nlohmann::json::array();
75 for (
const auto& sprite : room.GetSprites()) {
76 nlohmann::json entry = {
80 {
"subtype", sprite.subtype()},
81 {
"layer", sprite.layer()},
83 sprites.push_back(entry);
85 payload[
"sprites"] = sprites;
87 payload[
"encoded_objects"] = room.EncodeObjects();
88 payload[
"encoded_sprites"] = room.EncodeSprites();
90 return payload.dump();
93absl::Status LoadGameDataForRom(Rom* rom, zelda3::GameData* data,
94 const zelda3::LoadOptions& options) {
96 return absl::FailedPreconditionError(
"ROM not loaded");
99 return absl::InvalidArgumentError(
"GameData is null");
105absl::Status LoadMetadataForRom(Rom* rom, zelda3::GameData* data) {
107 return absl::FailedPreconditionError(
"ROM not loaded");
110 return absl::InvalidArgumentError(
"GameData is null");
118RomServiceImpl::RomServiceImpl(RomGetter rom_getter,
119 RomVersionManager* version_manager,
120 ProposalApprovalManager* approval_manager)
121 : rom_getter_(rom_getter),
122 version_mgr_(version_manager),
123 approval_mgr_(approval_manager) {}
125void RomServiceImpl::SetConfig(
const Config& config) {
129grpc::Status RomServiceImpl::ReadBytes(grpc::ServerContext* context,
130 const rom_svc::ReadBytesRequest* request,
131 rom_svc::ReadBytesResponse* response) {
132 Rom*
rom = rom_getter_();
134 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
138 uint32_t offset = request->offset();
139 uint32_t length = request->length();
142 if (offset + length >
rom->
size()) {
143 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
144 absl::StrFormat(
"Read beyond ROM: 0x%X+%d > %d", offset,
149 const auto* data =
rom->
data() + offset;
150 response->set_data(data, length);
152 return grpc::Status::OK;
155grpc::Status RomServiceImpl::WriteBytes(
156 grpc::ServerContext* context,
const rom_svc::WriteBytesRequest* request,
157 rom_svc::WriteBytesResponse* response) {
158 Rom*
rom = rom_getter_();
160 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
164 uint32_t offset = request->offset();
165 const std::string& data = request->data();
168 if (offset + data.size() >
rom->
size()) {
169 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
170 absl::StrFormat(
"Write beyond ROM: 0x%X+%zu > %d",
171 offset, data.size(),
rom->
size()));
174 if (config_.require_approval_for_writes || request->require_approval()) {
175 if (!approval_mgr_) {
176 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
177 "Approval manager not initialized");
180 std::string
description = request->require_approval()
181 ?
"WriteBytes (proposal)"
182 :
"WriteBytes (approval required)";
183 if (!request->data().empty()) {
184 description = absl::StrFormat(
"WriteBytes at 0x%X (%zu bytes)", offset,
185 request->data().size());
188 nlohmann::json proposal_data = {{
"description",
description},
189 {
"type",
"write_bytes"},
191 {
"size", request->data().size()}};
192 std::vector<uint8_t> bytes(request->data().begin(), request->data().end());
193 proposal_data[
"data"] = bytes;
195 std::string proposal_id = GenerateProposalId();
196 auto status = approval_mgr_->SubmitProposal(proposal_id,
"grpc",
197 description, proposal_data);
199 return grpc::Status(grpc::StatusCode::INTERNAL,
200 std::string(status.message()));
203 response->set_success(
true);
204 response->set_proposal_id(proposal_id);
205 return grpc::Status::OK;
209 auto status = MaybeCreateSnapshot(absl::StrFormat(
210 "Auto-snapshot before write at 0x%X (%zu bytes)", offset, data.size()));
213 grpc::StatusCode::INTERNAL,
214 "Failed to create safety snapshot: " + std::string(status.message()));
218 std::vector<uint8_t> bytes(data.begin(), data.end());
221 if (!write_status.ok()) {
222 return grpc::Status(grpc::StatusCode::INTERNAL,
223 std::string(write_status.message()));
225 response->set_success(
true);
227 return grpc::Status::OK;
230grpc::Status RomServiceImpl::GetRomInfo(
231 grpc::ServerContext* context,
const rom_svc::GetRomInfoRequest* request,
232 rom_svc::GetRomInfoResponse* response) {
233 Rom*
rom = rom_getter_();
235 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
240 response->set_size(
rom->
size());
242 auto metadata = std::make_unique<zelda3::GameData>(rom);
243 if (LoadMetadataForRom(rom, metadata.get()).ok()) {
244 response->set_version(metadata->version == zelda3_version::JP ?
"JP" :
"US");
246 response->set_is_expanded(
rom->
size() > 0x200000);
248 return grpc::Status::OK;
251grpc::Status RomServiceImpl::ReadOverworldMap(
252 grpc::ServerContext* context,
253 const rom_svc::ReadOverworldMapRequest* request,
254 rom_svc::ReadOverworldMapResponse* response) {
255 Rom*
rom = rom_getter_();
257 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
261 uint32_t map_id = request->map_id();
263 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
264 "Invalid map_id (0-159)");
267 auto metadata = std::make_unique<zelda3::GameData>(rom);
268 auto metadata_status = LoadMetadataForRom(rom, metadata.get());
269 if (!metadata_status.ok()) {
270 return grpc::Status(grpc::StatusCode::INTERNAL,
271 std::string(metadata_status.message()));
275 zelda3::Overworld overworld(rom, metadata.get());
276 auto status = overworld.Load(rom);
278 return grpc::Status(grpc::StatusCode::INTERNAL,
279 "Failed to load Overworld: " +
280 std::string(status.message()));
284 int local_id = map_id;
288 }
else if (map_id >= 64) {
294 auto& blockset = overworld.GetMapTiles(world);
298 int maps_per_row = 8;
299 int map_col = local_id % maps_per_row;
300 int map_row = local_id / maps_per_row;
301 int global_start_x = map_col * 32;
302 int global_start_y = map_row * 32;
304 response->set_map_id(map_id);
306 if (blockset.empty() || blockset[0].empty()) {
307 return grpc::Status(grpc::StatusCode::INTERNAL,
308 "Overworld tiles not initialized");
312 if (global_start_x + 32 >
static_cast<int>(blockset.size()) ||
313 global_start_y + 32 >
static_cast<int>(blockset[0].size())) {
315 grpc::StatusCode::INTERNAL,
316 absl::StrFormat(
"Map bounds error: %d (local %d) -> (%d, %d)", map_id,
317 local_id, global_start_x, global_start_y));
321 for (
int y = 0; y < 32; ++y) {
322 for (
int x = 0; x < 32; ++x) {
323 response->add_tile16_data(
324 blockset[global_start_x + x][global_start_y + y]);
328 return grpc::Status::OK;
331grpc::Status RomServiceImpl::WriteOverworldTile(
332 grpc::ServerContext* context,
333 const rom_svc::WriteOverworldTileRequest* request,
334 rom_svc::WriteOverworldTileResponse* response) {
335 Rom*
rom = rom_getter_();
337 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
341 uint32_t map_id = request->map_id();
342 uint32_t x = request->x();
343 uint32_t y = request->y();
344 uint32_t tile_id = request->tile16_id();
346 if (map_id >= 160 || x >= 32 || y >= 32) {
347 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
"Invalid coordinates");
350 if (config_.require_approval_for_writes || request->require_approval()) {
351 if (!approval_mgr_) {
352 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
353 "Approval manager not initialized");
356 std::string
description = request->description().empty()
358 "Update OW tile: Map %d (%d,%d) -> %d",
359 map_id, x, y, tile_id)
361 nlohmann::json proposal_data = {{
"description",
description},
362 {
"type",
"overworld_tile_write"},
366 {
"tile16_id", tile_id}};
368 std::string proposal_id = GenerateProposalId();
369 auto status = approval_mgr_->SubmitProposal(proposal_id,
"grpc",
370 description, proposal_data);
372 return grpc::Status(grpc::StatusCode::INTERNAL,
373 std::string(status.message()));
376 response->set_success(
true);
377 response->set_proposal_id(proposal_id);
378 return grpc::Status::OK;
381 auto metadata = std::make_unique<zelda3::GameData>(rom);
382 auto metadata_status = LoadMetadataForRom(rom, metadata.get());
383 if (!metadata_status.ok()) {
384 return grpc::Status(grpc::StatusCode::INTERNAL,
385 std::string(metadata_status.message()));
389 zelda3::Overworld overworld(rom, metadata.get());
390 auto status = overworld.Load(rom);
392 return grpc::Status(grpc::StatusCode::INTERNAL,
393 "Failed to load Overworld");
397 int local_id = map_id;
401 }
else if (map_id >= 64) {
407 int maps_per_row = 8;
408 int map_col = local_id % maps_per_row;
409 int map_row = local_id / maps_per_row;
410 int global_x = (map_col * 32) + x;
411 int global_y = (map_row * 32) + y;
414 auto& blockset = overworld.GetMapTiles(world);
415 if (blockset.empty() || blockset[0].empty()) {
416 return grpc::Status(grpc::StatusCode::INTERNAL,
417 "Overworld tiles not initialized");
419 if (global_x >=
static_cast<int>(blockset.size()) ||
420 global_y >=
static_cast<int>(blockset[0].size())) {
421 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
422 "Global coordinates out of range");
424 blockset[global_x][global_y] =
static_cast<uint16_t
>(tile_id);
427 auto snap_status = MaybeCreateSnapshot(
428 absl::StrFormat(
"Update OW tile: Map %d (%d,%d) -> %d", map_id, x, y, tile_id));
429 if (!snap_status.ok()) {
430 return grpc::Status(grpc::StatusCode::INTERNAL,
"Snapshot failed");
434 status = overworld.Save(rom);
436 return grpc::Status(grpc::StatusCode::INTERNAL,
437 "Failed to save Overworld: " + std::string(status.message()));
440 response->set_success(
true);
441 return grpc::Status::OK;
444grpc::Status RomServiceImpl::ReadDungeonRoom(
445 grpc::ServerContext* context,
446 const rom_svc::ReadDungeonRoomRequest* request,
447 rom_svc::ReadDungeonRoomResponse* response) {
448 Rom*
rom = rom_getter_();
450 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
"ROM not loaded");
453 uint32_t room_id = request->room_id();
454 if (room_id >= 296) {
455 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
"Invalid room_id");
458 auto game_data = std::make_unique<zelda3::GameData>(rom);
459 zelda3::LoadOptions options;
460 options.load_graphics =
true;
461 options.load_palettes =
true;
462 options.load_gfx_groups =
true;
463 options.populate_metadata =
true;
464 options.expand_rom =
false;
465 auto game_status = LoadGameDataForRom(rom,
game_data.get(), options);
466 if (!game_status.ok()) {
467 return grpc::Status(grpc::StatusCode::INTERNAL,
468 std::string(game_status.message()));
472 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
476 room.RenderRoomGraphics();
478 response->set_room_id(room_id);
484 const auto& buffer = room.bg1_buffer().buffer();
486 for (uint16_t tile : buffer) {
487 response->add_tile16_data(tile);
490 const std::string raw_json = BuildRoomRawJson(room);
491 if (!raw_json.empty()) {
492 response->set_raw_data(raw_json);
495 return grpc::Status::OK;
498grpc::Status RomServiceImpl::WriteDungeonTile(
499 grpc::ServerContext* context,
500 const rom_svc::WriteDungeonTileRequest* request,
501 rom_svc::WriteDungeonTileResponse* response) {
502 if (!approval_mgr_) {
503 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
504 "Approval manager not initialized");
507 std::string
description = request->description().empty()
509 "Dungeon tile write: Room %d (%d,%d) -> %d",
510 request->room_id(), request->x(),
511 request->y(), request->tile16_id())
513 nlohmann::json proposal_data = {{
"description",
description},
514 {
"type",
"dungeon_tile_write"},
515 {
"room_id", request->room_id()},
518 {
"tile16_id", request->tile16_id()}};
520 std::string proposal_id = GenerateProposalId();
521 auto status = approval_mgr_->SubmitProposal(proposal_id,
"grpc",
522 description, proposal_data);
524 return grpc::Status(grpc::StatusCode::INTERNAL,
525 std::string(status.message()));
528 response->set_success(
true);
529 response->set_proposal_id(proposal_id);
530 return grpc::Status::OK;
533grpc::Status RomServiceImpl::ReadSprite(
534 grpc::ServerContext* context,
const rom_svc::ReadSpriteRequest* request,
535 rom_svc::ReadSpriteResponse* response) {
536 Rom*
rom = rom_getter_();
538 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
"ROM not loaded");
541 uint32_t sprite_id = request->sprite_id();
542 if (sprite_id >= 256) {
543 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
"Invalid sprite_id (0-255)");
547 const uint32_t kSpriteHPTable = 0x6B173;
548 const uint32_t kSpriteDamageTable = 0x6B266;
549 const uint32_t kSpritePaletteTable = 0x6B35B;
550 const uint32_t kSpritePropertiesTable = 0x6B450;
552 response->set_sprite_id(sprite_id);
554 auto read_byte = [&](uint32_t offset, uint8_t* out) -> absl::Status {
555 auto val =
rom->
ReadByte(
static_cast<int>(offset));
560 return absl::OkStatus();
563 std::vector<uint8_t> data;
566 auto status = read_byte(kSpriteHPTable + sprite_id, &value);
568 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
569 std::string(status.message()));
571 data.push_back(value);
572 status = read_byte(kSpriteDamageTable + sprite_id, &value);
574 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
575 std::string(status.message()));
577 data.push_back(value);
578 status = read_byte(kSpritePaletteTable + sprite_id, &value);
580 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
581 std::string(status.message()));
583 data.push_back(value);
584 for (
int i = 0; i < 4; ++i) {
586 read_byte(kSpritePropertiesTable + (sprite_id * 4) + i, &value);
588 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
589 std::string(status.message()));
591 data.push_back(value);
594 response->set_sprite_data(data.data(), data.size());
596 return grpc::Status::OK;
599grpc::Status RomServiceImpl::SubmitRomProposal(
600 grpc::ServerContext* context,
601 const rom_svc::SubmitRomProposalRequest* request,
602 rom_svc::SubmitRomProposalResponse* response) {
603 if (!approval_mgr_) {
604 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
605 "Approval manager not initialized");
612 std::string username = request->username().empty() ?
"grpc" : request->username();
614 nlohmann::json proposal_data = {{
"description",
description}};
616 if (request->has_write_bytes()) {
617 const auto& write = request->write_bytes();
618 proposal_data[
"type"] =
"write_bytes";
619 proposal_data[
"offset"] = write.offset();
620 proposal_data[
"size"] = write.data().size();
621 std::vector<uint8_t> bytes(write.data().begin(), write.data().end());
622 proposal_data[
"data"] = bytes;
623 }
else if (request->has_overworld_tile()) {
624 const auto& write = request->overworld_tile();
625 proposal_data[
"type"] =
"overworld_tile_write";
626 proposal_data[
"map_id"] = write.map_id();
627 proposal_data[
"x"] = write.x();
628 proposal_data[
"y"] = write.y();
629 proposal_data[
"tile16_id"] = write.tile16_id();
630 }
else if (request->has_dungeon_tile()) {
631 const auto& write = request->dungeon_tile();
632 proposal_data[
"type"] =
"dungeon_tile_write";
633 proposal_data[
"room_id"] = write.room_id();
634 proposal_data[
"x"] = write.x();
635 proposal_data[
"y"] = write.y();
636 proposal_data[
"tile16_id"] = write.tile16_id();
638 return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
639 "No proposal payload provided");
642 std::string proposal_id = GenerateProposalId();
643 auto status = approval_mgr_->SubmitProposal(proposal_id, username, description,
646 return grpc::Status(grpc::StatusCode::INTERNAL,
647 std::string(status.message()));
650 response->set_success(
true);
651 response->set_proposal_id(proposal_id);
652 return grpc::Status::OK;
654grpc::Status RomServiceImpl::GetProposalStatus(
655 grpc::ServerContext* context,
656 const rom_svc::GetProposalStatusRequest* request,
657 rom_svc::GetProposalStatusResponse* response) {
658 if (!approval_mgr_) {
659 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
660 "Approval manager not initialized");
663 auto status_or = approval_mgr_->GetProposalStatus(request->proposal_id());
664 if (!status_or.ok()) {
665 return grpc::Status(grpc::StatusCode::NOT_FOUND,
666 std::string(status_or.status().message()));
669 const auto& status = *status_or;
670 response->set_proposal_id(status.proposal_id);
671 response->set_status(status.status);
675 for (
const auto& [user, approved] : status.votes) {
676 response->add_voters(user);
683 response->set_approval_count(approvals);
684 response->set_rejection_count(rejections);
685 return grpc::Status::OK;
687grpc::Status RomServiceImpl::CreateSnapshot(
688 grpc::ServerContext* context,
const rom_svc::CreateSnapshotRequest* request,
689 rom_svc::CreateSnapshotResponse* response) {
691 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
692 "Version manager not initialized");
695 auto result = version_mgr_->CreateSnapshot(
696 request->description(),
697 request->username().empty() ?
"grpc" : request->username(),
698 request->is_checkpoint());
700 response->set_success(
false);
701 response->set_error(std::string(result.status().message()));
702 return grpc::Status::OK;
705 response->set_success(
true);
706 response->set_snapshot_id(*result);
707 return grpc::Status::OK;
709grpc::Status RomServiceImpl::RestoreSnapshot(
710 grpc::ServerContext* context,
711 const rom_svc::RestoreSnapshotRequest* request,
712 rom_svc::RestoreSnapshotResponse* response) {
714 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
715 "Version manager not initialized");
718 auto status = version_mgr_->RestoreSnapshot(request->snapshot_id());
720 response->set_success(
false);
721 response->set_error(std::string(status.message()));
722 return grpc::Status::OK;
725 response->set_success(
true);
726 return grpc::Status::OK;
728grpc::Status RomServiceImpl::ListSnapshots(
729 grpc::ServerContext* context,
const rom_svc::ListSnapshotsRequest* request,
730 rom_svc::ListSnapshotsResponse* response) {
732 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
733 "Version manager not initialized");
736 auto snapshots = version_mgr_->GetSnapshots(
false);
737 uint32_t max_results = request->max_results();
738 if (max_results > 0 && snapshots.size() > max_results) {
739 snapshots.resize(max_results);
742 for (
const auto& snapshot : snapshots) {
743 auto* info = response->add_snapshots();
744 info->set_snapshot_id(snapshot.snapshot_id);
745 info->set_description(snapshot.description);
746 info->set_username(snapshot.creator);
747 info->set_timestamp(snapshot.timestamp);
748 info->set_is_checkpoint(snapshot.is_checkpoint);
749 info->set_is_safe_point(snapshot.is_safe_point);
750 info->set_size_bytes(snapshot.rom_data.size());
753 return grpc::Status::OK;
756grpc::Status RomServiceImpl::ValidateRomLoaded() {
757 Rom*
rom = rom_getter_();
759 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
762 return grpc::Status::OK;
765absl::Status RomServiceImpl::MaybeCreateSnapshot(
766 const std::string& description) {
767 if (!config_.enable_version_management || !version_mgr_) {
768 return absl::OkStatus();
770 return version_mgr_->CreateSnapshot(description,
"gRPC",
false).status();
absl::StatusOr< uint8_t > ReadByte(int offset) const
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Rom * rom()
Get the current ROM instance.
::yaze::zelda3::GameData * game_data()
Get the current game data instance.
absl::Status LoadGameData(Rom &rom, GameData &data, const LoadOptions &options)
Loads all Zelda3-specific game data from a generic ROM.
absl::Status LoadMetadata(const Rom &rom, GameData &data)