diff --git a/proto/cityio/service/v1/map.proto b/proto/cityio/service/v1/map.proto index 6429a7b..1d62b9b 100644 --- a/proto/cityio/service/v1/map.proto +++ b/proto/cityio/service/v1/map.proto @@ -30,27 +30,36 @@ message GetTileResponse { message GetTerrainRequest {} -// GetTerrainResponse carries the whole map in one call. +// VisibleTerrain is the ground a player can see right now. // -// Each plane is packed one byte per tile in row-major order, so the value for -// (x, y) is at index y * width + x. At the current map size that is about 5 KB -// per plane — small enough that chunking or viewport queries would cost more -// than they save, and it lets the client cache the world for the session. +// Vision is ephemeral: it comes from cities, from armies while they stand +// somewhere, and from structures held against an enemy. Tiles therefore leave +// this set as often as they enter it, so it is always sent whole rather than as +// a reveal-only delta — replacing the set outright is the only way client and +// server cannot disagree about what is still lit. // -// Terrain is generated by the server from `seed` and does not change, so this -// response is stable for the lifetime of a world. +// Tiles are addressed by row-major index (y * width + x). Each plane holds one +// byte per entry of `indices`, in the same order. +message VisibleTerrain { + repeated int32 indices = 1; + bytes terrain = 2; // cityio.entity.v1.TerrainType + bytes relief = 3; // cityio.entity.v1.ReliefType + bytes feature = 4; // cityio.entity.v1.FeatureType + bytes special = 5; // cityio.entity.v1.SpecialType + // rivers holds a 6-bit mask per tile: bit i means a river continues toward + // neighbour i. Both tiles either side of a step carry the reciprocal bit, so + // each renders its own half and rivers occupy no tile of their own. + bytes rivers = 6; +} + +// GetTerrainResponse bootstraps a client with the map's dimensions and whatever +// ground it can currently see. Everything else is unknown, and stays unknown +// until something of the player's is watching it. message GetTerrainResponse { int32 width = 1; int32 height = 2; int64 seed = 3; - bytes terrain = 4; // cityio.entity.v1.TerrainType per tile - bytes relief = 5; // cityio.entity.v1.ReliefType per tile - bytes feature = 6; // cityio.entity.v1.FeatureType per tile - bytes special = 7; // cityio.entity.v1.SpecialType per tile - // rivers holds a 6-bit mask per tile: bit i means a river continues toward - // neighbour i. Both tiles either side of a step carry the reciprocal bit, so - // each renders its own half and rivers occupy no tile of their own. - bytes rivers = 8; + VisibleTerrain visible = 4; } // MapService serves world snapshots read from the persistence layer. diff --git a/proto/cityio/service/v1/user.proto b/proto/cityio/service/v1/user.proto index bedb30d..be609de 100644 --- a/proto/cityio/service/v1/user.proto +++ b/proto/cityio/service/v1/user.proto @@ -5,6 +5,7 @@ package cityio.service.v1; import "cityio/entity/v1/common.proto"; import "cityio/entity/v1/user.proto"; import "cityio/entity/v1/bag.proto"; +import "cityio/service/v1/map.proto"; message RegisterRequest { string email = 1; @@ -42,6 +43,10 @@ message StreamStateRequest {} // StreamStateResponse wraps an EntityBag pushed to a client whenever state changes. message StreamStateResponse { cityio.entity.v1.EntityBag entities = 1; + // The player's currently visible ground, sent whole whenever it changes and + // omitted when it has not. Absent on most ticks, since vision only moves when + // a city, army or watchtower does. + optional VisibleTerrain visible_terrain = 2; } // UserService manages player accounts and the per-user resource stream. diff --git a/src/lib/game/world/index.ts b/src/lib/game/world/index.ts index b187edd..2cb8115 100644 --- a/src/lib/game/world/index.ts +++ b/src/lib/game/world/index.ts @@ -18,6 +18,55 @@ export interface WorldMap { special: Uint8Array; /** 6-bit mask per tile: bit i means a river continues toward neighbor i. */ rivers: Uint8Array; + /** + * 1 where the player can see the ground right now. Vision is ephemeral — it + * lasts only while a city, army or held structure is watching — so tiles + * leave this set as readily as they enter it, and the planes above are only + * meaningful where it is set. + */ + visible: Uint8Array; +} + +export function emptyWorld(w: number, h: number, seed: bigint): WorldMap { + const n = w * h; + return { + w, + h, + seed, + terrain: new Uint8Array(n), + relief: new Uint8Array(n), + feature: new Uint8Array(n), + special: new Uint8Array(n), + rivers: new Uint8Array(n), + visible: new Uint8Array(n) + }; +} + +/** + * Replace what the player can see with the set the server just sent. + * + * The whole set arrives every time rather than a reveal-only delta, because + * losing sight of ground is as common as gaining it. Clearing first is what + * makes a tile actually go dark when the army watching it walks away. + */ +export function applyVisibleTerrain(world: WorldMap, v: { indices: number[]; terrain: Uint8Array; relief: Uint8Array; feature: Uint8Array; special: Uint8Array; rivers: Uint8Array }): void { + world.terrain.fill(0); + world.relief.fill(0); + world.feature.fill(0); + world.special.fill(0); + world.rivers.fill(0); + world.visible.fill(0); + + for (let k = 0; k < v.indices.length; k++) { + const i = v.indices[k]; + if (i < 0 || i >= world.visible.length) continue; + world.terrain[i] = v.terrain[k]; + world.relief[i] = v.relief[k]; + world.feature[i] = v.feature[k]; + world.special[i] = v.special[k]; + world.rivers[i] = v.rivers[k]; + world.visible[i] = 1; + } } export const isWater = (t: TerrainType) => t >= TerrainType.DEEP_OCEAN && t <= TerrainType.LAKE; diff --git a/src/lib/gen/cityio/service/v1/map_pb.ts b/src/lib/gen/cityio/service/v1/map_pb.ts index 8822def..80b2bd4 100644 --- a/src/lib/gen/cityio/service/v1/map_pb.ts +++ b/src/lib/gen/cityio/service/v1/map_pb.ts @@ -14,7 +14,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file cityio/service/v1/map.proto. */ export const file_cityio_service_v1_map: GenFile = /*@__PURE__*/ - fileDesc("ChtjaXR5aW8vc2VydmljZS92MS9tYXAucHJvdG8SEWNpdHlpby5zZXJ2aWNlLnYxIg8KDUdldE1hcFJlcXVlc3QinwEKDkdldE1hcFJlc3BvbnNlEioKCGNpdHlfaWRzGAEgAygLMhguY2l0eWlvLmVudGl0eS52MS5DaXR5SWQSMgoMYnVpbGRpbmdfaWRzGAIgAygLMhwuY2l0eWlvLmVudGl0eS52MS5CdWlsZGluZ0lkEi0KCGVudGl0aWVzGAMgASgLMhsuY2l0eWlvLmVudGl0eS52MS5FbnRpdHlCYWcioAEKBFRpbGUSCQoBeBgBIAEoBRIJCgF5GAIgASgFEi4KB2NpdHlfaWQYAyABKAsyGC5jaXR5aW8uZW50aXR5LnYxLkNpdHlJZEgAiAEBEjYKC2J1aWxkaW5nX2lkGAQgASgLMhwuY2l0eWlvLmVudGl0eS52MS5CdWlsZGluZ0lkSAGIAQFCCgoIX2NpdHlfaWRCDgoMX2J1aWxkaW5nX2lkIj8KDkdldFRpbGVSZXF1ZXN0Ei0KBmNvb3JkcxgBIAEoCzIdLmNpdHlpby5lbnRpdHkudjEuQ29vcmRpbmF0ZXMiOAoPR2V0VGlsZVJlc3BvbnNlEiUKBHRpbGUYASABKAsyFy5jaXR5aW8uc2VydmljZS52MS5UaWxlIhMKEUdldFRlcnJhaW5SZXF1ZXN0IpQBChJHZXRUZXJyYWluUmVzcG9uc2USDQoFd2lkdGgYASABKAUSDgoGaGVpZ2h0GAIgASgFEgwKBHNlZWQYAyABKAMSDwoHdGVycmFpbhgEIAEoDBIOCgZyZWxpZWYYBSABKAwSDwoHZmVhdHVyZRgGIAEoDBIPCgdzcGVjaWFsGAcgASgMEg4KBnJpdmVycxgIIAEoDDKIAgoKTWFwU2VydmljZRJNCgZHZXRNYXASIC5jaXR5aW8uc2VydmljZS52MS5HZXRNYXBSZXF1ZXN0GiEuY2l0eWlvLnNlcnZpY2UudjEuR2V0TWFwUmVzcG9uc2USUAoHR2V0VGlsZRIhLmNpdHlpby5zZXJ2aWNlLnYxLkdldFRpbGVSZXF1ZXN0GiIuY2l0eWlvLnNlcnZpY2UudjEuR2V0VGlsZVJlc3BvbnNlElkKCkdldFRlcnJhaW4SJC5jaXR5aW8uc2VydmljZS52MS5HZXRUZXJyYWluUmVxdWVzdBolLmNpdHlpby5zZXJ2aWNlLnYxLkdldFRlcnJhaW5SZXNwb25zZWIGcHJvdG8z", [file_cityio_entity_v1_common, file_cityio_entity_v1_bag]); + fileDesc("ChtjaXR5aW8vc2VydmljZS92MS9tYXAucHJvdG8SEWNpdHlpby5zZXJ2aWNlLnYxIg8KDUdldE1hcFJlcXVlc3QinwEKDkdldE1hcFJlc3BvbnNlEioKCGNpdHlfaWRzGAEgAygLMhguY2l0eWlvLmVudGl0eS52MS5DaXR5SWQSMgoMYnVpbGRpbmdfaWRzGAIgAygLMhwuY2l0eWlvLmVudGl0eS52MS5CdWlsZGluZ0lkEi0KCGVudGl0aWVzGAMgASgLMhsuY2l0eWlvLmVudGl0eS52MS5FbnRpdHlCYWcioAEKBFRpbGUSCQoBeBgBIAEoBRIJCgF5GAIgASgFEi4KB2NpdHlfaWQYAyABKAsyGC5jaXR5aW8uZW50aXR5LnYxLkNpdHlJZEgAiAEBEjYKC2J1aWxkaW5nX2lkGAQgASgLMhwuY2l0eWlvLmVudGl0eS52MS5CdWlsZGluZ0lkSAGIAQFCCgoIX2NpdHlfaWRCDgoMX2J1aWxkaW5nX2lkIj8KDkdldFRpbGVSZXF1ZXN0Ei0KBmNvb3JkcxgBIAEoCzIdLmNpdHlpby5lbnRpdHkudjEuQ29vcmRpbmF0ZXMiOAoPR2V0VGlsZVJlc3BvbnNlEiUKBHRpbGUYASABKAsyFy5jaXR5aW8uc2VydmljZS52MS5UaWxlIhMKEUdldFRlcnJhaW5SZXF1ZXN0InQKDlZpc2libGVUZXJyYWluEg8KB2luZGljZXMYASADKAUSDwoHdGVycmFpbhgCIAEoDBIOCgZyZWxpZWYYAyABKAwSDwoHZmVhdHVyZRgEIAEoDBIPCgdzcGVjaWFsGAUgASgMEg4KBnJpdmVycxgGIAEoDCJ1ChJHZXRUZXJyYWluUmVzcG9uc2USDQoFd2lkdGgYASABKAUSDgoGaGVpZ2h0GAIgASgFEgwKBHNlZWQYAyABKAMSMgoHdmlzaWJsZRgEIAEoCzIhLmNpdHlpby5zZXJ2aWNlLnYxLlZpc2libGVUZXJyYWluMogCCgpNYXBTZXJ2aWNlEk0KBkdldE1hcBIgLmNpdHlpby5zZXJ2aWNlLnYxLkdldE1hcFJlcXVlc3QaIS5jaXR5aW8uc2VydmljZS52MS5HZXRNYXBSZXNwb25zZRJQCgdHZXRUaWxlEiEuY2l0eWlvLnNlcnZpY2UudjEuR2V0VGlsZVJlcXVlc3QaIi5jaXR5aW8uc2VydmljZS52MS5HZXRUaWxlUmVzcG9uc2USWQoKR2V0VGVycmFpbhIkLmNpdHlpby5zZXJ2aWNlLnYxLkdldFRlcnJhaW5SZXF1ZXN0GiUuY2l0eWlvLnNlcnZpY2UudjEuR2V0VGVycmFpblJlc3BvbnNlYgZwcm90bzM", [file_cityio_entity_v1_common, file_cityio_entity_v1_bag]); /** * @generated from message cityio.service.v1.GetMapRequest @@ -138,59 +138,50 @@ export const GetTerrainRequestSchema: GenMessage = /*@__PURE_ messageDesc(file_cityio_service_v1_map, 5); /** - * GetTerrainResponse carries the whole map in one call. + * VisibleTerrain is the ground a player can see right now. * - * Each plane is packed one byte per tile in row-major order, so the value for - * (x, y) is at index y * width + x. At the current map size that is about 5 KB - * per plane — small enough that chunking or viewport queries would cost more - * than they save, and it lets the client cache the world for the session. + * Vision is ephemeral: it comes from cities, from armies while they stand + * somewhere, and from structures held against an enemy. Tiles therefore leave + * this set as often as they enter it, so it is always sent whole rather than as + * a reveal-only delta — replacing the set outright is the only way client and + * server cannot disagree about what is still lit. * - * Terrain is generated by the server from `seed` and does not change, so this - * response is stable for the lifetime of a world. + * Tiles are addressed by row-major index (y * width + x). Each plane holds one + * byte per entry of `indices`, in the same order. * - * @generated from message cityio.service.v1.GetTerrainResponse + * @generated from message cityio.service.v1.VisibleTerrain */ -export type GetTerrainResponse = Message<"cityio.service.v1.GetTerrainResponse"> & { - /** - * @generated from field: int32 width = 1; - */ - width: number; - - /** - * @generated from field: int32 height = 2; - */ - height: number; - +export type VisibleTerrain = Message<"cityio.service.v1.VisibleTerrain"> & { /** - * @generated from field: int64 seed = 3; + * @generated from field: repeated int32 indices = 1; */ - seed: bigint; + indices: number[]; /** - * cityio.entity.v1.TerrainType per tile + * cityio.entity.v1.TerrainType * - * @generated from field: bytes terrain = 4; + * @generated from field: bytes terrain = 2; */ terrain: Uint8Array; /** - * cityio.entity.v1.ReliefType per tile + * cityio.entity.v1.ReliefType * - * @generated from field: bytes relief = 5; + * @generated from field: bytes relief = 3; */ relief: Uint8Array; /** - * cityio.entity.v1.FeatureType per tile + * cityio.entity.v1.FeatureType * - * @generated from field: bytes feature = 6; + * @generated from field: bytes feature = 4; */ feature: Uint8Array; /** - * cityio.entity.v1.SpecialType per tile + * cityio.entity.v1.SpecialType * - * @generated from field: bytes special = 7; + * @generated from field: bytes special = 5; */ special: Uint8Array; @@ -199,17 +190,53 @@ export type GetTerrainResponse = Message<"cityio.service.v1.GetTerrainResponse"> * neighbour i. Both tiles either side of a step carry the reciprocal bit, so * each renders its own half and rivers occupy no tile of their own. * - * @generated from field: bytes rivers = 8; + * @generated from field: bytes rivers = 6; */ rivers: Uint8Array; }; +/** + * Describes the message cityio.service.v1.VisibleTerrain. + * Use `create(VisibleTerrainSchema)` to create a new message. + */ +export const VisibleTerrainSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_cityio_service_v1_map, 6); + +/** + * GetTerrainResponse bootstraps a client with the map's dimensions and whatever + * ground it can currently see. Everything else is unknown, and stays unknown + * until something of the player's is watching it. + * + * @generated from message cityio.service.v1.GetTerrainResponse + */ +export type GetTerrainResponse = Message<"cityio.service.v1.GetTerrainResponse"> & { + /** + * @generated from field: int32 width = 1; + */ + width: number; + + /** + * @generated from field: int32 height = 2; + */ + height: number; + + /** + * @generated from field: int64 seed = 3; + */ + seed: bigint; + + /** + * @generated from field: cityio.service.v1.VisibleTerrain visible = 4; + */ + visible?: VisibleTerrain | undefined; +}; + /** * Describes the message cityio.service.v1.GetTerrainResponse. * Use `create(GetTerrainResponseSchema)` to create a new message. */ export const GetTerrainResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_cityio_service_v1_map, 6); + messageDesc(file_cityio_service_v1_map, 7); /** * MapService serves world snapshots read from the persistence layer. diff --git a/src/lib/gen/cityio/service/v1/user_pb.ts b/src/lib/gen/cityio/service/v1/user_pb.ts index 41877a9..45c03cd 100644 --- a/src/lib/gen/cityio/service/v1/user_pb.ts +++ b/src/lib/gen/cityio/service/v1/user_pb.ts @@ -10,13 +10,15 @@ import type { User } from "../../entity/v1/user_pb"; import { file_cityio_entity_v1_user } from "../../entity/v1/user_pb"; import type { EntityBag } from "../../entity/v1/bag_pb"; import { file_cityio_entity_v1_bag } from "../../entity/v1/bag_pb"; +import type { VisibleTerrain } from "./map_pb"; +import { file_cityio_service_v1_map } from "./map_pb"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file cityio/service/v1/user.proto. */ export const file_cityio_service_v1_user: GenFile = /*@__PURE__*/ - fileDesc("ChxjaXR5aW8vc2VydmljZS92MS91c2VyLnByb3RvEhFjaXR5aW8uc2VydmljZS52MSJECg9SZWdpc3RlclJlcXVlc3QSDQoFZW1haWwYASABKAkSEAoIdXNlcm5hbWUYAiABKAkSEAoIcGFzc3dvcmQYAyABKAkiTAoQUmVnaXN0ZXJSZXNwb25zZRIpCgd1c2VyX2lkGAEgASgLMhguY2l0eWlvLmVudGl0eS52MS5Vc2VySWQSDQoFdG9rZW4YAiABKAkiNAoMTG9naW5SZXF1ZXN0EhIKCmlkZW50aWZpZXIYASABKAkSEAoIcGFzc3dvcmQYAiABKAkiRAoNTG9naW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCRIkCgR1c2VyGAIgASgLMhYuY2l0eWlvLmVudGl0eS52MS5Vc2VyIjsKDkdldFVzZXJSZXF1ZXN0EikKB3VzZXJfaWQYASABKAsyGC5jaXR5aW8uZW50aXR5LnYxLlVzZXJJZCI3Cg9HZXRVc2VyUmVzcG9uc2USJAoEdXNlchgBIAEoCzIWLmNpdHlpby5lbnRpdHkudjEuVXNlciI+ChFEZWxldGVVc2VyUmVxdWVzdBIpCgd1c2VyX2lkGAEgASgLMhguY2l0eWlvLmVudGl0eS52MS5Vc2VySWQiFAoSRGVsZXRlVXNlclJlc3BvbnNlIhQKElN0cmVhbVN0YXRlUmVxdWVzdCJEChNTdHJlYW1TdGF0ZVJlc3BvbnNlEi0KCGVudGl0aWVzGAEgASgLMhsuY2l0eWlvLmVudGl0eS52MS5FbnRpdHlCYWcyuwMKC1VzZXJTZXJ2aWNlElMKCFJlZ2lzdGVyEiIuY2l0eWlvLnNlcnZpY2UudjEuUmVnaXN0ZXJSZXF1ZXN0GiMuY2l0eWlvLnNlcnZpY2UudjEuUmVnaXN0ZXJSZXNwb25zZRJKCgVMb2dpbhIfLmNpdHlpby5zZXJ2aWNlLnYxLkxvZ2luUmVxdWVzdBogLmNpdHlpby5zZXJ2aWNlLnYxLkxvZ2luUmVzcG9uc2USUAoHR2V0VXNlchIhLmNpdHlpby5zZXJ2aWNlLnYxLkdldFVzZXJSZXF1ZXN0GiIuY2l0eWlvLnNlcnZpY2UudjEuR2V0VXNlclJlc3BvbnNlElkKCkRlbGV0ZVVzZXISJC5jaXR5aW8uc2VydmljZS52MS5EZWxldGVVc2VyUmVxdWVzdBolLmNpdHlpby5zZXJ2aWNlLnYxLkRlbGV0ZVVzZXJSZXNwb25zZRJeCgtTdHJlYW1TdGF0ZRIlLmNpdHlpby5zZXJ2aWNlLnYxLlN0cmVhbVN0YXRlUmVxdWVzdBomLmNpdHlpby5zZXJ2aWNlLnYxLlN0cmVhbVN0YXRlUmVzcG9uc2UwAWIGcHJvdG8z", [file_cityio_entity_v1_common, file_cityio_entity_v1_user, file_cityio_entity_v1_bag]); + fileDesc("ChxjaXR5aW8vc2VydmljZS92MS91c2VyLnByb3RvEhFjaXR5aW8uc2VydmljZS52MSJECg9SZWdpc3RlclJlcXVlc3QSDQoFZW1haWwYASABKAkSEAoIdXNlcm5hbWUYAiABKAkSEAoIcGFzc3dvcmQYAyABKAkiTAoQUmVnaXN0ZXJSZXNwb25zZRIpCgd1c2VyX2lkGAEgASgLMhguY2l0eWlvLmVudGl0eS52MS5Vc2VySWQSDQoFdG9rZW4YAiABKAkiNAoMTG9naW5SZXF1ZXN0EhIKCmlkZW50aWZpZXIYASABKAkSEAoIcGFzc3dvcmQYAiABKAkiRAoNTG9naW5SZXNwb25zZRINCgV0b2tlbhgBIAEoCRIkCgR1c2VyGAIgASgLMhYuY2l0eWlvLmVudGl0eS52MS5Vc2VyIjsKDkdldFVzZXJSZXF1ZXN0EikKB3VzZXJfaWQYASABKAsyGC5jaXR5aW8uZW50aXR5LnYxLlVzZXJJZCI3Cg9HZXRVc2VyUmVzcG9uc2USJAoEdXNlchgBIAEoCzIWLmNpdHlpby5lbnRpdHkudjEuVXNlciI+ChFEZWxldGVVc2VyUmVxdWVzdBIpCgd1c2VyX2lkGAEgASgLMhguY2l0eWlvLmVudGl0eS52MS5Vc2VySWQiFAoSRGVsZXRlVXNlclJlc3BvbnNlIhQKElN0cmVhbVN0YXRlUmVxdWVzdCKZAQoTU3RyZWFtU3RhdGVSZXNwb25zZRItCghlbnRpdGllcxgBIAEoCzIbLmNpdHlpby5lbnRpdHkudjEuRW50aXR5QmFnEj8KD3Zpc2libGVfdGVycmFpbhgCIAEoCzIhLmNpdHlpby5zZXJ2aWNlLnYxLlZpc2libGVUZXJyYWluSACIAQFCEgoQX3Zpc2libGVfdGVycmFpbjK7AwoLVXNlclNlcnZpY2USUwoIUmVnaXN0ZXISIi5jaXR5aW8uc2VydmljZS52MS5SZWdpc3RlclJlcXVlc3QaIy5jaXR5aW8uc2VydmljZS52MS5SZWdpc3RlclJlc3BvbnNlEkoKBUxvZ2luEh8uY2l0eWlvLnNlcnZpY2UudjEuTG9naW5SZXF1ZXN0GiAuY2l0eWlvLnNlcnZpY2UudjEuTG9naW5SZXNwb25zZRJQCgdHZXRVc2VyEiEuY2l0eWlvLnNlcnZpY2UudjEuR2V0VXNlclJlcXVlc3QaIi5jaXR5aW8uc2VydmljZS52MS5HZXRVc2VyUmVzcG9uc2USWQoKRGVsZXRlVXNlchIkLmNpdHlpby5zZXJ2aWNlLnYxLkRlbGV0ZVVzZXJSZXF1ZXN0GiUuY2l0eWlvLnNlcnZpY2UudjEuRGVsZXRlVXNlclJlc3BvbnNlEl4KC1N0cmVhbVN0YXRlEiUuY2l0eWlvLnNlcnZpY2UudjEuU3RyZWFtU3RhdGVSZXF1ZXN0GiYuY2l0eWlvLnNlcnZpY2UudjEuU3RyZWFtU3RhdGVSZXNwb25zZTABYgZwcm90bzM", [file_cityio_entity_v1_common, file_cityio_entity_v1_user, file_cityio_entity_v1_bag, file_cityio_service_v1_map]); /** * @generated from message cityio.service.v1.RegisterRequest @@ -198,6 +200,15 @@ export type StreamStateResponse = Message<"cityio.service.v1.StreamStateResponse * @generated from field: cityio.entity.v1.EntityBag entities = 1; */ entities?: EntityBag | undefined; + + /** + * The player's currently visible ground, sent whole whenever it changes and + * omitted when it has not. Absent on most ticks, since vision only moves when + * a city, army or watchtower does. + * + * @generated from field: optional cityio.service.v1.VisibleTerrain visible_terrain = 2; + */ + visibleTerrain?: VisibleTerrain | undefined; }; /** diff --git a/src/routes/game/+layout.svelte b/src/routes/game/+layout.svelte index f5b1c93..5851865 100644 --- a/src/routes/game/+layout.svelte +++ b/src/routes/game/+layout.svelte @@ -1,6 +1,7 @@