Appearance
Release 32
April 2nd, 2025
Appearance
April 2nd, 2025
Hotfix release
Release 32.3 is the latest hotfix release for Release 32.
The new Metaplay CLI is a huge improvement over the previous CLI:

The new CLI packs a lot of useful functionality:
metaplay build image.metaplay deploy server.metaplay debug logs.metaplay debug run-shell to start a shell.metaplay debug collect-heap-dump to collect a memory heap dump and metaplay debug collect-cpu-profile to collect a CPU profile from a cloud server.metaplay dev ... commands to run the server, dashboard, botclient, or the Docker image locally.metaplay secrets ... commands.metaplay init project command.metaplay init dashboard command.For installation instructions and more details, see the GitHub page.
Compatibility with SDK releases
The new CLI replaces the old metaplay-auth CLI. The old CLI is now considered deprecated and will only receive critical updates.
When upgrading to R32, you should also switch to using the new CLI. The new CLI also works with R31 and older, with the exception of Docker image builds and server deploys.
Project configuration is now done via a new metaplay-project.yaml in the project root. This brings many benefits:
See the Project Configuration guide for more information.
We've rewritten our admin authentication system with a focus on performance, security, and extensibility. This ground-up redesign delivers significant improvements across the board:
Migration
For anyone using Metaplay managed cloud, the transition is handled transparently and requires no steps from you. For self-hosting customers, we will reach out to you to help with the migration.
We've updated our banning feature to include the ability to administer temporary bans to misbehaving players. Previously such temporary bans would have to be manually enforced by agents who would need to remember to lift a players' ban after a period of time. This new addition to the tool frees up your agents to think about more important things, as well as giving them new powers to manage players.

In the R31 we introduced Business Metrics, which allowed you to track your key performance indicators from a dedicated page on the LiveOps dashboard. In R32 we've taken things a step further by embedding relevant graphs directly into the pages where they make the most sense. You can now also easily add your own game-specific metrics. Both the embedded metrics and your own are fully configurable so that you can decide exactly what you'd like to see, and where.

Metaplay now supports Steam's microtransactions for in-app purchases when Steam authentication is enabled. Look for "Steam" in Getting Started with In-App Purchases and see Configuring Steam Purchases for more information.
logic nodes by default and doesn't require authentication by the infra layer. See Public Web Endpoints.PlayerDashboardActionAttribute to any Player Server Action, and it'll automatically show up on the dashboard with a generated form. See Tutorial: Add an Admin Action for more info.[MessageHandler] methods may now take in EntitySubscriber or EntitySubscription instead of EntityId to match only messages sent on the subscription.MetaplaySDK/version.yaml that includes metadata about the SDK version, required infra and Helm chart versions, as well as required .NET, Node.js, and PNPM versions.StartupRuntimeOptions alternative base class for runtime options that are made available to the server application configuration code before the RuntimeOptionsRegistry is initialized.DynamicEnum members are now supported in analytics events.integration-tests.py now supports --shared-code-dir to allow the shared code directory to be moved.IncidentReportCountsByType metric as an example.ClientSideConnectionError with contains exception of type MustReloginError now communicates that the client-side credentials service requires the user to (interactively) log in again. The error state TerminalError.LoginRejected is used to communicate that the server did not accept the given social credentials and that the credentials service did not have the means to automatically refresh the credentials. These errors are translated to CredentialsExpiredError and LoginRejected types in the ConnectionLostEvent provided to IMetaplayLifecycleDelegate.OnFailedToStartSession().PlayerModel.SessionAuthenticationKey to allow observing the Authentication Key used to start the current session.OnClientAppStatusChanged() method to allow reacting to client app being put into background or connection being lost.PersistedMultiplayerEntityActorBase.RequestShutdownAndEntityDeletion() to allow multiplayer entities to delete their state.api.players.gentle, api.players.disruptive, and api.players.dangerous permissions to simplify permission handling for custom features.EntityActor.ContinueTaskOnActorContext() overload for plain Tasks that do not return a value.EntityActor.EnqueueOnActorContext() which complements ExecuteOnActorContextAsync(). Unhandled exception in Enqueue- crash the actor, instead of returning a Task that completes with a failure.OnInAppProductRefunded() hook for PlayerModel for handling IAP refunds in a game-specific manner, for example by revoking the granted resources. Note that refund handling is currently not supported for App Store and for Google play is only supported when the refund is issued via the LiveOps Dashboard.[CommandHandler] methods no longer accept MetaMessages. See Migration Guide for details.AdminApiActor into WebApiBase for purpose of sharing this functionality with the newly introduced PublicWebApiActor.MetaplayHttpException class has been moved into Metaplay.Server.WebApi namespace and the Exceptions static class has been removed.AdminApiJsonSerialization class has been moved into Metaplay.Server.WebApi namespace and renamed to WebApiJsonSerialization to reflect the fact that it is shared between AdminApi and PublicWebApi.AuthenticationDomainConfig system used to configure AdminApi authentication per route, authentication is now configured directly in the AdminApiStartup.InAppPurchasePlatform is now a DynamicEnum rather than an enum. This is preparation for making it easier to integrate new IAP stores. This does not change InAppPurchasePlatform's serialization format.OrderId member in InAppPurchaseEvent and elsewhere in IAP code has been renamed to AlternativePurchaseId, because the old OrderId name was specific to Google Play but can now be used by other platforms as well. As an exception, analytics events still use the name OrderId for compatibility with existing analytics processing systems.InAppPurchaseStatus enum values ValidReceipt and InvalidReceipt have been renamed to the more general Successful and Failed, because not all purchasing platforms involve receipt validation.S3BlobStorage.GetPresignedUrl now always use V4 signatures. Previously, V2 signature was used for S3 buckets on us-east-1.integration-tests.py bot runs are shortened to 30sec, use 20 bots and 5 second sessions (from previous 2min run, 100 bots, and 30sec sessions)./.well-known/jwks.json) is now configured with KeyManager:PublicKeyLifetime runtime option. KeyManager:NumPublicKeysToRetain has been removed.node (instead of earlier project name) for better readability.entrypoint binary to Go 1.23.6. This fixes CVE-2024-45341 and CVE-2024-45336.metaplay-project.yaml.LoginFailureResponse before closing the connection.DualSocialAuthLoginMethod has been removed, as a new account is created on-demand anyway.AuthenticationPlatform and DidBindAccount to LoginSuccessResponse for better communication the outcome of a (social) login operation to the client.ISessionCredentialsService for reacting to login success (OnLoginSuccessAsync()) and failure (OnLoginFailureAsync()). Retired the OnPlayerIdUpdatedAsync() method as OnLoginSuccessAsync() covers the use case.UnityCredentialsService via overriding the InitSocialCredentialsProviders() function and implementing the ISocialCredentialsProvider interface. Moved the Steam authentication implementation into SteamCredentialsProvider.ServerEndpoint.BackupGatewaySpecs, consecutive entries with empty or null ServerHost are now considered as backup ports for the previous entry with ServerHost set. Previously all entries with unset ServerHost were considered as backup ports for the primary gateway.PlayerActionBase.Id has been removed.IPlayerClientContext.ExecuteAction overload with the out int id parameter has been removed.METAPLAY_ENABLE_WEBGL for WebGL builds has been retired. WebGL specific features in the SDK as well as the WebGL code analyzers are enabled when building for the WebGL platform.PlayerSessionParamsBase has been split into PlayerSessionParams and PlayerSessionGameParamsBase.PlayerEventPayloadBase and EventPayloadBase to Metaplay.Core.AuditLog.EntityAskAsync and SubscribeToAsync, will now throw EntityNotCreatedError instead of EntityCrashedError if the entity actor fails to initialize due to a missing database save for the entity.EntityActor's virtual method HandleUnknownCommand() can now be async (it returns a Task), consistent with HandleUnknownMessage().Logging:FormatTemplate and Logging:ColorTheme options have been removed. Use Logging:UseColor to enable/disable coloring.DateTime, the default format is now yyyy-MM-dd HH:mm:ss UTC for Utc kind dates and yyyy-MM-dd HH:mm:ss for Local and Unspecified dates.DateTimeOffset, the default format is now yyyy-MM-dd HH:mm:ss UTC+xx:yy.[NonSerialized] now produces a warning if used in MetaSerializable classes as it has no effect on Metaplay serialization.IChecksumContext.Step(string) has been removed. It had no effect for validation.ModelUtil.RunAction() no longer takes in IChecksumContext.MultiplayerEntityActorBase._journal has been removed, use MultiplayerEntityActorBase.Model instead.MultiplayerEntityActorBase.DesyncDebugging has been removed, use MultiplayerEntityActorBase.DebugOptions instead.MultiplayerEntityClientBase._enableJournalCheckpointing has been removed. It is now controlled with MultiplayerEntityActorBase.DebugOptions.MultiplayerEntity consistency checks are no longer controlled with Player's consistency checks but by MultiplayerEntityActorBase.DebugOptions instead.MetaplaySDK.DownloadCachePath is now stored on platform specific cache folder.ServerHello.FullProtocolHash and client-side reporting of potential mismatch. This is now replaced by the server communicating the protocol has mismatch via explicit message ClientFullProtocolHashMismatch. The mismatch is now reported only if the client is on the latest logic version, to prevent false positive warnings.FacebookDataDeletionWebhookController and FacebookDeauthorizeWebhookController have been merged into a single FacebookCallbacksController.facebook. The legacy endpoints in the AdminApi are considered deprecated and are subject to be removed in the future.GameConfigLibrary<,> key types override equality and ToString() methods now happens at an earlier time during game config build. Previously, the config build could fail with unclear error messages due to missing those methods; now, it will provide better error messages instead.[ServerOnly] now work correctly when using implicit member id generation.InvalidOperationExceptions in GeolocationUpdaterFromOrigin if server failed to connect to other cluster members.DynamicEnum now produces its name string rather than a JSON object containing id, name, and isValid.PlayerModel.GetMetaOfferGroupsToFinalize.us-east-1 region.MetaplaySDK.Stop() is called. Timer and socket callbacks happening after SDK deinitialization (including unity app shutdown) previously could cause page freezes.DateTimeOffset, DateTime and TimeSpan as simple values of the ToString() output of the type.WebSocketOptions.ListenHost now defaults to "127.0.0.1" in local builds rather than "localhost", as web clients connect with IP rather than hostname.GameConfigKeyValue sheets, a single variant override row can now specify multiple variants separated by commas. Previously this was only supported in GameConfigLibrary sheets.IMetaLogger.Error(Exception, Format, ...) are now labelled with ERR level in Loki (Grafana log search).WebAssembly.Table were unable to resolve javascript-to-c# callbacks due to function getWasmTableEntry not being available in .jspre files.MetaplayCore.Options.DefaultLanguage correctly. Previously a random supported language was erroneously picked when a localization matching the user's browser language was not found.TemporarilyUnavailable status.MessageDirection is now checked properly for messages sent by Client to a Multiplayer Entity.MetaRecurringCalendarSchedules are now sanity-checked (e.g. duration cannot exceed recurrence) during game config parsing. This sanity check existed in the past but was accidentally disabled in release 25.AssemblyBuilderFlags.EditorAssembly which fixes issues with incremental device builds incorrectly depending on device assemblies.New MRawDataCard Component: We've replaced the MetaRawData with a new MRawData component. The new iteration of this invaluable development tool is more readable, giving your developers better insight into the data that makes the dashboard work. 
New MInputToggleGroup Component: This new component combines a feature toggle with layout, allowing you to de-clutter your UI by hiding detailed information when it's not relevant. 
MDateTime component for an easy way to display time with extra tooltips for time zone conversions. Replaces the previous MetaTime component.@metaplay/meta-utilities now has four new functions to convert ISO time strings and Luxon DateTimes into UTC plain dates and times.MCountryCode component that converts ISO country codes to human readable flags and names.MIpAddress component that helps display IP addresses in a more human-readable format.MInputSwitch component now has support for labels and hint messages and also has improved interactive colors for better visual feedback.MInputSingleSelectDropdown component that allows for a single-select dropdown input with searching and auto-completion. All dropdowns in the dashboard have been upgraded to use this new component and many have gotten visual tweaks while at it.MInputSingleSelectAsyncDropdown component that allows for a single-select dropdown input with searching and auto-completion, with the options fetched asynchronously from external APIs.MDuration component to display durations in a human-readable format.MInputSingleSelectAsyncDropdown component and support infinite scrolling to load more search results. This is great for when you have many players with similar names.MInputSelectOption and MInputSelectClearButtonOption, to standardize and simplify the definition of options for input components.MRawDataCard component that displays raw data (for example parsed JSON responses from APIs) in a more human friendly format.useDeveloperUi composable that allows you to check and toggle the global developer UI setting.MInputToggleGroup component that lets users toggle optional sections in the dashboard.inlineLabel prop to the MInputCheckbox and MInputSingleSelectRadio components to allow for positioning the label inline with the input.hintMessage prop to the MInputSingleSelectRadio that lets you add optional hint messages for each radio button.updateUiComponent function that gives more flexibility to replace or update UiPlacements.metaplay/prefer-datetime-utc ESLint rule to enforce the use of DateTime.utc() instead of DateTime.now().MetaDuration, MetaCalendarDateTime, and MetaCalendarPeriod types.MetaEventStreamCard (call sites such as player event logs) will now show time stamps in UTC time instead of browser local time.MCard's header-right slot no longer has fixed width but instead better responds to dynamic widths. Use manual width classes to force a specific width (for example for a text input) if needed.MInputSingleSelectDropdown component. The underlying MessageAudienceForm component was renamed to PlayerSegmentsInput and will likely evolve further in future releases.MetaIpAddress component has been deprecated in favor of the new MIpAddress component.MetaCountryCodecomponent has been deprecated in favor of the new MCountryCode component.MetaAlert component has been removed in favour of the newer MCallout component.BAlert component from Bootstrap-Vue has been removed in favour of our own MCallout component.MetaInputSelect component has been removed in favor of the new MInputSingleSelectDropdown component.MetaDuration component has been removed in favor of the new MDuration component.MetaRawData component has been removed in favor of the new MRawDataCard component.MetaPluralLabel component has been removed in favor of using the underlying maybePluralString function directly.maybePluralString utility was changed to make all overloads mandatory to make its API more obvious and easier to use.dist/assets are now marked Cache-Control: immutable with a 1 month lifetime for more efficient caching.api/ will now set Cache-Control: no-store header by default to prevent polluting browser cache.parseDotnetTimeSpanToLuxon from Core to MetaUiNext.Ban Player admin action UI/UX has been redesigned to support temporary bans, allowing users to set a ban duration and send a custom message to the banned player.GameConfigActionPublish, GameConfigActionArchive, LocalizationActionArchive, and LocalizationActionPublish modals to clearly explain the actions available to the user.ScanJobsPauseAllJobs, PlayerActionSetDeveloper, PlayerActionSetTester, PlayerActionJoinExperiment and the PlayerActionToggleDebugMode modals to better explain the action being taken.durationToMilliseconds, use Duration.fromISO(...).toMillis() instead.MInputNumber and MInputTime components no longer require a focus change to output their values.testing phase on the experiments page.MetaEventStreamCard component that occurred when keyword filters were applied, causing the card to display incorrect information when no matching events were present. Now, the card correctly shows a message when no events match the filter criteria.EntityEventLogCard where the loading spinner would be shown forever if the event stream was empty.MInputSingleSelectRadio no longer accepts an undefined initial value, which caused an unexpected visual state.#MISSING#.index.html is no longer cached to fix issues with stale versions being served from the browser cache.MInputDuration now shows the duration as Days, Hours, and Seconds instead of as Milliseconds.MInputDurationOrEndTime's exact date picker switch no longer overflows to the next line on elements with a small width.MInputDurationOrEndTime now pre-fills the duration picker when switching from the exact date picker to the duration picker.MInputStartDateTimeAndDuration component. The duration input is now aligned with the start date and time input.MInputDurationOrEndDate component. The layout no longer jumps when toggling between inputs.We are aware of the following issues in this release:
NullReferenceException on Client Mono Builds If Connection FailsIf an initial connection to the backend fails or takes too long, the game client will attempt to determine if the game backend is in maintenance mode. This is determined by fetching a marker file via HTTP from backend CDN. When the game client is built with Mono scripting engine (as opposed to IL2CPP), Unity may strip some dotnet runtime resources. If certain System.Net.WebRequest resources are stripped, WebRequest.CreateHttp will throw a NullReferenceException. MetaplaySDK does not expect this error, nor does it handle it, causing connection to fail.
SocketException On Server Start On MacOS When Using .Net8If the game-server is started on MacOS with the .Net8 runtime, the server will print the following:
Failed to start Direct Transport Listener. Disabling for this node: System.Net.Sockets.SocketException (45): Operation not supportedDue to this error, direct UDP connections are disabled. To work around the issue, upgrade to .Net9.
Minutes selection in the dashboard datetime picker component sometimes behaves unexpectedly due to incorrect rounding.
If the game is uninstalled and then reinstalled on Android, the player loses access to the the original account.
MetaplaySDK stores account infromation in Android Block Store to persist user's account across reinstalls. In R32, a logic bug was introduced that caused this mechanism to fail with an error message:
[metaplay] Failed to read credentials from platform store: System.ArgumentNullException: Value cannot be null.
Parameter name: schedulerThis has been fixed in MetaplaySDK Release 33.
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.
The Metaplay SDK now requires Helm chart v0.8.0 or later for cloud deployments.
Migration Steps:
Update your metaplay-project.yaml to the latest Helm chart version.
serverChartVersion: 0.x.y
serverChartVersion: 0.8.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 migrations steps for features you are not using in your project.
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 MetaplayRelease32Then, 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.
metaplay-project.yamlThe new project config file metaplay-project.yaml will act as the main configuration file for your project going forward and is used by the CLI to interact with your project.
Migration Steps:
Generate the metaplay-project.yaml project config file using the CLI in your project's root directory (usually the repository root):
MyGame$ metaplay init project-configThe CLI will automatically detect most configuration parameters and ask you the rest. There are override flags in case any of the detection was not correct.
Your managed environments in Metaplay cloud are also automatically populated in the generated file.
Review the generated metaplay-project.yaml that everything looks correct.
Add the generated metaplay-project.yaml file to your version control system, eg:
MyGame$ git add metaplay-project.yaml
MyGame$ git commit -m "Add Metaplay project config file."All server and tool builds depending on MetaplaySDK R32 require using the .NET toolchain from the .NET 9 SDK, even if you choose to continue to target .NET 8 in your own projects.
Migration Steps:
Update the sdk entry in the global.json file of your project to specify at least version 9.0.100:
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestFeature"
}
}Once the R32 update is merged to your development branch, instruct all developers to install and switch to the .NET 9 SDK.
Update the .NET toolchain version used by any CI jobs.
Note that Dockerfile.server shipped with the Metaplay SDK has been updated to use the .NET 9 SDK so any docker image builds in CI will use the correct .NET SDK version without further action required.
Additionally we strongly recommend updating the target framework of your server and tools builds to target .NET 9 runtime. The actions for updating your project are:
Update the dotnetRuntimeVersion entry in the metaplay-project.yaml file to specify 9.0:
dotnetRuntimeVersion: "8.0"
dotnetRuntimeVersion: "9.0"Edit the TargetFramework property in all .csproj files to specify net9.0:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
...Verify that your code continues to build against .NET 9:
MyProject$ metaplay build serverNote
In R32 the Metaplay projects continue to additionally support .NET 8, in case updating your own projects proves to be problematic.
The CLI now automatically injects the default configuration for the Helm chart deployment, replacing need for the default parameters for the Helm values files in Backend/Deployments/*.yaml.
Migration Steps:
You can remove the following items from each Helm values file in your Backend/Deployments/*.yaml as these are now injected by the CLI automatically when doing a deploy.
environment: develop
environmentFamily: Development
config:
files:
- "./Config/Options.base.yaml"
- "./Config/Options.<env>.yaml"
tenant:
discoveryEnabled: true
shards:
- name: ...
singleton: ...
requests:
cpu: ...
memory: ...If you have not modified these files, you can just remove them.
If any YAML files remain (with only your customization in them), add a reference to the file in your metaplay-project.yaml for the environments you want to use it in, for example:
environments:
- name: develop
...
serverValuesFile: Backend/Deployments/develop-server.yamlFor more information, see the Configuring a Deployment guide.
The following LiveOps Dashboard changes affect projects that have a game-specific dashboard project:
You should ensure that you have Node version 22.14.0 (the latest 22.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 22.14.0 with Node Version Manager (nvm).
nvm install 22.14.0
# Use the new version.
nvm use 22.14.0Update the Node version in your project's package.json file to reflect the new requirement:
"engines": {
"node": "20.x"
"node": ">=20"
}Migration Steps:
git clean -fdx ':(glob)**/node_modules/*' from the root of your repository. This clears any currently installed dependencies.git clean -fdx ':(glob)**/dist/*' from the root of your repository. This clears any previously built files.pnpm-lock.yaml file from the root of your repository. This clears the cached dependency versions.As usual, we have updated the underlying dependencies and configurations of the LiveOps Dashboard. This causes changes to several 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".Run pnpm install in your 'Dashboard' folder. This recreates all of the above files and folders with the correct dependencies.
As a part of continued code review and polish of the dashboard codebase, we have done a few breaking changes to internal APIs that you may be using in your custom components.
As part of this release, the following components have been removed in favor of the new MetaUiNext components:
MetaIpAddress and meta-ip-address are replaced by MIpAddress.MetaCountryCode and meta-country-code are replaced by MCountryCode.MetaAlert, meta-alert, BAlert and b-alert are replaced by MCallout.MetaRawData and meta-raw-data are replaced by MRawDataCard.MetaDuration and meta-duration are replaced by MDuration.MetaTime and meta-time are replaced by MDateTime.MetaPluralLabel and meta-plural-label are replaced by using maybePluralString() directly.durationToMilliseconds is replaced by using Luxon's fromIso and toMillis functions.Migration Steps:
Find all instances of the removed components in your dashboard project.
Import the new component from the MetaUiNext package:
import { MIpAddress, MCountryCode, MCallout, MRawDataCard, MDuration, MDateTime } from '@metaplay/meta-ui-next'Replace the removed components with the new ones, and adjust the props and syntax as needed.
For MetaIpAddress replacement:
meta-ip-address(:ipAddress="ipAddress")
MIpAddress(:ipAddress="ipAddress")For MetaCountryCode replacement:
meta-country-code(:isoCode="countryCode")
MCountryCode(:isoCode="countryCode")For MetaAlert replacement:
- meta-alert(
+ MCallout(
+ title="Title"
...
)For BAlert replacement:
- b-alert(
+ MCallout(
+ title="Title"
...
)For MetaRawData replacement:
- meta-raw-data(
- :kvPair="data"
- name="someName"
+ MRawDataCard(
+ :data="data"
+ label="someName"
)For MetaDuration replacement:
// In your script block.
import { Duration } from 'luxon'
//- If you previously used a string, you will need to convert it to a Luxon Duration object first. Adjust your `showAs` prop to use 'topTwoUnits' and 'topThreeUnits' if needed.
meta-duration(:duration="durationString")
MDuration(:duration="Duration.fromISO(durationString)") The old MetaDuration component was more accepting of different source data types. In some cases, you may now see the component report invalid duration. The cause is often that the source data type is not in ISO format, but in Dotnet format. You should migrate these as follows:
// In your script block.
import { Duration } from 'luxon'
import { parseDotnetTimeSpanToLuxon } from '@metaplay/meta-ui-next'//- In your template block.
meta-duration(:duration="dotNetDurationString")
MDuration(:duration="parseDotnetTimeSpanToLuxon(dotNetDurationString)")For MetaTime replacement:
meta-time(:date="luxonDateTimeObject")
MDateTime(:instant="luxonDateTimeObject")The old MetaTime component was more accepting of different source data types. In some cases, you may now see the component report Invalid DateTime. The cause is often that the source data type is not in Luxon DateTime format, but an ISO string. You should migrate these as follows:
// In your script block.
import { DateTime } from 'luxon'//- In your template block.
meta-time(:date="isoDateTimeString")
MDateTime(:instant="DateTime.fromISO(isoDateTimeString)")The showAs formats for MDateTime are also different from the old MetaTime component.
For timeago use relative.
For timeagoSentenceCase use relativeSentenceCase.
For dateTime use dateTime - this name has not changed.
For time, date use dateTime (which will display using the long format "Sat, 1 Jan 2000, 12:34") or, if you require a more compact representation, use one of the following migration strategies instead.
// In your template block.
//- For ISO date time strings, showing as "time".
- meta-time(:date="isoDateTimeString" showAs="time")
+ span {{ isoDateTimeToUtcPlainTimeString(isoDateTimeString) }}
//- For Luxon DateTime objects, showing as "time".
- meta-time(:date="luxonDateTimeObject" showAs="time")
+ span {{ luxonDateTimeToUtcPlainTimeString(luxonDateTimeObject) }}
//- For ISO date time strings, showing as "date".
- meta-time(:date="isoDateTimeString" showAs="date")
+ span {{ isoDateTimeToUtcPlainDateString(isoDateTimeString) }}
//- For Luxon DateTime objects, showing as "date".
- meta-time(:date="luxonDateTimeObject" showAs="date")
+ span {{ luxonDateTimeToUtcPlainDateString(luxonDateTimeObject) }}// In your script block.
// Import the appropriate function, one of:
import { isoDateTimeToUtcPlainTimeString } from '@metaplay/meta-utilities'
import { luxonDateTimeToUtcPlainTimeString } from '@metaplay/meta-utilities'
import { isoDateTimeToUtcPlainDateString } from '@metaplay/meta-utilities'
import { luxonDateTimeToUtcPlainDateString } from '@metaplay/meta-utilities'For MetaPluralLabel replacement:
// In your script block.
import { maybePluralString } from '@metaplay/meta-utilities'// In your template block.
- meta-plural-label(:value="singleOfferData.statistics.numActivated" label="time")
+ p {{ maybePluralString(singleOfferData.statistics.numActivated, 'time', true) }}For durationToMilliseconds replacement:
const timeInMilliseconds = durationToMilliseconds(isoTime)
const timeInMilliseconds = Duration.fromISO(isoTime).toMillis() The following functions have been moved from Core Utils to the metaplay/meta-ui-next library.
parseDotnetTimeSpanToLuxonMigration Steps:
MetaUiNext package:For parseDotnetTimeSpanToLuxon:
import { parseDotnetTimeSpanToLuxon } from '../../coreUtils'
import { parseDotnetTimeSpanToLuxon } from '@metaplay/meta-ui-next'MetaInputSelect ComponentThe MetaInputSelect component has been removed in favor of the new MInputSingleSelectDropdown and MInputSingleSelectAsyncDropdown components.
Dropdowns are quite complex components, so you might find the (newly updated) interactive documentation helpful.
Migration Steps:
Find all instances of the MetaInputSelect component in your dashboard project.
Determine if you need async or non-async search functionality on the call site. Async search is needed if you do web requests to fetch the options.
async, use MInputSingleSelectDropdownasync, use MInputSingleSelectAsyncDropdown.Apply corresponding migration:
MInputSingleSelectDropdownReplace the import for MetaInputSelectOption with the new types:
MInputSelectOption for dropdowns that do not have an option to clear it.MInputSelectClearButtonOption for dropdowns that also should have an option to clear it. Please have a look at the interactive documentation for more details.import { MetaInputSelectOption } from '@metaplay/meta-ui'
import { type MInputSelectOption, type MInputSelectClearButtonOption } from '@metaplay/meta-ui-next'Import the new component from the MetaUiNext package:
import { MInputSingleSelectDropdown } from '@metaplay/meta-ui-next'Update the dropdown options structure to match the new selected type.
const exampleOptions = computed((): Array<MetaInputSelectOption<string>> =>
const exampleOptions = computed((): Array<MInputSelectOption<string>> =>
exampleArray.value.map((element) => ({
id: displayName,
label: displayName,
value: value,
}))Replace the removed components with the new components and update the props accordingly.
- meta-input-select(
- :value="value"
- options="options"
- ...)
+ MInputSingleSelectDropdown(
+ :model-value="value"
+ :options="options"
+ ...Optional: Add a custom search function to enable searching fields other than the option label. Please have a look at the interactive documentation for more details.
Declare the search-function attribute:
MInputSingleSelectDropdown(
:model-value="value"
:options="options"
+ :search-function="customSearchFunction"
...Implement a custom search filter:
// Example data type.
interface MyDataType {
playerId: string
}
const customSearchFunction = (
options: Array<MInputSelectOption<MyDataType>>,
query: string
): Array<MInputSelectOption<MyDataType>> => {
// Filter the options based on the query.
const lowerCaseQuery = query.toLocaleLowerCase()
return options.filter((option) => {
const displayName = option.label.toLocaleLowerCase()
// As an example value is an object with `playerId` field. Search with it too.
const playerId = option.value.playerId.toLocaleLowerCase()
return displayName.includes(lowerCaseQuery) || playerId.includes(lowerCaseQuery)
})
}MInputSingleSelectAsyncDropdownReplace the import for MetaInputSelectOption with the new types:
MInputSelectOption if you have a list of options where one of these options must always be selected.MInputSelectClearButtonOption if you have a list of options where it is possible for none of the options to be selected, ie: the value might be undefined.import { MetaInputSelectOption } from '@metaplay/meta-ui'
import { type MInputSelectOption, type MInputSelectClearButtonOption } from '@metaplay/meta-ui-next'Import the new component from the MetaUiNext package:
import { MInputSingleSelectAsyncDropdown } from '@metaplay/meta-ui-next'
import { type MInputSelectAsyncSearchFunctionResult } from '@metaplay/meta-ui-next'Add a custom search function to retrieve the options.
async function customSearch (
query: string,
fetchCount: number,
requestToken: never
) : Promise<MInputSelectAsyncSearchFunctionResult<T>> {
// Example network request. Note that we assume the backend performs filtering
// based on the `query`.
const results = await fetchResultsSomewhere(query, fetchCount)
// Convert results to the option type.
const options: Array<MInputSelectOption<T>> = results
.filter((resultEntry) => {
// Additional filtering on front-end.
return true
})
.map((resultEntry) => {
return {
label: resultEntry.DisplayName,
value: resultEntry,
}
})
return {
options,
hasMoreData: results.hasMoreData,
requestToken,
}
}Replace the removed components with the new components and update the props accordingly.
- meta-input-select(
- :value="value"
- :options="search"
- @input="$emit('input', $event)"
+ MInputSingleSelectAsyncDropdown(
+ :model-value="value"
+ :search-function="search"
+ @update:model-value="(newSelection) => value = newSelection"
placeholder="Search for a player..."
)Update option render template to access the option fields via value property.
- meta-input-select(...)
+ MInputSingleSelectAsyncDropdown(...)
template(#option="{ option }")
- span This option is for {{ option?.valueField }}
+ span This option is for {{ option.value?.valueField }}showDeveloperUi FlagThe showDeveloperUi flag from the useUiStore composable has been replaced with a dedicated useDeveloperUi composable.
Migration Steps:
Find all instances of the showDeveloperUi flag in your dashboard project.
Import the new useDeveloperUi composable from the MetaUiNext package:
import { useDeveloperUi } from '@metaplay/meta-ui-next'Access the new isDeveloperUiEnabled property from the useDeveloperUi composable:
const { isDeveloperUiEnabled } = useDeveloperUi() Update the usage of the showDeveloperUi flag in your code to use the new isDeveloperUiEnabled property:
if (uiStore.showDeveloperUi) {
if (isDeveloperUiEnabled) { Or
div(v-if="uiStore.showDeveloperUi")
div(v-if="isDeveloperUiEnabled")Remove any references to the showDeveloperUi flag in your code and remove any unused imports or calls to useUiStore.
If you have customized the role-to-permission table in your Options.*.yaml files, you need to add the new permissions to the table.
The following new dashboard admin permissions have been introduced:
api.metrics.view_sensitive - Allows viewing monetization metrics.api.players.gentle, api.players.disruptive, and api.players.dangerous - Generic permissions to simplify creating new admin actions.api.liveops_timeline.edit - Allows organizing the items, rows, and groups of the LiveOps Timeline.Migration Steps:
Specify which roles should have access to these permissions in your Backend/Server/Config/Options.*.yaml (whichever file you use to configure the access). You can modify the roles as needed.
AdminApi:
Permissions:
...
api.metrics.view_sensitive: [ game-admin ]
api.players.gentle: [ game-admin, customer-support-senior, customer-support-agent ]
api.players.disruptive: [ game-admin, customer-support-senior, customer-support-agent ]
api.players.dangerous: [ game-admin, customer-support-senior ]
api.liveops_timeline.edit: [ game-admin ]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.
[CommandHandler] Methods Receiving MetaMessagesTo avoid accidentally using a wrong handler attributes, [CommandHandler] now detects on server start if the received command is a MetaMessage, and hence if [MessageHandler] should have been used instead. If server fails at startup due to [CommandHandler] no longer accepting a MetaMessage, you should migrate code to use [MessageHandler] or wrap the MetaMessage into a non-MetaMessage wrapper object.
Migration Steps:
Either, replace [CommandHandler] to [MessageHandler] for the affected methods.
Or, wrap MetaMessage into a wrapper type:
public record class MyMetaMessageEvent(MyMetaMessage My);
void EventStreamExample()
{
Context.System.EventStream.Subscribe(_self, typeof(MyMetaMessage));
Context.System.EventStream.Subscribe(_self, typeof(MyMetaMessageEvent));
}
void SendExample(MyMetaMessage my)
{
Tell(_self, my);
Tell(_self, new MyMetaMessageEvent(my));
}
[CommandHandler]
void Handler(MyMetaMessage mymessage) {}
void Handler(MyMetaMessageEvent mymessage) {} HandleUnknownCommand() to Return TaskEntityActor's virtual method HandleUnknownCommand() now returns Task (instead of void) and can be async. You will need to update your overrides, if any.
Migration Steps:
EntityActor classes override HandleUnknownCommand(), change them to return Task.protected override void HandleUnknownCommand(object command)
protected override Task HandleUnknownCommand(object command)
{
// ... handle the command ...
return Task.CompletedTask;
}InAppPurchasePlatform switches to if-elsesInAppPurchasePlatform has been changed from enum to DynamicEnum in preparation of making new IAP stores easier to integrate. Unlike enum, DynamicEnum is not allowed in C#'s switch statements, so in case your code uses InAppPurchasePlatform in switches, you will need to change those to if-else chains.
Migration steps:
InAppPurchasePlatform-based switches to if-else.switch (iapPlatform)
{
case InAppPurchasePlatform.Apple:
HandleApplePurchase();
break;
case InAppPurchasePlatform.Google:
HandleGooglePurchase();
break;
default:
_log.Warning("Unhandled IAP platform {Platform}", iapPlatform);
break;
}
if (iapPlatform == InAppPurchasePlatform.Apple)
HandleApplePurchase();
else if (iapPlatform == InAppPurchasePlatform.Google)
HandleGooglePurchase();
else
_log.Warning("Unhandled IAP platform {Platform}", iapPlatform); OrderId VariablesThe OrderId member in InAppPurchaseEvent and elsewhere in IAP code has been renamed to AlternativePurchaseId to avoid confusion. It was originally used only for Google Play's Order ID, but has now been generalized to other platforms.
As an exception, it is still called OrderId in analytics events, such as PlayerEventInAppPurchased, to avoid interfering with external analytics processing systems.
Migration Steps:
InAppPurchaseEvent.OrderId to use InAppPurchaseEvent.AlternativePurchaseId instead.orderId not being found.InAppPurchaseStatus ValuesIn the InAppPurchaseStatus enum type, ValidReceipt has been renamed to Successful and InvalidReceipt to Failed. Similarly, IAPFlowTracker.FlowStep.FinishedWithInvalidReceipt has also been renamed to FinishedWithFailure.
They were renamed to better reflect the fact that on some purchasing platforms, such as Steam, the purchase process does not involve receipt validation, and a failed purchase does not necessarily mean a cheating attempt (as InvalidReceipt would imply).
Migration Steps:
InAppPurchaseStatus.ValidReceipt and InAppPurchaseStatus.InvalidReceipt with InAppPurchaseStatus.Successful and InAppPurchaseStatus.Failed, respectively.IAPFlowTracker.FlowStep.FinishedWithInvalidReceipt with IAPFlowTracker.FlowStep.FinishedWithFailure.ValidReceipt and InvalidReceipt with Successful and Failed, respectively.Some common services used by controller endpoint implementations have been renamed and changed namespace, custom controller implementation may need to be updated.
Migration Steps:
Update all references to Metaplay.Server.AdminApi.Exceptions.MetaplayHttpException to refer to Metaplay.Server.WebApi.MetaplayHttpException instead.
Update all references to Metaplay.Server.AdminApi.AdminApiJsonSerialization to refer to Metaplay.Server.WebApi.WebApiJsonSerialization instead.
S3BlobStorage.GetPresignedUrl() Methods with GetPresignedUrlAsync()To avoid accidentally blocking threads during credential refresh, S3BlobStorage.GetPresignedUrl has been deprecated. Use S3BlobStorage.GetPresignedUrlAsync instead.
Migration Steps:
Replace GetPresignedUrl() with await GetPresignedUrlAsync().
If your server environment is on us-east-1, ensure the new URL format works with the downstream systems.
ISessionCredentialService Implementations.Following the refactoring of the ISessionCredentialService interface, custom implementations of it need to be updated.
Migration Steps:
If your custom implementation of ISessionCredentialService simply adds a new social credentials provider, consider basing your class on the default UnityCredentialService and moving the social credentials functionality into a class implementing ISocialCredentialsProvider instead.
Remove updating MetaplaySDK.PlayerId from your implementation, as this is now done by the SDK.
Replace OnPlayerIdUpdatedAsync() with OnLoginSuccessAsync().
IntegrationRegistry.IntegrationClasses<T>() Calls with IntegrationRegistry.GetIntegrationClasses<T>()For clarity and naming consistency, IntegrationClasses<T>() is now GetIntegrationClasses<T>() and returns a Type[] instead of IEnumerable<Type>.
Migration Steps:
Replace IntegrationRegistry.IntegrationClasses() with IntegrationRegistry.GetIntegrationClasses().
If you need IEnumerable<Type>, you can cast the new return type to IEnumerable<Type>.
PlayerSessionParamsBase with PlayerSessionParams and PlayerSessionGameParamsBaseTo avoid further API breaks when SDK adds more parameters to SessionParams, all SDK parameters have been moved to PlayerSessionParams, leaving PlayerSessionGameParamsBase dedicated to game-specific data.
Migration Steps:
If overridden, replace PlayerActor OnClientSessionHandshakeAsync(PlayerSessionParamsBase) with OnClientSessionHandshakeAsync(PlayerSessionParams)
If overridden, replace PlayerActor OnSessionStartAsync(PlayerSessionParamsBase, bool) with OnSessionStartAsync(PlayerSessionParams, bool)
If PlayerSessionParamsBase was customized by inheriting it, inherit from PlayerSessionGameParamsBase instead. In all usages, in OnClientSessionHandshakeAsync and OnSessionStartAsync, retrieve the PlayerSessionGameParamsBase by casting PlayerSessionParamsBase.SessionGameParams
class MyPlayerSessionGameParams : PlayerSessionGameParamsBase { }
class MyPlayerSessionGameParams : PlayerSessionParamsBase { }
protected override async Task OnSessionFooAsync(PlayerSessionParamsBase sessionParams, bool isFirstLogin)
protected override async Task OnSessionFooAsync(PlayerSessionParams sessionParams, bool isFirstLogin)
{
MyPlayerSessionGameParams myParams = (MyPlayerSessionGameParams)sessionParams;
MyPlayerSessionGameParams myParams = (MyPlayerSessionGameParams)sessionParams.SessionGameParams; If PlayerSessionParamsBase was customized by inheriting it, update SessionActor implementation to return PlayerSessionGameParamsBase, and remove any SDK params from the constructor.
class MyPlayerSessionGameParams : PlayerSessionGameParamsBase { }
class MyPlayerSessionGameParams : PlayerSessionParamsBase { }
protected override PlayerSessionParamsBase GameCreatePlayerSessionParams(SessionStartParams sessionStart, SessionToken sessionToken)
protected override PlayerSessionGameParamsBase GameCreatePlayerSessionParams(SessionStartParams sessionStart, SessionToken sessionToken)
{
return new MyPlayerSessionGameParams(
sessionId: _entityId,
sessionToken: sessionToken,
deviceGuid: sessionStart.Meta.DeviceGuid,
...
myCustomParam: 123
);
}TryGetNextDueJob() in Custom DatabaseScanJobManagersThe return type of DatabaseScanJobManager.TryGetNextDueJob() has been changed from a tuple to a class due to new return values.
Migration Steps:
DatabaseScanJobManager classes, change their TryGetNextDueJob() methods to return a DueJobInfo object, or null if no job is due. The previously required canStart parameter is now optional and is true by default.public class MyScanJobManager : DatabaseScanJobManager
{
...
public override (DatabaseScanJobSpec jobSpec, bool canStart) TryGetNextDueJob(IContext context, MetaTime currentTime)
public override DueJobInfo TryGetNextDueJob(IContext context, MetaTime currentTime)
{
DatabaseScanJobSpec dueJobSpec = ...
bool canStart = ...
if (dueJobSpec == null)
return (null, false);
return null;
return (dueJobSpec, canStart);
return new DueJobInfo(dueJobSpec, canStart);
// Or if canStart is always true:
return new DueJobInfo(dueJobSpec);
}
}PlayerActorBase.CreateSocialAuthenticationEntry SignatureTo allow accessing user information of user's Social Account, CreateSocialAuthenticationEntry now contains IAuthenticationPlatformUserInfo parameter for platform-specific information.
Migration Steps:
If overridden, replace PlayerActor CreateSocialAuthenticationEntry(AuthenticationKey) with CreateSocialAuthenticationEntry(AuthenticationKey key, IAuthenticationPlatformUserInfo user)
If the overridden method uses PlayerAuthEntryBase.Default, pass the IAuthenticationPlatformUserInfo in PlayerAuthEntryBase.Default constructor.
If the overridden method uses custom PlayerAuthEntryBase, pass the IAuthenticationPlatformUserInfo to PlayerAuthEntryBase constructor.
PlayerEventPayloadBase and EventPayloadBasePlayerEventPayloadBase and EventPayloadBase were moved from Metaplay.Server.AdminApi.AuditLog to Metaplay.Core.AuditLog.
Migration Steps:
using Metaplay.Core.AuditLog; to all files that reference PlayerEventPayloadBase or EventPayloadBase.ModelUtil.RunAction()ModelUtil.RunAction() no longer takes in IChecksumContext. The parameter was unused.
Migration Steps:
Replace all instances of ModelUtil.RunAction(model, action, context) with ModelUtil.RunAction(model, action), i.e. remove the last argument.
In case generic parameters are not inferred, replace ModelUtil.RunAction<TModel, TAction, TChecksumCtx>(model, action, context) with ModelUtil.RunAction<TModel, TAction>(model, action), i.e. remove the last generic argument and parameter.
In a multiplayer entity, _journal is no longer used or exposed. Multiplayer entity debugging configuration has been unified under MultiplayerEntityActorBase.DebugOptions.
Migration Steps:
Replace all null-checks of MultiplayerEntityActorBase._journal to target MultiplayerEntityActorBase.Model.
If MultiplayerEntityActorBase.DesyncDebugging was overridden with a custom value, override MultiplayerEntityActorBase.DebugOptions instead and choose a suitable checksum mode.
If MultiplayerEntityClientBase._enableJournalCheckpointing was mutated, remove it. Instead, override MultiplayerEntityActorBase.DebugOptions and set a suitable consistency check mode.
ISessionCredentialService ConstructionThe function IMetaplayConnectionDelegate.GetSessionCredentialService() now gets the instance of the guest credentials store as parameter guestStore.
Migration Steps:
If your custom implementation of ISessionCredentialService derives from UnityCredentialsService then pass on the guestStore parameter to the base class constructor.
Update your implementation of IMetaplayConnectionDelegate.GetSessionCredentialService() to declare parameter CredentialsStore guestStore and pass it along to the constructor of the ISessionCredentialService as appropriate.
PlayerActionBase.IdThe field PlayerActionBase.Id was a client-generated incrementing integer. It has been removed as it was commonly useless, but at worst it lead to error-prone constructs. If your PlayerActions need an ID, the game client should generate and assign one itself.
Migration Steps:
Replace all uses of PlayerActionBase.Id with a new integer property in your Action.
public class MyPlayerAction : PlayerAction
{
public int TrackingId { get; set; } Assign the TrackingId of the value of a running counter, for example by incrementing a global variable.
public class MyActionManager
{
int _runningId;
public void PerformMyAction()
{
MyPlayerAction action = new MyPlayerAction();
action.TrackingId = _runningId++;
PlayerClientContext.ExecuteAction(action);
}IGameConfigSourceFetcherProvider for Custom IGameConfigSourceFetchers of the Custom GameConfigBuildSources.Previously, if a GameConfigBuildSource was also a IGameConfigSourceFetcher, it was automatically used as the fetcher for that source. Now, if you implement a custom GameConfigBuildSource, even if the same type is also an implementation for IGameConfigSourceFetcher, you must implement a custom IGameConfigSourceFetcherProvider that returns the fetcher instance for the source type.
Migration Steps:
Implement a custom IGameConfigSourceFetcherProvider for your custom fetcher and source.
public class MyCustomSourceAlsoFetcher : GameConfigBuildSource, IGameConfigSourceFetcher
{
public override string DisplayName => "MySource";
public IGameConfigSourceData Fetch(string itemName) => ...;
public Task<GameConfigBuildSourceMetadata> GetMetadataAsync(CancellationToken ct) => ...;
}
public class MyCustomFetcherProvider : DefaultGameConfigSourceFetcherProvider
{
public MyCustomFetcherProvider(GameConfigSourceFetcherConfigCore config) : base(config) {}
public override Task<IGameConfigSourceFetcher> GetFetcherForBuildSourceAsync(GameConfigBuildSource source, CancellationToken ct)
{
if (source is MyCustomSourceAlsoFetcher mySource)
return Task.FromResult<IGameConfigSourceFetcher>(mySource);
return base.GetFetcherForBuildSourceAsync(source, ct);
}
}Supply the custom IGameConfigSourceFetcherProvider to the build:
class MyGameConfigBuildIntegration : GameConfigBuildIntegration
{
public override IGameConfigSourceFetcherProvider MakeSourceFetcherProvider(IGameConfigSourceFetcherConfig config)
{
return new MyCustomFetcherProvider((GameConfigSourceFetcherConfigCore)config);
}
}Released on: April 4th, 2025
Released on: April 22nd, 2025
package.json for convenience, as this is often needed by projects with custom Dashboards.MInputSingleSelectAsyncDropdown component.PlayerSegmentsInput.vue over eagerly being in a pending state.Released on: May 14th, 2025