, world(block_types, wc, save)
 , chunk_renderer(world, wc.load.load_dist)
 , spawner(world, wc.gen.seed)
-, interface(ic, env, world)
+, interface(ic, env, world, *world.AddPlayer(ic.player_name))
 , preload(env, world.Loader(), chunk_renderer)
 , unload(env, world.Loader()) {
        TextureIndex tex_index;
 
 : public State {
 
 public:
-       explicit InteractiveState(MasterState &);
+       explicit InteractiveState(MasterState &, std::uint32_t player_id);
 
        World &GetWorld() noexcept { return world; }
        Interface &GetInterface() noexcept { return interface; }
 
 }
 
 
-InteractiveState::InteractiveState(MasterState &master)
+// TODO: this clutter is a giant mess
+InteractiveState::InteractiveState(MasterState &master, uint32_t player_id)
 : master(master)
 , block_types()
 , save(master.GetEnv().config.GetWorldPath(master.GetWorldConf().name, master.GetClientConf().host))
 , world(block_types, master.GetWorldConf(), save)
 , chunk_renderer(world, master.GetWorldConf().load.load_dist)
-, interface(master.GetInterfaceConf(), master.GetEnv(), world) {
+, interface(
+       master.GetInterfaceConf(),
+       master.GetEnv(),
+       world,
+       *world.AddPlayer(master.GetInterfaceConf().player_name, player_id)
+) {
        TextureIndex tex_index;
        master.GetEnv().loader.LoadBlockTypes("default", block_types, tex_index);
        chunk_renderer.LoadTextures(master.GetEnv().loader, tex_index);
                // joining game
                std::cout << "joined game" << std::endl;
        }
-       state.reset(new InteractiveState(*this));
+
+       uint32_t player_id;
+       pack.ReadPlayerID(player_id);
+       state.reset(new InteractiveState(*this, player_id));
 
        pack.ReadPlayer(state->GetInterface().Player());
 
 
                static constexpr std::size_t MAX_LEN = 100;
 
                void WritePlayer(const Entity &) noexcept;
+               void ReadPlayerID(std::uint32_t &) const noexcept;
                void ReadPlayer(Entity &) const noexcept;
                void WriteWorldName(const std::string &) noexcept;
                void ReadWorldName(std::string &) const noexcept;
 
 }
 
 void Packet::Join::WritePlayer(const Entity &player) noexcept {
-       // TODO: generate entity IDs
-       Write(uint32_t(1), 0);
+       Write(player.ID(), 0);
        Write(player.ChunkCoords(), 4);
        Write(player.Position(), 16);
        Write(player.Velocity(), 28);
        Write(player.AngularVelocity(), 56);
 }
 
+void Packet::Join::ReadPlayerID(uint32_t &id) const noexcept {
+       Read(id, 0);
+}
+
 void Packet::Join::ReadPlayer(Entity &player) const noexcept {
-       uint32_t id = 0;
        glm::ivec3 chunk_coords(0);
        glm::vec3 pos;
        glm::vec3 vel;
        glm::quat rot;
        glm::vec3 ang;
 
-       Read(id, 0);
        Read(chunk_coords, 4);
        Read(pos, 16);
        Read(vel, 28);
 
 
 namespace blank {
 
+class Entity;
 class Environment;
 class Viewport;
 class World;
                bool visual_disabled = false;
        };
 
-       Interface(const Config &, Environment &, World &);
+       Interface(const Config &, Environment &, World &, Entity &);
 
        Entity &Player() noexcept { return ctrl.Controlled(); }
        const Entity &Player() const noexcept { return ctrl.Controlled(); }
 
 Interface::Interface(
        const Config &config,
        Environment &env,
-       World &world)
+       World &world,
+       Entity &player)
 : env(env)
 , world(world)
 // let's assume this succeeds and hope for the best for now
-, ctrl(*world.AddPlayer(config.player_name))
+, ctrl(player)
 , hud(world.BlockTypes(), env.assets.small_ui_font)
 , aim{{ 0, 0, 0 }, { 0, 0, -1 }}
 , aim_world()
 
 
 Entity::Entity() noexcept
 : model()
+, id(-1)
 , name("anonymous")
 , bounds()
 , velocity(0, 0, 0)
 
 #include "../model/CompositeInstance.hpp"
 #include "../model/geometry.hpp"
 
+#include <cstdint>
 #include <string>
 #include <glm/glm.hpp>
 #include <glm/gtc/quaternion.hpp>
        CompositeInstance &GetModel() noexcept { return model; }
        const CompositeInstance &GetModel() const noexcept { return model; }
 
+       std::uint32_t ID() const noexcept { return id; }
+       void ID(std::uint32_t i) noexcept { id = i; }
+
        const std::string &Name() const noexcept { return name; }
        void Name(const std::string &n) { name = n; }
 
 private:
        CompositeInstance model;
 
+       std::uint32_t id;
        std::string name;
 
        AABB bounds;
 
        return &player;
 }
 
+Entity *World::AddPlayer(const std::string &name, std::uint32_t id) {
+       for (Entity *e : players) {
+               if (e->Name() == name) {
+                       return nullptr;
+               }
+       }
+       Entity *player = AddEntity(id);
+       if (!player) {
+               return nullptr;
+       }
+       player->Name(name);
+       // TODO: load from save file here
+       player->Bounds({ { -0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f } });
+       player->WorldCollidable(true);
+       player->Position(config.spawn);
+       players.push_back(player);
+       chunks.QueueSurrounding(player->ChunkCoords());
+       return player;
+}
+
+Entity &World::AddEntity() {
+       if (entities.empty()) {
+               entities.emplace_back();
+               entities.back().ID(1);
+               return entities.back();
+       }
+       if (entities.back().ID() < std::numeric_limits<std::uint32_t>::max()) {
+               std::uint32_t id = entities.back().ID() + 1;
+               entities.emplace_back();
+               entities.back().ID(id);
+               return entities.back();
+       }
+       std::uint32_t id = 1;
+       auto position = entities.begin();
+       auto end = entities.end();
+       while (position != end && position->ID() == id) {
+               ++id;
+               ++position;
+       }
+       auto entity = entities.emplace(position);
+       entity->ID(id);
+       return *entity;
+}
+
+Entity *World::AddEntity(std::uint32_t id) {
+       if (entities.empty() || entities.back().ID() < id) {
+               entities.emplace_back();
+               entities.back().ID(id);
+               return &entities.back();
+       }
+
+       auto position = entities.begin();
+       auto end = entities.end();
+       while (position != end && position->ID() < id) {
+               ++position;
+       }
+       if (position != end && position->ID() == id) {
+               return nullptr;
+       }
+       auto entity = entities.emplace(position);
+       entity->ID(id);
+       return &*entity;
+}
+
 
 namespace {
 
 
 #include "Entity.hpp"
 #include "Generator.hpp"
 
+#include <cstdint>
 #include <list>
 #include <string>
 #include <vector>
        /// add player with given name
        /// returns nullptr if the name is already taken
        Entity *AddPlayer(const std::string &name);
-       Entity &AddEntity() { entities.emplace_back(); return entities.back(); }
+       /// add player with given name and ID
+       /// returns nullptr if the name or ID is already taken
+       Entity *AddPlayer(const std::string &name, std::uint32_t id);
+       /// add an entity with an autogenerated ID
+       Entity &AddEntity();
+       /// add entity with given ID
+       /// returns nullptr if the ID is already taken
+       Entity *AddEntity(std::uint32_t id);
 
        const std::vector<Entity *> &Players() const noexcept { return players; }
        const std::list<Entity> &Entities() const noexcept { return entities; }