7#include "absl/strings/str_format.h"
18namespace rom_svc = ::yaze::proto;
26std::string GenerateProposalId() {
27 auto now = std::chrono::system_clock::now();
28 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
29 now.time_since_epoch())
31 return absl::StrFormat(
"proposal_%lld", ms);
34std::string BuildRoomRawJson(
const zelda3::Room& room) {
35 nlohmann::json payload = nlohmann::json::object();
36 payload[
"room_id"] = room.id();
37 payload[
"layout"] = room.layout_id();
38 payload[
"floor1"] = room.floor1();
39 payload[
"floor2"] = room.floor2();
40 payload[
"blockset"] = room.blockset();
41 payload[
"spriteset"] = room.spriteset();
42 payload[
"palette"] = room.palette();
43 payload[
"effect"] =
static_cast<int>(room.effect());
44 payload[
"tag1"] =
static_cast<int>(room.tag1());
45 payload[
"tag2"] =
static_cast<int>(room.tag2());
47 auto objects = nlohmann::json::array();
48 for (
const auto& obj : room.GetTileObjects()) {
49 nlohmann::json entry = {
50 {
"id",
static_cast<int>(obj.id_)},
54 {
"layer", obj.GetLayerValue()},
55 {
"size_x_bits", obj.size_x_bits_},
56 {
"size_y_bits", obj.size_y_bits_},
58 objects.push_back(entry);
60 payload[
"objects"] = objects;
62 auto doors = nlohmann::json::array();
63 for (
const auto& door : room.GetDoors()) {
64 nlohmann::json entry = {
65 {
"byte1", door.byte1},
66 {
"byte2", door.byte2},
67 {
"position", door.position},
68 {
"direction",
static_cast<int>(door.direction)},
69 {
"type",
static_cast<int>(door.type)},
71 doors.push_back(entry);
73 payload[
"doors"] = doors;
75 auto sprites = nlohmann::json::array();
76 for (
const auto& sprite : room.GetSprites()) {
77 nlohmann::json entry = {
78 {
"id", sprite.id()}, {
"x", sprite.x()},
79 {
"y", sprite.y()}, {
"subtype", sprite.subtype()},
80 {
"layer", sprite.layer()},
82 sprites.push_back(entry);
84 payload[
"sprites"] = sprites;
86 payload[
"encoded_objects"] = room.EncodeObjects();
87 payload[
"encoded_sprites"] = room.EncodeSprites();
89 return payload.dump();
92absl::Status LoadGameDataForRom(Rom* rom, zelda3::GameData* data,
93 const zelda3::LoadOptions& options) {
95 return absl::FailedPreconditionError(
"ROM not loaded");
98 return absl::InvalidArgumentError(
"GameData is null");
104absl::Status LoadMetadataForRom(Rom* rom, zelda3::GameData* data) {
106 return absl::FailedPreconditionError(
"ROM not loaded");
109 return absl::InvalidArgumentError(
"GameData is null");
117RomServiceImpl::RomServiceImpl(RomGetter rom_getter,
118 RomVersionManager* version_manager,
119 ProposalApprovalManager* approval_manager)
120 : rom_getter_(rom_getter),
121 version_mgr_(version_manager),
122 approval_mgr_(approval_manager) {}
124void RomServiceImpl::SetConfig(
const Config& config) {
128grpc::Status RomServiceImpl::ReadBytes(grpc::ServerContext* context,
129 const rom_svc::ReadBytesRequest* request,
130 rom_svc::ReadBytesResponse* response) {
131 Rom*
rom = rom_getter_();
133 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
137 uint32_t offset = request->offset();
138 uint32_t length = request->length();
141 if (offset + length >
rom->
size()) {
142 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
143 absl::StrFormat(
"Read beyond ROM: 0x%X+%d > %d", offset,
148 const auto* data =
rom->
data() + offset;
149 response->set_data(data, length);
151 return grpc::Status::OK;
154grpc::Status RomServiceImpl::WriteBytes(
155 grpc::ServerContext* context,
const rom_svc::WriteBytesRequest* request,
156 rom_svc::WriteBytesResponse* response) {
162 auto& ui_queue = util::UiThreadQueue::Get();
163 if (!ui_queue.RunsInline()) {
165 absl::Status queued = ui_queue.RunOnUiThread([&]() {
166 result =
WriteBytes(context, request, response);
167 return absl::OkStatus();
172 return grpc::Status(
static_cast<grpc::StatusCode
>(queued.code()),
173 std::string(queued.message()));
178 Rom*
rom = rom_getter_();
180 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
184 uint32_t offset = request->offset();
185 const std::string& data = request->data();
188 if (offset + data.size() >
rom->
size()) {
189 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
190 absl::StrFormat(
"Write beyond ROM: 0x%X+%zu > %d",
191 offset, data.size(),
rom->
size()));
194 if (config_.require_approval_for_writes || request->require_approval()) {
195 if (!approval_mgr_) {
196 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
197 "Approval manager not initialized");
200 std::string
description = request->require_approval()
201 ?
"WriteBytes (proposal)"
202 :
"WriteBytes (approval required)";
203 if (!request->data().empty()) {
204 description = absl::StrFormat(
"WriteBytes at 0x%X (%zu bytes)", offset,
205 request->data().size());
208 nlohmann::json proposal_data = {{
"description",
description},
209 {
"type",
"write_bytes"},
211 {
"size", request->data().size()}};
212 std::vector<uint8_t> bytes(request->data().begin(), request->data().end());
213 proposal_data[
"data"] = bytes;
215 std::string proposal_id = GenerateProposalId();
216 auto status = approval_mgr_->SubmitProposal(proposal_id,
"grpc",
217 description, proposal_data);
219 return grpc::Status(grpc::StatusCode::INTERNAL,
220 std::string(status.message()));
223 response->set_success(
true);
224 response->set_proposal_id(proposal_id);
225 return grpc::Status::OK;
229 auto status = MaybeCreateSnapshot(absl::StrFormat(
230 "Auto-snapshot before write at 0x%X (%zu bytes)", offset, data.size()));
233 grpc::StatusCode::INTERNAL,
234 "Failed to create safety snapshot: " + std::string(status.message()));
238 std::vector<uint8_t> bytes(data.begin(), data.end());
241 if (!write_status.ok()) {
242 return grpc::Status(grpc::StatusCode::INTERNAL,
243 std::string(write_status.message()));
245 response->set_success(
true);
247 return grpc::Status::OK;
250grpc::Status RomServiceImpl::GetRomInfo(
251 grpc::ServerContext* context,
const rom_svc::GetRomInfoRequest* request,
252 rom_svc::GetRomInfoResponse* response) {
253 Rom*
rom = rom_getter_();
255 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
260 response->set_size(
rom->
size());
262 auto metadata = std::make_unique<zelda3::GameData>(rom);
263 if (LoadMetadataForRom(rom, metadata.get()).ok()) {
264 response->set_version(metadata->version == zelda3_version::JP ?
"JP"
267 response->set_is_expanded(
rom->
size() > 0x200000);
269 return grpc::Status::OK;
272grpc::Status RomServiceImpl::ReadOverworldMap(
273 grpc::ServerContext* context,
274 const rom_svc::ReadOverworldMapRequest* request,
275 rom_svc::ReadOverworldMapResponse* response) {
276 Rom*
rom = rom_getter_();
278 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
282 uint32_t map_id = request->map_id();
284 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
285 "Invalid map_id (0-159)");
288 auto metadata = std::make_unique<zelda3::GameData>(rom);
289 auto metadata_status = LoadMetadataForRom(rom, metadata.get());
290 if (!metadata_status.ok()) {
291 return grpc::Status(grpc::StatusCode::INTERNAL,
292 std::string(metadata_status.message()));
296 zelda3::Overworld overworld(rom, metadata.get());
297 auto status = overworld.Load(rom);
300 grpc::StatusCode::INTERNAL,
301 "Failed to load Overworld: " + std::string(status.message()));
305 int local_id = map_id;
309 }
else if (map_id >= 64) {
315 auto& blockset = overworld.GetMapTiles(world);
319 int maps_per_row = 8;
320 int map_col = local_id % maps_per_row;
321 int map_row = local_id / maps_per_row;
322 int global_start_x = map_col * 32;
323 int global_start_y = map_row * 32;
325 response->set_map_id(map_id);
327 if (blockset.empty() || blockset[0].empty()) {
328 return grpc::Status(grpc::StatusCode::INTERNAL,
329 "Overworld tiles not initialized");
333 if (global_start_x + 32 >
static_cast<int>(blockset.size()) ||
334 global_start_y + 32 >
static_cast<int>(blockset[0].size())) {
336 grpc::StatusCode::INTERNAL,
337 absl::StrFormat(
"Map bounds error: %d (local %d) -> (%d, %d)", map_id,
338 local_id, global_start_x, global_start_y));
342 for (
int y = 0; y < 32; ++y) {
343 for (
int x = 0; x < 32; ++x) {
344 response->add_tile16_data(
345 blockset[global_start_x + x][global_start_y + y]);
349 return grpc::Status::OK;
352grpc::Status RomServiceImpl::WriteOverworldTile(
353 grpc::ServerContext* context,
354 const rom_svc::WriteOverworldTileRequest* request,
355 rom_svc::WriteOverworldTileResponse* response) {
356 Rom*
rom = rom_getter_();
358 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
362 uint32_t map_id = request->map_id();
363 uint32_t x = request->x();
364 uint32_t y = request->y();
365 uint32_t tile_id = request->tile16_id();
367 if (map_id >= 160 || x >= 32 || y >= 32) {
368 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
"Invalid coordinates");
371 if (config_.require_approval_for_writes || request->require_approval()) {
372 if (!approval_mgr_) {
373 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
374 "Approval manager not initialized");
378 request->description().empty()
379 ? absl::StrFormat(
"Update OW tile: Map %d (%d,%d) -> %d", map_id, x,
382 nlohmann::json proposal_data = {{
"description",
description},
383 {
"type",
"overworld_tile_write"},
387 {
"tile16_id", tile_id}};
389 std::string proposal_id = GenerateProposalId();
390 auto status = approval_mgr_->SubmitProposal(proposal_id,
"grpc",
391 description, proposal_data);
393 return grpc::Status(grpc::StatusCode::INTERNAL,
394 std::string(status.message()));
397 response->set_success(
true);
398 response->set_proposal_id(proposal_id);
399 return grpc::Status::OK;
402 auto metadata = std::make_unique<zelda3::GameData>(rom);
403 auto metadata_status = LoadMetadataForRom(rom, metadata.get());
404 if (!metadata_status.ok()) {
405 return grpc::Status(grpc::StatusCode::INTERNAL,
406 std::string(metadata_status.message()));
410 zelda3::Overworld overworld(rom, metadata.get());
411 auto status = overworld.Load(rom);
413 return grpc::Status(grpc::StatusCode::INTERNAL,
"Failed to load Overworld");
417 int local_id = map_id;
421 }
else if (map_id >= 64) {
427 int maps_per_row = 8;
428 int map_col = local_id % maps_per_row;
429 int map_row = local_id / maps_per_row;
430 int global_x = (map_col * 32) + x;
431 int global_y = (map_row * 32) + y;
434 auto& blockset = overworld.GetMapTiles(world);
435 if (blockset.empty() || blockset[0].empty()) {
436 return grpc::Status(grpc::StatusCode::INTERNAL,
437 "Overworld tiles not initialized");
439 if (global_x >=
static_cast<int>(blockset.size()) ||
440 global_y >=
static_cast<int>(blockset[0].size())) {
441 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
442 "Global coordinates out of range");
444 blockset[global_x][global_y] =
static_cast<uint16_t
>(tile_id);
447 auto snap_status = MaybeCreateSnapshot(absl::StrFormat(
448 "Update OW tile: Map %d (%d,%d) -> %d", map_id, x, y, tile_id));
449 if (!snap_status.ok()) {
450 return grpc::Status(grpc::StatusCode::INTERNAL,
"Snapshot failed");
454 status = overworld.Save(rom);
457 grpc::StatusCode::INTERNAL,
458 "Failed to save Overworld: " + std::string(status.message()));
461 response->set_success(
true);
462 return grpc::Status::OK;
465grpc::Status RomServiceImpl::ReadDungeonRoom(
466 grpc::ServerContext* context,
467 const rom_svc::ReadDungeonRoomRequest* request,
468 rom_svc::ReadDungeonRoomResponse* response) {
469 Rom*
rom = rom_getter_();
471 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
475 uint32_t room_id = request->room_id();
476 if (room_id >= 296) {
477 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
"Invalid room_id");
480 auto game_data = std::make_unique<zelda3::GameData>(rom);
481 zelda3::LoadOptions options;
482 options.load_graphics =
true;
483 options.load_palettes =
true;
484 options.load_gfx_groups =
true;
485 options.populate_metadata =
true;
486 options.expand_rom =
false;
487 auto game_status = LoadGameDataForRom(rom,
game_data.get(), options);
488 if (!game_status.ok()) {
489 return grpc::Status(grpc::StatusCode::INTERNAL,
490 std::string(game_status.message()));
494 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
498 room.RenderRoomGraphics();
500 response->set_room_id(room_id);
506 const auto& buffer = room.bg1_buffer().buffer();
508 for (uint16_t tile : buffer) {
509 response->add_tile16_data(tile);
512 const std::string raw_json = BuildRoomRawJson(room);
513 if (!raw_json.empty()) {
514 response->set_raw_data(raw_json);
517 return grpc::Status::OK;
520grpc::Status RomServiceImpl::WriteDungeonTile(
521 grpc::ServerContext* context,
522 const rom_svc::WriteDungeonTileRequest* request,
523 rom_svc::WriteDungeonTileResponse* response) {
524 if (!approval_mgr_) {
525 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
526 "Approval manager not initialized");
530 request->description().empty()
531 ? absl::StrFormat(
"Dungeon tile write: Room %d (%d,%d) -> %d",
532 request->room_id(), request->x(), request->y(),
533 request->tile16_id())
535 nlohmann::json proposal_data = {{
"description",
description},
536 {
"type",
"dungeon_tile_write"},
537 {
"room_id", request->room_id()},
540 {
"tile16_id", request->tile16_id()}};
542 std::string proposal_id = GenerateProposalId();
543 auto status = approval_mgr_->SubmitProposal(proposal_id,
"grpc", description,
546 return grpc::Status(grpc::StatusCode::INTERNAL,
547 std::string(status.message()));
550 response->set_success(
true);
551 response->set_proposal_id(proposal_id);
552 return grpc::Status::OK;
555grpc::Status RomServiceImpl::ReadSprite(
556 grpc::ServerContext* context,
const rom_svc::ReadSpriteRequest* request,
557 rom_svc::ReadSpriteResponse* response) {
558 Rom*
rom = rom_getter_();
560 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
564 uint32_t sprite_id = request->sprite_id();
565 if (sprite_id >= 256) {
566 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
567 "Invalid sprite_id (0-255)");
571 const uint32_t kSpriteHPTable = 0x6B173;
572 const uint32_t kSpriteDamageTable = 0x6B266;
573 const uint32_t kSpritePaletteTable = 0x6B35B;
574 const uint32_t kSpritePropertiesTable = 0x6B450;
576 response->set_sprite_id(sprite_id);
578 auto read_byte = [&](uint32_t offset, uint8_t* out) -> absl::Status {
579 auto val =
rom->
ReadByte(
static_cast<int>(offset));
584 return absl::OkStatus();
587 std::vector<uint8_t> data;
590 auto status = read_byte(kSpriteHPTable + sprite_id, &value);
592 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
593 std::string(status.message()));
595 data.push_back(value);
596 status = read_byte(kSpriteDamageTable + sprite_id, &value);
598 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
599 std::string(status.message()));
601 data.push_back(value);
602 status = read_byte(kSpritePaletteTable + sprite_id, &value);
604 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
605 std::string(status.message()));
607 data.push_back(value);
608 for (
int i = 0; i < 4; ++i) {
609 status = read_byte(kSpritePropertiesTable + (sprite_id * 4) + i, &value);
611 return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
612 std::string(status.message()));
614 data.push_back(value);
617 response->set_sprite_data(data.data(), data.size());
619 return grpc::Status::OK;
622grpc::Status RomServiceImpl::SubmitRomProposal(
623 grpc::ServerContext* context,
624 const rom_svc::SubmitRomProposalRequest* request,
625 rom_svc::SubmitRomProposalResponse* response) {
626 if (!approval_mgr_) {
627 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
628 "Approval manager not initialized");
635 std::string username =
636 request->username().empty() ?
"grpc" : request->username();
638 nlohmann::json proposal_data = {{
"description",
description}};
640 if (request->has_write_bytes()) {
641 const auto& write = request->write_bytes();
642 proposal_data[
"type"] =
"write_bytes";
643 proposal_data[
"offset"] = write.offset();
644 proposal_data[
"size"] = write.data().size();
645 std::vector<uint8_t> bytes(write.data().begin(), write.data().end());
646 proposal_data[
"data"] = bytes;
647 }
else if (request->has_overworld_tile()) {
648 const auto& write = request->overworld_tile();
649 proposal_data[
"type"] =
"overworld_tile_write";
650 proposal_data[
"map_id"] = write.map_id();
651 proposal_data[
"x"] = write.x();
652 proposal_data[
"y"] = write.y();
653 proposal_data[
"tile16_id"] = write.tile16_id();
654 }
else if (request->has_dungeon_tile()) {
655 const auto& write = request->dungeon_tile();
656 proposal_data[
"type"] =
"dungeon_tile_write";
657 proposal_data[
"room_id"] = write.room_id();
658 proposal_data[
"x"] = write.x();
659 proposal_data[
"y"] = write.y();
660 proposal_data[
"tile16_id"] = write.tile16_id();
662 return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT,
663 "No proposal payload provided");
666 std::string proposal_id = GenerateProposalId();
667 auto status = approval_mgr_->SubmitProposal(proposal_id, username,
668 description, proposal_data);
670 return grpc::Status(grpc::StatusCode::INTERNAL,
671 std::string(status.message()));
674 response->set_success(
true);
675 response->set_proposal_id(proposal_id);
676 return grpc::Status::OK;
678grpc::Status RomServiceImpl::GetProposalStatus(
679 grpc::ServerContext* context,
680 const rom_svc::GetProposalStatusRequest* request,
681 rom_svc::GetProposalStatusResponse* response) {
682 if (!approval_mgr_) {
683 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
684 "Approval manager not initialized");
687 auto status_or = approval_mgr_->GetProposalStatus(request->proposal_id());
688 if (!status_or.ok()) {
689 return grpc::Status(grpc::StatusCode::NOT_FOUND,
690 std::string(status_or.status().message()));
693 const auto& status = *status_or;
694 response->set_proposal_id(status.proposal_id);
695 response->set_status(status.status);
699 for (
const auto& [user, approved] : status.votes) {
700 response->add_voters(user);
707 response->set_approval_count(approvals);
708 response->set_rejection_count(rejections);
709 return grpc::Status::OK;
711grpc::Status RomServiceImpl::CreateSnapshot(
712 grpc::ServerContext* context,
const rom_svc::CreateSnapshotRequest* request,
713 rom_svc::CreateSnapshotResponse* response) {
715 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
716 "Version manager not initialized");
719 auto result = version_mgr_->CreateSnapshot(
720 request->description(),
721 request->username().empty() ?
"grpc" : request->username(),
722 request->is_checkpoint());
724 response->set_success(
false);
725 response->set_error(std::string(result.status().message()));
726 return grpc::Status::OK;
729 response->set_success(
true);
730 response->set_snapshot_id(*result);
731 return grpc::Status::OK;
733grpc::Status RomServiceImpl::RestoreSnapshot(
734 grpc::ServerContext* context,
735 const rom_svc::RestoreSnapshotRequest* request,
736 rom_svc::RestoreSnapshotResponse* response) {
738 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
739 "Version manager not initialized");
742 auto status = version_mgr_->RestoreSnapshot(request->snapshot_id());
744 response->set_success(
false);
745 response->set_error(std::string(status.message()));
746 return grpc::Status::OK;
749 response->set_success(
true);
750 return grpc::Status::OK;
752grpc::Status RomServiceImpl::ListSnapshots(
753 grpc::ServerContext* context,
const rom_svc::ListSnapshotsRequest* request,
754 rom_svc::ListSnapshotsResponse* response) {
756 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
757 "Version manager not initialized");
760 auto snapshots = version_mgr_->GetSnapshots(
false);
761 uint32_t max_results = request->max_results();
762 if (max_results > 0 && snapshots.size() > max_results) {
763 snapshots.resize(max_results);
766 for (
const auto& snapshot : snapshots) {
767 auto* info = response->add_snapshots();
768 info->set_snapshot_id(snapshot.snapshot_id);
769 info->set_description(snapshot.description);
770 info->set_username(snapshot.creator);
771 info->set_timestamp(snapshot.timestamp);
772 info->set_is_checkpoint(snapshot.is_checkpoint);
773 info->set_is_safe_point(snapshot.is_safe_point);
774 info->set_size_bytes(snapshot.rom_data.size());
777 return grpc::Status::OK;
780grpc::Status RomServiceImpl::ValidateRomLoaded() {
781 Rom*
rom = rom_getter_();
783 return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
786 return grpc::Status::OK;
789absl::Status RomServiceImpl::MaybeCreateSnapshot(
790 const std::string& description) {
791 if (!config_.enable_version_management || !version_mgr_) {
792 return absl::OkStatus();
794 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 WriteBytes(std::ostream &out, const void *data, size_t size)
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)