// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id: 0744b929e840294cebbfa519767ff258fa598d55 $
//
// Copyright (C) 2021 by Alex Mayfield.
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// DESCRIPTION:
//   Server message functions.
//   - Functions should send exactly one message.
//   - Functions should be named after the message they send.
//   - Functions should be self-contained and not rely on global state.
//   - Functions should take a buf_t& as a first parameter.
//
//-----------------------------------------------------------------------------


#include "odamex.h"

#include <bitset>

#include "svc_message.h"

#include "common.pb.h"
#include "d_main.h"
#include "g_gametype.h"
#include "i_system.h"
#include "i_time.h"
#include "m_strindex.h"
#include "p_lnspec.h"
#include "p_local.h"
#include "p_mobj.h"
#include "p_unlag.h"

/**
 * @brief Pack an array of booleans into a bitfield.
 */
static uint32_t PackBoolArray(nonstd::span<const bool> bools)
{
	uint32_t out = 0;
	for (size_t i = 0; i < bools.size(); i++)
	{
		if (bools[i])
		{
			out |= (1 << i);
		}
	}
	return out;
}

odaproto::svc::Disconnect SVC_Disconnect(const char* message)
{
	odaproto::svc::Disconnect msg;

	if (message != NULL)
	{
		msg.set_message(message);
	}

	return msg;
}

/**
 * @brief Send information about a player.
 */
odaproto::svc::PlayerInfo SVC_PlayerInfo(const player_t& player)
{
	odaproto::svc::PlayerInfo msg;

	uint32_t packedweapons = PackBoolArray(player.weaponowned);
	msg.mutable_player()->set_weaponowned(packedweapons);

	uint32_t packedcards = PackBoolArray(player.cards);
	msg.mutable_player()->set_cards(packedcards);

	msg.mutable_player()->set_backpack(player.backpack);

	for (int i = 0; i < NUMAMMO; i++)
	{
		msg.mutable_player()->add_ammo(player.ammo[i]);
		msg.mutable_player()->add_maxammo(player.maxammo[i]);
	}

	msg.mutable_player()->set_health(player.health);
	msg.mutable_player()->set_armorpoints(player.armorpoints);
	msg.mutable_player()->set_armortype(player.armortype);
	msg.mutable_player()->set_lives(player.lives);
	msg.mutable_player()->set_readyweapon(player.readyweapon);
	msg.mutable_player()->set_pendingweapon(player.pendingweapon);

	for (int i = 0; i < NUMPOWERS; i++)
	{
		msg.mutable_player()->add_powers(player.powers[i]);
	}

	if (!player.spectator)
		msg.mutable_player()->set_cheats(player.cheats);

	return msg;
}

/**
 * @brief Change the location of a player.
 */
odaproto::svc::MovePlayer SVC_MovePlayer(const player_t& player, const int tic)
{
	odaproto::svc::MovePlayer msg;

	odaproto::Actor* act = msg.mutable_actor();
	odaproto::Player* pl = msg.mutable_player();

	pl->set_playerid(player.id); // player number

	// [SL] 2011-09-14 - the most recently processed ticcmd from the
	// client we're sending this message to.
	msg.set_tic(tic);

	odaproto::Vec3* pos = act->mutable_pos();
	pos->set_x(player.mo->x);
	pos->set_y(player.mo->y);
	pos->set_z(player.mo->z);

	act->set_angle(player.mo->angle);
	act->set_pitch(player.mo->pitch);

	if (player.mo->frame == 32773)
	{
		msg.set_frame(PLAYER_FULLBRIGHTFRAME);
	}
	else
	{
		msg.set_frame(player.mo->frame);
	}

	// write velocity
	odaproto::Vec3* mom = act->mutable_mom();
	mom->set_x(player.mo->momx);
	mom->set_y(player.mo->momy);
	mom->set_z(player.mo->momz);

	// Send all available powers information to show the clients
	pl->mutable_powers()->Resize(NUMPOWERS + 1, 0);
	pl->set_powers(pw_invulnerability, player.powers[pw_invulnerability]);
	pl->set_powers(pw_strength, player.powers[pw_strength] > 0 ? 1 : 0);
	pl->set_powers(pw_invisibility, player.powers[pw_invisibility]);
	pl->set_powers(pw_ironfeet, player.powers[pw_ironfeet]);
	pl->set_powers(pw_allmap, player.powers[pw_allmap]);
	pl->set_powers(pw_infrared, player.powers[pw_infrared]);

	return msg;
}

/**
 * @brief Send the local player position for a client.
 */
odaproto::svc::UpdateLocalPlayer SVC_UpdateLocalPlayer(const AActor& mo, const int tic)
{
	odaproto::svc::UpdateLocalPlayer msg;

	// client player will update his position if packets were missed
	odaproto::Actor* act = msg.mutable_actor();

	// client-tic of the most recently processed ticcmd for this client
	msg.set_tic(tic);

	odaproto::Vec3* pos = act->mutable_pos();
	pos->set_x(mo.x);
	pos->set_y(mo.y);
	pos->set_z(mo.z);

	odaproto::Vec3* mom = act->mutable_mom();
	mom->set_x(mo.momx);
	mom->set_y(mo.momy);
	mom->set_z(mo.momz);

	act->set_waterlevel(mo.waterlevel);

	return msg;
}

/**
 * @brief Persist level locals to the client.
 *
 * @param b Buffer to write to.
 * @param locals Level locals struct to send.
 * @param flags SVC_LL_* bit flags to designate what gets sent.
 */
odaproto::svc::LevelLocals SVC_LevelLocals(const level_locals_t& locals, uint32_t flags)
{
	odaproto::svc::LevelLocals msg;

	msg.set_flags(flags);

	if (flags & SVC_LL_TIME)
	{
		msg.set_time(locals.time);
	}

	if (flags & SVC_LL_TOTALS)
	{
		msg.set_total_secrets(locals.total_secrets);
		msg.set_total_items(locals.total_items);
		msg.set_total_monsters(locals.total_monsters);
	}

	if (flags & SVC_LL_SECRETS)
	{
		msg.set_found_secrets(locals.found_secrets);
	}

	if (flags & SVC_LL_ITEMS)
	{
		msg.set_found_items(locals.found_items);
	}

	if (flags & SVC_LL_MONSTERS)
	{
		msg.set_killed_monsters(locals.killed_monsters);
	}

	if (flags & SVC_LL_MONSTER_RESPAWNS)
	{
		msg.set_respawned_monsters(locals.respawned_monsters);
	}

	return msg;
}

/**
 * @brief Send the server's current time in MS to the client.
 */
odaproto::svc::PingRequest SVC_PingRequest()
{
	odaproto::svc::PingRequest msg;

	msg.set_ms_time(I_MSTime());

	return msg;
}

odaproto::svc::UpdatePing SVC_UpdatePing(const player_t& player)
{
	odaproto::svc::UpdatePing msg;

	msg.set_pid(player.id);
	msg.set_ping(player.ping);

	return msg;
}

odaproto::svc::SpawnMobj SVC_SpawnMobj(const AActor* mo)
{
	odaproto::svc::SpawnMobj msg;

	odaproto::Actor* base = msg.mutable_baseline();
	odaproto::Vec3* bpos = base->mutable_pos();
	odaproto::Vec3* bmom = base->mutable_mom();

	odaproto::Actor* cur = msg.mutable_current();
	odaproto::Vec3* apos = cur->mutable_pos();
	odaproto::Vec3* amom = cur->mutable_mom();

	uint32_t spawnFlags = 0;

	// Contents of the baseline

	bpos->set_x(mo->baseline.pos.x);
	bpos->set_y(mo->baseline.pos.y);
	bpos->set_z(mo->baseline.pos.z);
	bmom->set_x(mo->baseline.mom.x);
	bmom->set_y(mo->baseline.mom.y);
	bmom->set_z(mo->baseline.mom.z);
	base->set_angle(mo->baseline.angle);
	base->set_targetid(mo->baseline.targetid);
	base->set_tracerid(mo->baseline.tracerid);
	base->set_movecount(mo->baseline.movecount);
	base->set_movedir(mo->baseline.movedir);
	base->set_rndindex(mo->baseline.rndindex);

	// Current location could be different than the baseline

	uint32_t bflags = P_GetMobjBaselineFlags(*mo);
	msg.set_baseline_flags(bflags);

	if (bflags & baseline_t::POSX)
	{
		apos->set_x(mo->x);
	}
	if (bflags & baseline_t::POSY)
	{
		apos->set_y(mo->y);
	}
	if (bflags & baseline_t::POSZ)
	{
		apos->set_z(mo->z);
	}
	if (bflags & baseline_t::ANGLE)
	{
		cur->set_angle(mo->angle);
	}
	if (bflags & baseline_t::MOVEDIR)
	{
		cur->set_movedir(mo->movedir);
	}
	if (bflags & baseline_t::MOVECOUNT)
	{
		cur->set_movecount(mo->movecount);
	}
	if (bflags & baseline_t::RNDINDEX)
	{
		cur->set_rndindex(mo->rndindex);
	}
	if (bflags & baseline_t::TARGET)
	{
		cur->set_targetid(mo->target ? mo->target->netid : 0);
	}
	if (bflags & baseline_t::TRACER)
	{
		cur->set_tracerid(mo->tracer ? mo->tracer->netid : 0);
	}
	if (bflags & baseline_t::MOMX)
	{
		amom->set_x(mo->momx);
	}
	if (bflags & baseline_t::MOMY)
	{
		amom->set_y(mo->momy);
	}
	if (bflags & baseline_t::MOMZ)
	{
		amom->set_z(mo->momz);
	}

	cur->set_type(mo->type);
	cur->set_netid(mo->netid);

	// denis - sending state fixes monster ghosts appearing under doors
	cur->set_statenum(mo->state->statenum);

	if (mo->type == MT_FOUNTAIN)
	{
		msg.add_args(mo->args[0]);
	}

	if (mo->type == MT_ZDOOMBRIDGE)
	{
		msg.add_args(mo->args[0]);
		msg.add_args(mo->args[1]);
	}

	if (mo->type == MT_UPPERSTACK || mo->type == MT_LOWERSTACK)
	{
		msg.add_args(mo->tid);
		msg.add_args(mo->args[0]); // boundary flat alpha
	}

	if (mo->type == MT_SKYVIEWPOINT)
	{
		msg.add_args(mo->tid);
	}

	if (mo->type == MT_SKYPICKER)
	{
		msg.add_args(mo->args[0]);
		msg.add_args(mo->args[1]); // which planes to apply the viewpoint to
	}

	// denis - check type as that is what the client will be spawning
	if (mo->flags & MF_MISSILE || mobjinfo[mo->type].flags & MF_MISSILE)
	{
		msg.set_target_netid(mo->target ? mo->target->netid : 0);
	}
	else if (mo->flags & MF_AMBUSH || mo->flags & MF_DROPPED)
	{
		spawnFlags |= SVC_SM_FLAGS;
		cur->set_flags(mo->flags);
	}

	if (mo->flags2 & MF2_DORMANT)
	{
		spawnFlags |= SVC_SM_FLAGS2;
		cur->set_flags2(mo->flags2);
	}

	// odamex flags - only monster flags for now
	if (mo->oflags & hordeBossModMask)
	{
		spawnFlags |= SVC_SM_OFLAGS;
		cur->set_oflags(mo->oflags & hordeBossModMask);
	}

	// animating corpses
	if ((mo->flags & MF_CORPSE) && mo->state->statenum != S_GIBS)
	{
		// This sets off some additional logic on the client.
		spawnFlags |= SVC_SM_CORPSE;
		cur->set_frame(mo->frame);
		cur->set_tics(mo->tics);
	}

	msg.set_spawn_flags(spawnFlags);

	return msg;
}

odaproto::svc::DisconnectClient SVC_DisconnectClient(const player_t& player)
{
	odaproto::svc::DisconnectClient msg;

	msg.set_pid(player.id);

	return msg;
}

/**
 * @brief Sends a message to a player telling them to change to the specified
 *        WAD and DEH patch files and load a map.
 */
odaproto::svc::LoadMap SVC_LoadMap(const OResFiles& wadnames, const OResFiles& patchnames,
                                   const std::string& mapname, int time)
{
	odaproto::svc::LoadMap msg;

	// send list of wads (skip over wadnames[0] == odamex.wad)
	size_t wadcount = wadnames.size() - 1;
	for (size_t i = 1; i < wadcount + 1; i++)
	{
		odaproto::svc::LoadMap_Resource* wad = msg.add_wadnames();
		wad->set_name(wadnames[i].getBasename());
		wad->set_hash(wadnames[i].getMD5().getHexStr());
	}

	// send list of DEH/BEX patches
	size_t patchcount = patchnames.size();
	for (size_t i = 0; i < patchcount; i++)
	{
		odaproto::svc::LoadMap_Resource* patch = msg.add_patchnames();
		patch->set_name(patchnames[i].getBasename());
		patch->set_hash(patchnames[i].getMD5().getHexStr());
	}

	msg.set_mapname(mapname);
	msg.set_time(time);

	return msg;
}

odaproto::svc::ConsolePlayer SVC_ConsolePlayer(const player_t& player,
                                               const std::string& digest)
{
	odaproto::svc::ConsolePlayer msg;

	msg.set_pid(player.id);
	msg.set_digest(digest);

	return msg;
}

odaproto::svc::ExplodeMissile SVC_ExplodeMissile(const AActor& mobj)
{
	odaproto::svc::ExplodeMissile msg;

	msg.set_netid(mobj.netid);

	return msg;
}

odaproto::svc::RemoveMobj SVC_RemoveMobj(const AActor& mobj)
{
	odaproto::svc::RemoveMobj msg;

	msg.set_netid(mobj.netid);

	return msg;
}

odaproto::svc::UserInfo SVC_UserInfo(const player_t& player, int64_t time)
{
	odaproto::svc::UserInfo msg;

	msg.set_pid(player.id);
	msg.set_netname(player.userinfo.netname);
	msg.set_team(player.userinfo.team);
	msg.set_gender(player.userinfo.gender);

	// [AM] Alpha is always 255.
	odaproto::Color* color = msg.mutable_color();
	color->set_r(player.userinfo.color[1]);
	color->set_g(player.userinfo.color[2]);
	color->set_b(player.userinfo.color[3]);

	msg.set_join_time(time);

	return msg;
}

/**
 * @brief Update mobj data on the client compared to the baseline.
 */
odaproto::svc::UpdateMobj SVC_UpdateMobj(const AActor& mobj)
{
	odaproto::svc::UpdateMobj msg;

	uint32_t flags = P_GetMobjBaselineFlags(mobj);
	msg.set_flags(flags);

	odaproto::Actor* act = msg.mutable_actor();
	odaproto::Vec3* pos = act->mutable_pos();
	odaproto::Vec3* mom = act->mutable_mom();

	act->set_netid(mobj.netid);

	if (flags & baseline_t::POSX)
	{
		pos->set_x(mobj.x);
	}
	if (flags & baseline_t::POSY)
	{
		pos->set_y(mobj.y);
	}
	if (flags & baseline_t::POSZ)
	{
		pos->set_z(mobj.z);
	}
	if (flags & baseline_t::ANGLE)
	{
		act->set_angle(mobj.angle);
	}
	if (flags & baseline_t::MOVEDIR)
	{
		act->set_movedir(mobj.movedir);
	}
	if (flags & baseline_t::MOVECOUNT)
	{
		act->set_movecount(mobj.movecount);
	}
	if (flags & baseline_t::RNDINDEX)
	{
		act->set_rndindex(mobj.rndindex);
	}
	if (flags & baseline_t::TARGET)
	{
		act->set_targetid(mobj.target ? mobj.target->netid : 0);
	}
	if (flags & baseline_t::TRACER)
	{
		act->set_tracerid(mobj.tracer ? mobj.tracer->netid : 0);
	}
	if (flags & baseline_t::MOMX)
	{
		mom->set_x(mobj.momx);
	}
	if (flags & baseline_t::MOMY)
	{
		mom->set_y(mobj.momy);
	}
	if (flags & baseline_t::MOMZ)
	{
		mom->set_z(mobj.momz);
	}

	return msg;
}

EXTERN_CVAR(sv_sharekeys);

odaproto::svc::SpawnPlayer SVC_SpawnPlayer(const player_t& player)
{
	odaproto::svc::SpawnPlayer msg;

	msg.set_pid(player.id);

	odaproto::Actor* act = msg.mutable_actor();
	if (player.mo)
	{
		act->set_netid(player.mo->netid);
		act->set_angle(player.mo->angle);
		act->mutable_pos()->set_x(player.mo->x);
		act->mutable_pos()->set_y(player.mo->y);
		act->mutable_pos()->set_z(player.mo->z);
	}
	else
	{
		// The client hasn't yet received his own position from the server
		// This happens with cl_autorecord
		// Just fake a position for now
		act->set_netid(limits::MAXSHORT);
	}


	if (sv_sharekeys)
	{
		const uint32_t packedcards = PackBoolArray(player.cards);
		msg.set_cards(packedcards);
	}

	return msg;
}

odaproto::svc::DamagePlayer SVC_DamagePlayer(const player_t& player, const AActor* inflictor, int health, int armor)
{
	odaproto::svc::DamagePlayer msg;

	msg.set_netid(player.mo->netid);
	msg.set_inflictorid(inflictor ? inflictor->netid : 0);
	msg.set_health_damage(health);
	msg.set_armor_damage(armor);
	msg.mutable_player()->set_health(player.health);
	msg.mutable_player()->set_armorpoints(player.armorpoints);

	return msg;
}

/**
 * @brief Kill a mobj.
 */
odaproto::svc::KillMobj SVC_KillMobj(const AActor* source, const AActor* target, const AActor* inflictor,
                                     int mod, bool joinkill)
{
	odaproto::svc::KillMobj msg;

	odaproto::Actor* tgt = msg.mutable_target();

	msg.set_source_netid(source ? source->netid : 0);
	msg.set_inflictor_netid(inflictor ? inflictor->netid : 0);
	msg.set_health(target->health);
	msg.set_mod(mod);
	msg.set_joinkill(joinkill);

	// [AM] Confusingly, we send the lives _before_ we take it away, so
	//      the lives logic can live in the kill function.
	msg.set_lives(target->player ? target->player->lives : -1);

	// send death location first
	tgt->set_netid(target->netid);
	tgt->set_rndindex(target->rndindex);

	// [SL] 2012-12-26 - Get real position since this actor is at
	// a reconciled position due to unlag
	fixed_t xoffs = 0, yoffs = 0, zoffs = 0;
	if (target->player)
	{
		Unlag::getInstance().getReconciliationOffset(target->player->id, xoffs, yoffs,
		                                             zoffs);
	}

	odaproto::Vec3* tgtpos = tgt->mutable_pos();
	tgtpos->set_x(target->x + xoffs);
	tgtpos->set_y(target->y + yoffs);
	tgtpos->set_z(target->z + zoffs);

	tgt->set_angle(target->angle);

	odaproto::Vec3* tgtmom = tgt->mutable_mom();
	tgtmom->set_x(target->momx);
	tgtmom->set_y(target->momy);
	tgtmom->set_z(target->momz);

	return msg;
}

/**
 * @brief Resurrect a mobj.
 */
odaproto::svc::RaiseMobj SVC_RaiseMobj(const AActor* source, const AActor* corpse)
{
	odaproto::svc::RaiseMobj msg;

	odaproto::Actor* cps = msg.mutable_corpse();

	// corpse netid
	cps->set_netid(corpse->netid);

	cps->set_rndindex(corpse->rndindex);

	odaproto::Vec3* cpspos = cps->mutable_pos();
	cpspos->set_x(corpse->x);
	cpspos->set_y(corpse->y);
	cpspos->set_z(corpse->z);

	cps->set_angle(corpse->angle);

	odaproto::Vec3* cpsmom = cps->mutable_mom();
	cpsmom->set_x(corpse->momx);
	cpsmom->set_y(corpse->momy);
	cpsmom->set_z(corpse->momz);

	msg.set_source_netid(source ? source->netid : 0);

	return msg;
}

odaproto::svc::FireWeapon SVC_FireWeapon(const player_t& player)
{
	odaproto::svc::FireWeapon msg;

	msg.set_readyweapon(player.readyweapon);
	msg.set_servertic(player.tic);

	return msg;
}

odaproto::svc::UpdateSector SVC_UpdateSector(const sector_t& sector)
{
	odaproto::svc::UpdateSector msg;

	msg.set_sectornum(&sector - ::sectors);
	odaproto::Sector* secmsg = msg.mutable_sector();

	secmsg->set_floor_height(P_FloorHeight(&sector));
	secmsg->set_ceiling_height(P_CeilingHeight(&sector));
	secmsg->set_floorpic(sector.floorpic);
	secmsg->set_ceilingpic(sector.ceilingpic);
	secmsg->set_special(sector.special);

	return msg;
}

odaproto::svc::Print SVC_Print(printlevel_t level, const std::string& str)
{
	odaproto::svc::Print msg;

	msg.set_level(level);
	msg.set_message(str);

	return msg;
}

/**
 * @brief Persist player information to the client that is less vital.
 *
 * @param b Buffer to write to.
 * @param player Player to send information about.
 * @param flags SVC_PM_* flags to designate what gets sent.
 */
odaproto::svc::PlayerMembers SVC_PlayerMembers(const player_t& player, byte flags)
{
	odaproto::svc::PlayerMembers msg;

	msg.set_pid(player.id);
	msg.set_flags(flags);

	if (flags & SVC_PM_SPECTATOR)
	{
		msg.set_spectator(player.spectator);
	}

	if (flags & SVC_PM_READY)
	{
		msg.set_ready(player.ready);
	}

	if (flags & SVC_PM_LIVES)
	{
		msg.set_lives(player.lives);
	}

	if (flags & SVC_PM_DAMAGE)
	{
		msg.set_monsterdmgcount(player.monsterdmgcount);
	}

	if (flags & SVC_PM_SCORE)
	{
		// [AM] Just send everything instead of trying to be clever about
		//      what we send based on game modes.  There's less room for
		//      breakage when adding game modes, and we have varints now,
		//      which means we're probably saving bandwidth anyway.
		msg.set_roundwins(player.roundwins);
		msg.set_points(player.points);
		msg.set_fragcount(player.fragcount);
		msg.set_deathcount(player.deathcount);
		msg.set_killcount(player.killcount);
		// [AM] Is there any reason we would ever care about itemcount?
		msg.set_secretcount(player.secretcount);
		msg.set_totalpoints(player.totalpoints);
		msg.set_totaldeaths(player.totaldeaths);
	}

	if (flags & SVC_PM_CHEATS)
	{
		if (!player.spectator)
			msg.set_cheats(player.cheats);
	}

	return msg;
}

/**
 * Persist mutable team information to the client.
 */
odaproto::svc::TeamMembers SVC_TeamMembers(team_t team)
{
	odaproto::svc::TeamMembers msg;

	TeamInfo* info = GetTeamInfo(team);

	msg.set_team(team);
	msg.set_points(info->Points);
	msg.set_roundwins(info->RoundWins);

	return msg;
}

odaproto::svc::ActivateLine SVC_ActivateLine(const line_t* line, const AActor* mo, int side,
                                             LineActivationType type)
{
	odaproto::svc::ActivateLine msg;

	msg.set_linenum(line ? line - ::lines : -1);
	msg.set_activator_netid(mo ? mo->netid : 0);
	msg.set_side(side);
	msg.set_activation_type(type);

	return msg;
}

odaproto::svc::MovingSector SVC_MovingSector(const sector_t& sector)
{
	odaproto::svc::MovingSector msg;

	ptrdiff_t sectornum = &sector - ::sectors;

	// Determine which moving planes are in this sector - governs which
	// fields to send to the other side.
	movertype_t floor_mover = SEC_INVALID, ceiling_mover = SEC_INVALID;

	if (sector.ceilingdata && sector.ceilingdata->IsA(RUNTIME_CLASS(DCeiling)))
	{
		ceiling_mover = SEC_CEILING;
	}
	if (sector.ceilingdata && sector.ceilingdata->IsA(RUNTIME_CLASS(DDoor)))
	{
		ceiling_mover = SEC_DOOR;
	}
	if (sector.floordata && sector.floordata->IsA(RUNTIME_CLASS(DFloor)))
	{
		floor_mover = SEC_FLOOR;
	}
	if (sector.floordata && sector.floordata->IsA(RUNTIME_CLASS(DPlat)))
	{
		floor_mover = SEC_PLAT;
	}
	if (sector.ceilingdata && sector.ceilingdata->IsA(RUNTIME_CLASS(DElevator)))
	{
		ceiling_mover = SEC_ELEVATOR;
		floor_mover = SEC_INVALID;
	}
	if (sector.ceilingdata && sector.ceilingdata->IsA(RUNTIME_CLASS(DPillar)))
	{
		ceiling_mover = SEC_PILLAR;
		floor_mover = SEC_INVALID;
	}

	// no moving planes?  skip it.
	if (ceiling_mover == SEC_INVALID && floor_mover == SEC_INVALID)
	{
		return msg;
	}

	// Create bitfield to denote moving planes in this sector
	byte movers = byte(ceiling_mover) | (byte(floor_mover) << 4);

	msg.set_sector(sectornum);
	msg.set_ceiling_height(P_CeilingHeight(&sector));
	msg.set_floor_height(P_FloorHeight(&sector));
	msg.set_movers(movers);

	if (ceiling_mover == SEC_ELEVATOR)
	{
		DElevator* Elevator = static_cast<DElevator*>(sector.ceilingdata);

		odaproto::svc::MovingSector_Snapshot* ceil = msg.mutable_ceiling_mover();
		ceil->set_ceil_type(Elevator->m_Type);
		ceil->set_ceil_status(Elevator->m_Status);
		ceil->set_ceil_dir(Elevator->m_Direction);
		ceil->set_floor_dest(Elevator->m_FloorDestHeight);
		ceil->set_ceil_dest(Elevator->m_CeilingDestHeight);
		ceil->set_ceil_speed(Elevator->m_Speed);
	}

	if (ceiling_mover == SEC_PILLAR)
	{
		DPillar* Pillar = static_cast<DPillar*>(sector.ceilingdata);

		odaproto::svc::MovingSector_Snapshot* ceil = msg.mutable_ceiling_mover();
		ceil->set_ceil_type(Pillar->m_Type);
		ceil->set_ceil_status(Pillar->m_Status);
		ceil->set_floor_speed(Pillar->m_FloorSpeed);
		ceil->set_ceil_speed(Pillar->m_CeilingSpeed);
		ceil->set_floor_dest(Pillar->m_FloorTarget);
		ceil->set_ceil_dest(Pillar->m_CeilingTarget);
		ceil->set_ceil_crush(Pillar->m_Crush);
	}

	if (ceiling_mover == SEC_CEILING)
	{
		DCeiling* Ceiling = static_cast<DCeiling*>(sector.ceilingdata);

		odaproto::svc::MovingSector_Snapshot* ceil = msg.mutable_ceiling_mover();
		ceil->set_ceil_type(Ceiling->m_Type);
		ceil->set_ceil_low(Ceiling->m_BottomHeight);
		ceil->set_ceil_high(Ceiling->m_TopHeight);
		ceil->set_ceil_speed(Ceiling->m_Speed);
		ceil->set_crusher_speed_1(Ceiling->m_Speed1);
		ceil->set_crusher_speed_2(Ceiling->m_Speed2);
		ceil->set_ceil_crush(Ceiling->m_Crush);
		ceil->set_silent(Ceiling->m_Silent);
		ceil->set_ceil_dir(Ceiling->m_Direction);
		ceil->set_ceil_tex(Ceiling->m_Texture);
		ceil->set_ceil_new_special(Ceiling->m_NewSpecial);
		ceil->set_ceil_tag(Ceiling->m_Tag);
		ceil->set_ceil_old_dir(Ceiling->m_OldDirection);
	}

	if (ceiling_mover == SEC_DOOR)
	{
		DDoor* Door = static_cast<DDoor*>(sector.ceilingdata);

		odaproto::svc::MovingSector_Snapshot* ceil = msg.mutable_ceiling_mover();
		ceil->set_ceil_type(Door->m_Type);
		ceil->set_ceil_height(Door->m_TopHeight);
		ceil->set_ceil_speed(Door->m_Speed);
		ceil->set_ceil_wait(Door->m_TopWait);
		ceil->set_ceil_counter(Door->m_TopCountdown);
		ceil->set_ceil_status(Door->m_Status);
		// Check for an invalid m_Line (doors triggered by tag 666)
		ceil->set_ceil_line(Door->m_Line ? (Door->m_Line - lines) : -1);
	}

	if (floor_mover == SEC_FLOOR)
	{
		DFloor* Floor = static_cast<DFloor*>(sector.floordata);

		odaproto::svc::MovingSector_Snapshot* floor = msg.mutable_floor_mover();
		floor->set_floor_type(Floor->m_Type);
		floor->set_floor_status(Floor->m_Status);
		floor->set_floor_crush(Floor->m_Crush);
		floor->set_floor_dir(Floor->m_Direction);
		floor->set_floor_speed(Floor->m_NewSpecial);
		floor->set_floor_tex(Floor->m_Texture);
		floor->set_floor_dest(Floor->m_FloorDestHeight);
		floor->set_floor_speed(Floor->m_Speed);
		floor->set_reset_counter(Floor->m_ResetCount);
		floor->set_orig_height(Floor->m_OrgHeight);
		floor->set_delay(Floor->m_Delay);
		floor->set_pause_time(Floor->m_PauseTime);
		floor->set_step_time(Floor->m_StepTime);
		floor->set_per_step_time(Floor->m_PerStepTime);
		floor->set_floor_line(Floor->m_Line ? (Floor->m_Line - lines) : -1);
	}

	if (floor_mover == SEC_PLAT)
	{
		DPlat* Plat = static_cast<DPlat*>(sector.floordata);

		odaproto::svc::MovingSector_Snapshot* floor = msg.mutable_floor_mover();
		floor->set_floor_speed(Plat->m_Speed);
		floor->set_floor_low(Plat->m_Low);
		floor->set_floor_high(Plat->m_High);
		floor->set_floor_wait(Plat->m_Wait);
		floor->set_floor_counter(Plat->m_Count);
		floor->set_floor_status(Plat->m_Status);
		floor->set_floor_old_status(Plat->m_OldStatus);
		floor->set_floor_crush(Plat->m_Crush);
		floor->set_floor_tag(Plat->m_Tag);
		floor->set_floor_type(Plat->m_Type);
		floor->set_floor_offset(Plat->m_Height);
		floor->set_floor_lip(Plat->m_Lip);
	}

	return msg;
}

odaproto::svc::PlaySound SVC_PlaySound(const PlaySoundType& type, int channel, int sfx_id,
                                       float volume, int attenuation)
{
	odaproto::svc::PlaySound msg;

	msg.set_channel(channel);
	msg.set_sfxid(sfx_id);
	msg.set_volume(volume);
	msg.set_attenuation(attenuation);

	switch (type.tag)
	{
	case PlaySoundType::PS_NONE:
		break;
	case PlaySoundType::PS_MOBJ:
		msg.set_netid(type.data.mo->netid);
		break;
	case PlaySoundType::PS_POS:
		msg.mutable_pos()->set_x(type.data.pos.x);
		msg.mutable_pos()->set_y(type.data.pos.y);
		break;
	}

	return msg;
}

odaproto::svc::TouchSpecial SVC_TouchSpecial(const AActor& mo)
{
	odaproto::svc::TouchSpecial msg;

	msg.set_netid(mo.netid);

	return msg;
}

/**
 * @brief Send information about a player
 */
odaproto::svc::PlayerState SVC_PlayerState(const player_t& player)
{
	odaproto::svc::PlayerState msg;

	odaproto::Player* pl = msg.mutable_player();

	pl->set_playerid(player.id);
	pl->set_health(player.health);
	pl->set_armortype(player.armortype);
	pl->set_armorpoints(player.armorpoints);
	pl->set_lives(player.lives);
	pl->set_readyweapon(player.readyweapon);

	std::bitset<6> cardBits;
	for (int i = 0; i < NUMCARDS; i++)
	{
		cardBits.set(i, player.cards[i]);
	}
	pl->set_cards(cardBits.to_ulong());

	for (int i = 0; i < NUMAMMO; i++)
	{
		pl->add_ammo(player.ammo[i]);
	}

	for (int i = 0; i < NUMPSPRITES; i++)
	{
		const pspdef_t* psp = &player.psprites[i];
		const int32_t state = psp->state ? psp->state->statenum : 0;
		odaproto::Player_Psp* plpsp = pl->add_psprites();
		plpsp->set_statenum(state);
	}

	for (int i = 0; i < NUMPOWERS; i++)
	{
		pl->add_powers(player.powers[i]);
	}

	if (!player.spectator)
		pl->set_cheats(player.cheats);

	return msg;
}

/**
 * @brief Send information about the server's LevelState to the client.
 */
odaproto::svc::LevelState SVC_LevelState(const SerializedLevelState& sls)
{
	odaproto::svc::LevelState msg;

	msg.set_state(sls.state);
	msg.set_countdown_done_time(sls.countdown_done_time);
	msg.set_ingame_start_time(sls.ingame_start_time);
	msg.set_round_number(sls.round_number);
	msg.set_last_wininfo_type(sls.last_wininfo_type);
	msg.set_last_wininfo_id(sls.last_wininfo_id);

	return msg;
}

odaproto::svc::PlayerQueuePos SVC_PlayerQueuePos(const player_t& source)
{
	odaproto::svc::PlayerQueuePos msg;

	msg.set_pid(source.id);
	msg.set_queuepos(source.QueuePosition);

	return msg;
}

odaproto::svc::ForceTeam SVC_ForceTeam(team_t team)
{
	odaproto::svc::ForceTeam msg;

	msg.set_team(team);

	return msg;
}

odaproto::svc::Switch SVC_Switch(const line_t& line, uint32_t state, uint32_t timer)
{
	odaproto::svc::Switch msg;

	msg.set_linenum(&line - ::lines);
	msg.set_switch_active(line.switchactive);
	msg.set_special(line.special);
	msg.set_state(state);
	msg.set_button_texture(P_GetButtonTexture(&line));
	msg.set_timer(timer);

	return msg;
}

odaproto::svc::Say SVC_Say(const bool visibility, const byte pid,
                           const std::string& message)
{
	odaproto::svc::Say msg;

	msg.set_visibility(visibility);
	msg.set_pid(pid);
	msg.set_message(message);

	return msg;
}

odaproto::svc::CTFRefresh SVC_CTFRefresh(const TeamsView& teams, const bool full)
{
	odaproto::svc::CTFRefresh msg;

	msg.set_full(full);

	for (const auto& team : teams)
	{
		odaproto::svc::CTFRefresh_TeamInfo* info = msg.add_team_info();

		info->set_points(team->Points);

		if (full)
		{
			info->set_flag_state(team->FlagData.state);
			info->set_flag_flagger(team->FlagData.flagger);
		}
	}

	return msg;
}

odaproto::svc::CTFEvent SVC_CTFEvent(const flag_score_t event, const team_t target,
                                     const player_t& player)
{
	odaproto::svc::CTFEvent msg;

	msg.set_event(event);
	msg.set_target_team(target);

	if (validplayer(player))
	{
		msg.set_player_team(player.userinfo.team);
		msg.set_player_id(player.id);
		if (G_IsRoundsGame())
		{
			msg.set_player_totalpoints(player.totalpoints);
		}
		else
		{
			msg.set_player_points(player.points);
		}
	}

	return msg;
}

/**
 * @brief Send information about a player who discovered a secret.
 */
odaproto::svc::SecretEvent SVC_SecretEvent(const player_t& player, const sector_t& sector)
{
	odaproto::svc::SecretEvent msg;

	msg.set_pid(player.id);
	msg.set_sectornum(&sector - ::sectors);
	msg.mutable_sector()->set_special(sector.special);

	return msg;
}

odaproto::svc::ServerSettings SVC_ServerSettings(const cvar_t& var)
{
	odaproto::svc::ServerSettings msg;

	msg.set_key(var.name());
	msg.set_value(var.str());

	return msg;
}

odaproto::svc::ConnectClient SVC_ConnectClient(const player_t& player)
{
	odaproto::svc::ConnectClient msg;

	msg.set_pid(player.id);

	return msg;
}

odaproto::svc::MidPrint SVC_MidPrint(const std::string& message, const int time)
{
	odaproto::svc::MidPrint msg;

	msg.set_message(message);
	msg.set_time(time);

	return msg;
}

odaproto::svc::ServerGametic SVC_ServerGametic(const byte tic)
{
	odaproto::svc::ServerGametic msg;

	msg.set_tic(tic);

	return msg;
}

odaproto::svc::IntTimeLeft SVC_IntTimeLeft(const unsigned int timeleft)
{
	odaproto::svc::IntTimeLeft msg;

	msg.set_timeleft(timeleft);

	return msg;
}

odaproto::svc::RailTrail SVC_RailTrail(const v3double_t& start, const v3double_t& end)
{
	odaproto::svc::RailTrail msg;

	odaproto::Vec3* stmsg = msg.mutable_start();
	stmsg->set_x(DOUBLE2FIXED(start.x));
	stmsg->set_y(DOUBLE2FIXED(start.y));
	stmsg->set_z(DOUBLE2FIXED(start.z));

	odaproto::Vec3* edmsg = msg.mutable_end();
	edmsg->set_x(DOUBLE2FIXED(end.x));
	edmsg->set_y(DOUBLE2FIXED(end.y));
	edmsg->set_z(DOUBLE2FIXED(end.z));

	return msg;
}

odaproto::svc::LineUpdate SVC_LineUpdate(const line_t& line)
{
	odaproto::svc::LineUpdate msg;

	msg.set_linenum(&line - ::lines);
	msg.set_flags(line.flags);
	msg.set_lucency(line.lucency);

	return msg;
}

odaproto::svc::SectorProperties SVC_SectorProperties(const sector_t& sector)
{
	odaproto::svc::SectorProperties msg;

	msg.set_sectornum(&sector - ::sectors);
	msg.set_changes(sector.SectorChanges);
	odaproto::Sector* secmsg = msg.mutable_sector();

	for (int i = 0, prop = 1; prop < SPC_Max; i++)
	{
		prop = 1 << i;
		if ((prop & sector.SectorChanges) == 0)
			continue;

		switch (prop)
		{
		case SPC_FlatPic:
			secmsg->set_floorpic(sector.floorpic);
			secmsg->set_ceilingpic(sector.ceilingpic);
			break;
		case SPC_LightLevel:
			secmsg->set_lightlevel(sector.lightlevel);
			break;
		case SPC_Color: {
			odaproto::Color* color = secmsg->mutable_colormap()->mutable_color();
			color->set_r(sector.colormap->color.getr());
			color->set_g(sector.colormap->color.getg());
			color->set_b(sector.colormap->color.getb());
			break;
		}
		case SPC_Fade: {
			odaproto::Color* color = secmsg->mutable_colormap()->mutable_fade();
			color->set_r(sector.colormap->fade.getr());
			color->set_g(sector.colormap->fade.getg());
			color->set_b(sector.colormap->fade.getb());
			break;
		}
		case SPC_Gravity:
			secmsg->set_gravity(sector.gravity);
			break;
		case SPC_Panning: {
			odaproto::Vec2* mc = secmsg->mutable_ceiling_offs();
			mc->set_x(sector.ceiling_xoffs);
			mc->set_y(sector.ceiling_yoffs);

			odaproto::Vec2* mf = secmsg->mutable_floor_offs();
			mf->set_x(sector.floor_xoffs);
			mf->set_y(sector.floor_yoffs);
			break;
		}
		case SPC_Scale: {
			odaproto::Vec2* cs = secmsg->mutable_ceiling_scale();
			cs->set_x(sector.ceiling_xscale);
			cs->set_y(sector.ceiling_yscale);

			odaproto::Vec2* fs = secmsg->mutable_floor_scale();
			fs->set_x(sector.floor_xscale);
			fs->set_y(sector.floor_yscale);
			break;
		}
		case SPC_Rotation:
			secmsg->set_floor_angle(sector.floor_angle);
			secmsg->set_ceiling_angle(sector.ceiling_angle);
			break;
		case SPC_AlignBase:
			secmsg->set_base_ceiling_angle(sector.base_ceiling_angle);
			secmsg->set_base_ceiling_yoffs(sector.base_ceiling_yoffs);
			secmsg->set_base_floor_angle(sector.base_floor_angle);
			secmsg->set_base_floor_yoffs(sector.base_floor_yoffs);
			break;
		case SPC_Special:
			secmsg->set_special(sector.special);
			break;
		default:
			break;
		}
	}

	return msg;
}

odaproto::svc::LineSideUpdate SVC_LineSideUpdate(const line_t& line, const int sideNum)
{
	odaproto::svc::LineSideUpdate msg;

	side_t* currentSideDef = sides + line.sidenum[sideNum];

	msg.set_linenum(&line - ::lines);
	msg.set_side(sideNum);
	msg.set_changes(currentSideDef->SidedefChanges);

	for (int i = 0, prop = 1; prop < SDPC_Max; i++)
	{
		prop = BIT(i);
		if ((prop & currentSideDef->SidedefChanges) == 0)
			continue;

		switch (prop)
		{
		case SDPC_TexTop:
			msg.set_toptexture(currentSideDef->toptexture);
			break;
		case SDPC_TexMid:
			msg.set_midtexture(currentSideDef->midtexture);
			break;
		case SDPC_TexBottom:
			msg.set_bottomtexture(currentSideDef->bottomtexture);
			break;
		default:
			break;
		}
	}

	return msg;
}

odaproto::svc::MobjState SVC_MobjState(const AActor* mo)
{
	odaproto::svc::MobjState msg;

	const int32_t mostate = mo->state ? mo->state->statenum : 0;

	msg.set_netid(mo->netid);
	msg.set_mostate(mostate);

	return msg;
}

odaproto::svc::DamageMobj SVC_DamageMobj(const AActor* target, const int pain)
{
	odaproto::svc::DamageMobj msg;

	msg.set_netid(target->netid);
	msg.set_health(target->health);
	msg.set_pain(pain);

	return msg;
}

odaproto::svc::ExecuteLineSpecial SVC_ExecuteLineSpecial(byte special, const line_t* line,
                                                         const AActor* mo, const int (&args)[5])
{
	odaproto::svc::ExecuteLineSpecial msg;

	msg.set_special(special);
	msg.set_linenum(line ? line - ::lines : -1);
	msg.set_activator_netid(mo ? mo->netid : 0);
	msg.set_arg0(args[0]);
	msg.set_arg1(args[1]);
	msg.set_arg2(args[2]);
	msg.set_arg3(args[3]);
	msg.set_arg4(args[4]);

	return msg;
}

/**
 * @brief Execute an ACS special on the client.
 *
 * @param special Special ID.
 * @param activator Activator pointer, NULL if no activator.
 * @param print String to send to client, NULL if no string.
 * @param args Arguments to pass to the client.
 */
odaproto::svc::ExecuteACSSpecial SVC_ExecuteACSSpecial(const byte special,
                                                       const AActor* activator,
                                                       const char* print,
                                                       const nonstd::span<const int> args)
{
	odaproto::svc::ExecuteACSSpecial msg;

	msg.set_special(special);
	if (activator != NULL)
	{
		msg.set_activator_netid(activator->netid);
	}
	if (print != NULL)
	{
		msg.set_print(print);
	}

	for (const auto& arg : args)
	{
		msg.add_args(arg);
	}

	return msg;
}

odaproto::svc::ThinkerUpdate SVC_ThinkerUpdate(const DThinker* thinker)
{
	odaproto::svc::ThinkerUpdate msg;

	if (thinker->IsA(RUNTIME_CLASS(DScroller)))
	{
		const DScroller* scroller = static_cast<const DScroller*>(thinker);
		odaproto::svc::ThinkerUpdate_Scroller* smsg = msg.mutable_scroller();
		smsg->set_type(scroller->GetType());
		smsg->set_scroll_x(scroller->GetScrollX());
		smsg->set_scroll_y(scroller->GetScrollY());
		smsg->set_affectee(scroller->GetAffectee());
		smsg->set_control(scroller->GetControl());
		smsg->set_accel(scroller->GetAccel());
	}
	else if (thinker->IsA(RUNTIME_CLASS(DFireFlicker)))
	{
		const DFireFlicker* fireFlicker = static_cast<const DFireFlicker*>(thinker);
		odaproto::svc::ThinkerUpdate_FireFlicker* ffmsg = msg.mutable_fire_flicker();
		ffmsg->set_sector(fireFlicker->GetSector() - sectors);
		ffmsg->set_min_light(fireFlicker->GetMinLight());
		ffmsg->set_max_light(fireFlicker->GetMaxLight());
	}
	else if (thinker->IsA(RUNTIME_CLASS(DFlicker)))
	{
		const DFlicker* flicker = static_cast<const DFlicker*>(thinker);
		odaproto::svc::ThinkerUpdate_Flicker* fmsg = msg.mutable_flicker();
		fmsg->set_sector(flicker->GetSector() - sectors);
		fmsg->set_min_light(flicker->GetMinLight());
		fmsg->set_max_light(flicker->GetMaxLight());
	}
	else if (thinker->IsA(RUNTIME_CLASS(DLightFlash)))
	{
		const DLightFlash* lightFlash = static_cast<const DLightFlash*>(thinker);
		odaproto::svc::ThinkerUpdate_LightFlash* lfmsg = msg.mutable_light_flash();
		lfmsg->set_sector(lightFlash->GetSector() - sectors);
		lfmsg->set_min_light(lightFlash->GetMinLight());
		lfmsg->set_max_light(lightFlash->GetMaxLight());
	}
	else if (thinker->IsA(RUNTIME_CLASS(DStrobe)))
	{
		const DStrobe* strobe = static_cast<const DStrobe*>(thinker);
		odaproto::svc::ThinkerUpdate_Strobe* smsg = msg.mutable_strobe();
		smsg->set_sector(strobe->GetSector() - sectors);
		smsg->set_min_light(strobe->GetMinLight());
		smsg->set_max_light(strobe->GetMaxLight());
		smsg->set_dark_time(strobe->GetDarkTime());
		smsg->set_bright_time(strobe->GetBrightTime());
		smsg->set_count(strobe->GetCount());
	}
	else if (thinker->IsA(RUNTIME_CLASS(DGlow)))
	{
		const DGlow* glow = static_cast<const DGlow*>(thinker);
		odaproto::svc::ThinkerUpdate_Glow* gmsg = msg.mutable_glow();
		gmsg->set_sector(glow->GetSector() - sectors);
	}
	else if (thinker->IsA(RUNTIME_CLASS(DGlow2)))
	{
		const DGlow2* glow2 = static_cast<const DGlow2*>(thinker);
		odaproto::svc::ThinkerUpdate_Glow2* g2msg = msg.mutable_glow2();
		g2msg->set_sector(glow2->GetSector() - sectors);
		g2msg->set_start(glow2->GetStart());
		g2msg->set_end(glow2->GetEnd());
		g2msg->set_max_tics(glow2->GetMaxTics());
		g2msg->set_one_shot(glow2->GetOneShot());
	}
	else if (thinker->IsA(RUNTIME_CLASS(DPhased)))
	{
		const DPhased* phased = static_cast<const DPhased*>(thinker);
		odaproto::svc::ThinkerUpdate_Phased* pmsg = msg.mutable_phased();
		pmsg->set_sector(phased->GetSector() - sectors);
		pmsg->set_base_level(phased->GetBaseLevel());
		pmsg->set_phase(phased->GetPhase());
	}

	return msg;
}

odaproto::svc::VoteUpdate SVC_VoteUpdate(const vote_state_t& state)
{
	odaproto::svc::VoteUpdate msg;

	msg.set_result(state.result);
	msg.set_votestring(state.votestring);
	msg.set_countdown(state.countdown);
	msg.set_yes(state.yes);
	msg.set_yes_needed(state.yes_needed);
	msg.set_no(state.no);
	msg.set_no_needed(state.no_needed);
	msg.set_abs(state.abs);

	return msg;
}

odaproto::svc::Maplist SVC_Maplist(const maplist_status_t status)
{
	odaproto::svc::Maplist msg;

	msg.set_status(status);

	return msg;
}

odaproto::svc::MaplistUpdate SVC_MaplistUpdate(const maplist_status_t status,
                                               const maplist_qrows_t* maplist)
{
	odaproto::svc::MaplistUpdate msg;
	msg.set_status(status);

	if (status == MAPLIST_OUTDATED)
	{
		assert(maplist != NULL && "Maplist pointer must be non-null");

		// In order to cut down on bandwidth, this message uses mappings of
		// strings to unique ID's to save bandwidth.  Some of these mappings
		// are already known on the receiving end.
		OStringIndexer indexer = OStringIndexer::maplistFactory();

		for (const auto& [_, entry] : *maplist)
		{
			// Create a row and add an indexed map to it.
			odaproto::svc::MaplistUpdate::Row* row = msg.add_maplist();
			const std::string& map = entry->map;
			const uint32_t mapidx = indexer.getIndex(map);
			row->set_map(mapidx);

			const std::string lastmaps = fmt::format("{}", entry->lastmaps);
			const uint32_t lastmapidx = indexer.getIndex(lastmaps);
			row->set_lastmap(lastmapidx);

			for (const auto& wad : entry->wads)
			{
				// Push an indexed WAD into the message.
				std::string filename = D_CleanseFileName(wad);
				const uint32_t wadidx = indexer.getIndex(filename);
				row->add_wads(wadidx);
			}
		}

		// Populate the dictionary.
		for (const auto& [string, idx] : indexer.indexes)
		{
			if (!indexer.shouldTransmit(idx))
				continue;

			typedef google::protobuf::MapPair<uint32_t, std::string> DictPair;
			msg.mutable_dict()->insert(DictPair(idx, string));
		}
	}

	return msg;
}

odaproto::svc::MaplistIndex SVC_MaplistIndex(const byte count, const size_t this_index,
                                             const size_t next_index)
{
	odaproto::svc::MaplistIndex msg;

	msg.set_count(count);
	if (count > 0)
	{
		msg.set_next_index(next_index);
		if (count > 1)
		{
			msg.set_this_index(this_index);
		}
	}

	return msg;
}

odaproto::svc::Toast SVC_Toast(const toast_t& toast)
{
	odaproto::svc::Toast msg;

	msg.set_flags(toast.flags);
	msg.set_left(toast.left);
	msg.set_left_pid(toast.left_pid);
	msg.set_right(toast.right);
	msg.set_right_pid(toast.right_pid);
	msg.set_icon(toast.icon);
	msg.set_points(toast.points);
	msg.set_spree_color(toast.spree_color);

	return msg;
}

odaproto::svc::HordeInfo SVC_HordeInfo(const hordeInfo_t& horde)
{
	odaproto::svc::HordeInfo msg;

	msg.set_state(horde.state);
	msg.set_wave(horde.wave);
	msg.set_wave_time(horde.waveTime);
	msg.set_boss_time(horde.bossTime);
	msg.set_define_id(horde.defineID);
	msg.set_spawned_health(horde.spawnedHealth);
	msg.set_killed_health(horde.killedHealth);
	msg.set_boss_health(horde.bossHealth);
	msg.set_boss_damage(horde.bossDamage);
	msg.set_wave_start_health(horde.waveStartHealth);

	return msg;
}

odaproto::svc::Spree SVC_Spree(const SpreeRecord_t& spree)
{
	odaproto::svc::Spree msg;

	msg.set_pid(spree.playerId);
	msg.set_spree_level(spree.spreeLevel);

	return msg;
}

odaproto::svc::SpreeBreaker SVC_SpreeBreaker(const SpreeBreaker_t& breaker, const int level, const SpreeBreakerType breakerType)
{
	odaproto::svc::SpreeBreaker msg;

	msg.set_victim_pid(breaker.spreeEndedPlayerId);
	msg.set_victim_name(breaker.spreeEndedName);
	msg.set_source_pid(breaker.spreeEnderPlayerId);
	msg.set_source_name(breaker.spreeEnderName);
	msg.set_spree_level(level);
	msg.set_spree_points(breaker.endedPoints);
	msg.set_spree_breaker_type(breakerType);

	return msg;
}

odaproto::svc::NetdemoCap SVC_NetdemoCap(const player_t* player)
{
	odaproto::svc::NetdemoCap msg;

	odaproto::Actor* act = msg.mutable_actor();
	odaproto::Player* play = msg.mutable_player();

	const AActor* mo = player->mo;

	player->cmd.serialize(*msg.mutable_player_cmd());

	act->set_waterlevel(mo->waterlevel);
	act->mutable_pos()->set_x(mo->x);
	act->mutable_pos()->set_y(mo->y);
	act->mutable_pos()->set_z(mo->z);
	act->mutable_mom()->set_x(mo->momx);
	act->mutable_mom()->set_y(mo->momy);
	act->mutable_mom()->set_z(mo->momz);
	act->set_angle(mo->angle);
	act->set_pitch(mo->pitch);
	play->set_viewz(player->viewz);
	play->set_viewheight(player->viewheight);
	play->set_deltaviewheight(player->deltaviewheight);
	play->set_jumptics(player->jumpTics);
	act->set_reactiontime(mo->reactiontime);
	play->set_readyweapon(player->readyweapon);
	play->set_pendingweapon(player->pendingweapon);

	return msg;
}

VERSION_CONTROL(svc_message, "$Id: 0744b929e840294cebbfa519767ff258fa598d55 $")
