syntax = "proto3";

option java_package = "com.soulfiremc.grpc.generated";
option java_multiple_files = true;

package soulfire.v1;

import "soulfire/api_docs.proto";
import "soulfire/bot.proto";
import "soulfire/common.proto";
import "soulfire/domain.proto";
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/timestamp.proto";

// ============================================================================
// BotLiveService - Streaming Events, Interactions, and Pathfinding
// ============================================================================
//
// BotLiveService is the remote-control API surface for SoulFire bots. It
// complements BotService (which is polling/snapshot oriented) by providing:
//
// - A single streaming event channel per bot (state deltas, chat, lifecycle,
//   entities, block updates) so remote controllers don't need to poll.
// - High-level imperative actions (dig, place, attack, use, interact) that
//   target specific coordinates or entity ids rather than relying on the bot's
//   current look direction.
// - World queries (block lookup, block search, nearby entities) against the
//   bot's loaded world view.
// - Pathfinding as a streaming RPC: submit a goal, receive progress/completion
//   events.
//
// The service is intended to make SoulFire feel like a library from the
// caller's perspective (mineflayer/azalea style) while keeping the server
// authoritative.
//
// All RPCs require READ_BOT_INFO (for reads and event streams) or
// CONTROL_BOT_ACTIONS (for actions) on the owning instance.
// ============================================================================

// The face of a block that an action targets. Used when placing a block or
// right-clicking a block with an item to indicate which side the player is
// interacting with.
enum BlockFace {
  BLOCK_FACE_UNSPECIFIED = 0;
  BLOCK_FACE_DOWN = 1;
  BLOCK_FACE_UP = 2;
  BLOCK_FACE_NORTH = 3;
  BLOCK_FACE_SOUTH = 4;
  BLOCK_FACE_WEST = 5;
  BLOCK_FACE_EAST = 6;
}

// Which hand to use for an action. Mirrors Minecraft's main-hand / off-hand
// distinction.
enum Hand {
  HAND_UNSPECIFIED = 0;
  HAND_MAIN = 1;
  HAND_OFF = 2;
}

// A block observed in the bot's world view, including its type and runtime
// property state (e.g. "facing": "north", "waterlogged": "true").
message BlockState {
  // The block position.
  BlockPosition position = 1 [(google.api.field_behavior) = REQUIRED];
  // The namespaced block id (e.g. "minecraft:oak_log").
  string block_id = 2 [(google.api.field_behavior) = REQUIRED];
  // Flattened block state properties (name -> value) for the block. Empty for
  // blocks with no variants.
  map<string, string> properties = 3;
}

// A brief summary of an entity in the bot's world view. Intended for use as
// a repeated item in list responses and streaming events.
message NearbyEntity {
  // The numeric network entity id assigned by the server.
  // Use this to target the entity in AttackEntity / InteractEntity.
  int32 entity_id = 1 [(google.api.field_behavior) = REQUIRED];
  // The namespaced entity type (e.g. "minecraft:zombie", "minecraft:player").
  string entity_type = 2 [(google.api.field_behavior) = REQUIRED];
  // Current world position of the entity.
  WorldPosition position = 3 [(google.api.field_behavior) = REQUIRED];
  // Straight-line distance from the bot, in blocks.
  float distance = 4 [(google.api.field_behavior) = REQUIRED];
  // Display name if the entity has one. For players this is the username.
  optional string display_name = 5;
  // True if this entity is a player.
  bool is_player = 6;
  // Current health, if known. Not populated for entities without a health
  // attribute or when the bot cannot observe it.
  optional float health = 7;
}

// ============================================================================
// Event stream
// ============================================================================

// Options controlling which categories of events are delivered by
// WatchBotEvents. Each category is opt-in; a stream with no categories
// enabled still emits the initial snapshot and then stays silent.
message BotEventFilter {
  // Deliver position / rotation / health / food / xp / dimension / game mode
  // changes as BotStateDelta.
  bool include_state_deltas = 1;
  // Deliver incoming chat and system messages as BotChatEvent.
  bool include_chat = 2;
  // Deliver connect / disconnect / spawn / death / respawn events as
  // BotLifecycleEvent.
  bool include_lifecycle = 3;
  // Deliver entity spawn / update / despawn within entity_radius as
  // BotEntityEvent. Setting this without a positive radius defaults to 32
  // blocks to keep the stream manageable.
  bool include_entity_events = 4;
  // Maximum radius (blocks) for entity events. Ignored if
  // include_entity_events is false. Server clamps to a sane upper bound.
  float entity_radius = 5;
  // Deliver block change events within block_radius around the bot as
  // BotBlockUpdateEvent. Setting this without a positive radius defaults to
  // 16 blocks.
  bool include_block_updates = 6;
  // Maximum radius (blocks) for block updates. Ignored if
  // include_block_updates is false. Server clamps to a sane upper bound.
  float block_radius = 7;
  // Deliver inventory and open-container snapshots when their state changes.
  bool include_inventory = 8;
  // Deliver explicit damage events.
  bool include_damage = 9;
  // Deliver world clock, weather, and other server game-state changes.
  bool include_environment = 10;
  // Deliver tab-list additions, removals, and metadata updates.
  bool include_player_list = 11;
  // Deliver boss-bar lifecycle and property updates.
  bool include_boss_bars = 12;
  // Deliver sound playback and stop events.
  bool include_sounds = 13;
  // Deliver particle spawn events.
  bool include_particles = 14;
  // Deliver objectives, scores, display slots, and team updates.
  bool include_scoreboard = 15;
  // Deliver server resource-pack offers and removals.
  bool include_resource_packs = 16;
  // Deliver title, subtitle, timing, clear, and reset events.
  bool include_titles = 17;
  // Deliver chunk-column load and unload events. This can be a high-volume
  // stream while the bot is moving, so it is not enabled by default.
  bool include_chunks = 18;
}

// Request to subscribe to a bot's live event stream.
message WatchBotEventsRequest {
  // Owning instance UUID.
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Target bot profile UUID.
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Which event categories to include in the stream.
  BotEventFilter filter = 3 [(google.api.field_behavior) = REQUIRED];
  // Last sequence confirmed by the caller. When the requested event is no
  // longer resumable the server emits a resync_required event followed by a
  // fresh snapshot.
  uint64 after_sequence = 4;
  // Stream epoch associated with after_sequence.
  optional string stream_epoch = 5 [(soulfire.v1.api_field).format = "uuid"];
  // Requested heartbeat interval. The server clamps this to its supported
  // range. Zero selects the server default.
  uint32 heartbeat_interval_seconds = 6;
}

// Partial update to the bot's live state. Only fields present have changed
// since the last delta or snapshot. Clients should merge deltas onto their
// local copy of BotLiveState.
message BotStateDelta {
  optional double x = 1;
  optional double y = 2;
  optional double z = 3;
  optional float x_rot = 4;
  optional float y_rot = 5;
  optional float health = 6;
  optional float max_health = 7;
  optional int32 food_level = 8;
  optional float saturation_level = 9;
  optional int32 selected_hotbar_slot = 10;
  optional string dimension = 11;
  optional int32 experience_level = 12;
  optional float experience_progress = 13;
  optional GameMode game_mode = 14;
}

// Source of a chat message.
enum ChatSource {
  CHAT_SOURCE_UNSPECIFIED = 0;
  // Sent by another player (player chat).
  CHAT_SOURCE_PLAYER = 1;
  // Server-wide broadcast or system announcement.
  CHAT_SOURCE_SYSTEM = 2;
  // Action bar overlay (the thin strip above the hotbar).
  CHAT_SOURCE_ACTION_BAR = 3;
  // Whisper directed at this bot.
  CHAT_SOURCE_WHISPER = 4;
  // The client received a message but Minecraft did not expose reliable
  // source metadata for it.
  CHAT_SOURCE_UNKNOWN = 5;
}

// A single chat or system message observed by the bot.
message BotChatEvent {
  // Where the message came from.
  ChatSource source = 1 [(google.api.field_behavior) = REQUIRED];
  // Plain-text rendering of the message with formatting codes stripped.
  // Stable across Minecraft versions; suitable for pattern matching.
  string plain_text = 2 [(google.api.field_behavior) = REQUIRED];
  // Original JSON text component for callers that need formatting, colors,
  // click events, or translation keys. Present when available from the server.
  optional string json_component = 3;
  // Sender display name for PLAYER and WHISPER sources.
  optional string sender_name = 4;
  // Sender profile UUID for PLAYER and WHISPER sources, when resolvable.
  optional string sender_id = 5;
  // Server-side timestamp at which the message was received.
  google.protobuf.Timestamp received_at = 6;
}

// Lifecycle event kind for the bot's Minecraft session.
enum BotLifecycleKind {
  BOT_LIFECYCLE_UNSPECIFIED = 0;
  BOT_LIFECYCLE_CONNECTING = 1;
  BOT_LIFECYCLE_CONNECTED = 2;
  BOT_LIFECYCLE_SPAWNED = 3;
  BOT_LIFECYCLE_DIED = 4;
  BOT_LIFECYCLE_RESPAWNED = 5;
  BOT_LIFECYCLE_DISCONNECTED = 6;
}

// Lifecycle event for the bot's connection and player state.
message BotLifecycleEvent {
  BotLifecycleKind kind = 1 [(google.api.field_behavior) = REQUIRED];
  // Free-form detail message (disconnect reason, death message, etc.).
  optional string message = 2;
}

// Entity event kind.
enum EntityEventKind {
  ENTITY_EVENT_UNSPECIFIED = 0;
  // Entity entered the bot's observation radius.
  ENTITY_EVENT_SPAWN = 1;
  // Entity metadata or position updated.
  ENTITY_EVENT_UPDATE = 2;
  // Entity left the observation radius or was removed.
  ENTITY_EVENT_DESPAWN = 3;
}

// An entity change within the bot's observation radius.
message BotEntityEvent {
  EntityEventKind kind = 1 [(google.api.field_behavior) = REQUIRED];
  // Entity summary at the time of the event. For DESPAWN, only entity_id is
  // guaranteed to be populated.
  NearbyEntity entity = 2 [(google.api.field_behavior) = REQUIRED];
  // Full semantic entity state at the time of the event. Present for spawn
  // and update events. A despawn uses `entity` as its stable tombstone.
  optional EntitySnapshot snapshot = 3;
}

// A block change observed within the bot's block radius.
message BotBlockUpdateEvent {
  BlockPosition position = 1 [(google.api.field_behavior) = REQUIRED];
  // Previous block id, if the client tracked it. Can be empty on initial
  // observation.
  string old_block_id = 2;
  // New block id after the change.
  string new_block_id = 3 [(google.api.field_behavior) = REQUIRED];
  // Full semantic block state after the change, including properties,
  // lighting, fluid, mining hints, and interaction metadata.
  optional BlockSnapshot block = 4;
}

// Current inventory or open-container state after a change.
message BotInventoryEvent {
  BotInventoryStateResponse state = 1 [(google.api.field_behavior) = REQUIRED];
}

// Damage observed by the bot.
message BotDamageEvent {
  float previous_health = 1 [(google.api.field_behavior) = REQUIRED];
  float health = 2 [(google.api.field_behavior) = REQUIRED];
  float amount = 3 [(google.api.field_behavior) = REQUIRED];
}

message ClockSnapshot {
  string clock_id = 1 [(google.api.field_behavior) = REQUIRED];
  int64 total_ticks = 2;
  float partial_tick = 3;
  float rate = 4;
}

message BotTimeEvent {
  int64 game_time = 1;
  repeated ClockSnapshot clocks = 2;
}

enum WeatherEventKind {
  WEATHER_EVENT_UNSPECIFIED = 0;
  WEATHER_EVENT_STARTED_RAINING = 1;
  WEATHER_EVENT_STOPPED_RAINING = 2;
  WEATHER_EVENT_RAIN_LEVEL_CHANGED = 3;
  WEATHER_EVENT_THUNDER_LEVEL_CHANGED = 4;
}

message BotWeatherEvent {
  WeatherEventKind kind = 1 [(google.api.field_behavior) = REQUIRED];
  optional float level = 2;
}

message BotGameEvent {
  // Stable snake-case event name, such as `immediate_respawn`.
  string event = 1 [(google.api.field_behavior) = REQUIRED];
  float parameter = 2;
}

message BotEnvironmentEvent {
  oneof change {
    BotTimeEvent time = 1;
    BotWeatherEvent weather = 2;
    BotGameEvent game_event = 3;
  }
}

message PlayerListEntrySnapshot {
  string profile_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  optional string profile_name = 2;
  bool listed = 3;
  int32 latency_ms = 4;
  GameMode game_mode = 5;
  optional TextComponent display_name = 6;
  bool show_hat = 7;
  int32 list_order = 8;
  // Packet actions applied to this entry, such as `add_player` or
  // `update_latency`. This lets consumers distinguish an omitted value from
  // a value that was not part of the update.
  repeated string changed_fields = 9;
}

enum PlayerListEventKind {
  PLAYER_LIST_EVENT_UNSPECIFIED = 0;
  PLAYER_LIST_EVENT_UPSERT = 1;
  PLAYER_LIST_EVENT_REMOVE = 2;
}

message BotPlayerListEvent {
  PlayerListEventKind kind = 1 [(google.api.field_behavior) = REQUIRED];
  repeated PlayerListEntrySnapshot entries = 2;
  repeated string removed_profile_ids = 3 [(soulfire.v1.api_field).format = "uuid"];
}

enum BossBarEventKind {
  BOSS_BAR_EVENT_UNSPECIFIED = 0;
  BOSS_BAR_EVENT_ADD = 1;
  BOSS_BAR_EVENT_REMOVE = 2;
  BOSS_BAR_EVENT_UPDATE_PROGRESS = 3;
  BOSS_BAR_EVENT_UPDATE_NAME = 4;
  BOSS_BAR_EVENT_UPDATE_STYLE = 5;
  BOSS_BAR_EVENT_UPDATE_PROPERTIES = 6;
}

message BotBossBarEvent {
  string boss_bar_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  BossBarEventKind kind = 2 [(google.api.field_behavior) = REQUIRED];
  optional TextComponent name = 3;
  optional float progress = 4;
  optional string color = 5;
  optional string overlay = 6;
  optional bool darken_screen = 7;
  optional bool play_music = 8;
  optional bool create_world_fog = 9;
}

enum SoundEventKind {
  SOUND_EVENT_UNSPECIFIED = 0;
  SOUND_EVENT_PLAY_AT_POSITION = 1;
  SOUND_EVENT_PLAY_AT_ENTITY = 2;
  SOUND_EVENT_STOP = 3;
}

message BotSoundEvent {
  SoundEventKind kind = 1 [(google.api.field_behavior) = REQUIRED];
  optional string sound_id = 2;
  optional string source = 3;
  optional WorldPosition position = 4;
  optional int32 entity_id = 5;
  optional float volume = 6;
  optional float pitch = 7;
  optional int64 seed = 8;
}

message BotParticleEvent {
  string particle_id = 1 [(google.api.field_behavior) = REQUIRED];
  WorldPosition position = 2 [(google.api.field_behavior) = REQUIRED];
  Vec3 offset = 3;
  float max_speed = 4;
  int32 count = 5;
  bool always_show = 6;
  bool override_limiter = 7;
  // Version-specific particle options rendered as a diagnostic string.
  optional string options = 8;
}

enum ScoreboardEventKind {
  SCOREBOARD_EVENT_UNSPECIFIED = 0;
  SCOREBOARD_EVENT_OBJECTIVE_ADD = 1;
  SCOREBOARD_EVENT_OBJECTIVE_REMOVE = 2;
  SCOREBOARD_EVENT_OBJECTIVE_UPDATE = 3;
  SCOREBOARD_EVENT_DISPLAY_OBJECTIVE = 4;
  SCOREBOARD_EVENT_SCORE_SET = 5;
  SCOREBOARD_EVENT_SCORE_RESET = 6;
  SCOREBOARD_EVENT_TEAM_ADD = 7;
  SCOREBOARD_EVENT_TEAM_REMOVE = 8;
  SCOREBOARD_EVENT_TEAM_UPDATE = 9;
  SCOREBOARD_EVENT_TEAM_PLAYERS_ADD = 10;
  SCOREBOARD_EVENT_TEAM_PLAYERS_REMOVE = 11;
}

message BotScoreboardEvent {
  ScoreboardEventKind kind = 1 [(google.api.field_behavior) = REQUIRED];
  optional string objective_name = 2;
  optional TextComponent display_name = 3;
  optional string render_type = 4;
  optional string display_slot = 5;
  optional string owner = 6;
  optional int32 score = 7;
  optional string team_name = 8;
  repeated string players = 9;
  optional TextComponent prefix = 10;
  optional TextComponent suffix = 11;
  optional string color = 12;
  optional string name_tag_visibility = 13;
  optional string collision_rule = 14;
  optional bool allow_friendly_fire = 15;
  optional bool see_friendly_invisibles = 16;
}

enum ResourcePackEventKind {
  RESOURCE_PACK_EVENT_UNSPECIFIED = 0;
  RESOURCE_PACK_EVENT_OFFERED = 1;
  RESOURCE_PACK_EVENT_REMOVED = 2;
  RESOURCE_PACK_EVENT_CLEARED = 3;
}

message BotResourcePackEvent {
  ResourcePackEventKind kind = 1 [(google.api.field_behavior) = REQUIRED];
  optional string pack_id = 2 [(soulfire.v1.api_field).format = "uuid"];
  optional string url = 3;
  optional string hash = 4;
  optional bool required = 5;
  optional TextComponent prompt = 6;
}

enum TitleEventKind {
  TITLE_EVENT_UNSPECIFIED = 0;
  TITLE_EVENT_TITLE = 1;
  TITLE_EVENT_SUBTITLE = 2;
  TITLE_EVENT_TIMES = 3;
  TITLE_EVENT_CLEAR = 4;
  TITLE_EVENT_RESET = 5;
}

message BotTitleEvent {
  TitleEventKind kind = 1 [(google.api.field_behavior) = REQUIRED];
  optional TextComponent text = 2;
  optional int32 fade_in_ticks = 3;
  optional int32 stay_ticks = 4;
  optional int32 fade_out_ticks = 5;
}

enum ChunkEventKind {
  CHUNK_EVENT_UNSPECIFIED = 0;
  CHUNK_EVENT_LOAD = 1;
  CHUNK_EVENT_UNLOAD = 2;
}

message BotChunkEvent {
  ChunkEventKind kind = 1 [(google.api.field_behavior) = REQUIRED];
  int32 chunk_x = 2 [(google.api.field_behavior) = REQUIRED];
  int32 chunk_z = 3 [(google.api.field_behavior) = REQUIRED];
  string dimension = 4 [(google.api.field_behavior) = REQUIRED];
}

// Metadata shared by every event emitted on a bot stream.
message BotEventEnvelope {
  string bot_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string stream_epoch = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  uint64 sequence = 3 [(google.api.field_behavior) = REQUIRED];
  google.protobuf.Timestamp observed_at = 4 [(google.api.field_behavior) = REQUIRED];
  uint64 snapshot_revision = 5 [(google.api.field_behavior) = REQUIRED];
  optional string causation_id = 6 [(soulfire.v1.api_field).format = "uuid"];
  optional string task_id = 7 [(soulfire.v1.api_field).format = "uuid"];
}

// Signals that continuity could not be preserved and the caller must replace
// its synchronized state with the snapshots that follow.
message BotResyncRequired {
  string reason = 1 [(google.api.field_behavior) = REQUIRED];
  optional string requested_epoch = 2 [(soulfire.v1.api_field).format = "uuid"];
  uint64 requested_after_sequence = 3;
}

// Keeps idle streams observable and allows clients to detect dead transports.
message BotHeartbeat {}

// A single message on the bot event stream.
message BotEvent {
  oneof event {
    // Initial snapshot or periodic resync. Sent once on stream open and any
    // time the server detects the client's view may have diverged.
    BotLiveState snapshot = 1;
    BotStateDelta state_delta = 2;
    BotChatEvent chat = 3;
    BotLifecycleEvent lifecycle = 4;
    BotEntityEvent entity_event = 5;
    BotBlockUpdateEvent block_update = 6;
    // Persistent desired state and controller state. This is emitted first and
    // whenever it changes, including while the bot is offline or reconnecting.
    BotStatus status = 7;
    BotInventoryEvent inventory = 8;
    BotDamageEvent damage = 9;
    BotResyncRequired resync_required = 10;
    BotHeartbeat heartbeat = 11;
    BotEnvironmentEvent environment = 12;
    BotPlayerListEvent player_list = 13;
    BotBossBarEvent boss_bar = 14;
    BotSoundEvent sound = 15;
    BotParticleEvent particle = 16;
    BotScoreboardEvent scoreboard = 17;
    BotResourcePackEvent resource_pack = 18;
    BotTitleEvent title = 19;
    BotChunkEvent chunk = 21;
  }
  BotEventEnvelope envelope = 20 [(google.api.field_behavior) = REQUIRED];
}

// Final outcome of an imperative bot action.
enum BotActionStatus {
  BOT_ACTION_STATUS_UNSPECIFIED = 0;
  BOT_ACTION_STATUS_COMPLETED = 1;
  BOT_ACTION_STATUS_CANCELLED = 2;
  BOT_ACTION_STATUS_FAILED = 3;
}

message BotActionResult {
  string action_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  BotActionStatus status = 2 [(google.api.field_behavior) = REQUIRED];
  optional string error = 3;
}

// ============================================================================
// Chat
// ============================================================================

message SendChatRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // The chat message to send. Treated as a literal chat message. To execute a
  // slash command, include the leading "/"; the server will route it
  // appropriately.
  string message = 3 [(google.api.field_behavior) = REQUIRED];
}

message SendChatResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

// ============================================================================
// World queries
// ============================================================================

message GetBlockRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  BlockPosition position = 3 [(google.api.field_behavior) = REQUIRED];
}

message GetBlockResponse {
  // True if the chunk containing the requested position is loaded for this
  // bot. When false, the block field is not populated.
  bool loaded = 1 [(google.api.field_behavior) = REQUIRED];
  // The observed block state. Absent when loaded is false.
  optional BlockState block = 2;
}

message FindBlocksRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Block ids to match (namespaced, e.g. "minecraft:diamond_ore"). A block
  // matches if its id equals any entry in this list.
  repeated string block_ids = 3 [(google.api.field_behavior) = REQUIRED];
  // Maximum search radius in blocks, measured from the bot's current block
  // position. Server clamps to an implementation-defined maximum.
  int32 max_distance = 4 [(google.api.field_behavior) = REQUIRED];
  // Maximum number of matches to return. Results are ordered by ascending
  // distance from the bot.
  int32 max_count = 5 [(google.api.field_behavior) = REQUIRED];
}

message FindBlocksResponse {
  repeated BlockState blocks = 1;
}

message ListNearbyEntitiesRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Maximum radius in blocks from the bot. Server clamps to an
  // implementation-defined maximum.
  float radius = 3 [(google.api.field_behavior) = REQUIRED];
  // Optional filter: if non-empty, only entities whose namespaced type is in
  // this list are returned.
  repeated string entity_types = 4;
  // If true, include players in the result. Defaults to true.
  bool include_players = 5;
}

message ListNearbyEntitiesResponse {
  // Entities ordered by ascending distance from the bot.
  repeated NearbyEntity entities = 1;
}

// ============================================================================
// Imperative actions
// ============================================================================

message DigBlockRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Block to break. Must be within reach of the bot.
  BlockPosition position = 3 [(google.api.field_behavior) = REQUIRED];
  // If true, cancel an in-progress dig on this block instead of starting one.
  bool cancel = 4;
}

message DigBlockResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message PlaceBlockRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // The block the bot is placing against (the block being clicked).
  BlockPosition against = 3 [(google.api.field_behavior) = REQUIRED];
  // Which face of `against` is being clicked. The new block is placed adjacent
  // to that face.
  BlockFace face = 4 [(google.api.field_behavior) = REQUIRED];
  // Which hand holds the item being placed.
  Hand hand = 5 [(google.api.field_behavior) = REQUIRED];
}

message PlaceBlockResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message InteractBlockRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Block to activate. Must be loaded and within interaction reach.
  BlockPosition position = 3 [(google.api.field_behavior) = REQUIRED];
  // Face to click.
  BlockFace face = 4 [(google.api.field_behavior) = REQUIRED];
  Hand hand = 5 [(google.api.field_behavior) = REQUIRED];
  // Temporarily sneaks for this interaction. Useful when activating a block
  // while holding an item whose normal use would take precedence.
  bool sneaking = 6;
}

message InteractBlockResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message UseItemRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Which hand's item to use (e.g. eat food, draw a bow, throw a splash
  // potion).
  Hand hand = 3 [(google.api.field_behavior) = REQUIRED];
}

message UseItemResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message ReleaseItemRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
}

message ReleaseItemResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message AttackEntityRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Network entity id from NearbyEntity.entity_id. Must currently be in the
  // bot's observation set.
  int32 entity_id = 3 [(google.api.field_behavior) = REQUIRED];
  // If true, perform a sprint-boosted critical hit when applicable (swings
  // the main hand and applies knockback).
  bool sprinting = 4;
  // Observation epoch that produced entity_id. When present, the action fails
  // if the bot has reconnected since that observation.
  optional string connection_epoch = 5 [(soulfire.v1.api_field).format = "uuid"];
}

message AttackEntityResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message InteractEntityRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Network entity id from NearbyEntity.entity_id.
  int32 entity_id = 3 [(google.api.field_behavior) = REQUIRED];
  // Which hand to interact with.
  Hand hand = 4 [(google.api.field_behavior) = REQUIRED];
  // True if the bot is sneaking during the interaction (affects e.g. whether
  // a villager trade menu opens).
  bool sneaking = 5;
  // Observation epoch that produced entity_id. When present, the action fails
  // if the bot has reconnected since that observation.
  optional string connection_epoch = 6 [(soulfire.v1.api_field).format = "uuid"];
}

message InteractEntityResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message SwingArmRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  Hand hand = 3 [(google.api.field_behavior) = REQUIRED];
}

message SwingArmResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message RespawnRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
}

message RespawnResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message SleepRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  BlockPosition bed = 3 [(google.api.field_behavior) = REQUIRED];
  Hand hand = 4;
}

message SleepResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message WakeRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
}

message WakeResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message MountEntityRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  int32 entity_id = 3 [(google.api.field_behavior) = REQUIRED];
  Hand hand = 4;
  // Observation epoch that produced entity_id. When present, the action fails
  // if the bot has reconnected since that observation.
  optional string connection_epoch = 5 [(soulfire.v1.api_field).format = "uuid"];
}

message MountEntityResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
  optional EntityReference vehicle = 2;
}

message DismountRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
}

message DismountResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message SetVehicleControlRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  optional bool forward = 3;
  optional bool backward = 4;
  optional bool left = 5;
  optional bool right = 6;
  optional bool jump = 7;
  optional bool sneak = 8;
  optional bool sprint = 9;
  optional float yaw = 10;
  optional float pitch = 11;
}

message SetVehicleControlResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
  optional EntityReference vehicle = 2;
}

message UpdateSignRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  BlockPosition position = 3 [(google.api.field_behavior) = REQUIRED];
  bool front_text = 4;
  // Exactly four plain-text lines. Empty strings clear lines.
  repeated string lines = 5 [(google.api.field_behavior) = REQUIRED];
}

message UpdateSignResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message WriteBookRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Player inventory slot from zero through eight containing a writable book.
  int32 inventory_slot = 3 [(google.api.field_behavior) = REQUIRED];
  repeated string pages = 4 [(google.api.field_behavior) = REQUIRED];
  // Providing a title signs the book.
  optional string title = 5;
}

message WriteBookResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

enum ResourcePackResponse {
  RESOURCE_PACK_RESPONSE_UNSPECIFIED = 0;
  RESOURCE_PACK_RESPONSE_ACCEPTED = 1;
  RESOURCE_PACK_RESPONSE_DOWNLOADED = 2;
  RESOURCE_PACK_RESPONSE_SUCCESSFULLY_LOADED = 3;
  RESOURCE_PACK_RESPONSE_DECLINED = 4;
  RESOURCE_PACK_RESPONSE_FAILED_DOWNLOAD = 5;
  RESOURCE_PACK_RESPONSE_INVALID_URL = 6;
  RESOURCE_PACK_RESPONSE_FAILED_RELOAD = 7;
  RESOURCE_PACK_RESPONSE_DISCARDED = 8;
}

message RespondResourcePackRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string pack_id = 3 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  ResourcePackResponse response = 4 [(google.api.field_behavior) = REQUIRED];
}

message RespondResourcePackResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message SetFlyingRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  bool flying = 3;
}

message SetFlyingResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message StartElytraFlightRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
}

message StartElytraFlightResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message CreativeItemStack {
  string item_id = 1 [(google.api.field_behavior) = REQUIRED];
  int32 count = 2 [(google.api.field_behavior) = REQUIRED];
}

message SetCreativeSlotRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Inventory-menu slot. Values zero through 45 are supported.
  int32 slot = 3 [(google.api.field_behavior) = REQUIRED];
  // Omit the item to clear the slot.
  optional CreativeItemStack item = 4;
}

message SetCreativeSlotResponse {
  BotActionResult result = 1 [(google.api.field_behavior) = REQUIRED];
}

message WaitForChunksRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Chebyshev radius around the bot's current chunk. Zero waits for only the
  // current chunk. The server rejects values above its safety limit.
  uint32 radius_chunks = 3;
  // Maximum wait in milliseconds. Zero uses the server default.
  uint32 timeout_ms = 4;
}

message WaitForChunksResponse {
  int32 center_chunk_x = 1 [(google.api.field_behavior) = REQUIRED];
  int32 center_chunk_z = 2 [(google.api.field_behavior) = REQUIRED];
  uint32 loaded_chunks = 3 [(google.api.field_behavior) = REQUIRED];
  uint32 required_chunks = 4 [(google.api.field_behavior) = REQUIRED];
  string dimension = 5 [(google.api.field_behavior) = REQUIRED];
}

// ============================================================================
// Pathfinding
// ============================================================================

// A pathfinding destination. Exactly one variant must be set.
message PathfindGoal {
  // Reach a specific block within `radius` blocks (euclidean).
  message BlockGoal {
    BlockPosition position = 1 [(google.api.field_behavior) = REQUIRED];
    // Acceptable distance from `position` at which to consider the goal
    // reached. 0 means "stand on the block".
    float radius = 2;
  }
  // Reach a world position within `radius` blocks.
  message NearGoal {
    WorldPosition position = 1 [(google.api.field_behavior) = REQUIRED];
    float radius = 2;
  }
  // Reach the current location of an entity, within `radius` blocks.
  // The path will be updated as the entity moves (follow behavior).
  message EntityGoal {
    int32 entity_id = 1 [(google.api.field_behavior) = REQUIRED];
    float radius = 2;
    optional string connection_epoch = 3 [(soulfire.v1.api_field).format = "uuid"];
  }
  // Reach an XZ column, ignoring Y.
  message XZGoal {
    double x = 1 [(google.api.field_behavior) = REQUIRED];
    double z = 2 [(google.api.field_behavior) = REQUIRED];
    string dimension = 3;
    float radius = 4;
  }
  // Reach a Y level while allowing the planner to choose X and Z.
  message YGoal {
    int32 y = 1 [(google.api.field_behavior) = REQUIRED];
    string dimension = 2;
  }
  // Reach and break one exact block.
  message BreakBlockGoal {
    BlockPosition position = 1 [(google.api.field_behavior) = REQUIRED];
  }
  // Reach and place the currently selected placeable item at one exact block.
  message PlaceBlockGoal {
    BlockPosition position = 1 [(google.api.field_behavior) = REQUIRED];
  }
  // Move until the bot is at least `radius` blocks away from a position.
  message AwayFromPositionGoal {
    WorldPosition position = 1 [(google.api.field_behavior) = REQUIRED];
    float radius = 2 [(google.api.field_behavior) = REQUIRED];
  }
  // Move until the bot is at least `radius` blocks away from an entity.
  message AwayFromEntityGoal {
    int32 entity_id = 1 [(google.api.field_behavior) = REQUIRED];
    float radius = 2 [(google.api.field_behavior) = REQUIRED];
    optional string connection_epoch = 3 [(soulfire.v1.api_field).format = "uuid"];
  }
  // Complete when any nested goal is reached.
  message AnyGoal {
    repeated PathfindGoal goals = 1 [(google.api.field_behavior) = REQUIRED];
  }
  oneof goal {
    BlockGoal block = 1;
    NearGoal near = 2;
    EntityGoal entity = 3;
    XZGoal xz = 4;
    YGoal y = 5;
    BreakBlockGoal break_block = 6;
    PlaceBlockGoal place_block = 7;
    AwayFromPositionGoal away_from_position = 8;
    AwayFromEntityGoal away_from_entity = 9;
    AnyGoal any = 10;
  }
}

message PathfindOptions {
  // If true, the bot may break blocks along the path when necessary.
  bool allow_mining = 1;
  // If true, the bot may place blocks (e.g. bridge, pillar) when necessary.
  bool allow_placing = 2;
  // Maximum wall-clock seconds for planning + traversal. 0 means server default.
  int32 timeout_seconds = 3;
  // Overrides the server's path search timeout. 0 means server default.
  uint32 search_timeout_seconds = 4;
  // Extra planner cost for breaking a block. Unset uses server settings.
  optional double break_block_penalty = 5;
  // Extra planner cost for placing a block. Unset uses server settings.
  optional double place_block_penalty = 6;
  // If true, candidate movements whose feet or head would enter fluid are
  // excluded from the route.
  bool avoid_fluids = 7;
  // Additional block item ids that pathfinding may place after exhausting its
  // built-in disposable building blocks. This is request-scoped and should be
  // used sparingly for recovery paths that would otherwise leave a bot
  // stranded.
  repeated string additional_place_item_ids = 8;
  // Whether ordinary forward path traversal should sprint. Unset preserves the
  // server default of sprinting. Set false for hunger-efficient travel.
  optional bool sprint = 9;
  // Optional inclusive elevation bounds for the bot's feet along the route.
  // These keep a route on the surface or within a mining layer without
  // constraining blocks that actions may reach from the route.
  optional int32 minimum_y = 10;
  optional int32 maximum_y = 11;
}

message GoToRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  PathfindGoal goal = 3 [(google.api.field_behavior) = REQUIRED];
  PathfindOptions options = 4;
}

// Status of a pathfinding attempt.
enum PathfindStatus {
  PATHFIND_STATUS_UNSPECIFIED = 0;
  // Planning has started; no path yet.
  PATHFIND_STATUS_PLANNING = 1;
  // A path was found and traversal has started.
  PATHFIND_STATUS_MOVING = 2;
  // The goal was reached.
  PATHFIND_STATUS_COMPLETED = 3;
  // No path could be found (unreachable) or a timeout / obstacle aborted
  // traversal. See `error` for detail.
  PATHFIND_STATUS_FAILED = 4;
  // Aborted by the client via StopPathfinding.
  PATHFIND_STATUS_CANCELLED = 5;
}

message PathfindProgress {
  PathfindStatus status = 1 [(google.api.field_behavior) = REQUIRED];
  // Remaining straight-line distance to the goal in blocks, if known.
  optional float distance_remaining = 2;
  // Current bot position, populated on MOVING updates.
  optional WorldPosition position = 3;
  // Present when status is FAILED. Human-readable failure reason.
  optional string error = 4;
  string action_id = 5 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
}

message StopPathfindingRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
}

message StopPathfindingResponse {
}

// Exclusive control lease for coordinating multiple SDK clients, scripts, and
// independent SDK controllers and server tasks against the same bot.
message AcquireBotControlRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  // Requested lease lifetime. The server clamps this to its configured range.
  uint32 ttl_seconds = 3;
}

message BotControlLease {
  string token = 1 [(google.api.field_behavior) = REQUIRED];
  google.protobuf.Timestamp expires_at = 2 [(google.api.field_behavior) = REQUIRED];
}

message AcquireBotControlResponse {
  BotControlLease lease = 1 [(google.api.field_behavior) = REQUIRED];
}

message RenewBotControlRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string token = 3 [(google.api.field_behavior) = REQUIRED];
  uint32 ttl_seconds = 4;
}

message RenewBotControlResponse {
  BotControlLease lease = 1 [(google.api.field_behavior) = REQUIRED];
}

message ReleaseBotControlRequest {
  string instance_id = 1 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string bot_id = 2 [(google.api.field_behavior) = REQUIRED, (soulfire.v1.api_field).format = "uuid"];
  string token = 3 [(google.api.field_behavior) = REQUIRED];
}

message ReleaseBotControlResponse {
}

// ============================================================================
// Service
// ============================================================================

// Live bot remote-control service. See file header for scope and design notes.
//
// Permissions:
// - READ_BOT_INFO: required for WatchBotEvents, GetBlock, FindBlocks,
//                  ListNearbyEntities.
// - CONTROL_BOT_ACTIONS: required for chat, interactions, pathfinding, and
//                        exclusive control leases.
//
// Error handling mirrors BotService:
// - NOT_FOUND: instance or bot does not exist.
// - FAILED_PRECONDITION: bot is not online, target not loaded, or the
//                        requested action is not currently valid.
// - OUT_OF_RANGE: target is outside the bot's reach / observation radius.
// - INTERNAL: unexpected server errors.
service BotLiveService {
  // Subscribes to a single multiplexed event stream for one bot. The first
  // message is always the current BotStatus. The stream remains open while the
  // bot is stopped and follows the configured bot across reconnects. Live
  // snapshots and filtered events begin whenever a connection becomes ready.
  rpc WatchBotEvents (WatchBotEventsRequest) returns (stream BotEvent) {
    option (soulfire.v1.api_method) = {
      display_name: "Watch bot events"
      permissions: "READ_BOT_INFO"
      scope: "instance.bot"
      execution: "Opens a live event stream until the client cancels or the account is removed."
      side_effects: "Allocates server-side subscription resources for the bot."
    };
  }

  // Sends a chat message (or slash command) from the bot to the connected
  // Minecraft server. Messages prefixed with "/" are routed as commands.
  rpc SendChat (SendChatRequest) returns (SendChatResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/chat:send"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Send chat"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online."
      execution: "Completes after the message has been submitted on the bot game thread."
      side_effects: "Transmits the message to the Minecraft server."
    };
  }

  // Returns the block at a world position, if the chunk is loaded.
  rpc GetBlock (GetBlockRequest) returns (GetBlockResponse) {
    option (google.api.http) = {
      get: "/v1/instances/{instance_id}/bots/{bot_id}/live/world/block"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Get block"
      permissions: "READ_BOT_INFO"
      scope: "instance.bot"
      preconditions: "Bot must be online."
    };
  }

  // Finds blocks of the given ids within a radius of the bot. Results are
  // sorted by ascending distance.
  rpc FindBlocks (FindBlocksRequest) returns (FindBlocksResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/live/world/blocks:find"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Find blocks"
      permissions: "READ_BOT_INFO"
      scope: "instance.bot"
      preconditions: "Bot must be online."
    };
  }

  // Lists entities within a radius of the bot, optionally filtered by type.
  rpc ListNearbyEntities (ListNearbyEntitiesRequest) returns (ListNearbyEntitiesResponse) {
    option (google.api.http) = {
      get: "/v1/instances/{instance_id}/bots/{bot_id}/live/world/entities"
    };
    option (soulfire.v1.api_method) = {
      display_name: "List nearby entities"
      permissions: "READ_BOT_INFO"
      scope: "instance.bot"
      preconditions: "Bot must be online."
    };
  }

  // Starts (or stops, when `cancel` is set) breaking a block at a specific
  // position. Unlike MouseClick, the target is explicit, with no raytrace from
  // the current look direction is performed.
  rpc DigBlock (DigBlockRequest) returns (DigBlockResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/actions/dig"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Dig block"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online."
      preconditions: "Target block must be within reach and loaded."
      execution: "Completes when the block is broken, cancelled, or the action fails."
      side_effects: "Breaks or cancels breaking the target block."
    };
  }

  // Places the currently held item as a block, against a specific face of a
  // specific block.
  rpc PlaceBlock (PlaceBlockRequest) returns (PlaceBlockResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/actions/place"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Place block"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online."
      preconditions: "Target adjacent position must be within reach and loaded."
      execution: "Immediate"
      side_effects: "Attempts to place the held item as a block."
    };
  }

  // Activates a specific block face without assigning placement semantics to
  // the operation. This covers doors, buttons, beds, redstone controls, and
  // modded interactive blocks.
  rpc InteractBlock (InteractBlockRequest) returns (InteractBlockResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/actions/interact-block"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Interact with block"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online."
      preconditions: "Target block must be loaded and within reach."
      execution: "Immediate"
      side_effects: "Activates the selected block face and may use the held item."
    };
  }

  // Uses the item in the specified hand (eat, drink, throw, draw bow, etc.).
  rpc UseItem (UseItemRequest) returns (UseItemResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/actions/use-item"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Use item"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online."
      execution: "Immediate"
      side_effects: "Triggers the held item's use behaviour."
    };
  }

  // Releases an item that is currently being used, such as firing a drawn bow
  // or stopping eating.
  rpc ReleaseItem (ReleaseItemRequest) returns (ReleaseItemResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/actions/release-item"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Release item"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online."
      execution: "Completes after the release is performed on the bot game thread."
    };
  }

  // Attacks a specific entity by network id.
  rpc AttackEntity (AttackEntityRequest) returns (AttackEntityResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/actions/attack"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Attack entity"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online."
      preconditions: "Target entity must be observable and within reach."
      execution: "Immediate"
      side_effects: "Swings the main hand and damages the target entity."
    };
  }

  // Right-clicks / interacts with a specific entity by network id. Used for
  // trading with villagers, mounting mobs, etc.
  rpc InteractEntity (InteractEntityRequest) returns (InteractEntityResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/actions/interact"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Interact with entity"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online."
      preconditions: "Target entity must be observable and within reach."
      execution: "Immediate"
      side_effects: "Sends an interaction packet for the target entity."
    };
  }

  // Swings the specified arm without a target (visual animation only).
  rpc SwingArm (SwingArmRequest) returns (SwingArmResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/actions/swing"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Swing arm"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online."
      execution: "Immediate"
      side_effects: "Sends an arm swing animation packet."
    };
  }

  // Respawns a dead bot.
  rpc Respawn (RespawnRequest) returns (RespawnResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/actions:respawn"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Respawn bot"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online and dead."
      execution: "Completes after the respawn request is sent."
    };
  }

  // Uses a reachable bed and completes only after the server confirms that the
  // bot entered the sleeping state.
  rpc Sleep (SleepRequest) returns (SleepResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/actions:sleep"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Sleep in bed"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online and the target must be a reachable bed."
      execution: "Completes when the server confirms sleep or the action times out."
      side_effects: "Interacts with the bed and changes the bot sleeping state."
    };
  }

  // Leaves the current bed.
  rpc Wake (WakeRequest) returns (WakeResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/actions:wake"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Wake from bed"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online and sleeping."
      execution: "Completes after the wake request is sent."
      side_effects: "Leaves the current bed."
    };
  }

  rpc MountEntity (MountEntityRequest) returns (MountEntityResponse) {
    option (google.api.http) = { post: "/v1/instances/{instance_id}/bots/{bot_id}:mount" body: "*" };
    option (soulfire.v1.api_method) = {
      display_name: "Mount entity"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      side_effects: "Interacts with a rideable entity and waits for server confirmation."
    };
  }

  rpc Dismount (DismountRequest) returns (DismountResponse) {
    option (google.api.http) = { post: "/v1/instances/{instance_id}/bots/{bot_id}:dismount" body: "*" };
    option (soulfire.v1.api_method) = {
      display_name: "Dismount"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
    };
  }

  rpc SetVehicleControl (SetVehicleControlRequest) returns (SetVehicleControlResponse) {
    option (google.api.http) = { post: "/v1/instances/{instance_id}/bots/{bot_id}/vehicle:control" body: "*" };
    option (soulfire.v1.api_method) = {
      display_name: "Set vehicle control"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      side_effects: "Changes persistent movement input while the bot controls a vehicle."
    };
  }

  rpc UpdateSign (UpdateSignRequest) returns (UpdateSignResponse) {
    option (google.api.http) = { post: "/v1/instances/{instance_id}/bots/{bot_id}/sign:update" body: "*" };
    option (soulfire.v1.api_method) = {
      display_name: "Update sign"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
    };
  }

  rpc WriteBook (WriteBookRequest) returns (WriteBookResponse) {
    option (google.api.http) = { post: "/v1/instances/{instance_id}/bots/{bot_id}/book:write" body: "*" };
    option (soulfire.v1.api_method) = {
      display_name: "Write book"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
    };
  }

  rpc RespondResourcePack (RespondResourcePackRequest) returns (RespondResourcePackResponse) {
    option (google.api.http) = { post: "/v1/instances/{instance_id}/bots/{bot_id}/resource-pack:respond" body: "*" };
    option (soulfire.v1.api_method) = {
      display_name: "Respond to resource pack"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
    };
  }

  rpc SetFlying (SetFlyingRequest) returns (SetFlyingResponse) {
    option (google.api.http) = { post: "/v1/instances/{instance_id}/bots/{bot_id}:setFlying" body: "*" };
    option (soulfire.v1.api_method) = {
      display_name: "Set flying state"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
    };
  }

  // Requests survival-mode elytra flight. Minecraft validates that the bot is
  // falling and has usable elytra equipment.
  rpc StartElytraFlight (StartElytraFlightRequest) returns (StartElytraFlightResponse) {
    option (google.api.http) = { post: "/v1/instances/{instance_id}/bots/{bot_id}/elytra:start" body: "*" };
    option (soulfire.v1.api_method) = {
      display_name: "Start elytra flight"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online and airborne. Minecraft validates elytra equipment."
      execution: "Completes after the fall-flying request is submitted on the bot game thread."
    };
  }

  rpc SetCreativeSlot (SetCreativeSlotRequest) returns (SetCreativeSlotResponse) {
    option (google.api.http) = { post: "/v1/instances/{instance_id}/bots/{bot_id}/creative-slot:set" body: "*" };
    option (soulfire.v1.api_method) = {
      display_name: "Set creative slot"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
    };
  }

  // Waits until every chunk in a square radius around the bot is loaded.
  rpc WaitForChunks (WaitForChunksRequest) returns (WaitForChunksResponse) {
    option (google.api.http) = { post: "/v1/instances/{instance_id}/bots/{bot_id}/chunks:wait" body: "*" };
    option (soulfire.v1.api_method) = {
      display_name: "Wait for chunks"
      permissions: "READ_BOT_INFO"
      scope: "instance.bot"
      preconditions: "Bot must be online and have a loaded client level."
      execution: "Polls the bot world view until the requested chunk square is loaded or the deadline expires."
    };
  }

  // Requests the bot to pathfind to a goal. Returns a stream of progress
  // events terminating with COMPLETED, FAILED, or CANCELLED. A new GoTo for
  // the same bot supersedes any in-progress pathfinding; the previous stream
  // is closed with CANCELLED.
  rpc GoTo (GoToRequest) returns (stream PathfindProgress) {
    option (soulfire.v1.api_method) = {
      display_name: "Pathfind to goal"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      preconditions: "Bot must be online."
      execution: "Starts a pathfinding session that streams progress updates."
      side_effects: "Drives bot movement until the goal is reached, aborted, or cancelled."
    };
  }

  // Cancels any in-progress pathfinding session for this bot.
  rpc StopPathfinding (StopPathfindingRequest) returns (StopPathfindingResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/pathfind:stop"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Stop pathfinding"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      execution: "Immediate"
      side_effects: "Cancels any in-progress pathfinding session for this bot."
    };
  }

  // Acquires exclusive action control for a bot. While the lease is active,
  // action calls must include its token in the X-SoulFire-Control-Token
  // request header.
  rpc AcquireBotControl (AcquireBotControlRequest) returns (AcquireBotControlResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/control:acquire"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Acquire bot control"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
      side_effects: "Prevents other clients from issuing actions until the lease is released or expires."
    };
  }

  rpc RenewBotControl (RenewBotControlRequest) returns (RenewBotControlResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/control:renew"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Renew bot control"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
    };
  }

  rpc ReleaseBotControl (ReleaseBotControlRequest) returns (ReleaseBotControlResponse) {
    option (google.api.http) = {
      post: "/v1/instances/{instance_id}/bots/{bot_id}/control:release"
      body: "*"
    };
    option (soulfire.v1.api_method) = {
      display_name: "Release bot control"
      permissions: "CONTROL_BOT_ACTIONS"
      scope: "instance.bot"
    };
  }
}
