Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3f508f2
meta: Attach unitypackages from inside unity repo
kirre-bylund Jul 6, 2026
cddedbe
meta: Robustness changes to open-release-pr.yml
kirre-bylund Jul 6, 2026
93ce035
meta: Further robustness fixes to workflows
kirre-bylund Jul 9, 2026
955c934
fix: Allow "clear local player data" button when disabled extension
kirre-bylund Jul 16, 2026
e5ef68e
Add List Platform Keys endpoint
kirre-bylund Jul 17, 2026
0c4ad76
Fix .meta file and add doxygen group for PlatformKeys
kirre-bylund Jul 17, 2026
ea5468f
feat: Add support for WLL custom sign up fields
kirre-bylund Jul 9, 2026
03f5eb2
fix: Decouple custom_fields from login request, fix test assertions
kirre-bylund Jul 17, 2026
4b0081c
feat: align SDKs with "better files" backend changes
kirre-bylund Aug 26, 2026
97b3ba3
fixes after review
kirre-bylund Aug 27, 2026
5aada3f
ci: Add sign up fields for testing
kirre-bylund Aug 27, 2026
6a0e6cb
feat: add ConnectSteamAccount, ConnectXboxAccount, ConnectNintendo, C…
kirre-bylund Aug 21, 2026
e011ddf
feat: add auto_create_profile optional to session requests
kirre-bylund Aug 21, 2026
76f01b4
feat: add GetInt/SetInt to ILootLockerStateWriter and expose GetState…
kirre-bylund Aug 21, 2026
45a15e6
fix: use WaitForSecondsRealtime instead of WaitForSeconds in coroutines
kirre-bylund Aug 21, 2026
677d3e7
chore: reorganize project settings UI with foldouts, suppress unreach…
kirre-bylund Aug 21, 2026
c6c4b36
fix: Fixes after review
kirre-bylund Aug 28, 2026
9f63eb3
fix: Custom sign up field request should be json primitive
kirre-bylund Aug 28, 2026
de7ee7f
fix: file tests
kirre-bylund Sep 2, 2026
b7cea13
Bump version to 8.2.0
github-actions[bot] Sep 2, 2026
37a2dc1
feat: Enable the error reporting feature by default
kirre-bylund Sep 22, 2026
17673fc
fix: replace GetInstanceID with GetEntityId on Unity 6.5+
kirre-bylund Sep 22, 2026
63d279b
Add LootLockerCI category and guard clamping asserts in timezone tests
kirre-bylund Sep 22, 2026
2808031
feat: add Items and Item Templates support
kirre-bylund Sep 25, 2026
e1f8c40
fix: address review remarks and add Assets 2.0 metadata sources
kirre-bylund Sep 25, 2026
774c05b
fix(unity): correct item DTO types and inline API manager methods
kirre-bylund Sep 28, 2026
7b09bfc
test(unity): add functional tests for items and item templates
kirre-bylund Sep 28, 2026
32e4697
refactor(unity): rename player item APIs to player inventory items
kirre-bylund Sep 30, 2026
eeb50a3
fix(unity): allow insecure http in local test runs
kirre-bylund Sep 30, 2026
52dabb9
fix(unity): support optional item consumption and local test settings
kirre-bylund Sep 30, 2026
866392b
test(unity): space out player file revisions by one second
kirre-bylund Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .doxygen/groups.dox
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,15 @@
///
/// See the [LootLocker documentation](https://docs.lootlocker.com/commerce/wallets).

/// @defgroup PlatformKeys Platform Keys
/// @brief List platform keys redeemed by the authenticated player.
///
/// Platform keys are keys distributed through LootLocker's Campaign system
/// (e.g. Steam keys, Discord keys). This endpoint returns all keys redeemed
/// by the currently authenticated player, grouped by campaign.
///
/// See the [LootLocker documentation](https://docs.lootlocker.com/campaigns/overview).

/// @defgroup Catalog Catalog
/// @brief Browse item listings and prices in the LootLocker storefront catalog.
///
Expand Down
44 changes: 44 additions & 0 deletions .github/scripts/run-tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ function Initialize-TestProject {
if (-not [string]::IsNullOrEmpty($scriptingDefines)) {
$psContent += $nl + ' scriptingDefineSymbols:' + $nl + ' 1: ' + $scriptingDefines
}
if ($UseLocalEnv) {
# The local devenv is plain http; Unity blocks insecure connections unless this is set.
$psContent += $nl + ' insecureHttpOption: 2'
}
[IO.File]::WriteAllText((Join-Path $TempProject 'ProjectSettings\ProjectSettings.asset'), $psContent)
}

Expand All @@ -144,6 +148,46 @@ if (-not [string]::IsNullOrWhiteSpace($CustomProject) -and (Test-Path $CustomPro
$ProjectPath = $TempProject
}

# Reused and custom projects need the same local settings as freshly initialized projects.
if ($UseLocalEnv) {
$ProjectSettingsFile = Join-Path $ProjectPath "ProjectSettings\ProjectSettings.asset"
if (Test-Path $ProjectSettingsFile) {
$nl = [char]10
$settingsContent = [IO.File]::ReadAllText($ProjectSettingsFile)
if ($settingsContent -notmatch 'LOOTLOCKER_COMMANDLINE_SETTINGS') {
$defineProperty = [Regex]::Match($settingsContent, '(?m)^ scriptingDefineSymbols:[^\r\n]*(?:\r?\n|$)')
if ($defineProperty.Success) {
$groupOne = [Regex]::Match($settingsContent.Substring($defineProperty.Index + $defineProperty.Length), '\A(?: [^\r\n]*\r?\n)*? 1:[ \t]*[^\r\n]*')
if ($groupOne.Success) {
$start = $defineProperty.Index + $defineProperty.Length + $groupOne.Index
$newGroup = if ($groupOne.Value -match '^ 1:[ \t]*$') {
' 1: LOOTLOCKER_COMMANDLINE_SETTINGS'
} else {
$groupOne.Value + ';LOOTLOCKER_COMMANDLINE_SETTINGS'
}
$settingsContent = $settingsContent.Remove($start, $groupOne.Length).Insert($start, $newGroup)
} else {
$settingsContent = $settingsContent.Insert($defineProperty.Index + $defineProperty.Length, ' 1: LOOTLOCKER_COMMANDLINE_SETTINGS' + $nl)
}
} else {
$settingsContent = $settingsContent.TrimEnd() + $nl + ' scriptingDefineSymbols:' + $nl + ' 1: LOOTLOCKER_COMMANDLINE_SETTINGS' + $nl
}
}
if ($settingsContent -match '(?m)^[ \t]*insecureHttpOption:') {
$updatedContent = [Regex]::Replace($settingsContent, '(?m)^[ \t]*insecureHttpOption:.*$', ' insecureHttpOption: 2')
} else {
$updatedContent = $settingsContent.TrimEnd() + $nl + ' insecureHttpOption: 2' + $nl
}
if ($updatedContent -ne $settingsContent) {
[IO.File]::WriteAllText($ProjectSettingsFile, $updatedContent)
Write-Step "Enabled insecure http in $ProjectSettingsFile"
}
} else {
Write-Fail "ProjectSettings.asset not found at $ProjectSettingsFile; cannot enable local http."
exit 1
}
}

$ResultsFile = Join-Path $ProjectPath "TestResults.xml"
$LogFile = Join-Path $ProjectPath "test-run.log"

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:

PR_JSON=$(gh pr view "$PR_NUMBER" --json number,body,labels,title,state --jq '.')
PR_TITLE=$(echo "$PR_JSON" | jq -r '.title')
HAS_RC_LABEL=$(echo "$PR_JSON" | jq -r '[.labels[].name] | index("release candidate") != null')
HAS_RC_LABEL=$(echo "$PR_JSON" | jq -r '.labels[].name' | tr '[:upper:]' '[:lower:]' | grep -q 'release candidate' && echo "true" || echo "false")
PR_STATE=$(echo "$PR_JSON" | jq -r '.state')

echo "Found PR #${PR_NUMBER}: ${PR_TITLE} (state: ${PR_STATE})"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/enforce-release-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
name: Validate release candidate PR
runs-on: ubuntu-latest
timeout-minutes: 3
if: contains(github.event.pull_request.labels.*.name, 'release candidate')
if: contains(github.event.pull_request.labels.*.name, 'Release Candidate') || contains(github.event.pull_request.labels.*.name, 'release candidate')

steps:
- name: Checkout
Expand Down
45 changes: 24 additions & 21 deletions .github/workflows/open-release-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,21 @@ jobs:
id: current
run: |
VERSION=$(jq -r '.version' package.json)
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
echo "::error::Could not read version from package.json"
exit 1
fi
echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Current version: ${VERSION}"

- name: Get previous release tag and commit log
id: prev-tag
run: |
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "TAG=${LATEST_TAG}" >> "$GITHUB_OUTPUT"
echo "Previous tag: ${LATEST_TAG:-none}"

# Collect commits since last tag (or all commits if no tag yet)
echo "Previous tag: ${LATEST_TAG}"
REV_RANGE=""
if [ -n "$LATEST_TAG" ]; then
if [ -n "$LATEST_TAG" ] && [ "$LATEST_TAG" != "v0.0.0" ]; then
REV_RANGE="${LATEST_TAG}..HEAD"
else
REV_RANGE="HEAD"
Expand Down Expand Up @@ -81,23 +83,24 @@ jobs:
if [ "$BUMP" = "auto" ] && [ "$ALREADY_CALCULATED" = "false" ]; then
echo "Auto-detecting version from OpenRouter..."
if [ -z "${{ secrets.OPENROUTER_API_KEY }}" ]; then
echo "::error::OPENROUTER_API_KEY secret not set. Use manual bump or set secret."
exit 1
fi
COMMITS=$(head -100 /tmp/commits.log)
printf 'Based on these commit messages since the last release (v%s), determine the correct semver version for the next release. Analyze breaking changes, new features, and fixes. Respond with only the version number (e.g., 9.0.0).\n\n%s\n' "$CURRENT" "$COMMITS" > /tmp/version-prompt.txt
RESPONSE=$(curl -s "https://openrouter.ai/api/v1/chat/completions" \
-H "Authorization: Bearer ${{ secrets.OPENROUTER_API_KEY }}" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile prompt /tmp/version-prompt.txt '{model:"deepseek/deepseek-v4-pro",messages:[{role:"user",content:$prompt}],reasoning:{effort:"low"},max_tokens:2000,temperature:0}')")
NEW=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // ""' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [ -z "$NEW" ]; then
echo "::warning::AI returned no parseable version, falling back to patch bump."
PATCH=$(echo "$CURRENT" | cut -d. -f3)
NEW="$(echo "$CURRENT" | cut -d. -f1).$(echo "$CURRENT" | cut -d. -f2).$((PATCH + 1))"
echo "::warning::OPENROUTER_API_KEY not set. Falling back to patch bump."
BUMP="patch"
else
COMMITS=$(head -100 /tmp/commits.log)
printf 'The current version is v%s. Based on these commit messages since the last release, determine the correct semver version for the next release. Analyze breaking changes, new features, and fixes. Respond with only the version number (e.g., 9.0.0).\n\n%s\n' "$CURRENT" "$COMMITS" > /tmp/version-prompt.txt
RESPONSE=$(curl -s "https://openrouter.ai/api/v1/chat/completions" \
-H "Authorization: Bearer ${{ secrets.OPENROUTER_API_KEY }}" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile prompt /tmp/version-prompt.txt '{model:"deepseek/deepseek-v4-pro",messages:[{role:"user",content:$prompt}],reasoning:{effort:"low"},max_tokens:2000,temperature:0}')")
NEW=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // ""' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [ -z "$NEW" ]; then
echo "::warning::AI returned no parseable version. Falling back to patch bump."
BUMP="patch"
else
echo "AI suggested version: ${NEW}"
ALREADY_CALCULATED=true
fi
fi
echo "AI suggested version: ${NEW}"
ALREADY_CALCULATED=true
fi

if [ "$ALREADY_CALCULATED" = "false" ]; then
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/package-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ jobs:
- name: Attach .unitypackage to release
if: github.event_name == 'release'
run: |
for f in unity-sdk-packager/LootLockerSDK*.unitypackage; do
cd unity-sdk
for f in ../unity-sdk-packager/LootLockerSDK*.unitypackage; do
echo "Uploading $f"
gh release upload --clobber "${{ github.ref_name }}" "$f"
done
Expand Down
23 changes: 23 additions & 0 deletions Runtime/Client/LootLockerEndPoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class LootLockerEndPoints
// White Label Login
[Header("White Label Login")]
public static EndPointClass whiteLabelSignUp = new EndPointClass("white-label-login/sign-up", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base);
public static EndPointClass whiteLabelSignUpFields = new EndPointClass("white-label-login/sign-up/fields", LootLockerHTTPMethod.GET, LootLockerEnums.LootLockerCallerRole.Base);
public static EndPointClass whiteLabelLogin = new EndPointClass("white-label-login/login", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base);
public static EndPointClass whiteLabelVerifySession = new EndPointClass("white-label-login/verify-session", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base);
public static EndPointClass whiteLabelRequestPasswordReset = new EndPointClass("white-label-login/request-reset-password", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base);
Expand Down Expand Up @@ -73,6 +74,14 @@ public class LootLockerEndPoints
public static EndPointClass uploadPlayerFile = new EndPointClass("player/files", LootLockerHTTPMethod.UPLOAD_FILE);
public static EndPointClass updatePlayerFile = new EndPointClass("/player/files/{0}", LootLockerHTTPMethod.UPDATE_FILE);
public static EndPointClass deletePlayerFile = new EndPointClass("/player/files/{0}", LootLockerHTTPMethod.DELETE);
public static EndPointClass listPlayerFileRevisions = new EndPointClass("player/files/{0}/revisions", LootLockerHTTPMethod.GET);
public static EndPointClass getPlayerFileRevision = new EndPointClass("player/files/{0}/revisions/{1}", LootLockerHTTPMethod.GET);
public static EndPointClass promotePlayerFileRevision = new EndPointClass("player/files/{0}/revisions/{1}/current", LootLockerHTTPMethod.POST);
public static EndPointClass getPlayerFileByKey = new EndPointClass("player/files/key/{0}", LootLockerHTTPMethod.GET);
public static EndPointClass listPlayerFileRevisionsByKey = new EndPointClass("player/files/key/{0}/revisions", LootLockerHTTPMethod.GET);
public static EndPointClass getPlayerFileRevisionByKey = new EndPointClass("player/files/key/{0}/revisions/{1}", LootLockerHTTPMethod.GET);
public static EndPointClass promotePlayerFileRevisionByKey = new EndPointClass("player/files/key/{0}/revisions/{1}/current", LootLockerHTTPMethod.POST);
public static EndPointClass deletePlayerFileByKey = new EndPointClass("player/files/key/{0}", LootLockerHTTPMethod.DELETE);

// Player Progressions
[Header("Player Progressions")]
Expand Down Expand Up @@ -253,6 +262,10 @@ public class LootLockerEndPoints
public static EndPointClass getCurrencyDetails = new EndPointClass("currency/code/{0}", LootLockerHTTPMethod.GET);
public static EndPointClass getCurrencyDenominationsByCode = new EndPointClass("currency/code/{0}/denominations", LootLockerHTTPMethod.GET);

// Platform Keys
[Header("Platform Keys")]
public static EndPointClass listPlatformKeys = new EndPointClass("platform-keys/v1", LootLockerHTTPMethod.GET);

// Balances
[Header("Balances")]
public static EndPointClass listBalancesInWallet = new EndPointClass("balances/wallet/{0}", LootLockerHTTPMethod.GET);
Expand Down Expand Up @@ -336,6 +349,16 @@ public class LootLockerEndPoints
// Presence (WebSocket)
[Header("Presence")]
public static EndPointClass presenceWebSocket = new EndPointClass("presence/v1", LootLockerHTTPMethod.GET);

// Items
[Header("Items")]
public static EndPointClass listItemTemplates = new EndPointClass("player/inventory/v1", LootLockerHTTPMethod.GET);
public static EndPointClass listPlayerInventoryItems = new EndPointClass("player/inventory/v1/my", LootLockerHTTPMethod.GET);
public static EndPointClass getPlayerInventoryItem = new EndPointClass("player/inventory/v1/{0}", LootLockerHTTPMethod.GET);
public static EndPointClass deletePlayerInventoryItem = new EndPointClass("player/inventory/v1/{0}", LootLockerHTTPMethod.DELETE);
public static EndPointClass consumePlayerInventoryItem = new EndPointClass("player/inventory/v1/{0}/consume", LootLockerHTTPMethod.POST);
public static EndPointClass splitPlayerInventoryItemStack = new EndPointClass("player/inventory/v1/{0}/split", LootLockerHTTPMethod.POST);
public static EndPointClass mergePlayerInventoryItemStacks = new EndPointClass("player/inventory/v1/merge", LootLockerHTTPMethod.POST);
}

[Serializable]
Expand Down
2 changes: 0 additions & 2 deletions Runtime/Client/LootLockerHTTPClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -747,12 +747,10 @@ private void CallListenersAndMarkDone(LootLockerHTTPExecutionQueueItem execution

response.requestContext = new LootLockerRequestContext(executionItem.RequestData.ForPlayerWithUlid, executionItem.RequestData.RequestStartTime, executionItem.RequestData.RequestId);

#if LOOTLOCKER_BETA_ENABLE_ERROR_REPORTING
if (response != null && !response.success)
{
StoreFailedRequestReport(response, executionItem);
}
#endif

CurrentlyOngoingRequests.Remove(executionItem.RequestData.RequestId);
executionItem.IsWaitingForSessionRefresh = false;
Expand Down
31 changes: 24 additions & 7 deletions Runtime/Client/LootLockerLifecycleManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ public class LootLockerLifecycleManager : MonoBehaviour
#region Instance Handling

private static LootLockerLifecycleManager _instance;
private static int _instanceId = 0;
#if UNITY_6000_5_OR_NEWER
private static EntityId _instanceId = default;
#else
private static int _instanceId = default;
#endif
private static GameObject _hostingGameObject = null;
private static readonly object _instanceLock = new object();

Expand All @@ -70,7 +74,7 @@ private static void AutoInitialize()
{
Debug.LogWarning($"[LootLocker] LifecycleManager auto-initialization failed: {ex.Message}. Services will be initialized on first use.");
_instance = null;
_instanceId = 0;
_instanceId = default;
if (_hostingGameObject != null)
{
Destroy(_hostingGameObject);
Expand Down Expand Up @@ -115,7 +119,11 @@ private static void Instantiate()
{
var gameObject = new GameObject("LootLockerLifecycleManager");
_instance = gameObject.AddComponent<LootLockerLifecycleManager>();
#if UNITY_6000_5_OR_NEWER
_instanceId = _instance.GetEntityId();
#else
_instanceId = _instance.GetInstanceID();
#endif
_hostingGameObject = gameObject;

if (Application.isPlaying)
Expand Down Expand Up @@ -150,7 +158,7 @@ private static void TeardownInstance()
#endif

_instance = null;
_instanceId = 0;
_instanceId = default;
_hostingGameObject = null;
}

Expand All @@ -168,13 +176,22 @@ public static IEnumerator CleanUpOldInstances()
#endif
foreach (LootLockerLifecycleManager manager in managers)
{
if (manager != null && _instanceId != manager.GetInstanceID() && manager.gameObject != null && ((LootLockerLifecycleManager)manager)._isInitialized)
if (manager != null && manager.gameObject != null && ((LootLockerLifecycleManager)manager)._isInitialized)
{
bool isCurrentInstance;
#if UNITY_6000_5_OR_NEWER
isCurrentInstance = _instanceId == manager.GetEntityId();
#else
isCurrentInstance = _instanceId == manager.GetInstanceID();
#endif
if (!isCurrentInstance)
{
#if UNITY_EDITOR
DestroyImmediate(manager.gameObject);
DestroyImmediate(manager.gameObject);
#else
Destroy(manager.gameObject);
Destroy(manager.gameObject);
#endif
}
}
}
yield return null;
Expand Down Expand Up @@ -572,7 +589,7 @@ private IEnumerator ServiceHealthMonitor()

while (_serviceHealthMonitoringEnabled && Application.isPlaying)
{
yield return new WaitForSeconds(healthCheckInterval);
yield return new WaitForSecondsRealtime(healthCheckInterval);

if (_state != LifecycleManagerState.Ready)
{
Expand Down
6 changes: 3 additions & 3 deletions Runtime/Client/LootLockerPresenceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ private IEnumerator WaitForConnectionAndUpdateStatus(string status, Dictionary<s
int waitCount = 0;
while(!IsConnectedAndAuthenticated && waitCount < maxWaitTimes)
{
yield return new WaitForSeconds(0.1f);
yield return new WaitForSecondsRealtime(0.1f);
waitCount++;
}

Expand Down Expand Up @@ -1122,7 +1122,7 @@ private IEnumerator PingCoroutine()
while (IsConnectedAndAuthenticated)
{
SendPing();
yield return new WaitForSeconds(PING_INTERVAL);
yield return new WaitForSecondsRealtime(PING_INTERVAL);
}
}

Expand All @@ -1138,7 +1138,7 @@ private IEnumerator ScheduleReconnectCoroutine(float customDelay = -1f)
LootLockerLogger.Log($"Scheduling Presence reconnect attempt {reconnectAttempts}/{MAX_RECONNECT_ATTEMPTS} in {delayToUse} seconds", LootLockerLogger.LogLevel.Debug);
ChangeConnectionState(LootLockerPresenceConnectionState.Reconnecting);

yield return new WaitForSeconds(delayToUse);
yield return new WaitForSecondsRealtime(delayToUse);

if (shouldReconnect && connectionState == LootLockerPresenceConnectionState.Reconnecting)
{
Expand Down
2 changes: 1 addition & 1 deletion Runtime/Client/LootLockerPresenceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,7 @@ private IEnumerator _AutoConnectExistingSessions()
ConnectPresence(state.ULID);

// Small delay between connections to avoid overwhelming the system
yield return new WaitForSeconds(0.1f);
yield return new WaitForSecondsRealtime(0.1f);
}
}
}
Expand Down
Loading
Loading