Appearance
Release 35
November 27th, 2025
Appearance
November 27th, 2025
The LiveOps Dashboard timeline view now includes Experiments and Broadcasts. This provides a comprehensive overview of your game's live operations, allowing you to visualize and manage these activities alongside other timeline events.

The timeline is now zoomable, allowing you to smoothly transition from looking at the big picture to looking at a single day, and back again.
Events that are scheduled for "player local" time now indicate the full range of times that they could be active using a ghosted outline. This takes into account the time zones that your players could be playing in.

The timeline has also undergone a series of improvements to its visual styling, UX and performance. Not only does it now look and feel better, but it also runs smoother!
The time it takes a player from starting the application to being able to play the game continues to be the focus of our SDK optimization work. In Metaplay R35, we're now wrapping up the effort of porting initialization of SDK registries to use Roslyn source generators in place of reflection. This means that there is no remaining reflection type scanning during the SDK initialization in client, reducing the cost of the call to MetaplaySDK.InitializeForClient() further.
Another bottleneck in the overall load time on client has been the loading and transforming of game config data to memory. We have received reports from projects with large and complex game config payloads seeing game config load times of multiple seconds on lower end devices, even when the current config archive is successfully read from the local filesystem cache. Metaplay R35 brings a number of micro-optimizations to the CPU cost of loading config data and resolving the intra-config dependencies. In our benchmarks, these optimizations were measured to provide an improvement of up to 50% to the overall game config load time. A large part of the optimization work involves reducing the complexity of the generic types involved in the game config system, and these optimizations will provide benefits to the initialization time of the application also outside of game config loading!
IPlayerModel.InAppPurchaseHistorySummary.PlayerModel in the database, but instead has no PlayerModel at all (the Payload column is null). This uses less space in the database and allows deleted players to be efficiently ignored by database scan jobs. See The Lifecycle of a Player for more information. To get the benefits of this change for your pre-existing data, run a one-time Entity Refresher Job for players in each environment at a convenient time after upgrading to this SDK version. However, the system will continue to work normally even if you don't.MetaDictionary and GameConfigLibrary, have been refactored to declare fewer generic methods and generic type dependencies. This is especially important with the IL2CPP runtime where type initialization of generic type specializations is expensive and can result in slow game load times.EntityAskAsync() now optionally accepts the timeout for the request.EntityAsk object now has a convenience EntityAsk.Reply() and EntityAsk.Refuse() methods.IReadOnlyList<>, IReadOnlyCollection<> and ArraySegment<> in serialization.game_player_incident_reports_by_country that counts incident reports per player country. See Implementing Player IP Geolocation for how to enable the country information.[SupportedSchemaVersions(min, max)] and therefore cannot be migrated, the entity will now fail to start instead of being automatically reset. This gives a chance to reinstate the old schema version in the C# code in case it was removed by mistake. Players can still be manually reset via the LiveOps Dashboard.MetaDictionary and GameConfigLibrary key and value collections can no longer be accidentally created via collection expression conversion, to match System.Collections.Generic.Dictionary behaviour.EntityAsk and EntityAsk<T> context parameters in ask handler methods has been moved from Metaplay.Cloud.Sharding.EntityShard into Metaplay.Cloud.Entity.entrypoint binary to use Go v1.25.4.JournalModelRerunChecker consistency check that ensured actions were deterministic on client. The check was expensive in both CPU and memory use. Determinism is already validated by the server-side checksum checks, resulting the same test coverage with a lower cost.MetaMessageRepository no longer uses reflection for initialization and now uses source generation to create the relevant data.AnalyticsEventRegistry no longer uses reflection for initialization and now uses source generation to create the relevant data.MultiplayerEntityActorBase, PersistedMultiplayerEntityActorBase, EphemeralMultiplayerEntityActorBase) accepting logChannelName have been deprecated. Log channel name should be given with a LogChannelName virtual property.insertAll streaming API by default. Support for Storage Write API was added in Release 31. The older API is still available for use by setting the AnalyticsSinkBigQuery.UseV2 server option to false.PlayerModel in the database. Instead, it has no PlayerModel at all (the Payload column is null) and its deletion state is stored in dedicated columns.DynamicEnum-deriving types property AllValues and methods FromId() and FromName() can no longer be used before the Metaplay SDK core has been initialized; an exception will be thrown. Previously, they were not explicitly forbidden but had ill-defined behavior if used before at least AssemblyUtil.LoadLocalAssemblies() had been called.DynamicEnum static members with [IgnoreDataMember] are now ignored.Payload by default. This includes uninitialized and deleted players (except for pre-existing deleted players that have not yet been migrated to the new null-Payload form).MetaSerializableType.Members has been changed from List<MetaSerializableMember> into MetaSerializableMember[].ClientLoggingConfig.LogLevelOverrides has been changed from List<LogLevelOverrideSpec> into LogLevelOverrideSpec[].ConnectionEndpointConfig.BackupGateways has been changed from List<ServerGatewaySpec> into ServerGatewaySpec[].EntityAskAsync() will now fail with a EntityUnreachableError{ ErrorCause = SenderActorIsAlreadyDead } instead of waiting for the timeout.StringId implementation for better performance, in particular the Add() path.MetaDictionary to depend on fewer types on static type initialization, to reduce per-specialization initialization time.MetaDictionary<object, IGameConfigData> as storage when the key type is not a value type.MetaRef resolving.onProgress handler.Newtonsoft.Json-for-Unity.Converters that caused MetaplaySDK fail initialization.JournalModelChecksumChecker could cause timeline state to be mutated, resulting in potentially different error behaviour being observed when running with and without journal checkers enabled.Secrets folder to exist.InnerException message in server logs for Entity crash stack traces.EntityUnreachableError error.ClientLaunchStatistics error while using Firebase Analytics.ClientDrivenIAPClientDelegate implementation.TerminalNetworkError with subtype SocialAuthenticateForceReconnectConnectionError.TaskQueueExecutor created for a ThreadPool scheduler could inherit Synchronization Context from Unity's main thread if enqueued task was inlined.overflowCard has been added to MCard that controls whether slot content can overflow the card's boundaries. For example, use to allow tooltips to overflow the card.freshTestGuild for performing end-to-end tests with Guilds.DailyCohortsChart and CohortsChart components have been deprecated in favor of the new chart components. If you had direct custom use of the old chart components, contact us for migration assistance!default variant from all input components to keep the types consistent. Simply leave the field undefined for default styling.MDateTime component now shows "a few seconds ago" for times under 1 second.MDateTime tooltips no longer show milliseconds for improved readability.GET /api/gameConfig/{id}?binary=true into GET /api/gameConfig/{id}/download@metaplay/meta-ui-next to @metaplay/meta-utilities package for better separation of concerns.MInputSelectDropdown component now maintains proper width and layout on smaller screens.GameServerMessageCenterView.MInputNumber component no longer triggers a validation error when you clear a field that has allow-undefined enabled and a min and/or max value defined.MInputNumber component clear button no longer disappears when you enter value 0 for a field that has the allow-undefined enabled.We are aware of the following issues in this release:
FileUtil.ReadAllBytesAsync on AndroidFileUtil.ReadAllBytesAsync() is documented to throw IOException if the target file does not exist. When accessing Android assets filesystem (i.e. files included in the APK itself), FileUtil.ReadAllBytesAsync() may throw NullReferenceException instead.
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.
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.
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 --version 9.0.11
# Then, generate the migration code:
Backend/Server$ dotnet ef migrations add MetaplayRelease35Then, 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.
We have updated the NUnit version used for the MetaplaySDK backend unit test projects, if you run Metaplay's Unit tests as part of your own tests. We recommend updating to v4.4.0 as well to prevent issues due to mismatching versions.
For more information on the migration, see NUnit 4.0 Migration Guidance. Note that to use the automatic Roslyn CodeFixes, you might have to update to NUnit 4 before starting the MetaplaySDK update.
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 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".Copy and overwrite the following files:
tsconfig.jsontsconfig.node.jsonTo 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 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.0Migration 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.pnpm install in your 'Dashboard' folder. This recreates all of the above files and folders with the correct dependencies.'default' VariantOur input components no longer have the 'default' variant. Simply leave the field undefined for default styling or use 'primary' if you want to be more explicit.
Migration Steps:
Run a type-check on your dashboard project to find any uses of the 'default' variant in your code
# Run from anywhere inside your project directory structure
metaplay build dashboardReplace the 'default' variant with undefined or 'primary' in the relevant places.
For example:
MInputText(
:model-value="someValue"
@update:model-value="(newValue) => someValue = newValue"
:variant="someValue === '' ? 'danger' : 'default'"
:variant="someValue === '' ? 'danger' : undefined"
)or
MInputTextArea(
:model-value="someValue"
@update:model-value="(newValue) => someValue = newValue"
:variant="someValue === '' ? 'danger' : 'default'"
:variant="someValue === '' ? 'danger' : 'primary'"
)As part of the FontAwesome dependency update, icon sizes and padding have changed. Review your custom dashboard components and, if necessary, adjust icon sizes or spacing as follows:
Migration Steps:
Review your dashboard for any custom FontAwesome icon usage.
Adjust icon sizes or padding/margins as needed. For example, if you previously used size="sm", you might change it to size="xs", and/or remove any custom padding/margin that is no longer necessary.
fa-icon(
icon="wrench"
size="sm"
size="xs" // Adjust size as needed (e.g., 'xs' instead of 'sm'
class="tw-px-2" // Remove/reduce custom padding/margin if it is not needed anymore.
)The duration utility functions have been moved from @metaplay/meta-ui-next to @metaplay/meta-utilities package.
Migration Steps:
Search your dashboard project for any imports of duration utility functions from @metaplay/meta-ui-next.
import { parseDotnetTimeSpanToLuxon } from '@metaplay/meta-ui-next'Update the import statements to use @metaplay/meta-utilities instead.
import { parseDotnetTimeSpanToLuxon } from '@metaplay/meta-utilities'Vue has updated the default TSConfig to match closer the Microsoft's default TSConfig. This includes enabling noUncheckedIndexedAccess flag.
To ensure indexed access result in the correct type, you need to ensure stricter typing or add null assertions. Or alternatively you can disable the noUncheckedIndexedAccess flag.
Migration Steps:
Either update typing to ensure indexed elements exist:
const myArray: string[] = [ "1", "2" ]
const myArray: [string, ...string[]] = [ "1", "2" ]
const firstField: string = myArray[0]Or add not-null-assertion on accesses when the element is known or checked to exist:
const myArray: string[] = [ "1", "2" ]
if (myArray.length > 0) {
const firstField: string = myArray[0]
const firstField: string = myArray[0]!
}Or use ? or ?? operators to handle possibility of the missing element:
const myNumber = 3
const numberToFriendlyString: Record<number, string> = { 0: "none", 1: "just one", 2: "pair"}
const friendlyName: string = numberToFriendlyString[myNumber]
const friendlyName: string = numberToFriendlyString[myNumber] ?? "some"If the above apporaches are not suitable, index access type checking can be disabled with:
{
...
"compilerOptions": {
...
"noUncheckedIndexedAccess": false, The change of turning the full in-app purchase history server-only means that any existing use of it on client (or shared game logic) will no longer work. The most common shared use of this data is for player segmentation conditions, as the segment conditions need to be evaluatable both on server and client. For example, a segment condition for the number of lifetime purchases a player has made could previously simply count the entries in PlayerModelBase.InAppPurchaseHistory.
For continuing to be able to implement purchase-related logic in shared code, the SDK will now generate and maintain a "summary" of the purchase history as member PlayerModelBase.InAppPurchaseHistorySummary. This data is never persisted to database, but rather created whenever the model is loaded and then updated in response to changes in the full purchase history.
Migration Steps:
PlayerModelBase.InAppPurchaseHistory. In R35, a member by that name no longer exists so all legacy access will generate compile errors that need to be addressed.PlayerModelBase.FullInAppPurchaseHistory instead. The data contained in the full history has not changed, so no further action should be required.PlayerModelBase.InAppPurchaseHistorySummary. The SDK provides a number of common aggregations over the full history of events, as well as price data for purchases within the last 30 days that can be used for aggregations involving recent player behavior.InAppPurchaseHistory into the project, add serializable members for your custom aggregations and populate them in the constructor.PlayerModelBase.CreateInAppPurchaseHistorySummary() to create your custom summary class.The interfaces for defining new database scan jobs have been adjusted to avoid needing custom modifications to the SDK code. These migrations only apply to you if you have already defined custom scan jobs.
The SDK's scan job priority numbers in DatabaseScanJobPriorities have been updated to set them further apart from each other, allowing for more granularity in your custom priority numbers between the SDK's priorities.
If you have defined custom priority numbers for the Priority property in your DatabaseScanJobSpec subclasses, you should update them accordingly.
The SDK's DatabaseScanJobPriorities have been changed as follows:
Migration Steps:
In each of your classes derived from DatabaseScanJobSpec (including from the MaintenanceJobSpec intermediate class), update the Priority number if needed.
a. If reusing an SDK-defined constant from DatabaseScanJobPriorities: no change needed.
// OK, will keep working as before.
public override int Priority => DatabaseScanJobPriorities.ScheduledPlayerDeletion;b. If using some other number: consider if the number needs to be updated. To keep the correct priority relative to the SDK-defined jobs, see the list above.
// Intends to be within the SDK's priority range.
public override int Priority => 1;
public override int Priority => 200;
// Or alternatively: can now fit numbers strictly between SDK's priority numbers.
public override int Priority => 250;
// Intends to be higher than all other jobs.
public override int Priority => 3;
public override int Priority => 10000; DatabaseScanJobManager IntegrationThis only applies to you if you have defined custom subclasses of DatabaseScanJobManager. Custom database scan job managers are now detected and instantiated automatically and no longer need modifications to the SDK-side file DatabaseScanCoordinator.cs.
Note that custom jobs using the MaintenanceJobSpec base class are not affected, because they use an SDK-defined manager class.
Migration Steps:
Add the [MetaSerializableDerived(TYPE_CODE)] attribute to your manager class. TYPE_CODE must be the same number as the corresponding enum value in LegacyDatabaseScanJobManagerKind.
[MetaSerializable]
[MetaSerializableDerived(1000)]
public class MyCustomScanJobManager
{
}Remove the custom enum value from the SDK-side LegacyDatabaseScanJobManagerKind.
public enum LegacyDatabaseScanJobManagerKind
{
...
MyCustomScanJobManager = 1000,
}Remove the custom code from the SDK-side GetJobManagerOfKind(). This method has been removed entirely.
DatabaseScanJobManager GetJobManagerOfKind(DatabaseScanJobManagerKind kind)
{
switch (kind)
{
...
case DatabaseScanJobManagerKind.MyCustomScanJobManager: return _state.MyCustomScanJobManager;
...
}
} The custom member in DatabaseScanCoordinatorState will be automatically migrated and set to null when the server starts. Therefore, you should not remove this member immediately, but can do so after the server has been successfully running in all important environments (especially production).
public class DatabaseScanCoordinatorState : ISchemaMigratable
{
...
[MetaMember(1000)] public MyCustomScanJobManager MyCustomScanJobManager { get; private set; } = new MyCustomScanJobManager();
// TODO: Legacy member. This can removed after this version of the code has been successfully running in production.
[MetaMember(1000)] public MyCustomScanJobManager Legacy_MyCustomScanJobManager { get; private set; } = new MyCustomScanJobManager();
}Starting with this release, database scan jobs now skip entities with null Payload by default. This includes players in an uninitialized state (a usually short-lived state during a new player account creation) as well as deleted players (except for pre-existing deleted players that have not yet been migrated to the new null-Payload form introduced in this release).
Skipping null Payloads is almost always what you want in scan jobs. If this is the case for you, there is no change needed, except for a possible code simplification. If you do not want to skip null Payloads, a simple override is needed in your scan job.
Migration Steps:
Find all of your custom implementations of the DatabaseScanProcessor base class. Note that the SDK also has implementations of this class, but you can ignore those.
public class MyCustomScanJobProcessor : DatabaseScanProcessor<...>
{
...
}Inspect the StartProcessItemBatchAsync(...) method and understand what it does with items that have null Payload.
If it doesn't do anything important, such as only perhaps records some statistics and skips the item, you can simply remove the code specific to null Payload, because such items are now skipped by default:
public class MyCustomScanJobProcessor : DatabaseScanProcessor<SomeItemType>
{
...
public override Task StartProcessItemBatchAsync(IContext context, IEnumerable<SomeItemType> items)
{
foreach (SomeItemType item in items)
{
if (item.Payload == null)
continue;
...
}
}
}However, if null-Payload items are actually important and should be processed, then override the ItemFilter property to return DatabaseItemSpec.ItemFilter.IncludeAllItems:
public class MyCustomScanJobProcessor : DatabaseScanProcessor<SomeItemType>
{
...
public override DatabaseItemSpec.ItemFilter ItemFilter => DatabaseItemSpec.ItemFilter.IncludeAllItems;
}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.
The following runtime options have been removed:
PushNotifications.UseLegacyApi - The underlying API has been deprecated.Database.EnableMySqlLogging - MySql logging is now enabled by default.Migration Steps:
Remove unused runtime options
PushNotifications:
UseLegacyApi: true
Database:
EnableMySqlLogging: trueEntityAsk and EntityAsk<T> Types.To make discovery of the types easier, EntityAsk and EntityAsk<T> have been moved from being in nested class Metaplay.Cloud.Sharding.EntityShard into the Metaplay.Cloud.Entity namespace.
Migration Steps:
Update references EntityShard.EntityAsk and EntityShard.EntityAsk<T> in Entity ask handlers into EntityAsk and EntityAsk<T>
using Metaplay.Cloud.Entity;
...
[EntityAskHandler]
public void HandleExample(EntityShard.EntityAsk<ExampleResponse> entityAsk, ExampleRequest request)
public void HandleExample(EntityAsk<ExampleResponse> entityAsk, ExampleRequest request)
{
}Base classes for Entity Actor no longer take constructor parameters. This removes the need for dummy forwarding constructors when creating an Entity Actor.
Entity actor constructors accepting EntityId parameter have been deprecated. The EntityActor now automatically discovers the EntityId. Additionally, multiplayer entity actor constructors for setting logChannelName is deprecated. Log channel name should be given with a LogChannelName virtual property.
Migration Steps:
For all Entity Actors, remove dummy forwarding constructors
class MyActor
{
...
public MyActor(EntityId entityId) : base(entityId) {}
}For all Entity Actors with a costructor, remove EntityId entityId constructor parameter. Replace uses of entityId with _entityId.
class MyActor
{
...
public MyActor(EntityId entityId) : base(entityId)
public MyActor()
{
DoSomethingWithId(entityId)
DoSomethingWithId(_entityId)
}
}For all Multiplayer Entity Actors that pass logChannelName parameter, set the name using LogChannelName virtual property.
class MyActor
{
protected override string LogChannelName => "foo";
...
public MyActor(EntityId entityId) : base(entityId, logChannelName: "foo")
public MyActor()
{
}
}The initialization schema of the DynamicEnum value registry has been reimplemented using source generators instead of reflection. The registry is now created at a well-defined time during Metaplay SDK initialization rather than lazily on first use. As a result of this, the AllValues property as well as the TryFromId(), FromId(), TryFromName(), and FromName() accessors cannot be used before SDK initialization. Previously, it was allowed but with ill-defined behavior.
Migration Steps:
To reduce the number of dependent generic types, MetaDictionary<TKey, TValue>.GetEnumerator() now returns an enumerator that doesn't implement IEnumerator<KeyValuePair<TKey, TValue>> as this isn't required in the typical case of enumerating dictionary entries directly.
To get an iterator of type IEnumerator<KeyValuePair<TKey, TValue>> you will have to explicitly invoke the IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() method by casting the dictionary first.
Migration Steps:
If you need IEnumerator<> of a MetaDictionary<,>, cast the dictionary into IEnumerable<> first:
MetaDictionary<TKey, TValue> dict;
IEnumerator<KeyValuePair<TKey, TValue>> enumerator = dict.GetEnumerator();
IEnumerator<KeyValuePair<TKey, TValue>> enumerator = ((IEnumerable<KeyValuePair<TKey, TValue>>)dict).GetEnumerator(); GameConfigLibrary<TKey, TValue> Is No Longer new()ableAs part of the GameConfig loading optimization efforst, the GameConfigLibrary class has been changed to abstract. If you were previously relying on the public parameterless constructor for creating empty config libraries, you'll need to use GameConfigLibrary<TKey, TValue>.CreateEmpty() instead.
Migration Steps:
If you use new GameConfigLibrary<,> to create an empty library, replace that with CreateEmpty():
public GameConfigLibrary<ExampleId, ExampleId> ExampleLibrary { get; set; } = new GameConfigLibrary<ExampleId, ExampleId>();
public GameConfigLibrary<ExampleId, ExampleId> ExampleLibrary { get; set; } = GameConfigLibrary<ExampleId, ExampleId>.CreateEmpty(); IEnvironmentConfigProviderThe collection types in EnvironmentConfig have been changed from List<> into Array to both communicate they are immutable but also to reduce IL2CPP metadata initialization caused by List<> types. If you have implemented IEnvironmentConfigProvider and are supplying these values, you need to supply them as arrays instead.
Migration Steps:
If you have implemented IEnvironmentConfigProvider, and provide ClientLoggingConfig.LogLevelOverrides, create an array instead:
new ClientLoggingConfig
{
LogLevelOverrides = new List<LogLevelOverrideSpec>() { ... },
LogLevelOverrides = new LogLevelOverrideSpec[] { ... },
};If you have implemented IEnvironmentConfigProvider, and provide ConnectionEndpointConfig.BackupGateways, create an array instead:
new ConnectionEndpointConfig()
{
BackupGateways = new List<ServerGatewaySpec>() { ... },
BackupGateways = new ServerGatewaySpec[] { ... },
};The API endpoint for downloading a GameConfig from server has been changed from GET /api/gameConfig/{id}?binary=true into GET /api/gameConfig/{id}/download.
Migration Steps:
If you have calls to fetch /api/gameConfig/{id}?binary=true, replace these with /api/gameConfig/{id}/download
await httpClient.GetAsync($"{host}/api/gameConfig/{guid}?binary=true")
await httpClient.GetAsync($"{host}/api/gameConfig/{guid}/download") 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