Appearance
Release 38
August 26th, 2026
Appearance
August 26th, 2026
Your coding agent can now be an expert Metaplay developer. Metaplay Agent teaches Claude Code, Codex, OpenCode, and other coding harnesses how the Metaplay SDK actually works. This allows you to build new features faster, catch SDK usage mistakes in your code, and diagnose everything from player incidents to server errors.
To give it a try, prompt your agent with:
Run `metaplay skills try` to discover available skills, then review all the PlayerActions in my code.Happy with it? Run metaplay skills install, commit the result, and your whole team gets the skills in every session.
See the Metaplay Agent guide for more information on how to use it effectively.
The Localization Contents view has been rewritten to handle larger datasets and to improve search with locale-aware behavior. This makes it easier to view and debug your localization strings.
Most of the LiveOps Dashboard's workflows are based on viewing lists of data. We're rolling out a new MListCard that is faster and better in many ways: renders faster, supports more data, has full type safety, supports presets, has fast searching, supports more complex combinations of filtering/sorting, supports highlighting in addition to filtering.
The new list card in action.
Presets allow you to quickly access frequently used filters.
Full filtering capabilities inside the utility menu.
The majority of the dashboard's own list cards have been migrated to use this new component. We will gradually complete this migration and deprecate the old MetaListCard but, for now, this new component will stay as an internal component in @metaplay/meta-ui-next/unstable. We will make it publicly available in an upcoming release once the API has stabilized.
Implementing offers with an in-game currency cost (instead of real-money IAP) is now better supported and requires less custom work. See In-Game Currency Offers.
Metaplay now supports game clients that run entirely in the browser. The client is written in C# with Blazor WebAssembly instead of Unity, and runs against a normal Metaplay game server with the usual shared game logic.
It is aimed at fast, AI-driven prototyping of new games. A browser client is all code and no editor, which makes it a natural fit for coding agents: they can generate the client and the backend in one C# codebase, iterate in seconds, and test the result by driving the running game in a real browser.
This is a preview feature: it is not yet supported or documented. The Samples/HelloBlazorWasm sample shows a full working project, so if you're curious, let your agent loose on it! We'll publish more soon. Stay tuned.
GuildActor.OnMemberJoinedAsync() and OnMemberLeftAsync() for reacting to members joining or leaving the guild.PlayerModel, making it readable by player actions. See Synchronizing Data Between Player and Guild for details.X-Forwarded-For header (WebSocket connections behind an HTTP-aware proxy) or via the PROXY protocol v2 header (WebSocket and raw TCP connections behind an L4 load balancer). Select the mechanism with the new ClientConnectionOptions.ForwardedClientInfoSource runtime option.PlayerInAppPurchased analytics event now includes the purchased Offer, Offer Group, and Placement.DeveloperOnly config field, restricting them to Developer Players for testing.PlayerActor.GuildComponentBase.OnKickedFromGuildAsync() for reacting to the player being kicked from a guild.GuildClient.CreateGuildWithActionAsync() for creating a guild with a PlayerAction to allow creation to have a game-resource cost or depend on player state. See Guild Creation for details.F32Vec2.Cross() and F64Vec2.Cross() for computing the scalar 2D cross product.MetaTask.ContinueWithOnUnityThread() overloads for scheduling task continuations on the Unity main thread.PrettyPrint's default length limit (1 MiB) can now be overridden with a parameter to Compact() and Verbose().WireMessageTooLargeException and the corresponding NetworkMessageTooLargeError incident report now contain more information about the content of the too-large message. In particular, when the message is a PlayerEnqueueSynchronizedServerAction (the consequence of EnqueueServerAction(...) in PlayerActor), the error message now contains the type name of the action.ClientConnection:MaxCompressedPacketPayloadSize. This is intended to help notice bloated messages before they grow big enough to hit the hard limit and cause problems. By default, the warning threshold is 90% of the hard limit, and can be configured with ClientConnection:PacketPayloadSizeWarningThreshold.MetaSerialization.SerializeTableTagged(), DeserializeTableTagged(), CloneTableTagged(), ResolveMetaRefsInTable(), and TraverseMetaRefsInTable(), along with the visitTableTopLevelConfigItem callback of MetaSerializationMetaRefTraversalParams. Game config libraries now serialize as ordinary value collections: use SerializeTagged()/DeserializeTagged() over a GameConfigDataContent<TInfo>[], GameConfigUtil.CloneLibraryItems(), or GameConfigUtil.TraverseConfigItemMetaRefs() instead. The replacements take no maxCollectionSizeOverride, as game config libraries are no longer size-limited.DefaultPlayerClientContext constructor now takes IMetaplaySubClientServices as the first parameter, and no longer accepts individual services as is.BotClientBase.HandleStartSession() has been renamed to OnSessionStartedAsync() and is now async. The signature changed from (SessionStartSuccess, IPlayerModelBase, ISharedGameConfig) to (BotSessionStartedArgs args).BotClientBase now handles SessionStartSuccess, PlayerAckActions, PlayerExecuteUnsynchronizedServerAction, and PlayerChecksumMismatch automatically, removing the need for manual processing in OnNetworkMessage() overrides.TaggedSerializerRoslyn no longer exposes generic helper overloads. Use overloads with Type and object types instead.Metaplay.Core.Serialization.Editor, instead of Metaplay.Unity.Editor.IncidentReportFactory.CreateUnhandledException() now requires the exception type name as an argument.GuildModel.HasPermissionTo*() methods now have default implementations, defaulting to false.GuildActor.CloseGuild() has been renamed to CloseGuildAsync().Task-returning GuildClient.Begin*() methods have been renamed to ...Async(). The old methods remain as obsolete wrappers. The callback overloads are unchanged.PlayerActor's guild component method TryCreateGuildCreationParamsFromRequest() is now async, is given the invoking action, and has been renamed to ...Async().GuildRequirementsValidator's overrides are now async and have been renamed to ...Async().GuildMemberPlayerDataBase.IsUpToDate().PlayerModelBase no longer takes the TPlayerGuildState type parameter.MetaplaySDK/Client/PrebuiltAnalyzers/ directory shared by the Unity and backend builds, de-duplicating the analyzer that was previously shipped as a separate copy for each.MetaplaySDK/Client/Unity/ into the new MetaplaySDK/Client/ClientCore/ directory and are now compiled into the Metaplay assembly instead of Metaplay.Unity.Directory.Build.targets and global.json files moved from MetaplaySDK/Backend/ to the MetaplaySDK/ root.METAPLAY_APP_BUILD_NUMBER and METAPLAY_APP_COMMIT_ID environment variables, which the server Docker image sets automatically from the BUILD_NUMBER and COMMIT_ID build args.IMetaplayClient interface (in the Metaplay.Client namespace).Metaplay.Unity.DefaultIntegration and Metaplay.Unity namespaces to Metaplay.Client, to communicate that they are used with non-Unity clients as well.Fingerprint field from the PlayerEventIncidentRecorded analytics event. The same value is derivable from the event's existing Type, SubType, and Reason fields.PlayerActorBase.OnPlayerIncidentRecorded() now takes the incident metadata (incidentId, type, subType, reason) instead of the full PlayerIncidentReport, as reports may be persisted outside the PlayerActor. See the migration guide.Util.ComputeMD5().Util.IsBase64Encoded().Metaplay.Unity.UnityLogger migration stub. The class was renamed to Metaplay.Core.MetaClientLogger in R34, and the stub has been a compile error since.Metaplay.Core.WebGL.WebSupportLib. It backed Unity WebGL early-launch localization and threw NotSupportedException on every other platform. Its functionality now lives directly in BuiltinLanguageRepository.MetaplayController.AskEntityAsync(), MetaplaySDK.RunOnUnityThreadAsync(), or MetaplaySDKConfig.AutoCreateMetaplaySDKBehavior / MetaplayClientOptions.AutoCreateMetaplaySDKBehavior is now a compile error instead of a warning. These have been deprecated since R29–R36 and will be removed in R39. Use EntityAskAsync() and RunOnMainThreadAsync(). For AutoCreateMetaplaySDKBehavior, drop the assignment: it no longer has any effect, and SDK status is now shown in the Menu > Metaplay > Status Window instead of the MetaplaySDKBehavior Inspector.TimeSpan Runtime Option values must now use the [d.]hh:mm:ss[.fffffff] or d:hh:mm:ss[.fffffff] format. Previously ambiguous inputs such as 6 (parsed as 6 days) or 6:12 (parsed as 6 hours 12 minutes) are now rejected.ConnectionConfig.MaxSessionRetainingFrameDuration setting.Execute() now results in a clearer error message.entrypoint binary to use Go v1.26.5.toybox binary from v0.8.11 to v0.8.14.metaplay init project now use the XML-based .slnx solution format (Backend/Server.slnx) instead of .sln. Existing projects should migrate their backend solution to .slnx — see the upgrade steps below.CompressUtil.DeflateCompress() now takes an explicit CompressionLevel. The overload without one is obsolete; pass CompressionLevel.Fastest to keep the previous behavior.ClientConnection:EnableWireCompression runtime option. Wire compression of large packets is now always enabled, as there should be no reason to disable it.PlayerActor.RegisterSynchronizedServerAction() now returns a RegisteredSynchronizedServerActionHandle instead of an int tracking id.ClientConnection:ProtocolHashMismatchIsError server runtime option. When enabled, client login fails with an error.IMetaplayConnectionDelegate.OnFullProtocolHashMismatch is now deprecated. Consider using ClientConnection:ProtocolHashMismatchIsError for version enforcement instead.PrettyPrinter.Difference() is deprecated. Use SerializedObjectComparer to compare serialized objects. Arbitrary .NET objects cannot be compared robustly.MetaplayConnection.Close() and CloseWithError() no longer take the flushEnqueuedMessages parameter. Enqueued messages are flushed in the background.ConnectionConfig.CloseFlushTimeout setting as the flush now completes in the background.Portable.BouncyCastle 1.9.0 to BouncyCastle.Cryptography 2.6.2.docker/dockerfile:1.25.0 build syntax and stages the runtime output in /out. The integration test image is about 250 MB smaller and quicker to build.Microsoft.Extensions.Caching.Memory, Microsoft.AspNetCore.Mvc.NewtonsoftJson, System.IO.Hashing, Serilog.AspNetCore) moved from the 9.0.x line to 10.0.x, and the AWS SDK, gRPC, Protobuf, Dapper, Serilog, YamlDotNet, System.Interactive.Async and ZstdSharp.Port moved to their latest releases.metaplay test integration. The test projects, including the Playwright.NET system tests, are all built while building the image, so the test container doesn't restore NuGet packages or compile code when it runs.MetaSerializableTypeProviderAttribute and MetaSerializableTypeGetterAttribute have been removed because they're little-used and incompatible with our plans of refactoring the serializer generation using C# source generators. The SDK-side use cases of these attributes have been inlined in MetaSerializerTypeScanner. No direct replacement has been created for project-specific uses - please contact support if you use these attributes and cannot achieve the same by other means.Grpc.Net.Client instead of Grpc.AspNetCore. Your server builds download 80 MB less, since Grpc.AspNetCore pulled in protoc binaries that were never used. If your own code uses gRPC hosting APIs, add Grpc.AspNetCore to your server project.com.unity.collections v2.6.8 (up from v1.4.0) and com.unity.burst v1.8.29 (up from v1.8.17), which require Unity 2022.3.20f1 or newer. Checksum hashing now uses Unity's xxHash3 on all platforms and input sizes; the SDK's bundled copy, which worked around Unity bugs fixed in Collections v2.5.2 and v2.6.8, has been removed.OrderedDictionary<,> or ToOrderedDictionary(), renamed to MetaDictionary<,> and ToMetaDictionary() in R31, is now a compile error instead of a warning. The MP_OD_01 and MP_OD_02 analyzers that automated the rename have been removed.MetaIndexedDB and MetaWebBlobStore) moved onto a globalThis.Metaplay namespace.event_time, which devices showed as a notification dated to the epoch and sorted to the bottom of the notification panel.<unspecified>.GuildActor.CloseGuild() now properly kicks all members.Direct transport ... unexpectedly lost warning for multiplayer entities.DebugLog output now follows the same log formatting rules as other loggers.[PlayerDashboardAction(...)] classes can now be explicitly marked with a suitable [ModelActionExecuteFlags(...)] attribute as an alternative to inheriting from PlayerSynchronizedServerAction or PlayerUnsynchronizedServerAction.PrettyPrint now respects the length limit (1 MiB by default) exactly. If the limit would be exceeded, the output marks it clearly with suffix <truncated> instead of overflowing the limit with garbled output.PlayerModelBase.FullInAppPurchaseHistory and MailInbox no longer hit the default collection size limit. Neither is truncated by the SDK, so players could accumulate more than 16,384 entries and then fail to serialize. Their limit is now raised to 1,000,000 entries.OnApplicationAboutToBePaused(...) would sometimes be ignored when the server connection was very low-latency, such as with a localhost server.internal, protected internal or private protected, or was a property without a getter.System.Security.Cryptography.Xml to 10.0.10 (from 9.0.15) to silence advisory GHSA-g8r8-53c2-pm3f (CVE-2026-47304). The SDK does not use the vulnerable XML-encryption code path and is not affected.FileNotFoundException for Metaplay.Generated. The build now verifies that the assembly is actually included in the player and fails with a clear error if it is not, instead of producing a broken build. Cleaning up the temporary Assets/MetaplayGenerated_DONOTSAVE folder is also simpler: it is deleted with plain file operations rather than through the asset database and version control, and on Unity 6000.3 and newer it is cleaned after failed and canceled builds as well.gameConfig admin API endpoint now uses a more precise caching timestamp in the ETag header instead of Last-Modified-Since. This fixes the dashboard's game config list sometimes showing stale data, particularly when a game config build failed very soon after being started.MLoadingSkeleton component for displaying animated skeleton placeholders while content loads.MLoadingSpinner component for displaying an animated spinner while content loads.always-real-time prop for MDateTime, letting a timestamp be displayed in wall clock time even while a game time skip is active in an environment.neutral variant to MInputCheckbox and MInputMultiSelectCheckbox.update:open event on MInputSingleSelectDropdown, emitted when its options popover opens or closes.MDataExplorer now highlights search matches per cell instead of per row. This leads to a more accurate search visualization in Runtime Options and Raw Data views.integrationApi's GameSpecificPlayerResource.getAmount() has been changed from number to number | string to allow more custom visualizations of player resources and other data in that vein.MInputNumber, MInputSingleFile, MInputSingleSelectAsyncDropdown, MInputSingleSelectDropdown, MInputText and MInputTextArea components for a more consistent style.MTimeSeriesBarChart, MTimeSeriesStackedBarChart, and MDailyCohortsBarChart) now share a unified height API. The height value 'half' has been renamed to 'compact', and all chart types now render at the same 'full'/'compact' heights.MInputSwitch has more visually obvious disabled states.MClipboardCopy has moved from unstable to stable.MClipboardCopy no longer renders a hardcoded copy-to-clipboard test id. Pass the new data-testid prop instead, as with the other components. Every instance used to render the same id, so a page with two copy buttons produced an ambiguous selector.@metaplay/meta-ui-next/unstable entry point. If you import one of these components, change the import to @metaplay/meta-ui-next/unstable.Servers tab of the Environment page no longer erroneously applies the game time skip that may have been applied in development environments.MCard no longer clips the focus rings of inputs such as MInputSwitch and MInputCheckbox placed in the card body.MInputSwitch no longer has a too-large focus ring.MIconButton is now square instead of being taller than it is wide, and no longer grows the height of the line it sits on. This makes it usable inline next to text.MInputSingleSelectDropdown, MInputMultiSelectDropdown and MInputDate.Samples/HelloWorld/RawClient) that runs the shared game logic without Unity..slnx solution format.There are no known issues in this release.
Please apply the following changes to your project to ensure compatibility with the latest Metaplay SDK.
Backward-Incompatible Changes
Bump your game's MetaplayCoreOptions.supportedLogicVersions to force a synchronized update of your game client and server.
R38 changes the wire encoding of game config archives. The server rewrites the client-facing archive when it starts, so clients built with an earlier SDK can no longer load the game config once the R38 server is deployed. Without the version bump below, those clients fail while loading the config instead of being rejected at login and prompted to update.
Migration Steps:
Raise both ends of supportedLogicVersions so pre-R38 clients fall below the minimum, and set clientLogicVersion to the new maximum:
supportedLogicVersions: new MetaVersionRange(1, 1),
supportedLogicVersions: new MetaVersionRange(2, 2),
clientLogicVersion: 1,
clientLogicVersion: 2, Note that the new archive format cannot be read by an earlier SDK version so you cannot publish an archive made with Release 38 to a server running an older SDK.
Release 38 requires the metaplay-gameserver Helm chart v0.10.0 or later for cloud deployments. From this version the chart always serves the WebSocket port used by browser-based (WebGL) clients.
Migration Steps:
Update your metaplay-project.yaml to the latest Helm chart version.
serverChartVersion: 0.x.y
serverChartVersion: 0.10.0Roll out the change to each environment by running the CI job to build and deploy a new version.
You can do this for each environment separately, whenever is a good time for you.
Premium SDK Update Support
If your support contract includes Metaplay-provided SDK updates, all the following steps have already been applied to your project. You can skip this migration guide!
This guide offers step-by-step instructions for migrating your project to the latest version of the Metaplay SDK. You can skip the migration steps for features you are not using in your project.
It's a good idea to run the Metaplay integration test suite on your project before and after upgrading to the latest SDK version.
MyProject$ metaplay test integrationYou should get a clean test run before starting the upgrade process to know that your project is in a good state, and know that any test failures after the upgrade are related to the upgrade itself.
The following core SDK changes affect all Metaplay projects:
Metaplay's built-in database schema has changed, and you need to apply the migration steps to your project.
Migration Steps:
Generate the database schema migration code with:
# Install or update the EFCore tool:
Backend/Server$ dotnet tool install -g dotnet-ef
# Then, generate the migration code:
Backend/Server$ dotnet ef migrations add MetaplayRelease38Then, add the generated files to your project's source control. For example, using Git:
Backend/Server$ git add .
Backend/Server$ git commit -m "Database schema migrations"The migration steps will be automatically applied when you deploy the updated game server into an environment.
The following LiveOps Dashboard changes affect projects that have a game-specific dashboard project:
As usual, we have updated the underlying dependencies and configurations of the LiveOps Dashboard. This causes changes to configuration files, which you will need to update in your dashboard project. We use the MetaplaySDK/Frontend/DefaultDashboard folder as the source of truth for these files.
Migration Steps:
Update the package.json to update your project's dependencies.
package.json file directly from the MetaplaySDK/Frontend/DefaultDashboard directory. Next, restore the name property inside the file to that of your project. This is typically of the form "name": "<projectName>-dashboard".Vite 8 transforms sources with Oxc and pre-bundles dependencies with Rolldown instead of esbuild. The old esbuild options are deprecated and their target is ignored, so the build logs a deprecation warning and falls back to Vite's default browser target.
Migration Steps:
In your dashboard project's vite.config.ts, rename esbuild to oxc and move the optimizeDeps target into rolldownOptions.transform:
esbuild: {
oxc: {
target: 'es2022',
},
optimizeDeps: {
esbuildOptions: {
target: 'es2022',
},
rolldownOptions: {
transform: {
target: 'es2022',
},
},
},To ensure that your dashboard project has the correct dependencies, you will need to clear the existing cached files and recreate them.
You should ensure that you have Node version 24.13.0 (the latest 24.x version at the time of writing) installed. To check the current version, run node --version. If you are using nvm, you can update Node with:
# Install Node 24.13.0 with Node Version Manager (nvm).
nvm install 24.13.0
# Use the new version.
nvm use 24.13.0Migration Steps:
git clean -fdx ':(glob)**/dist/*' from the root of your repository. This clears any previously built files.pnpm clean --lockfile in your 'Dashboard' folder. This clears any currently installed dependencies.pnpm install in your 'Dashboard' folder. This recreates all of the above files and folders with the correct dependencies.Earlier SDK versions shipped install, vite-plugin-eslint and @vue/devtools-api in the dashboard template. None of them is used.
Migration Steps:
Drop install, vite-plugin-eslint and @vue/devtools-api from your dashboard's package.json, unless your own files reference them. vite-plugin-eslint would appear in your vite.config.ts, and nothing imports install or @vue/devtools-api directly. @vue/devtools-api still reaches your dashboard through vue-router, which depends on it.
Run pnpm install in your 'Dashboard' folder, then pnpm typecheck and pnpm build to confirm.
@metaplay/meta-ui-next/unstableComponents that are not part of the stable API are no longer exported from @metaplay/meta-ui-next. They now live behind a separate @metaplay/meta-ui-next/unstable entry point, and importing one from the main entry point will cause your build to fail. Affected components include MEventTimeline, MProgressBar, MThreeColumnLayout and MTimeSeriesBarChart.
If you have previously imported any of these components in your custom dashboard, update the import as follows:
Migration Steps:
```ts
import { MTimeSeriesBarChart, MProgressBar } from '@metaplay/meta-ui-next' // [!code --]
import { MTimeSeriesBarChart, MProgressBar } from '@metaplay/meta-ui-next/unstable' // [!code ++]
```
pnpm build in your 'Dashboard' folder. Any missed import surfaces as a TS2305 error naming the symbol.copy-to-clipboardThe following migration is only required if you use the MClipboardCopy in your Playwright tests. MClipboardCopy no longer renders a hardcoded data-testid="copy-to-clipboard" attribute. Every instance rendered the same id, so any page with two copy buttons produced an ambiguous selector. Use the new data-testid prop to name the copy buttons your tests interact with.
Migration Steps:
Give each MClipboardCopy that a test needs to find its own data-testid:
MClipboardCopy(:contents="playerId")
MClipboardCopy(:contents="playerId" data-testid="copy-player-id")Point the test selectors at the new id:
page.getByTestId('copy-to-clipboard')
page.getByTestId('copy-player-id') If you already passed a data-testid to MClipboardCopy, it used to land on the element wrapping the button, so you had to look up copy-to-clipboard inside it. It now lands on the button itself, so drop the inner lookup:
page.getByTestId('copy-player-to-clipboard').getByTestId('copy-to-clipboard')
page.getByTestId('copy-player-to-clipboard') height Values from 'half' to 'compact'The metrics chart components (MTimeSeriesBarChart, MTimeSeriesStackedBarChart, and MDailyCohortsBarChart) now share a unified height API and no longer accept 'half' as a height value. If your dashboard renders any of these components directly, update the value to the new 'compact'.
MTimeSeriesBarChart also moved to the @metaplay/meta-ui-next/unstable entry point — see Update Imports for Components Moved to @metaplay/meta-ui-next/unstable above.
Migration Steps:
For MTimeSeriesBarChart the height lives inside the chartOptions prop. Replace all 'half' values with 'compact':
const chartOptions: MTimeSeriesBarChartOptions = {
...
height: 'half',
height: 'compact',
}For the other chart components (MTimeSeriesStackedBarChart and MDailyCohortsBarChart), replace 'half' with 'compact' wherever you set a chart's height.
height: 'half',
height: 'compact', These changes affect you in case you happen to use any of the APIs changed. You can build your project to get a list of any incompatibilities instead of going through the list one item at a time.
[MetaGameConfigBuildConstructor] Constructors Are Now UsedGame config build constructors are now detected regardless of their accessibility. Previously only public constructors were considered, so a [MetaGameConfigBuildConstructor] on a private, protected or internal constructor was silently ignored and the type was built by assigning its members instead.
developerOnly to MetaOfferInfoBase and MetaActivableParams ConstructorsThe MetaOfferInfoBase and MetaActivableParams constructors now take an additional developerOnly parameter.
Migration Steps:
Update all direct constructors:
new MetaActivableParams(
// ...
developerOnly: false); To support developerOnly in a custom config driven event, add DeveloperOnly in the config source data:
class MyEventConfigSourceItem : IGameConfigSourceItem<MyEventId, MyEventInfo>
{
// ...
public bool DeveloperOnly;
public MyEventInfo ToConfigData(GameConfigBuildLog buildLog)
{
return new MyEventInfo(
// ...
activableParams: new MetaActivableParams(
// ...
developerOnly: DeveloperOnly
}
}The client integration types are no longer Unity-specific and have moved out of the Metaplay.Unity and Metaplay.Unity.DefaultIntegration namespaces into the new Metaplay.Client namespace. The moved types are:
Metaplay.Unity: IMetaplayClientAnalyticsDelegate, IMetaplayClientConnectionDelegate, IMetaplayClientGameConfigDelegate, IMetaplayClientSocialAuthenticationDelegate, and ISessionContextProvider.Metaplay.Unity.DefaultIntegration: DefaultMetaplayConnectionDelegate, DefaultMetaplayLocalizationDelegate, MetaplayAppInfoOptions, MetaplayClientCreatePlayerContextFunc, MetaplayClientOptions, and MetaplayIAPOptions.Migration Steps:
Add the using directive to every file that implements one of the client delegates or references types listed above:
using Metaplay.Client;
// some code using MetaplayClientOptionsDefaultPlayerClientContext Constructor CallsDefaultPlayerClientContext constructor and MetaplayClientCreatePlayerContextFunc delegate now take IMetaplaySubClientServices as the first parameter. The individual LogChannel, ITimelineHistory, and SendToServer service parameters have been removed.
Migration Steps:
Update any direct DefaultPlayerClientContext constructor calls to pass the services object and remove the removed parameters:
_playerContext = new DefaultPlayerClientContext(
_logChannel,
Services,
playerModel,
success.PlayerState,
_actualPlayerId,
_logicVersion,
timelineHistory: null,
SendToServer,
MetaTime.Now);If you have a custom MetaplayClientOptions.CreatePlayerContext delegate, add IMetaplaySubClientServices services as the first parameter and update parameters:
Options.CreatePlayerContext = (model, initialState, playerId, logicVersion, now) =>
Options.CreatePlayerContext = (services, model, initialState, playerId, logicVersion, now) =>
{
return new DefaultPlayerClientContext(_log, model, initialState, playerId, logicVersion, null, SendToServer, now);
return new DefaultPlayerClientContext(services, model, initialState, playerId, logicVersion, now);
};BotClientBase now manages IPlayerClientContext automatically. HandleStartSession() has been renamed to OnSessionStartedAsync(), and it no longer should create the PlayerClientContext. In case custom Context type is required, BotClient may override CreatePlayerContext() method.
BotClientBase now handles SessionProtocol.SessionStartSuccess automatically, and you need to migrate SessionStartSuccess logic into OnSessionStartedAsync() method.
Additionally, BotClientBase now handles PlayerAckActions, PlayerExecuteUnsynchronizedServerAction, and PlayerChecksumMismatch automatically and these messages no longer reach OnNetworkMessage(). You should remove their boilerplate logic.
Migration Steps:
Update the data source of PlayerModel _playerModel property:
PlayerModel _playerModel => (PlayerModel)_playerContext.Journal.StagedModel;
PlayerModel _playerModel => (PlayerModel)PlayerContext.Model; Remove the DefaultPlayerClientContext _playerContext field, the PlayerContext override, and the context construction from HandleStartSession. The base class now provides PlayerContext:
DefaultPlayerClientContext _playerContext;
protected override IPlayerClientContext PlayerContext => _playerContext;
protected override void HandleStartSession(SessionProtocol.SessionStartSuccess success, IPlayerModelBase playerModelBase, ISharedGameConfig gameConfig)
{
PlayerModel playerModel = (PlayerModel)playerModelBase;
playerModel.ClientListener = this;
_playerContext = new DefaultPlayerClientContext(
Services, playerModel, success.PlayerState, _actualPlayerId, _logicVersion,
MetaTime.Now);
}If you need a custom DefaultPlayerClientContext subclass, override CreatePlayerContext() and provider a helper getter:
new MyCustomPlayerContext PlayerContext => (MyCustomPlayerContext)base.PlayerContext;
protected override IPlayerClientContext CreatePlayerContext(BotCreatePlayerContextArgs args)
{
return new MyCustomPlayerContext(
args.Services, args.PlayerModel, args.InitialPlayerState,
args.PlayerId, args.LogicVersion, args.Now);
} Rename your HandleStartSession() override to OnSessionStartedAsync(). Update the signature and access the player model via args.PlayerModel:
protected override void HandleStartSession(SessionProtocol.SessionStartSuccess success, IPlayerModelBase playerModelBase, ISharedGameConfig gameConfig)
protected override Task OnSessionStartedAsync(BotSessionStartedArgs args)
{
PlayerModel playerModel = (PlayerModel)playerModelBase;
PlayerModel playerModel = (PlayerModel)args.PlayerModel;
playerModel.ClientListener = this;
return Task.CompletedTask;
}Remove the case SessionProtocol.SessionStartSuccess success: case from your OnNetworkMessage() switch:
protected override Task OnNetworkMessage(MetaMessage message)
{
switch (message)
{
case SessionProtocol.SessionStartSuccess success:
State = BotClientState.Main;
break;
...
}
}Move any session-start work into the OnSessionStartedAsync():
protected override Task OnSessionStartedAsync(BotSessionStartedArgs args)
{
State = BotClientState.Main;
PlayerModel playerModel = (PlayerModel)args.PlayerModel;
playerModel.ClientListener = this;
return Task.CompletedTask;
}Remove the PlayerAckActions, PlayerExecuteUnsynchronizedServerAction, and PlayerChecksumMismatch cases from your OnNetworkMessage() switch:
protected override Task OnNetworkMessage(MetaMessage message)
{
switch (message)
{
case PlayerAckActions ackActions:
_playerContext.PurgeSnapshotsUntil(JournalPosition.FromTickOperationStep(ackActions.UntilPositionTick, ackActions.UntilPositionOperation, ackActions.UntilPositionStep));
break;
case PlayerExecuteUnsynchronizedServerAction executeUnsynchronizedServerAction:
_playerContext.ExecuteServerAction(executeUnsynchronizedServerAction);
break;
case PlayerChecksumMismatch checksumMismatch:
_log.Warning("PlayerChecksumMismatch: tick={Tick}, actionIndex={ActionIndex}", checksumMismatch.Tick, checksumMismatch.ActionIndex);
_playerContext.ResolveChecksumMismatch(checksumMismatch);
RequestShutdown();
break;
...
}
}Replace the lingering uses of _playerContext with PlayerContext:
_playerContext.ExecuteAction(new MyAction());
PlayerContext.ExecuteAction(new MyAction()); PlayerModelBase<> Type ParametersPlayerModelBase no longer takes the TPlayerGuildState type parameter.
Migration Steps:
Drop the PlayerGuildStateCore guild-state type argument from your PlayerModel base class declaration:
For the three-parameter variant:
public class PlayerModel :
PlayerModelBase<
PlayerModel,
PlayerStatisticsModel,
PlayerGuildStateCore
>
{
}For the four-parameter variant:
public class PlayerModel :
PlayerModelBase<
PlayerModel,
PlayerStatisticsModel,
PlayerOfferGroupsModel,
PlayerGuildStateCore
>
{
}GuildMemberPlayerDataBase.IsUpToDate() OverridesGuildMemberPlayerDataBase.IsUpToDate() has been removed. The staleness of player-to-guild mirrored data is now detected automatically, so the manual override is no longer needed.
Migration Steps:
Delete any IsUpToDate() override from your GuildMemberPlayerDataBase subclasses:
public override bool IsUpToDate(GuildMemberBase memberBase)
{
MyGuildMember member = (MyGuildMember)memberBase;
return member.PlayerIcon == PlayerIcon;
} TimeSpan Runtime OptionsTimeSpan Runtime Option values now require the [d.]hh:mm:ss[.fffffff] or d:hh:mm:ss[.fffffff] format. Previously, ambiguous values were silently accepted: 6 was interpreted as 6 days and 6:12 as 6 hours 12 minutes. Such values are now rejected at server startup.
Migration Steps:
If the server reports a TimeSpan parse error at startup, rewrite the value into [d.]hh:mm:ss or d:hh:mm:ss form:
SomeTimeout: 6
SomeTimeout: 6.00:00:00ClientConnection:EnableWireCompression Runtime OptionThe ClientConnection:EnableWireCompression runtime option has been removed. Wire compression of large packets is now always enabled.
Migration Steps:
If you set EnableWireCompression under ClientConnection in any of your Options.*.yaml files, remove it.
ClientConnection:
EnableWireCompression: falseUtil.ComputeMD5()Util.ComputeMD5() has been removed, as MD5 hashing is no longer available in the client SDK.
Migration Steps:
Replace any call to Util.ComputeMD5() with your own MD5 computation:
string hash = Util.ComputeMD5(input);
string hash = Util.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(input))); PlayerEventIncidentRecorded.FingerprintThe Fingerprint field has been removed from the PlayerEventIncidentRecorded (IncidentRecorded) analytics event, so it is no longer emitted to your analytics sinks. Any downstream query or dashboard reading this field will silently return no data after the upgrade.
Migration Steps:
Fingerprint field of the IncidentRecorded event to instead derive it from the event's Type, SubType, and Reason fields (the fingerprint was the MD5 of "{Type}/{SubType}/{Reason}").OnPlayerIncidentRecorded() OverridesThe PlayerActorBase.OnPlayerIncidentRecorded() hook now receives the incident's metadata instead of the full report:
protected virtual void OnPlayerIncidentRecorded(PlayerIncidentReport report, string reason)
protected virtual void OnPlayerIncidentRecorded(string incidentId, string type, string subType, string reason) Incident reports may now be persisted outside the PlayerActor — for example uploaded straight to blob storage — so the actor no longer always holds a deserialized report to hand over. An override using the old signature fails to compile with CS0115: no suitable method found to override.
Migration Steps:
Update your override to the new signature and read the values from the parameters:
protected override void OnPlayerIncidentRecorded(PlayerIncidentReport report, string reason)
protected override void OnPlayerIncidentRecorded(string incidentId, string type, string subType, string reason)
{
Model.EventStream.Event(new MyIncidentEvent(report.IncidentId, report.Type, report.SubType, reason));
Model.EventStream.Event(new MyIncidentEvent(incidentId, type, subType, reason));
}If your override reads data that exists only on the report — for example GetNetworkDiagnosticReportIfExists() or ClientSystemInfo — that data is no longer passed to the hook. It is available only for incidents delivered by the client, and the hook is now called for both those and incidents persisted elsewhere. Contact support if your integration depends on it.
RegisterSynchronizedServerAction() CallersPlayerActor.RegisterSynchronizedServerAction() now returns a RegisteredSynchronizedServerActionHandle instead of an int tracking id. The handle exposes the tracking id via its Id property, the registered Action, and an EnqueueOnClient() method for delivering the action to the client.
Migration Steps:
Read the tracking id from the handle's Id property:
int trackingId = RegisterSynchronizedServerAction(action);
int trackingId = RegisterSynchronizedServerAction(action).Id; .slnxNew Metaplay projects use the XML-based .slnx solution format. Convert your existing backend solution to the same format:
Migration Steps:
Convert the solution and remove the old file:
Backend$ dotnet sln Server.sln migrate
Backend$ rm Server.slnOpening .slnx requires Visual Studio 17.13 or later, Rider 2024.3 or later, or .NET SDK 9.0.200 or later.
flushEnqueuedMessages Argument from Close() and CloseWithError()MetaplayConnection.Close() and CloseWithError() no longer take the flushEnqueuedMessages parameter. Enqueued messages are always flushed before closing, and the flush now runs in the background instead of blocking.
Migration Steps:
Remove the flushEnqueuedMessages argument from your Close() and CloseWithError() calls:
Connection.Close(flushEnqueuedMessages: true);
Connection.Close();
Connection.CloseWithError(flushEnqueuedMessages: false, error);
Connection.CloseWithError(error); TryCreateGuildCreationParamsFromRequest AsyncPlayerActor guild component override TryCreateGuildCreationParamsFromRequest() is now async and has been renamed to TryCreateGuildCreationParamsFromRequestAsync(). It is also given the action that started the creation, if any. This lets the override perform asynchronous work, for example validating the request against a third-party service.
Migration Steps:
Rename your override, update signature and return a Task:
protected override GuildCreationParamsBase TryCreateGuildCreationParamsFromRequest(GuildCreationRequestParamsBase paramsBase)
protected override Task<GuildCreationParamsBase> TryCreateGuildCreationParamsFromRequestAsync(GuildCreationRequestParamsBase paramsBase, PlayerActionBase invokingAction)
{
return new GuildCreationParams { DisplayName = requestParams.DisplayName };
return Task.FromResult<GuildCreationParamsBase>(new GuildCreationParams { DisplayName = requestParams.DisplayName });
}GuildRequirementsValidator Overrides AsyncGuildRequirementsValidator overrides are now async and have been renamed to ...Async(). This lets the validation perform asynchronous work.
Migration Steps:
Rename each override and return a Task:
public override bool ValidateDisplayName(string displayName)
public override async Task<bool> ValidateDisplayNameAsync(string displayName)
{
if (!base.ValidateDisplayName(displayName))
if (!await base.ValidateDisplayNameAsync(displayName))
return false;
return true;
}
public override bool ValidateDescription(string description)
public override async Task<bool> ValidateDescriptionAsync(string description)
{
if (!base.ValidateDescription(description))
if (!await base.ValidateDescriptionAsync(description))
return false;
return true;
}
public override bool ValidateGuildCreation(GuildCreationParamsBase baseArgs)
public override async Task<bool> ValidateGuildCreationAsync(GuildCreationParamsBase baseArgs)
{
if (!base.ValidateGuildCreation(baseArgs))
if (!await base.ValidateGuildCreationAsync(baseArgs))
return false;
return true;
}You should run the Metaplay integration test suite on your project after the SDK upgrade to make sure everything is still working as expected:
MyProject$ metaplay test integration