Appearance
Guild Creation
How to create guilds, either directly from creation params or through a PlayerAction that depends on player state.
Appearance
How to create guilds, either directly from creation params or through a PlayerAction that depends on player state.
Guild creation is always initiated by the client and validated on the server. There are two flows:
PlayerAction that can read and mutate player state before triggering creation. Use this when creation depends on or modifies player state, for example a creation cost, a cooldown, a level requirement, or copying player state into the new guild. If creation is refused, a refund action reverts the changes the action made.Two related types carry the data, and it helps to keep them distinct:
GuildCreationRequestParamsBase) are the client-set, client-visible values sent from the client. They are not yet validated.GuildCreationParamsBase) are the server-side, validated values that the guild is actually set up from. The server produces them from the request params and may augment them with server-side state the client never sees.Both flows converge on the same server-side step that turns the creation request params into validated creation params before a guild is created.
This is the default flow for simple guilds. Call GuildClient.CreateGuildAsync() with your game-specific creation request params. The returned task completes with true on success, or false if the server refuses the request.
Creation request params are a game-specific subclass of GuildCreationRequestParamsBase that contains client-visible values that have not yet been validated. For example, we could define them as:
[MetaSerializableDerived(1)]
public sealed class GuildCreationRequestParams : GuildCreationRequestParamsBase
{
[MetaMember(101)] public string DisplayName { get; set; }
[MetaMember(102)] public string Description { get; set; }
[MetaMember(103)] public GuildEmblemId Emblem { get; set; }
}These params are then supplied to CreateGuildAsync():
async Task OnCreateGuildButtonClick()
{
Spinner.enabled = true;
bool created = await MetaplayClient.GuildClient.CreateGuildAsync(
new GuildCreationRequestParams()
{
DisplayName = "My Guild",
Description = "A cool guild.",
Emblem = GuildEmblemId.FromString("Emblem1")
});
Spinner.enabled = false;
// If created = true, then:
// MetaplayClient.GuildClient.Phase = GuildClientPhase.GuildActive
// MetaplayClient.GuildClient.GuildContext is set
}From here the flow continues on the server, which validates the request and sets up the guild. See Server-Side Processing.
When guild creation depends on player state, dispatch it through a PlayerAction instead. The action runs against the PlayerModel, so it can:
Use GuildClient.CreateGuildWithActionAsync() with a PlayerAction passing in a query ID. The action validates and mutates player state, then calls player.ServerListenerCore.TryCreateNewGuild(...), passing the creation parameters. TryCreateNewGuild() triggers the guild creation and it will complete asynchronously.
On success, the returned task completes with a successful result (the action's own MetaActionResult). If the action itself fails, for example due to a lack of resources, the failure result is returned and no guild creation is attempted. If the server refuses the operation, the method completes with MetaActionResult.GuildCreationFailed.
The server will refuse the creation if the player is already in a guild, the creation request params are invalid, or validation fails. In this case, you may want to refund or restore any changes the action has completed. In order to do this, supply ServerListenerCore.TryCreateNewGuild(...) with a refundAction restoring the state. The refund action is a PlayerSynchronizedServerAction applied on the server. It can revert any state the initiating action changed, not just resource costs. For example, if the action also started a cooldown timer, the refund action can clear it.
Define the initiating PlayerAction and a refund PlayerSynchronizedServerAction:
[ModelAction(ActionCodes.PlayerCreateGuildWithCost)]
public class PlayerCreateGuildWithCost : PlayerAction
{
public int QueryId { get; private set; }
public string DisplayName { get; private set; }
public string Description { get; private set; }
PlayerCreateGuildWithCost() { }
public PlayerCreateGuildWithCost(int queryId, string displayName, string description)
{
QueryId = queryId;
DisplayName = displayName;
Description = description;
}
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
// Check player has the resources.
// For a level limit or a cooldown check, we would check it here.
int gemCost = player.GameConfig.GlobalConfig.GuildCreationGemCost;
if (player.Wallet.NumGems < gemCost)
return ActionResult.NotEnoughResources;
if (commit)
{
// Consume resources.
// For a cooldown check, we would set the cooldown here
player.Wallet.NumGems -= gemCost;
// Build the request params from the client-supplied values plus player state:
// players with a VIP subscription get a special guild emblem.
bool hasVipSubscription = player.IAPSubscriptions.Subscriptions.Any(/* ... */);
GuildCreationRequestParams creationParams = new GuildCreationRequestParams
{
DisplayName = DisplayName,
Description = Description,
Emblem = hasVipSubscription ? GuildEmblemId.FromString("EmblemVip") : GuildEmblemId.FromString("Emblem1"),
};
// Start creation.
// On failure, we refund the gems paid
player.ServerListenerCore.TryCreateNewGuild(
this,
QueryId,
creationParams,
refundAction: new PlayerRefundGuildCreationGems(gemCost));
}
return ActionResult.Success;
}
}
[ModelAction(ActionCodes.PlayerRefundGuildCreationGems)]
public class PlayerRefundGuildCreationGems : PlayerSynchronizedServerAction
{
public int NumGems { get; private set; }
PlayerRefundGuildCreationGems() { }
public PlayerRefundGuildCreationGems(int numGems)
{
NumGems = numGems;
}
public override MetaActionResult Execute(PlayerModel player, bool commit)
{
if (commit)
player.Wallet.NumGems += NumGems;
return ActionResult.Success;
}
}Then call it from the client:
async Task OnCreateGuildButtonClick()
{
Spinner.enabled = true;
MetaActionResult result = await MetaplayClient.GuildClient.CreateGuildWithActionAsync(
queryId => new PlayerCreateGuildWithCost(
queryId: queryId,
displayName: "My Guild",
description: "A cool guild."));
Spinner.enabled = false;
// If result.IsSuccess, then:
// MetaplayClient.GuildClient.Phase = GuildClientPhase.GuildActive
// MetaplayClient.GuildClient.GuildContext is set
}From here the flow continues on the server, which validates the request and sets up the guild. See Server-Side Processing.
This is where both flows continue. Your PlayerActor guild component implements TryCreateGuildCreationParamsFromRequestAsync(), which converts the creation request params into creation params. This runs on the PlayerActor, so it can read player state (including ServerOnly state) and stamp the creation params with values the client cannot set.
Creation params are a game-specific subclass of GuildCreationParamsBase. The base type already carries DisplayName, Description, the creation query ID, and the refund action, so the subclass only adds game-specific fields:
[MetaSerializableDerived(1)]
public sealed class GuildCreationParams : GuildCreationParamsBase
{
[MetaMember(101)] public GuildEmblemId Emblem { get; set; }
[MetaMember(102)] public CountryId? Country { get; set; }
}TryCreateGuildCreationParamsFromRequestAsync() implements the conversion from request params to creation params, optionally refusing the creation:
public sealed class PlayerActor : PlayerActorBase<PlayerModel>, IPlayerModelServerListener
{
// ...
public sealed class GuildComponent : GuildComponentBase<PlayerActor>
{
protected override Task<GuildCreationParamsBase> TryCreateGuildCreationParamsFromRequestAsync(
GuildCreationRequestParamsBase paramsBase,
PlayerActionBase invokingAction)
{
GuildCreationRequestParams requestParams = (GuildCreationRequestParams)paramsBase;
// Refuse if client gives no parameters
if (requestParams == null)
return Task.FromResult<GuildCreationParamsBase>(null);
// If we expect Action flow, we could check invokingAction is not null
// Create params
return Task.FromResult<GuildCreationParamsBase>(new GuildCreationParams()
{
DisplayName = requestParams.DisplayName,
Description = requestParams.Description,
// We could implement the VIP emblems here too
Emblem = requestParams.Emblem,
// We can use Player data directly without having
// client pass them.
Country = Player.Model.LastKnownLocation?.Country,
});
}
}
}By default, the SDK imposes basic requirements on the guild's display name and description. Some sanity length limits are applied, and any control characters are disallowed. The Name and Description requirements apply both when creating a new Guild and when editing an existing one.
You can optionally change these limits, or add your own rules, by subclassing GuildRequirementsValidator. You can either override the length-limit properties to adjust the default rules, or override the methods completely to add custom checks such as profanity filtering.
public class MyGuildRequirementsValidator : GuildRequirementsValidator
{
// For example, customize default limits
public override int MaxDisplayNameLength => 16;
public override int MaxDescriptionLength => 120;
// For example, customize rules
// public override async Task<bool> ValidateDisplayNameAsync(string displayName) { ... }
// public override async Task<bool> ValidateDescriptionAsync(string description) { ... }
// public override async Task<bool> ValidateGuildCreationAsync(GuildCreationParamsBase baseArgs) { ... }
}If either creation param transformation or validation refuses, no guild is created. For the action flow, the refund action is then run on the player to revert the action's changes.
Once the creation params are accepted, the guild entity is created and GuildActor.SetupGuildWithCreationParams() is called with them. Cast the GuildCreationParamsBase to your game type and apply its fields to the new guild's Model. DisplayName and Description are applied by the SDK, so you only handle your game-specific fields.
public sealed class GuildActor : GuildActorBase<GuildModel, PersistedGuild>, IGuildModelServerListener
{
// ...
protected override void SetupGuildWithCreationParams(GuildCreationParamsBase baseArgs)
{
GuildCreationParams args = (GuildCreationParams)baseArgs;
Model.Emblem = args.Emblem;
Model.Country = args.Country;
}
}The Metaplay Status Window in the Unity Editor shows a Create Guild button while the player is not in a guild. By default it calls CreateGuildAsync(null), exercising the params flow with no params.
Games can override GuildCreateNewUI(GuildClient) to customize this button, for example to drive the action flow instead:
class MyMetaplaySDKWindowEditor : MetaplaySDKEditorWindow
{
protected override void GuildCreateNewUI(GuildClient guildClient)
{
if (GUILayout.Button("Create Guild"))
{
MetaTask.Run(async () =>
{
MetaActionResult result = await guildClient.CreateGuildWithActionAsync(
queryId => new PlayerCreateGuildWithCost(queryId, "My Guild", "A cool guild."));
Debug.Log($"[Metaplay] Create Guild result: {result}");
}, scheduler: MetaTask.UnityMainScheduler);
}
}
}