]> git.localhorst.tv Git - blank.git/commitdiff
sync entities with clients
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Tue, 8 Sep 2015 09:55:00 +0000 (11:55 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Tue, 8 Sep 2015 11:33:23 +0000 (13:33 +0200)
doc/protocol
src/app/ServerState.cpp
src/app/ServerState.hpp
src/client/MasterState.hpp
src/client/client.cpp
src/net/Client.hpp
src/net/ClientConnection.hpp
src/net/ConnectionHandler.hpp
src/net/Packet.hpp
src/net/net.cpp
src/world/World.hpp

index 43c14dae4933a0ad9de335dd7cf2bebd0f52d58b..a02ff2afa4bffa99b29dc32b7fe448e3d115af13 100644 (file)
@@ -85,3 +85,46 @@ Payload:
         0 chunk coords of the player, 3x 32bit signed int
        12 pos/vel/rot/ang of the player, 13x 32bit float
 Length: 64
+
+
+Spawn Entity
+------------
+
+Sent by the server to notify the client of an entity entering spawn range.
+
+Code: 5
+Payload:
+        0 entity ID, 32bit unsigned int
+        4 chunk coords of the entity, 3x 32bit signed int
+       16 pos/vel/rot/ang of the entity, 13x 32bit float
+       68 bounding box of the entity, 6x 32bit float
+       92 flags, 32bit bitfield with boolean values
+          1: world collision
+       96 entity name, max 32 byte UTF-8 string
+Length: 128
+
+
+Despawn Entity
+--------------
+
+Sent by the server to notify the client of an entity leaving spawn range.
+
+Code: 6
+Payload:
+       0 entity ID, 32bit unsigned int
+Length: 4
+
+
+Entity Update
+-------------
+
+Sent by the server to notify the client of updated entity properties.
+Contained entities must be ordered by ascending entity ID.
+
+Code: 7
+Payload:
+        0 number of entities, 32bit int, 1-7
+        4 chunk coords of the entity, 3x 32bit signed int
+       16 pos/vel/rot/ang of the entity, 13x 32bit float
+       68 next entity...
+Length: 4 + multiple of 64, max 452
index d26f552551fd258e13b5257469573a95c175d761..88e817d81f1a70d61f5011cead6f7e741619e034 100644 (file)
@@ -18,10 +18,13 @@ ServerState::ServerState(
 : env(env)
 , block_types()
 , world(block_types, wc, ws)
-, server(sc, world) {
+, server(sc, world)
+, push_timer(16) {
        TextureIndex tex_index;
        env.loader.LoadBlockTypes("default", block_types, tex_index);
 
+       push_timer.Start();
+
        std::cout << "listening on UDP port " << sc.port << std::endl;
 }
 
@@ -34,8 +37,12 @@ void ServerState::Handle(const SDL_Event &event) {
 
 
 void ServerState::Update(int dt) {
+       push_timer.Update(dt);
+
        server.Handle();
-       server.Update(dt);
+       if (push_timer.Hit()) {
+               server.Update(dt);
+       }
 }
 
 
index fc9ac1983d441e032846d6d53406920b98e2fb05..5a3c0dcacaac7c1b9feb5ac52c3e89bdfb0b0434 100644 (file)
@@ -1,6 +1,7 @@
 #ifndef BLANK_APP_SERVERSTATE_HPP_
 #define BLANK_APP_SERVERSTATE_HPP_
 
+#include "IntervalTimer.hpp"
 #include "State.hpp"
 #include "../net/Server.hpp"
 #include "../world/BlockTypeRegistry.hpp"
@@ -31,6 +32,7 @@ private:
        BlockTypeRegistry block_types;
        World world;
        Server server;
+       IntervalTimer push_timer;
 
 };
 
index 6bdb671836a9193a54068b67132a1284dad0e4c4..8eeadf051811596a8f3db3b47306b6b537925f62 100644 (file)
@@ -51,6 +51,9 @@ public:
 
        void On(const Packet::Join &) override;
        void On(const Packet::Part &) override;
+       void On(const Packet::SpawnEntity &) override;
+       void On(const Packet::DespawnEntity &) override;
+       void On(const Packet::EntityUpdate &) override;
 
 private:
        Environment &env;
index 9c87549a3e27094c1ad8ffc8cc47b3143c550b53..829cc6b1eae49697439bda9a13e697aab34f4a6e 100644 (file)
@@ -7,6 +7,9 @@
 #include "../app/TextureIndex.hpp"
 
 #include <iostream>
+#include <glm/gtx/io.hpp>
+
+using namespace std;
 
 
 namespace blank {
@@ -134,6 +137,9 @@ MasterState::MasterState(
 }
 
 void MasterState::Quit() {
+       if (!client.GetConnection().Closed()) {
+               client.SendPart();
+       }
        env.state.PopUntil(this);
 }
 
@@ -160,7 +166,7 @@ void MasterState::Render(Viewport &) {
 }
 
 
-void MasterState::OnPacketLost(std::uint16_t id) {
+void MasterState::OnPacketLost(uint16_t id) {
        if (id == login_packet) {
                login_packet = client.SendLogin(intf_conf.player_name);
        }
@@ -168,8 +174,9 @@ void MasterState::OnPacketLost(std::uint16_t id) {
 
 void MasterState::OnTimeout() {
        if (client.GetConnection().Closed()) {
-               Quit();
                // TODO: push disconnected message
+               cout << "connection timed out" << endl;
+               Quit();
        }
 }
 
@@ -178,10 +185,10 @@ void MasterState::On(const Packet::Join &pack) {
 
        if (state) {
                // changing worlds
-               std::cout << "server changing worlds" << std::endl;
+               cout << "server changing worlds to \"" << world_conf.name << '"' << endl;
        } else {
                // joining game
-               std::cout << "joined game" << std::endl;
+               cout << "joined game \"" << world_conf.name << '"' << endl;
        }
 
        uint32_t player_id;
@@ -197,13 +204,78 @@ void MasterState::On(const Packet::Join &pack) {
 void MasterState::On(const Packet::Part &pack) {
        if (state) {
                // kicked
-               std::cout << "kicked by server" << std::endl;
+               cout << "kicked by server" << endl;
        } else {
                // join refused
-               std::cout << "login refused by server" << std::endl;
+               cout << "login refused by server" << endl;
        }
        Quit();
 }
 
+void MasterState::On(const Packet::SpawnEntity &pack) {
+       if (!state) {
+               cout << "got entity spawn before world was created" << endl;
+               Quit();
+               return;
+       }
+       uint32_t entity_id;
+       pack.ReadEntityID(entity_id);
+       Entity *entity = state->GetWorld().AddEntity(entity_id);
+       if (!entity) {
+               cout << "entity ID inconsistency" << endl;
+               Quit();
+               return;
+       }
+       pack.ReadEntity(*entity);
+       cout << "spawned entity " << entity->Name() << " at " << entity->AbsolutePosition() << endl;
+}
+
+void MasterState::On(const Packet::DespawnEntity &pack) {
+       if (!state) {
+               cout << "got entity despawn before world was created" << endl;
+               Quit();
+               return;
+       }
+       uint32_t entity_id;
+       pack.ReadEntityID(entity_id);
+       for (Entity &entity : state->GetWorld().Entities()) {
+               if (entity.ID() == entity_id) {
+                       entity.Kill();
+                       cout << "despawned entity " << entity.Name() << " at " << entity.AbsolutePosition() << endl;
+                       return;
+               }
+       }
+}
+
+void MasterState::On(const Packet::EntityUpdate &pack) {
+       if (!state) {
+               cout << "got entity update before world was created" << endl;
+               Quit();
+               return;
+       }
+
+       auto world_iter = state->GetWorld().Entities().begin();
+       auto world_end = state->GetWorld().Entities().end();
+
+       uint32_t count = 0;
+       pack.ReadEntityCount(count);
+
+       for (uint32_t i = 0; i < count; ++i) {
+               uint32_t entity_id = 0;
+               pack.ReadEntityID(entity_id, i);
+
+               while (world_iter != world_end && world_iter->ID() < entity_id) {
+                       ++world_iter;
+               }
+               if (world_iter == world_end) {
+                       // nothing can be done from here
+                       return;
+               }
+               if (world_iter->ID() == entity_id) {
+                       pack.ReadEntity(*world_iter, i);
+               }
+       }
+}
+
 }
 }
index 5c0a9c2b378344f8e4c7db1700de664093bd0b69..2848aedb342a37ebcac5ebd8c7052089c0696397 100644 (file)
@@ -32,6 +32,7 @@ public:
 
        std::uint16_t SendPing();
        std::uint16_t SendLogin(const std::string &);
+       std::uint16_t SendPart();
        std::uint16_t SendPlayerUpdate(const Entity &);
 
 private:
index 8c5306c07945d4f80d4af15c44400d160508126e..fb6420acfed074959df27449741d3cb80cb5526c 100644 (file)
@@ -4,6 +4,7 @@
 #include "Connection.hpp"
 #include "ConnectionHandler.hpp"
 
+#include <list>
 #include <SDL_net.h>
 
 
@@ -30,15 +31,42 @@ public:
        void DetachPlayer();
        bool HasPlayer() const noexcept { return player; }
        Entity &Player() noexcept { return *player; }
+       const Entity &Player() const noexcept { return *player; }
+
+private:
+       struct SpawnStatus {
+               // the entity in question
+               Entity *const entity = nullptr;
+               // sequence number of the spawn packet or -1 after it's been ack'd
+               std::int32_t spawn_pack = -1;
+               // sequence number of the despawn packet or -1 if no despawn has been sent
+               std::int32_t despawn_pack = -1;
+
+               explicit SpawnStatus(Entity &);
+               ~SpawnStatus();
+       };
+
+private:
+       void OnPacketReceived(std::uint16_t) override;
+       void OnPacketLost(std::uint16_t) override;
 
        void On(const Packet::Login &) override;
        void On(const Packet::Part &) override;
        void On(const Packet::PlayerUpdate &) override;
 
+       bool CanSpawn(const Entity &) const noexcept;
+       bool CanDespawn(const Entity &) const noexcept;
+
+       void SendSpawn(SpawnStatus &);
+       void SendDespawn(SpawnStatus &);
+       void SendUpdate(SpawnStatus &);
+
 private:
        Server &server;
        Connection conn;
        Entity *player;
+       std::list<SpawnStatus> spawns;
+       unsigned int confirm_wait;
 
 };
 
index ab9761af3d30391abca9ae640740d63918ef1ff5..56538c6178505d0ee35c764bcfac4b684e1723b4 100644 (file)
@@ -26,6 +26,9 @@ private:
        virtual void On(const Packet::Join &) { }
        virtual void On(const Packet::Part &) { }
        virtual void On(const Packet::PlayerUpdate &) { }
+       virtual void On(const Packet::SpawnEntity &) { }
+       virtual void On(const Packet::DespawnEntity &) { }
+       virtual void On(const Packet::EntityUpdate &) { }
 
 };
 
index 4ff45df53b6e5a136310b57991087b3249d35549..bb6140d5952cef743c69fe4d2ff3365123491d7b 100644 (file)
@@ -99,6 +99,40 @@ struct Packet {
                void ReadPlayer(Entity &) const noexcept;
        };
 
+       struct SpawnEntity : public Payload {
+               static constexpr std::uint8_t TYPE = 5;
+               static constexpr std::size_t MAX_LEN = 128;
+
+               void WriteEntity(const Entity &) noexcept;
+               void ReadEntityID(std::uint32_t &) const noexcept;
+               void ReadEntity(Entity &) const noexcept;
+       };
+
+       struct DespawnEntity : public Payload {
+               static constexpr std::uint8_t TYPE = 6;
+               static constexpr std::size_t MAX_LEN = 4;
+
+               void WriteEntityID(std::uint32_t) noexcept;
+               void ReadEntityID(std::uint32_t &) const noexcept;
+       };
+
+       struct EntityUpdate : public Payload {
+               static constexpr std::uint8_t TYPE = 7;
+               static constexpr std::size_t MAX_LEN = 452;
+
+               static constexpr std::uint32_t MAX_ENTITIES = 7;
+               static constexpr std::size_t GetSize(std::uint32_t num) noexcept {
+                       return 4 + (num * 64);
+               }
+
+               void WriteEntityCount(std::uint32_t) noexcept;
+               void ReadEntityCount(std::uint32_t &) const noexcept;
+
+               void WriteEntity(const Entity &, std::uint32_t) noexcept;
+               void ReadEntityID(std::uint32_t &, std::uint32_t) const noexcept;
+               void ReadEntity(Entity &, std::uint32_t) const noexcept;
+       };
+
 
        template<class PayloadType>
        PayloadType As() {
index 18761572bd13364cb10cddd6a2ac410c5d6b4b2d..d56e4093545526e980bde0ee95bf6e916025effc 100644 (file)
@@ -23,6 +23,9 @@ constexpr size_t Packet::Login::MAX_LEN;
 constexpr size_t Packet::Join::MAX_LEN;
 constexpr size_t Packet::Part::MAX_LEN;
 constexpr size_t Packet::PlayerUpdate::MAX_LEN;
+constexpr size_t Packet::SpawnEntity::MAX_LEN;
+constexpr size_t Packet::DespawnEntity::MAX_LEN;
+constexpr size_t Packet::EntityUpdate::MAX_LEN;
 
 namespace {
 
@@ -109,11 +112,18 @@ uint16_t Client::SendPlayerUpdate(const Entity &player) {
        return conn.Send(client_pack, client_sock);
 }
 
+uint16_t Client::SendPart() {
+       Packet::Make<Packet::Part>(client_pack);
+       return conn.Send(client_pack, client_sock);
+}
+
 
 ClientConnection::ClientConnection(Server &server, const IPaddress &addr)
 : server(server)
 , conn(addr)
-, player(nullptr) {
+, player(nullptr)
+, spawns()
+, confirm_wait(0) {
        conn.SetHandler(this);
 }
 
@@ -124,25 +134,165 @@ ClientConnection::~ClientConnection() {
 void ClientConnection::Update(int dt) {
        conn.Update(dt);
        if (Disconnected()) {
-               cout << "disconnect from " << conn.Address() << endl;
-       } else if (conn.ShouldPing()) {
+               return;
+       }
+       if (HasPlayer()) {
+               // sync entities
+               auto global_iter = server.GetWorld().Entities().begin();
+               auto global_end = server.GetWorld().Entities().end();
+               auto local_iter = spawns.begin();
+               auto local_end = spawns.end();
+
+               while (global_iter != global_end && local_iter != local_end) {
+                       if (global_iter->ID() == local_iter->entity->ID()) {
+                               // they're the same
+                               if (CanDespawn(*global_iter)) {
+                                       SendDespawn(*local_iter);
+                               } else {
+                                       // update
+                                       SendUpdate(*local_iter);
+                               }
+                               ++global_iter;
+                               ++local_iter;
+                       } else if (global_iter->ID() < local_iter->entity->ID()) {
+                               // global entity was inserted
+                               if (CanSpawn(*global_iter)) {
+                                       auto spawned = spawns.emplace(local_iter, *global_iter);
+                                       SendSpawn(*spawned);
+                               }
+                               ++global_iter;
+                       } else {
+                               // global entity was removed
+                               SendDespawn(*local_iter);
+                               ++local_iter;
+                       }
+               }
+
+               // leftover spawns
+               while (global_iter != global_end) {
+                       if (CanSpawn(*global_iter)) {
+                               spawns.emplace_back(*global_iter);
+                               SendSpawn(spawns.back());
+                       }
+                       ++global_iter;
+               }
+
+               // leftover despawns
+               while (local_iter != local_end) {
+                       SendDespawn(*local_iter);
+                       ++local_iter;
+               }
+       }
+       if (conn.ShouldPing()) {
                conn.SendPing(server.GetPacket(), server.GetSocket());
        }
 }
 
+ClientConnection::SpawnStatus::SpawnStatus(Entity &e)
+: entity(&e)
+, spawn_pack(-1)
+, despawn_pack(-1) {
+       entity->Ref();
+}
+
+ClientConnection::SpawnStatus::~SpawnStatus() {
+       entity->UnRef();
+}
+
+bool ClientConnection::CanSpawn(const Entity &e) const noexcept {
+       return
+               &e != player &&
+               !e.Dead() &&
+               manhattan_radius(e.ChunkCoords() - Player().ChunkCoords()) < 7;
+}
+
+bool ClientConnection::CanDespawn(const Entity &e) const noexcept {
+       return
+               e.Dead() ||
+               manhattan_radius(e.ChunkCoords() - Player().ChunkCoords()) > 7;
+}
+
+void ClientConnection::SendSpawn(SpawnStatus &status) {
+       // don't double spawn
+       if (status.spawn_pack != -1) return;
+
+       auto pack = Packet::Make<Packet::SpawnEntity>(server.GetPacket());
+       pack.WriteEntity(*status.entity);
+       status.spawn_pack = conn.Send(server.GetPacket(), server.GetSocket());
+       ++confirm_wait;
+}
+
+void ClientConnection::SendDespawn(SpawnStatus &status) {
+       // don't double despawn
+       if (status.despawn_pack != -1) return;
+
+       auto pack = Packet::Make<Packet::DespawnEntity>(server.GetPacket());
+       pack.WriteEntityID(status.entity->ID());
+       status.despawn_pack = conn.Send(server.GetPacket(), server.GetSocket());
+       ++confirm_wait;
+}
+
+void ClientConnection::SendUpdate(SpawnStatus &status) {
+       // don't send updates while spawn not ack'd or despawn sent
+       if (status.spawn_pack != -1 || status.despawn_pack != -1) return;
+
+       // TODO: pack entity updates
+       auto pack = Packet::Make<Packet::EntityUpdate>(server.GetPacket());
+       pack.WriteEntityCount(1);
+       pack.WriteEntity(*status.entity, 0);
+       server.GetPacket().len = Packet::EntityUpdate::GetSize(1);
+       conn.Send(server.GetPacket(), server.GetSocket());
+}
+
 void ClientConnection::AttachPlayer(Entity &new_player) {
        DetachPlayer();
        player = &new_player;
        player->Ref();
+       cout << "player \"" << player->Name() << "\" joined" << endl;
 }
 
 void ClientConnection::DetachPlayer() {
        if (!player) return;
        player->Kill();
        player->UnRef();
+       cout << "player \"" << player->Name() << "\" left" << endl;
        player = nullptr;
 }
 
+void ClientConnection::OnPacketReceived(uint16_t seq) {
+       if (!confirm_wait) return;
+       for (auto iter = spawns.begin(), end = spawns.end(); iter != end; ++iter) {
+               if (seq == iter->spawn_pack) {
+                       iter->spawn_pack = -1;
+                       --confirm_wait;
+                       return;
+               }
+               if (seq == iter->despawn_pack) {
+                       spawns.erase(iter);
+                       --confirm_wait;
+                       return;
+               }
+       }
+}
+
+void ClientConnection::OnPacketLost(uint16_t seq) {
+       if (!confirm_wait) return;
+       for (SpawnStatus &status : spawns) {
+               if (seq == status.spawn_pack) {
+                       status.spawn_pack = -1;
+                       --confirm_wait;
+                       SendSpawn(status);
+                       return;
+               }
+               if (seq == status.despawn_pack) {
+                       status.despawn_pack = -1;
+                       --confirm_wait;
+                       SendDespawn(status);
+                       return;
+               }
+       }
+}
+
 void ClientConnection::On(const Packet::Login &pack) {
        string name;
        pack.ReadPlayerName(name);
@@ -319,6 +469,12 @@ const char *Packet::Type2String(uint8_t t) noexcept {
                        return "Part";
                case PlayerUpdate::TYPE:
                        return "PlayerUpdate";
+               case SpawnEntity::TYPE:
+                       return "SpawnEntity";
+               case DespawnEntity::TYPE:
+                       return "DespawnEntity";
+               case EntityUpdate::TYPE:
+                       return "EntityUpdate";
                default:
                        return "Unknown";
        }
@@ -438,6 +594,106 @@ void Packet::PlayerUpdate::ReadPlayer(Entity &player) const noexcept {
        player.AngularVelocity(ang);
 }
 
+void Packet::SpawnEntity::WriteEntity(const Entity &e) noexcept {
+       Write(e.ID(), 0);
+       Write(e.ChunkCoords(), 4);
+       Write(e.Position(), 16);
+       Write(e.Velocity(), 28);
+       Write(e.Orientation(), 40);
+       Write(e.AngularVelocity(), 56);
+       Write(e.Bounds(), 68);
+       uint32_t flags = 0;
+       if (e.WorldCollidable()) {
+               flags |= 1;
+       }
+       Write(flags, 92);
+       WriteString(e.Name(), 96, 32);
+}
+
+void Packet::SpawnEntity::ReadEntityID(uint32_t &id) const noexcept {
+       Read(id, 0);
+}
+
+void Packet::SpawnEntity::ReadEntity(Entity &e) const noexcept {
+       glm::ivec3 chunk_coords(0);
+       glm::vec3 pos;
+       glm::vec3 vel;
+       glm::quat rot;
+       glm::vec3 ang;
+       AABB bounds;
+       uint32_t flags = 0;
+       string name;
+
+       Read(chunk_coords, 4);
+       Read(pos, 16);
+       Read(vel, 28);
+       Read(rot, 40);
+       Read(ang, 56);
+       Read(bounds, 68);
+       Read(flags, 92);
+       ReadString(name, 96, 32);
+
+       e.Position(chunk_coords, pos);
+       e.Velocity(vel);
+       e.Orientation(rot);
+       e.AngularVelocity(ang);
+       e.Bounds(bounds);
+       e.WorldCollidable(flags & 1);
+       e.Name(name);
+}
+
+void Packet::DespawnEntity::WriteEntityID(uint32_t id) noexcept {
+       Write(id, 0);
+}
+
+void Packet::DespawnEntity::ReadEntityID(uint32_t &id) const noexcept {
+       Read(id, 0);
+}
+
+void Packet::EntityUpdate::WriteEntityCount(uint32_t count) noexcept {
+       Write(count, 0);
+}
+
+void Packet::EntityUpdate::ReadEntityCount(uint32_t &count) const noexcept {
+       Read(count, 0);
+}
+
+void Packet::EntityUpdate::WriteEntity(const Entity &entity, uint32_t num) noexcept {
+       uint32_t off = 4 + (num * 64);
+
+       Write(entity.ID(), off);
+       Write(entity.ChunkCoords(), off + 4);
+       Write(entity.Position(), off + 16);
+       Write(entity.Velocity(), off + 28);
+       Write(entity.Orientation(), off + 40);
+       Write(entity.AngularVelocity(), off + 56);
+}
+
+void Packet::EntityUpdate::ReadEntityID(uint32_t &id, uint32_t num) const noexcept {
+       Read(id, 4 + (num * 64));
+}
+
+void Packet::EntityUpdate::ReadEntity(Entity &entity, uint32_t num) const noexcept {
+       uint32_t off = 4 + (num * 64);
+
+       glm::ivec3 chunk_coords(0);
+       glm::vec3 pos;
+       glm::vec3 vel;
+       glm::quat rot;
+       glm::vec3 ang;
+
+       Read(chunk_coords, off + 4);
+       Read(pos, off + 16);
+       Read(vel, off + 28);
+       Read(rot, off + 40);
+       Read(ang, off + 56);
+
+       entity.Position(chunk_coords, pos);
+       entity.Velocity(vel);
+       entity.Orientation(rot);
+       entity.AngularVelocity(ang);
+}
+
 
 void ConnectionHandler::Handle(const UDPpacket &udp_pack) {
        const Packet &pack = *reinterpret_cast<const Packet *>(udp_pack.data);
@@ -457,6 +713,15 @@ void ConnectionHandler::Handle(const UDPpacket &udp_pack) {
                case Packet::PlayerUpdate::TYPE:
                        On(Packet::As<Packet::PlayerUpdate>(udp_pack));
                        break;
+               case Packet::SpawnEntity::TYPE:
+                       On(Packet::As<Packet::SpawnEntity>(udp_pack));
+                       break;
+               case Packet::DespawnEntity::TYPE:
+                       On(Packet::As<Packet::DespawnEntity>(udp_pack));
+                       break;
+               case Packet::EntityUpdate::TYPE:
+                       On(Packet::As<Packet::EntityUpdate>(udp_pack));
+                       break;
                default:
                        // drop unknown or unhandled packets
                        break;
index 9739e21f18415eff4df70010fe9b588d82ba332b..c605d7779f61693c763dbe044345c4e09275b889 100644 (file)
@@ -81,6 +81,7 @@ public:
        Entity *AddEntity(std::uint32_t id);
 
        const std::vector<Entity *> &Players() const noexcept { return players; }
+       std::list<Entity> &Entities() noexcept { return entities; }
        const std::list<Entity> &Entities() const noexcept { return entities; }
 
        void Update(int dt);