v1.3.0.1 (Epic Store release)

This commit is contained in:
Regalis11
2024-03-28 18:34:33 +02:00
parent 81ca8637be
commit 3791670c42
269 changed files with 13160 additions and 2966 deletions
@@ -0,0 +1,319 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Barotrauma;
namespace EosInterfacePrivate;
static class OwnedSessionsPrivate
{
private static readonly Random rng = new Random();
private static readonly ConcurrentDictionary<Identifier, EosInterface.Sessions.OwnedSession> liveOwnedSessions = new ConcurrentDictionary<Identifier, EosInterface.Sessions.OwnedSession>();
private static Epic.OnlineServices.Utf8String IdentifierToAttributeKey(Identifier id)
{
// Attribute keys are always uppercase in the EOS developer page,
// so to minimize surprises let's match that here
return id.Value.ToUpperInvariant();
}
public static async Task<Result<EosInterface.Sessions.OwnedSession, EosInterface.Sessions.CreateError>> Create(Option<EosInterface.ProductUserId> selfUserIdOption, Identifier internalId, int maxPlayers)
{
var (success, failure) = Result<EosInterface.Sessions.OwnedSession, EosInterface.Sessions.CreateError>.GetFactoryMethods();
if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return failure(EosInterface.Sessions.CreateError.EosNotInitialized); }
if (liveOwnedSessions.ContainsKey(internalId)) { return failure(EosInterface.Sessions.CreateError.SessionAlreadyExists); }
using var janitor = Janitor.Start();
var bucketIndex = rng.Next(EosInterface.Sessions.MinBucketIndex, EosInterface.Sessions.MaxBucketIndex + 1);
string bucketName = EosInterface.Sessions.DefaultBucketName + bucketIndex;
var createSessionModificationOptions = new Epic.OnlineServices.Sessions.CreateSessionModificationOptions
{
SessionName = internalId.Value.ToUpperInvariant(),
BucketId = bucketName,
MaxPlayers = (uint)maxPlayers,
LocalUserId = selfUserIdOption.TryUnwrap(out var selfUserId)
? Epic.OnlineServices.ProductUserId.FromString(selfUserId.Value)
: null,
PresenceEnabled = false,
SessionId = null,
SanctionsEnabled = false
};
var sessionCreateResult = sessionsInterface.CreateSessionModification(ref createSessionModificationOptions, out var sessionModificationHandle);
if (sessionCreateResult != Epic.OnlineServices.Result.Success)
{
return failure(sessionCreateResult switch
{
Epic.OnlineServices.Result.InvalidUser => EosInterface.Sessions.CreateError.InvalidUser,
Epic.OnlineServices.Result.SessionsSessionAlreadyExists => EosInterface.Sessions.CreateError.SessionAlreadyExists,
_ => EosInterface.Sessions.CreateError.UnhandledErrorCondition
});
}
janitor.AddAction(sessionModificationHandle.Release);
var updateSessionOptions = new Epic.OnlineServices.Sessions.UpdateSessionOptions
{
SessionModificationHandle = sessionModificationHandle
};
var updateSessionWaiter = new CallbackWaiter<Epic.OnlineServices.Sessions.UpdateSessionCallbackInfo>();
sessionsInterface.UpdateSession(options: ref updateSessionOptions, clientData: null, completionDelegate: updateSessionWaiter.OnCompletion);
var updateSessionResultOption = await updateSessionWaiter.Task;
if (!updateSessionResultOption.TryUnwrap(out var updateSessionResult)) { return failure(EosInterface.Sessions.CreateError.TimedOut); }
if (updateSessionResult.ResultCode == Epic.OnlineServices.Result.Success)
{
var newSession = new EosInterface.Sessions.OwnedSession(
BucketId: bucketName,
InternalId: updateSessionResult.SessionName.ToIdentifier(),
GlobalId: updateSessionResult.SessionId.ToIdentifier(),
Attributes: new Dictionary<Identifier, string>());
liveOwnedSessions.TryAdd(internalId, newSession);
return success(newSession);
}
return failure(updateSessionResult.ResultCode.FailAndLogUnhandledError(EosInterface.Sessions.CreateError.UnhandledErrorCondition));
}
public static async Task<Result<Unit, EosInterface.Sessions.AttributeUpdateError>> UpdateOwnedSessionAttributes(EosInterface.Sessions.OwnedSession session)
{
if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return Result.Failure(EosInterface.Sessions.AttributeUpdateError.EosNotInitialized); }
using var janitor = Janitor.Start();
var updateSessionModificationOptions = new Epic.OnlineServices.Sessions.UpdateSessionModificationOptions
{
SessionName = session.InternalId.Value.ToUpperInvariant()
};
var sessionCreateResult = sessionsInterface.UpdateSessionModification(ref updateSessionModificationOptions, out var sessionModificationHandle);
if (sessionCreateResult != Epic.OnlineServices.Result.Success)
{
return Result.Failure(EosInterface.Sessions.AttributeUpdateError.FailedToCreateSessionModificationHandle);
}
janitor.AddAction(() => sessionModificationHandle.Release());
var keysToRemove = session.SyncedAttributes
.Except(session.Attributes)
.Select(kvp => kvp.Key)
.ToArray();
var attributesToAdd = session.Attributes
.Except(session.SyncedAttributes)
.ToArray();
var setBucketIdOptions = new Epic.OnlineServices.Sessions.SessionModificationSetBucketIdOptions
{
BucketId = session.BucketId
};
sessionModificationHandle.SetBucketId(ref setBucketIdOptions);
if (session.HostAddress.TryUnwrap(out var hostAddress))
{
var setHostAddressOptions = new Epic.OnlineServices.Sessions.SessionModificationSetHostAddressOptions
{
HostAddress = hostAddress
};
sessionModificationHandle.SetHostAddress(ref setHostAddressOptions);
}
foreach (Identifier key in keysToRemove)
{
var removeAttributeOptions = new Epic.OnlineServices.Sessions.SessionModificationRemoveAttributeOptions
{
Key = IdentifierToAttributeKey(key)
};
var removeResult = sessionModificationHandle.RemoveAttribute(ref removeAttributeOptions);
if (removeResult != Epic.OnlineServices.Result.Success)
{
return Result.Failure(
removeResult switch
{
Epic.OnlineServices.Result.InvalidParameters
=> EosInterface.Sessions.AttributeUpdateError.InvalidParametersForRemoveAttribute,
Epic.OnlineServices.Result.IncompatibleVersion
=> EosInterface.Sessions.AttributeUpdateError.IncompatibleVersionForRemoveAttribute,
_
=> EosInterface.Sessions.AttributeUpdateError.UnhandledErrorConditionForRemoveAttribute
});
}
}
foreach (var kvp in attributesToAdd)
{
// EOS doesn't like empty values so let's skip those
if (kvp.Value.IsNullOrEmpty()) { continue; }
var addAttributeOptions = new Epic.OnlineServices.Sessions.SessionModificationAddAttributeOptions
{
SessionAttribute = new Epic.OnlineServices.Sessions.AttributeData
{
Key = IdentifierToAttributeKey(kvp.Key),
Value = kvp.Value
},
AdvertisementType = Epic.OnlineServices.Sessions.SessionAttributeAdvertisementType.Advertise
};
var addResult = sessionModificationHandle.AddAttribute(ref addAttributeOptions);
if (addResult != Epic.OnlineServices.Result.Success)
{
return Result.Failure(
addResult switch
{
Epic.OnlineServices.Result.InvalidParameters
=> EosInterface.Sessions.AttributeUpdateError.InvalidParametersForAddAttribute,
Epic.OnlineServices.Result.IncompatibleVersion
=> EosInterface.Sessions.AttributeUpdateError.IncompatibleVersionForAddAttribute,
_
=> EosInterface.Sessions.AttributeUpdateError.UnhandledErrorConditionForAddAttribute
});
}
}
var updateSessionOptions = new Epic.OnlineServices.Sessions.UpdateSessionOptions
{
SessionModificationHandle = sessionModificationHandle
};
var updateSessionWaiter = new CallbackWaiter<Epic.OnlineServices.Sessions.UpdateSessionCallbackInfo>();
sessionsInterface.UpdateSession(options: ref updateSessionOptions, clientData: null, completionDelegate: updateSessionWaiter.OnCompletion);
var updateSessionResultOption = await updateSessionWaiter.Task;
if (!updateSessionResultOption.TryUnwrap(out var updateSessionResult)) { Result.Failure(EosInterface.Sessions.AttributeUpdateError.TimedOut); }
if (updateSessionResult.ResultCode != Epic.OnlineServices.Result.Success)
{
return updateSessionResult.ResultCode switch
{
Epic.OnlineServices.Result.InvalidParameters
=> Result.Failure(EosInterface.Sessions.AttributeUpdateError.InvalidParametersForSessionUpdate),
Epic.OnlineServices.Result.SessionsOutOfSync
=> Result.Failure(EosInterface.Sessions.AttributeUpdateError.SessionsOutOfSync),
Epic.OnlineServices.Result.NotFound
=> Result.Failure(EosInterface.Sessions.AttributeUpdateError.SessionNotFound),
Epic.OnlineServices.Result.NoConnection
=> Result.Failure(EosInterface.Sessions.AttributeUpdateError.NoConnection),
var unhandled
=> Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.Sessions.AttributeUpdateError.UnhandledErrorCondition))
};
}
session.SyncedAttributes = session.Attributes.ToImmutableDictionary();
return Result.Success(Unit.Value);
}
public static async Task<Result<Unit, EosInterface.Sessions.CloseError>> CloseOwnedSession(EosInterface.Sessions.OwnedSession session)
{
if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return Result.Failure(EosInterface.Sessions.CloseError.EosNotInitialized); }
liveOwnedSessions.TryRemove(session.InternalId, out _);
var options = new Epic.OnlineServices.Sessions.DestroySessionOptions
{
SessionName = session.InternalId.Value.ToUpperInvariant()
};
var callbackWaiter = new CallbackWaiter<Epic.OnlineServices.Sessions.DestroySessionCallbackInfo>();
sessionsInterface.DestroySession(options: ref options, clientData: null, completionDelegate: callbackWaiter.OnCompletion);
var resultOption = await callbackWaiter.Task;
if (!resultOption.TryUnwrap(out var result)) { return Result.Failure(EosInterface.Sessions.CloseError.TimedOut); }
return result.ResultCode switch
{
Epic.OnlineServices.Result.Success
=> Result.Success(Unit.Value),
Epic.OnlineServices.Result.InvalidParameters
=> Result.Failure(EosInterface.Sessions.CloseError.InvalidParameters),
Epic.OnlineServices.Result.AlreadyPending
=> Result.Failure(EosInterface.Sessions.CloseError.AlreadyPending),
Epic.OnlineServices.Result.NotFound
=> Result.Failure(EosInterface.Sessions.CloseError.NotFound),
var unhandled
=> Result.Failure(unhandled.FailAndLogUnhandledError(EosInterface.Sessions.CloseError.UnhandledErrorCondition))
};
}
public static Task CloseAllOwnedSessions()
{
return Task.WhenAll(liveOwnedSessions.Values
.ToArray()
.Select(CloseOwnedSession));
}
public static Task ForceUpdateAllOwnedSessions()
{
var sessionsToUpdate = liveOwnedSessions.Values.ToArray();
foreach (var session in sessionsToUpdate)
{
session.SyncedAttributes = ImmutableDictionary<Identifier, string>.Empty;
}
return Task.WhenAll(sessionsToUpdate
.Select(UpdateOwnedSessionAttributes));
}
public static async Task<Result<ImmutableArray<EosInterface.ProductUserId>, EosInterface.Sessions.RegisterError>> RegisterPlayers(EosInterface.Sessions.OwnedSession session, params EosInterface.ProductUserId[] puids)
{
if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return Result.Failure(EosInterface.Sessions.RegisterError.EosNotInitialized); }
var registerPlayersOptions = new Epic.OnlineServices.Sessions.RegisterPlayersOptions
{
SessionName = session.InternalId.Value.ToUpperInvariant(),
PlayersToRegister = puids.Select(puid => Epic.OnlineServices.ProductUserId.FromString(puid.Value)).ToArray()
};
var registerPlayersWaiter = new CallbackWaiter<Epic.OnlineServices.Sessions.RegisterPlayersCallbackInfo>();
sessionsInterface.RegisterPlayers(options: ref registerPlayersOptions, clientData: null, completionDelegate: registerPlayersWaiter.OnCompletion);
var registerResultOption = await registerPlayersWaiter.Task;
if (!registerResultOption.TryUnwrap(out var registerResult)) { return Result.Failure(EosInterface.Sessions.RegisterError.TimedOut); }
if (registerResult.ResultCode != Epic.OnlineServices.Result.Success)
{
return Result.Failure(registerResult.ResultCode.FailAndLogUnhandledError(EosInterface.Sessions.RegisterError.UnhandledErrorCondition));
}
return Result.Success(registerResult.RegisteredPlayers.Select(puid => new EosInterface.ProductUserId(puid.ToString())).ToImmutableArray());
}
public static async Task<Result<ImmutableArray<EosInterface.ProductUserId>, EosInterface.Sessions.UnregisterError>> UnregisterPlayers(EosInterface.Sessions.OwnedSession session, params EosInterface.ProductUserId[] puids)
{
if (CorePrivate.SessionsInterface is not { } sessionsInterface) { return Result.Failure(EosInterface.Sessions.UnregisterError.EosNotInitialized); }
var unregisterPlayersOptions = new Epic.OnlineServices.Sessions.UnregisterPlayersOptions
{
SessionName = session.InternalId.Value.ToUpperInvariant(),
PlayersToUnregister = puids.Select(puid => Epic.OnlineServices.ProductUserId.FromString(puid.Value)).ToArray()
};
var unregisterPlayersWaiter = new CallbackWaiter<Epic.OnlineServices.Sessions.UnregisterPlayersCallbackInfo>();
sessionsInterface.UnregisterPlayers(options: ref unregisterPlayersOptions, clientData: null, completionDelegate: unregisterPlayersWaiter.OnCompletion);
var unregisterResultOption = await unregisterPlayersWaiter.Task;
if (!unregisterResultOption.TryUnwrap(out var unregisterResult)) { return Result.Failure(EosInterface.Sessions.UnregisterError.TimedOut); }
if (unregisterResult.ResultCode != Epic.OnlineServices.Result.Success)
{
return Result.Failure(unregisterResult.ResultCode.FailAndLogUnhandledError(EosInterface.Sessions.UnregisterError.UnhandledErrorCondition));
}
return Result.Success(unregisterResult.UnregisteredPlayers.Select(puid => new EosInterface.ProductUserId(puid.ToString())).ToImmutableArray());
}
}
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
{
public override Task<Result<EosInterface.Sessions.OwnedSession, EosInterface.Sessions.CreateError>> CreateSession(Option<EosInterface.ProductUserId> selfUserIdOption, Identifier internalId, int maxPlayers)
=> TaskScheduler.Schedule(() => OwnedSessionsPrivate.Create(selfUserIdOption, internalId, maxPlayers));
public override Task<Result<Unit, EosInterface.Sessions.AttributeUpdateError>> UpdateOwnedSessionAttributes(EosInterface.Sessions.OwnedSession session)
=> TaskScheduler.Schedule(() => OwnedSessionsPrivate.UpdateOwnedSessionAttributes(session));
public override Task<Result<Unit, EosInterface.Sessions.CloseError>> CloseOwnedSession(EosInterface.Sessions.OwnedSession session)
=> TaskScheduler.Schedule(() => OwnedSessionsPrivate.CloseOwnedSession(session));
public override Task CloseAllOwnedSessions()
=> TaskScheduler.Schedule(OwnedSessionsPrivate.CloseAllOwnedSessions);
}
@@ -0,0 +1,159 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Barotrauma;
namespace EosInterfacePrivate;
static class RemoteSessionsPrivate
{
/// <summary>
/// Largest number that can be passed to CreateSessionSearchOptions.MaxSearchResults
/// before it will immediately result in an InvalidParameters error.
/// </summary>
private const uint MaxResultsUpperBound = Epic.OnlineServices.Sessions.SessionsInterface.MaxSearchResults;
public static async Task<Result<ImmutableArray<EosInterface.Sessions.RemoteSession>, EosInterface.Sessions.RemoteSession.Query.Error>> RunQuery(EosInterface.Sessions.RemoteSession.Query query)
{
if (CorePrivate.SessionsInterface is not { } sessionsInterface)
{
return Result.Failure(EosInterface.Sessions.RemoteSession.Query.Error.EosNotInitialized);
}
using var janitor = Janitor.Start();
var createSessionSearchOptions = new Epic.OnlineServices.Sessions.CreateSessionSearchOptions
{
MaxSearchResults = query.MaxResults
};
var createSessionSearchResult = sessionsInterface.CreateSessionSearch(ref createSessionSearchOptions, out var sessionSearchHandle);
if (createSessionSearchResult != Epic.OnlineServices.Result.Success)
{
return Result.Failure(
createSessionSearchResult switch
{
Epic.OnlineServices.Result.InvalidParameters when query.MaxResults > MaxResultsUpperBound
=> EosInterface.Sessions.RemoteSession.Query.Error.ExceededMaxAllowedResults,
Epic.OnlineServices.Result.InvalidParameters
=> EosInterface.Sessions.RemoteSession.Query.Error.InvalidParameters,
_
=> createSessionSearchResult.FailAndLogUnhandledError(EosInterface.Sessions.RemoteSession.Query.Error.UnhandledErrorCondition)
});
}
janitor.AddAction(sessionSearchHandle.Release);
var setParameterOptions = new Epic.OnlineServices.Sessions.SessionSearchSetParameterOptions
{
Parameter = new Epic.OnlineServices.Sessions.AttributeData
{
Key = Epic.OnlineServices.Sessions.SessionsInterface.SearchBucketId,
Value = new Epic.OnlineServices.Sessions.AttributeDataValue
{
AsUtf8 = EosInterface.Sessions.DefaultBucketName + query.BucketIndex
}
},
ComparisonOp = Epic.OnlineServices.ComparisonOp.Equal
};
sessionSearchHandle.SetParameter(ref setParameterOptions);
var findOptions = new Epic.OnlineServices.Sessions.SessionSearchFindOptions
{
LocalUserId = Epic.OnlineServices.ProductUserId.FromString(query.LocalUserId.Value)
};
var findCallbackWaiter = new CallbackWaiter<Epic.OnlineServices.Sessions.SessionSearchFindCallbackInfo>();
sessionSearchHandle.Find(options: ref findOptions, clientData: null, completionDelegate: findCallbackWaiter.OnCompletion);
var findResultOption = await findCallbackWaiter.Task;
if (!findResultOption.TryUnwrap(out var findResult))
{
return Result.Failure(EosInterface.Sessions.RemoteSession.Query.Error.TimedOut);
}
if (findResult.ResultCode != Epic.OnlineServices.Result.Success)
{
return Result.Failure(
findResult.ResultCode switch
{
Epic.OnlineServices.Result.NotFound
=> EosInterface.Sessions.RemoteSession.Query.Error.NotFound,
Epic.OnlineServices.Result.InvalidParameters
=> EosInterface.Sessions.RemoteSession.Query.Error.InvalidParameters,
_
=> EosInterface.Sessions.RemoteSession.Query.Error.EosNotInitialized
});
}
var boilerplate1 = new Epic.OnlineServices.Sessions.SessionSearchGetSearchResultCountOptions();
uint resultCount = sessionSearchHandle.GetSearchResultCount(ref boilerplate1);
var sessions = new List<EosInterface.Sessions.RemoteSession>();
foreach (int sessionIndex in Enumerable.Range(0, (int)resultCount))
{
var attributes = new Dictionary<Identifier, string>();
var copySessionDetailsOptions = new Epic.OnlineServices.Sessions.SessionSearchCopySearchResultByIndexOptions
{
SessionIndex = (uint)sessionIndex
};
var detailsCopyResult = sessionSearchHandle.CopySearchResultByIndex(ref copySessionDetailsOptions, out var sessionDetails);
if (detailsCopyResult != Epic.OnlineServices.Result.Success) { break; }
janitor.AddAction(sessionDetails.Release);
var copyInfoOptions = new Epic.OnlineServices.Sessions.SessionDetailsCopyInfoOptions();
var infoCopyResult = sessionDetails.CopyInfo(ref copyInfoOptions, out var sessionInfo);
if (infoCopyResult != Epic.OnlineServices.Result.Success) { break; }
if (sessionInfo is not
{
Settings:
{
BucketId: { } bucketId,
NumPublicConnections: var numPublicConnections
},
NumOpenPublicConnections: var numOpenPublicConnections,
SessionId: { } sessionId,
HostAddress: { } hostAddress
})
{
break;
}
var boilerplate2 = new Epic.OnlineServices.Sessions.SessionDetailsGetSessionAttributeCountOptions();
var attributeCount = sessionDetails.GetSessionAttributeCount(ref boilerplate2);
foreach (var attributeIndex in Enumerable.Range(0, (int)attributeCount))
{
var copyAttributeOptions =
new Epic.OnlineServices.Sessions.SessionDetailsCopySessionAttributeByIndexOptions
{
AttrIndex = (uint)attributeIndex
};
var attributeCopyResult = sessionDetails.CopySessionAttributeByIndex(ref copyAttributeOptions, out var attributeNullable);
if (attributeCopyResult != Epic.OnlineServices.Result.Success) { break; }
if (attributeNullable?.Data is not { } attributeData
|| attributeData.Value.ValueType != Epic.OnlineServices.AttributeType.String)
{
break;
}
attributes.Add(attributeData.Key.ToIdentifier(), attributeData.Value.AsUtf8);
}
sessions.Add(new EosInterface.Sessions.RemoteSession(
SessionId: sessionId,
HostAddress: hostAddress,
CurrentPlayers: (int)(numPublicConnections - numOpenPublicConnections),
MaxPlayers: (int)numPublicConnections,
Attributes: attributes.ToImmutableDictionary(),
BucketId: bucketId));
}
return Result.Success(sessions.ToImmutableArray());
}
}
internal sealed partial class ImplementationPrivate : EosInterface.Implementation
{
public override Task<Result<ImmutableArray<EosInterface.Sessions.RemoteSession>, EosInterface.Sessions.RemoteSession.Query.Error>> RunRemoteSessionQuery(EosInterface.Sessions.RemoteSession.Query query)
=> TaskScheduler.Schedule(() => RemoteSessionsPrivate.RunQuery(query));
}