Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions MiraAPI/Networking/MiraRpc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,9 @@ public enum MiraRpc : uint
/// Custom RPC to sync the Gamemode option.
/// </summary>
SyncGamemodeOption,

/// <summary>
/// Host gives a dead player a custom ghost role on every client.
/// </summary>
SetGhostRole,
}
15 changes: 11 additions & 4 deletions MiraAPI/Patches/Roles/RoleManagerPatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,24 +87,31 @@ public static void ModifierSelectionPatches()

[HarmonyPrefix]
[HarmonyPatch(nameof(RoleManager.AssignRoleOnDeath))]
public static bool AssignRoleOnDeath([HarmonyArgument(0)] PlayerControl plr)
public static bool AssignRoleOnDeath([HarmonyArgument(0)] PlayerControl plr, [HarmonyArgument(1)] bool specialRolesAllowed)
{
if (!plr || !plr.Data.IsDead)
{
return false;
}

if (plr.Data.Role is not ICustomRole role)
if (plr.Data.Role is ICustomRole role && role.Configuration.GhostRole is not (RoleTypes.CrewmateGhost or RoleTypes.ImpostorGhost))
{
plr.RpcSetRole(role.Configuration.GhostRole);
return false;
}

if (!specialRolesAllowed || !AmongUsClient.Instance.AmHost || RoleManager.IsGhostRole(plr.Data.Role.Role))
{
return true;
}

if (role.Configuration.GhostRole is RoleTypes.CrewmateGhost or RoleTypes.ImpostorGhost)
var ghostRole = GhostRoleAssignment.Roll(plr.Data.Role.IsImpostor);
if (ghostRole == null)
{
return true;
}

plr.RpcSetRole(role.Configuration.GhostRole);
GhostRoleAssignment.RpcSetGhostRole(PlayerControl.LocalPlayer, plr, (ushort)ghostRole.Value);
return false;
}
}
2 changes: 2 additions & 0 deletions MiraAPI/Roles/CustomRoleConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ public CustomRoleConfiguration(ICustomRole role)

/// <summary>
/// Gets or sets a value indicating whether the role should show up in the Role Options menu.
/// Defaults to true for ghost roles. A ghost role that turns it off is also handed out on death from its count and
/// chance, like vanilla's Guardian Angel and Spirit Guide.
/// </summary>
public bool HideSettings { get; set; }

Expand Down
78 changes: 78 additions & 0 deletions MiraAPI/Roles/GhostRoleAssignment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Linq;
using AmongUs.GameOptions;
using MiraAPI.Networking;
using Reactor.Networking.Attributes;

namespace MiraAPI.Roles;

/// <summary>
/// Hands out custom ghost roles (a role whose <see cref="RoleBehaviour.IsDead"/> is true) when a player dies, from the
/// role's lobby count and chance, the way vanilla hands out Guardian Angel and Spirit Guide. Only ghost roles with
/// <see cref="CustomRoleConfiguration.HideSettings"/> turned off take part; ghost roles are hidden by default because
/// most are reached through another role's <see cref="CustomRoleConfiguration.GhostRole"/> instead. Freeplay assigns
/// any ghost role whose count is above zero, so enabling one in the lobby is enough to test it.
/// </summary>
internal static class GhostRoleAssignment
{
public static RoleTypes? Roll(bool impostor)
{
var roleOptions = GameOptionsManager.Instance.CurrentGameOptions.RoleOptions;
var freeplay = AmongUsClient.Instance.NetworkMode == NetworkModes.FreePlay;

var candidates = CustomRoleManager.CustomRoleBehaviours
.Where(role => role.IsDead && role.IsImpostor == impostor && CustomRoleUtils.CanSpawnOnCurrentMode(role))
.Select(role => role.Role)
.OrderBy(_ => HashRandom.Next(int.MaxValue));

foreach (var role in candidates)
{
var maxCount = roleOptions.GetNumPerGame(role);
if (maxCount <= 0)
{
continue;
}

if (freeplay)
{
return role;
}

var held = PlayerControl.AllPlayerControls.ToArray()
.Count(player => player != null && player.Data != null && player.Data.Role != null && player.Data.Role.Role == role);
if (held < maxCount && HashRandom.Next(101) < roleOptions.GetChancePerGame(role))
{
return role;
}
}

return null;
}

// What vanilla's CoSetRole does for a ghost role, run on every client from the host's decision.
[MethodRpc((uint)MiraRpc.SetGhostRole)]
public static void RpcSetGhostRole(PlayerControl sender, PlayerControl target, ushort roleId)
{
if (sender == null || AmongUsClient.Instance == null || sender.OwnerId != AmongUsClient.Instance.HostId)
{
return;
}

if (target == null || target.Data == null || target.Data.Disconnected)
{
return;
}

var role = (RoleTypes)roleId;
if (!RoleManager.InstanceExists || !RoleManager.IsGhostRole(role))
{
return;
}

RoleManager.Instance.SetRole(target, role);
target.Data.Role.SpawnTaskHeader(target);
if (target == PlayerControl.LocalPlayer && HudManager.InstanceExists)
{
HudManager.Instance.ReportButton.gameObject.SetActive(false);
}
}
}
Loading