foxygit / GZDoom Log in
commits tags

/src/net/webjoin/wj_ticcmd.cpp · 9.17 KB

raw
// wj_ticcmd.cpp -- main-thread-only. See wj_ticcmd.h and wj_input.h for the
// threading contract. Two jobs, both mirroring existing bot-admission code
// (src/playsim/bots/b_game.cpp FCajunMaster::DoAddBot) but skipping the
// network command stream, since a phone-joined player is always local:
// there is no other real node to keep in sync with (doomcom.numnodes stays
// 1 for this whole feature -- see the plan's non-goals).
//
//   1. Admit phones that finished their WS handshake into a free player
//      slot, once a level is actually running.
//   2. Each gametic, turn every remote-input slot's latest buffered stick/
//      button state into a ticcmd_t and splice it into netcmds[], exactly
//      where DBot::Think() (b_think.cpp) splices in bot commands.

#include "wj_ticcmd.h"
#include "wj_input.h"

#include <cmath>
#include <cstring>
#include <cstdlib>

#include "doomdef.h"
#include "doomstat.h"
#include "d_player.h"
#include "d_net.h"
#include "d_netinf.h"
#include "d_event.h"
#include "g_levellocals.h"
#include "events.h"
#include "zstring.h"
#include "printf.h"
#include "c_cvars.h"
#include "c_dispatch.h"

// Not exposed via a header (g_game.cpp keeps them as plain globals); this
// mirrors the exact scaling the joystick input path uses in G_BuildTiccmd.
extern int forwardmove[2], sidemove[2];

EXTERN_CVAR(Float, maxviewpitch)

namespace
{
	void AdmitPendingJoins()
	{
		if (gamestate != GS_LEVEL)
			return; // nothing to spawn into yet; leave the queue for later

		std::vector<WJPendingJoin> pending;
		{
			std::lock_guard<std::mutex> lock(wj_joinQueue.mtx);
			if (wj_joinQueue.pending.empty())
				return;
			pending.swap(wj_joinQueue.pending);
		}

		std::vector<WJPendingJoin> stillWaiting;
		for (const WJPendingJoin &join : pending)
		{
			int bnum;
			for (bnum = 0; bnum < MAXPLAYERS; bnum++)
			{
				if (!playeringame[bnum])
					break;
			}
			if (bnum == MAXPLAYERS)
			{
				stillWaiting.push_back(join); // full, try again next tic
				continue;
			}

			// Matches FCajunMaster::DoAddBot (b_game.cpp): DoReborn()'s branch
			// for a *singleplayer* death reloads the whole level instead of
			// just respawning the player, because `multiplayer` was decided
			// once at level start, before any phone had joined. Without
			// this, admitting a second player still looks like solo play to
			// DoReborn and it reloads MapName instead of spawning slot bnum.
			multiplayer = true;

			playeringame[bnum] = true;
			players[bnum].mo = nullptr;
			players[bnum].playerstate = PST_ENTER;
			players[bnum].RemoteInputSlot = true;

			FString info;
			int hue = (bnum * 47) % 360; // spread slot colors out without picking a palette
			float h = hue / 60.f, r, g, b;
			// Cheap hue->RGB without pulling in the HSV helper header here.
			float x = 1 - fabsf(fmodf(h, 2.f) - 1);
			if (h < 1) { r = 1; g = x; b = 0; }
			else if (h < 2) { r = x; g = 1; b = 0; }
			else if (h < 3) { r = 0; g = 1; b = x; }
			else if (h < 4) { r = 0; g = x; b = 1; }
			else if (h < 5) { r = x; g = 0; b = 1; }
			else { r = 1; g = 0; b = x; }
			info.Format("\\name\\Phone %d\\color\\%02x %02x %02x", bnum + 1,
				int(r * 255), int(g * 255), int(b * 255));

			const char *infoPtr = info.GetChars();
			D_ReadUserInfoStrings(bnum, (uint8_t **)&infoPtr, false);

			// connToken < 0 marks a fake test player from the webjoin_spawn
			// CCMD below -- there's no real WS connection to associate, so
			// it just sits idle (BuildRemoteTiccmd treats "no connection"
			// the same as "phone hasn't sent anything yet": stands still).
			if (join.connToken >= 0 && !WJ_OnSlotAssigned(join.connToken, bnum))
			{
				// The phone vanished between finishing its handshake and
				// getting here -- undo the admission, nothing will ever
				// drive this slot.
				playeringame[bnum] = false;
				players[bnum].RemoteInputSlot = false;
				continue;
			}

			primaryLevel->DoReborn(bnum);
			primaryLevel->localEventManager->PlayerEntered(bnum, false);
			Printf("webjoin: %s joined as player slot %d\n", players[bnum].userinfo.GetName(), bnum);
		}

		if (!stillWaiting.empty())
		{
			std::lock_guard<std::mutex> lock(wj_joinQueue.mtx);
			wj_joinQueue.pending.insert(wj_joinQueue.pending.end(),
				stillWaiting.begin(), stillWaiting.end());
		}
	}

	void BuildRemoteTiccmd(int slot, ticcmd_t *cmd)
	{
		// Real clients get their pitch clamp from player_t::SendPitchLimits
		// (p_user.cpp), but that only ever sends for consoleplayer -- "the
		// local player is the only one our data is valid for" -- since it's
		// meant to travel over the network command stream to everyone else,
		// a step remote-input slots don't go through. It also gets reset to
		// 0/0 (fully locked) on every spawn and respawn by the same code
		// that resets player_t::RemoteInputSlot (p_mobj.cpp's player spawn
		// reset, "will be filled in by PostBeginPlay()/netcode"). Cheapest
		// correct fix: just keep reapplying it every tic instead of hooking
		// every spawn path.
		if (players[slot].mo != nullptr)
		{
			players[slot].MinPitch = DAngle::fromDeg(-maxviewpitch);
			players[slot].MaxPitch = DAngle::fromDeg(maxviewpitch);
		}

		WJInputPacket packet;
		bool connected;
		{
			WJRemoteInput &ri = wj_remoteInput[slot];
			std::lock_guard<std::mutex> lock(ri.mtx);
			connected = ri.connected;
			packet = ri.packet;
		}

		memset(cmd, 0, sizeof(*cmd));

		if (!connected)
		{
			// The phone hung up. Leave the slot in the game but idle for
			// this tic; AdmitPendingJoins doesn't reclaim it -- dropping a
			// player mid-game needs the same care as a real client
			// disconnecting, which is follow-up work, not this pass.
			return;
		}

		const float scale = 1.f / 32768.f;
		float moveX = packet.moveX * scale;
		float moveY = packet.moveY * scale;
		float turnX = packet.turnX * scale;
		float turnY = packet.turnY * scale;

		// G_BuildTiccmd's local input path (g_game.cpp) left-shifts forward/
		// sidemove by 8 as its very last step before returning -- easy to
		// miss since it's not near where the values are otherwise computed.
		// Skipping it doesn't break movement, just makes it ~256x slower
		// than it should be, since the walk/run code that later reads
		// these fields expects that fixed-point-style scale.
		cmd->ucmd.forwardmove = (short)(((int)(forwardmove[1] * moveY)) << 8);
		cmd->ucmd.sidemove = (short)(((int)(sidemove[1] * moveX)) << 8);

		// Full-speed turn/look rate, matching the joystick input path's
		// feel: +/-1 stick deflection turns 180 degrees per second.
		// Yaw is negated: positive angle in this engine turns the view
		// counterclockwise (left), so dragging the stick right (positive
		// turnX) needs a negative yaw delta to turn the view right instead
		// of left.
		//
		// Pitch is NOT negated, and this was gotten wrong once already: per
		// DBot::Think (b_think.cpp) -- "actor->Angles.Pitch = oldpitch -
		// DAngle::fromDeg(cmd->ucmd.pitch * ...)" -- a positive ucmd.pitch
		// *decreases* Angles.Pitch, and per player_t::MinPitch's own comment
		// ("negative is up, positive is down") a smaller/more negative
		// Angles.Pitch means looking up. So positive ucmd.pitch -> look up,
		// matching turnY already being positive for "stick dragged up" once
		// the phone page's own -lookStick.y flip (wj_page.h) is accounted
		// for. A negation here was previously "confirmed" by eyeballing a
		// single screenshot's perspective, which turned out to be an
		// unreliable way to judge pitch direction -- this was wrong, and
		// inverted look up/down for anyone actually playing.
		const float turnUnitsPerSec = 65536.f * 0.5f;
		cmd->ucmd.yaw = (short)(-turnX * turnUnitsPerSec / TICRATE);
		cmd->ucmd.pitch = (short)(turnY * turnUnitsPerSec / TICRATE);

		cmd->ucmd.buttons = packet.buttons &
			(BT_ATTACK | BT_USE | BT_JUMP | BT_CROUCH | BT_ALTATTACK | BT_SPEED);
	}
}

void WJ_Tick()
{
	AdmitPendingJoins();

	for (int i = 0; i < MAXPLAYERS; i++)
	{
		if (!playeringame[i] || !players[i].RemoteInputSlot)
			continue;

		ticcmd_t *cmd = &netcmds[i][((gametic + 1) / ticdup) % BACKUPTICS];
		BuildRemoteTiccmd(i, cmd);
	}
}

// Testing aids: add/remove stationary phone-shaped player slots without
// needing an actual phone. Spawned players stand still (no wj_remoteInput
// connection ever marks them connected, which BuildRemoteTiccmd already
// treats as "send nothing") -- good enough to test splitscreen layout,
// admission, and the roster/lobby display without a real device.
CCMD(webjoin_spawn)
{
	int count = argv.argc() > 1 ? atoi(argv[1]) : 1;
	if (count < 1) count = 1;

	std::lock_guard<std::mutex> lock(wj_joinQueue.mtx);
	for (int i = 0; i < count; i++)
		wj_joinQueue.pending.push_back(WJPendingJoin{ -1, std::string() });
}

CCMD(webjoin_despawn)
{
	int count = argv.argc() > 1 ? atoi(argv[1]) : 1;
	if (count < 1) count = 1;

	int removed = 0;
	for (int i = MAXPLAYERS - 1; i >= 0 && removed < count; i--)
	{
		if (playeringame[i] && players[i].RemoteInputSlot && players[i].playerstate != PST_GONE)
		{
			// G_Ticker (called right after WJ_Tick each gametic) pops any
			// player in this state on its own -- see G_DoPlayerPop in
			// g_game.cpp -- so this is the same path a real disconnect
			// takes, not a shortcut around it.
			players[i].playerstate = PST_GONE;
			removed++;
		}
	}
	Printf("webjoin: despawned %d player%s\n", removed, removed == 1 ? "" : "s");
}