/// <summary> /// Build a LegoDataDictionary from path config values. Path configs have a different serialization than LDF in /// Lvl files has. /// </summary> /// <remarks> /// Some config entries have no type defined, for example `pathspeed=0.8` or `delay=5`. /// This code tries to interpret those values first as an int, then as a float, and if /// neither succeeds, stores it as a string. /// </remarks> /// <seealso cref="SerializePathConfigs"/> public static void DeserializePathConfigs(this LegoDataDictionary @this, BitReader reader) { var configCount = reader.Read <uint>(); for (var i = 0; i < configCount; i++) { var key = reader.ReadNiString(true, true); var typeAndValue = reader.ReadNiString(true, true); var firstColon = typeAndValue.IndexOf(':'); if (firstColon == -1) { if (int.TryParse(typeAndValue, out var intValue)) { @this.Add(key, intValue); } else if (float.TryParse(typeAndValue, out var floatVal)) { @this.Add(key, floatVal); } else { @this.Add(key, typeAndValue); } continue; } var type = int.Parse(typeAndValue[..firstColon]);
/// <summary> /// Prepares an LDF wot use with ScriptNetworkVarUpdateMessage and /// sets the internal LDF data of the script component. /// </summary> /// <param name="name">Name of the variable to store.</param> /// <param name="value">Value of the variable to store.</param> /// <param name="ldf">Dictionary to set up for sending with ScriptNetworkVarUpdateMessage.</param> private void PrepareDictionary(string name, object value, LegoDataDictionary ldf) { // Add the value. if (value is IList list) { // Add the list entries. // These aren't stored in the script. for (var i = 0; i < list.Count; i++) { var subValue = list[i]; var key = name + "." + (i + 1); if (subValue is GameObject gameObject) { ldf.Add(key, gameObject.Id); } else { ldf.Add(key, subValue); } } } else { // Convert the object. var newValue = value; if (value is GameObject gameObject) { newValue = (long)gameObject.Id; } // Store the object. if (this.GameObject.TryGetComponent <LuaScriptComponent>(out var component)) { component.Data ??= new LegoDataDictionary(); component.Data.Add(name, newValue); } ldf.Add(name, newValue); } }
public void RequestActivitySummaryLeaderboardDataMessageHandler(RequestActivitySummaryLeaderboardDataMessage message, Player player) { using var ctx = new UchuContext(); // Get current year and week number according to ISO 8601 var yearAndWeek = ISOWeek.GetYear(DateTime.Now) * 100 + ISOWeek.GetWeekOfYear(DateTime.Now); // Find leaderboard entries for this activity // If weekly leaderboard is requested, only return results from current week var leaderboardQueryable = ctx.ActivityScores .Where(score => score.Activity == message.GameId && (message.Weekly ? score.Week == yearAndWeek : score.Week == 0)); // Find leaderboard type var activity = ClientCache.Find <Activities>(message.GameId); var leaderboardType = (LeaderboardType)(activity?.LeaderboardType ?? -1); // For some reason, whoever made this in 2011 gave the the NS and NT footraces the // same activity ID. So for footraces, we filter by zone ID to ensure we don't mix // the leaderboards. But this check shouldn't be done for other leaderboards, as // Survival minigames have their leaderboards accessible from multiple zones. if (leaderboardType == LeaderboardType.Footrace) { leaderboardQueryable = leaderboardQueryable.Where(score => score.Zone == (int)player.Zone.ZoneId); } // Order either by time ascending or time descending depending on which kind of activity it is if (leaderboardType == LeaderboardType.Footrace || leaderboardType == LeaderboardType.AvantGardensSurvival || leaderboardType == LeaderboardType.BattleOfNimbusStation) { leaderboardQueryable = leaderboardQueryable.OrderByDescending(score => score.Time); } else { leaderboardQueryable = leaderboardQueryable.OrderBy(score => score.Time); } var leaderboard = leaderboardQueryable.ToList(); // Dictionary <rank, score> // Rank is what the client will show as position on the leaderboard var toSend = new Dictionary <int, ActivityScore>(); switch (message.QueryType) { case QueryType.TopSocial: // TODO: Friends. break; case QueryType.TopAll: // Top 10. for (var i = message.ResultsStart; i < message.ResultsEnd && i < leaderboard.Count; i++) { toSend.Add(i + 1, leaderboard[i]); } break; case QueryType.TopCharacter: // Leaderboard around this player's rank. var playerIndex = leaderboard.FindIndex(score => score.CharacterId == player.Id); // If player is not in leaderboard, return (client will show a friendly message telling the player // to first complete the activity) if (playerIndex == -1) { break; } var availableBefore = playerIndex; var availableAfter = leaderboard.Count - playerIndex; // By default we show 5 scores before this player's, and 4 after (last index isn't included). var includeBefore = 5; var includeAfter = 5; // For every step we can't go before, add one to after includeAfter += Math.Max(0, 5 - availableBefore); // For every step we can't go after, add one to before includeBefore += Math.Max(0, 5 - availableAfter); // Ensure we don't go outside the leaderboard limits var startIndex = Math.Max(0, playerIndex - includeBefore); var stopIndex = Math.Min(leaderboard.Count, playerIndex + includeAfter); for (var i = startIndex; i < stopIndex; i++) { toSend.Add(i + 1, leaderboard[i]); } break; } // "Properly" implementing this odd nested-dictionaries-and-arrays-inside-LDF didn't seem // particularly fun and/or useful; this implementation just does everything needed for leaderboards. var data = new LegoDataDictionary { { "ADO.Result", true }, { "Result.Count", 1 }, { "Result[0].Index", "RowNumber" }, { "Result[0].RowCount", toSend.Count }, }; var index = 0; foreach (var(rank, activityScore) in toSend) { var characterName = ctx.Characters.FirstOrDefault(c => c.Id == activityScore.CharacterId)?.Name ?? "Deleted Character"; data.Add($"Result[0].Row[{index}].CharacterID", activityScore.CharacterId); data.Add($"Result[0].Row[{index}].LastPlayed", activityScore.LastPlayed); data.Add($"Result[0].Row[{index}].NumPlayed", activityScore.NumPlayed); data.Add($"Result[0].Row[{index}].RowNumber", rank); data.Add($"Result[0].Row[{index}].Time", activityScore.Time); data.Add($"Result[0].Row[{index}].Points", activityScore.Points); data.Add($"Result[0].Row[{index}].name", characterName); // TODO: ".Relationship" variable (int). // (AGS client script: if not 0, FoundFriendGuild set to true. Teams?) // data.Add($"Result[0].Row[{index}].Relationship", 0); index++; } player.Message(new SendActivitySummaryLeaderboardDataMessage { Associate = player, GameId = message.GameId, InfoType = (int)message.QueryType, LeaderboardData = data, Throttled = false, Weekly = message.Weekly, }); }