├── docker ├── linux │ ├── buildAndPush.sh │ ├── push.sh │ ├── build.sh │ └── buildNoCache.sh └── windows │ ├── docker-build-push.bat │ ├── docker-push.bat │ ├── docker-build.bat │ └── docker-build-nocache.bat ├── Tetrio.Foxhole.Network ├── Api │ ├── Tetrio │ │ ├── Models │ │ │ ├── Blitz.cs │ │ │ ├── Sprint.cs │ │ │ ├── QuickPlay.cs │ │ │ ├── ZenithRecords.cs │ │ │ ├── SlimUserInfo.cs │ │ │ ├── Badges.cs │ │ │ ├── Summary.cs │ │ │ ├── TetrioUser.cs │ │ │ ├── Achievement.cs │ │ │ └── TetraLeague.cs │ │ └── ApiResponse.cs │ ├── Discord │ │ ├── Models │ │ │ ├── DiscordUserResponse.cs │ │ │ └── DiscordTokenResponse.cs │ │ └── DiscordApi.cs │ └── ApiBase.cs └── Tetrio.Foxhole.Network.csproj ├── Tetrio.Foxhole.Backend.Runtime ├── Web │ ├── resources │ │ └── cr.ttf │ ├── overlay.html │ └── slide.html ├── wwwroot │ └── web │ │ ├── res │ │ ├── img │ │ │ ├── a.png │ │ │ ├── b.png │ │ │ ├── c.png │ │ │ ├── d.png │ │ │ ├── s.png │ │ │ ├── u.png │ │ │ ├── x.png │ │ │ ├── z.png │ │ │ ├── a+.png │ │ │ ├── a-.png │ │ │ ├── b+.png │ │ │ ├── b-.png │ │ │ ├── c+.png │ │ │ ├── c-.png │ │ │ ├── d+.png │ │ │ ├── duo.png │ │ │ ├── s+.png │ │ │ ├── s-.png │ │ │ ├── ss.png │ │ │ ├── x+.png │ │ │ ├── error.png │ │ │ ├── error1.gif │ │ │ ├── error2.gif │ │ │ ├── error3.png │ │ │ ├── expert.png │ │ │ ├── messy.png │ │ │ ├── nohold.png │ │ │ ├── pento.png │ │ │ ├── allspin.png │ │ │ ├── gravity.png │ │ │ ├── invisible.png │ │ │ ├── volatile.png │ │ │ ├── doublehole.png │ │ │ ├── leaderboard1.png │ │ │ ├── messy_reversed.png │ │ │ ├── pento_reversed.png │ │ │ ├── allspin_reversed.png │ │ │ ├── expert_reversed.png │ │ │ ├── gravity_reversed.png │ │ │ ├── nohold_reversed.png │ │ │ ├── doublehole_reversed.png │ │ │ ├── invisible_reversed.png │ │ │ └── volatile_reversed.png │ │ ├── fonts │ │ │ ├── cb.ttf │ │ │ └── cr.ttf │ │ ├── css │ │ │ ├── blitz.css │ │ │ ├── sprint.css │ │ │ ├── achievement.css │ │ │ ├── zenith.css │ │ │ ├── splits.css │ │ │ ├── base.css │ │ │ ├── user.css │ │ │ └── league.css │ │ └── js │ │ │ ├── blitz.js │ │ │ ├── sprint.js │ │ │ ├── achievement.js │ │ │ ├── splits.js │ │ │ ├── base.js │ │ │ └── zenith.js │ │ ├── slide.html │ │ ├── achievement.html │ │ ├── blitz.html │ │ ├── sprint.html │ │ ├── zenith.html │ │ ├── league.html │ │ ├── splits.html │ │ └── user.html ├── appsettings.Development.json ├── appsettings.json ├── Properties │ └── launchSettings.json ├── Program.cs └── Tetrio.Foxhole.Backend.Runtime.csproj ├── .gitignore ├── .idea └── .idea.TetraLeagueOverlay │ └── .idea │ ├── encodings.xml │ ├── indexLayout.xml │ ├── discord.xml │ ├── vcs.xml │ └── .gitignore ├── Tetrio.Foxhole.Database ├── Configurations │ ├── ModConfiguration.cs │ ├── BaseConfiguration.cs │ ├── ConditionRangeConfiguration.cs │ ├── ZenithSplitConfiguration.cs │ ├── RunConfiguration.cs │ ├── ChallengeConditionConfiguration.cs │ ├── MasteryChallengeConfiguration.cs │ ├── ChallengeConfiguration.cs │ ├── CommunityChallengeConfiguration.cs │ ├── MasteryAttemptConfiguration.cs │ ├── CommunityContributionConfiguration.cs │ └── UserConfiguration.cs ├── Enums │ ├── Difficulty.cs │ └── ConditionType.cs ├── Entities │ ├── Mod.cs │ ├── ConditionRange.cs │ ├── MasteryChallenge.cs │ ├── BaseEntity.cs │ ├── CommunityContribution.cs │ ├── Challenge.cs │ ├── ZenithSplit.cs │ ├── CommunityChallenge.cs │ ├── MasteryAttempt.cs │ ├── ChallengeCondition.cs │ ├── User.cs │ └── Run.cs ├── Migrations │ ├── 20250727150543_TetrioRank.cs │ ├── 20250902105324_SplitsMods.cs │ ├── 20250922123258_ZenithSplitChanges.cs │ ├── 20250418141239_LateContributions.cs │ ├── 20251219080948_CC-OptionalMods.cs │ ├── 20250426094609_UpdatedExpertValues.cs │ ├── 20250730123819_SessionTokenNullable.cs │ ├── 20251029091747_UpdateComminutyChallenges.cs │ ├── 20250409173503_NewDefaultValues.cs │ ├── 20250727134226_AddBack2BackCondition.cs │ ├── 20251214160108_ReverseWOM.cs │ ├── 20250415181219_UpdatedConditions.cs │ ├── 20250411134524_Scaling.cs │ ├── 20250411220903_CommunityChallenges.cs │ └── 20250412220351_CommunityChallengeContributions.cs ├── Tetrio.Foxhole.Database.csproj ├── TetrioContext.cs └── EncryptionService.cs ├── .dockerignore ├── Tetrio.Foxhole.Overlay ├── Tetrio.Foxhole.Overlay.csproj └── Controllers │ ├── UserController.cs │ ├── BlitzController.cs │ ├── SprintController.cs │ ├── TetraLeagueController.cs │ └── AchievementController.cs ├── Tetrio.Foxhole.Backend.Base ├── Tetrio.Foxhole.Backend.Base.csproj └── Controllers │ ├── BaseController.cs │ └── MinBaseController.cs ├── Tetrio.Foxhole.ZenithDailyChallenge ├── Tetrio.Foxhole.ZenithDailyChallenge.csproj ├── ChallengeGeneration │ ├── ChallengeGenerator.cs │ ├── Community │ │ └── CommunityChallengeGenerator.cs │ ├── Daily │ │ └── ReverseChallengeGenerator.cs │ └── BaseChallengeGenerator.cs ├── Models │ └── ZenithSplitResult.cs └── Controllers │ └── ArchiveController.cs ├── Dockerfile ├── Tetrio.Overlay.sln └── README.md /docker/linux/buildAndPush.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./build.sh 3 | ./push.sh -------------------------------------------------------------------------------- /docker/windows/docker-build-push.bat: -------------------------------------------------------------------------------- 1 | call docker-build.bat 2 | call docker-push.bat -------------------------------------------------------------------------------- /docker/windows/docker-push.bat: -------------------------------------------------------------------------------- 1 | docker image push founntain/tetrio.overlay.api:latest -------------------------------------------------------------------------------- /docker/linux/push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker image push founntain/tetrio.overlay.api:latest -------------------------------------------------------------------------------- /docker/windows/docker-build.bat: -------------------------------------------------------------------------------- 1 | docker build -t founntain/tetrio.overlay.api:latest ../../. -------------------------------------------------------------------------------- /docker/linux/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker build -t founntain/tetrio.overlay.api:latest ../../. -------------------------------------------------------------------------------- /docker/windows/docker-build-nocache.bat: -------------------------------------------------------------------------------- 1 | docker build --no-cache -t founntain/tetrio.overlay.api:latest ../../. -------------------------------------------------------------------------------- /docker/linux/buildNoCache.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker build --no-cache -t founntain/tetrio.overlay.api:latest ../../. -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Tetrio/Models/Blitz.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Network.Api.Tetrio.Models; 2 | 3 | public class Blitz : ApiRecord 4 | { 5 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/Web/resources/cr.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/Web/resources/cr.ttf -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Tetrio/Models/Sprint.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Network.Api.Tetrio.Models; 2 | 3 | public class Sprint : ApiRecord 4 | { 5 | 6 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Tetrio/Models/QuickPlay.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Network.Api.Tetrio.Models; 2 | 3 | public class QuickPlay : ApiRecord 4 | { 5 | 6 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/a.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/b.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/c.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/d.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/s.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/u.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/x.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/z.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | *.sln.DotSettings.user 7 | *.db 8 | *.db-shm 9 | *.db-wal 10 | .name 11 | .idea/ -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/fonts/cb.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/fonts/cb.ttf -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/fonts/cr.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/fonts/cr.ttf -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/a+.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/a+.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/a-.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/a-.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/b+.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/b+.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/b-.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/b-.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/c+.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/c+.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/c-.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/c-.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/d+.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/d+.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/duo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/duo.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/s+.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/s+.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/s-.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/s-.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/ss.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/x+.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/x+.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/error.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/error1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/error1.gif -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/error2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/error2.gif -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/error3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/error3.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/expert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/expert.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/messy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/messy.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/nohold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/nohold.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/pento.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/pento.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/allspin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/allspin.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/gravity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/gravity.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/invisible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/invisible.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/volatile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/volatile.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/doublehole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/doublehole.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/leaderboard1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/leaderboard1.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/messy_reversed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/messy_reversed.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/pento_reversed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/pento_reversed.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/allspin_reversed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/allspin_reversed.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/expert_reversed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/expert_reversed.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/gravity_reversed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/gravity_reversed.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/nohold_reversed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/nohold_reversed.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/doublehole_reversed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/doublehole_reversed.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/invisible_reversed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/invisible_reversed.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/volatile_reversed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Founntain/TETRIO.Overlay/HEAD/Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/img/volatile_reversed.png -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.idea/.idea.TetraLeagueOverlay/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Configurations/ModConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Tetrio.Foxhole.Database.Entities; 2 | 3 | namespace Tetrio.Foxhole.Database.Configurations; 4 | 5 | public class ModConfiguration : BaseConfiguration 6 | { 7 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /.idea/.idea.TetraLeagueOverlay/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Enums/Difficulty.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Database.Enums; 2 | 3 | public enum Difficulty 4 | { 5 | Community = 0, 6 | VeryEasy = 1, 7 | Easy = 2, 8 | Normal = 3, 9 | Hard = 5, 10 | Expert = 8, 11 | Reverse = 12, 12 | } -------------------------------------------------------------------------------- /.idea/.idea.TetraLeagueOverlay/.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Tetrio/Models/ZenithRecords.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Tetrio.Foxhole.Network.Api.Tetrio.Models; 4 | 5 | public class ZenithRecords 6 | { 7 | [JsonPropertyName("entries")] public IList Entries { get; set; } 8 | } -------------------------------------------------------------------------------- /.idea/.idea.TetraLeagueOverlay/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Enums/ConditionType.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Database.Enums; 2 | 3 | public enum ConditionType 4 | { 5 | Special = -1, 6 | Height, 7 | KOs, 8 | Quads, 9 | Spins, 10 | AllClears, 11 | Apm, 12 | Pps, 13 | Vs, 14 | Finesse, 15 | BackToBack, 16 | TotalBonus, 17 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Entities/Mod.cs: -------------------------------------------------------------------------------- 1 | using Tetrio.Foxhole.Database.Enums; 2 | 3 | namespace Tetrio.Foxhole.Database.Entities; 4 | 5 | public class Mod : BaseEntity 6 | { 7 | public string Name { get; set; } 8 | public Difficulty MinDifficulty { get; set; } 9 | public byte Weight { get; set; } 10 | public double Scaling { get; set; } 11 | } -------------------------------------------------------------------------------- /.idea/.idea.TetraLeagueOverlay/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /modules.xml 6 | /contentModel.xml 7 | /projectSettingsUpdater.xml 8 | /.idea.TetraLeagueOverlay.iml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Entities/ConditionRange.cs: -------------------------------------------------------------------------------- 1 | using Tetrio.Foxhole.Database.Enums; 2 | 3 | namespace Tetrio.Foxhole.Database.Entities; 4 | 5 | public class ConditionRange : BaseEntity 6 | { 7 | public ConditionType ConditionType { get; set; } 8 | public Difficulty Difficulty { get; set; } 9 | public double Min { get; set; } 10 | public double Max { get; set; } 11 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Entities/MasteryChallenge.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Database.Entities; 2 | 3 | public class MasteryChallenge : BaseEntity 4 | { 5 | public DateOnly Date { get; set; } 6 | 7 | public virtual ISet Conditions { get; set; } = new HashSet(); 8 | public virtual ISet MasteryAttempts { get; set; } = new HashSet(); 9 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Tetrio/Models/SlimUserInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Network.Api.Tetrio.Models; 2 | 3 | public class SlimUserInfo 4 | { 5 | public string UserId { get; set; } 6 | public string Username { get; set; } 7 | public string Avatar { get; set; } 8 | public string Banner { get; set; } 9 | public double? AvatarRevision { get; set; } 10 | public double? BannerRevision { get; set; } 11 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Tetrio/Models/Badges.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Tetrio.Foxhole.Network.Api.Tetrio.Models; 4 | 5 | public class Badges 6 | { 7 | [JsonPropertyName("id")] 8 | public string Id { get; set; } 9 | 10 | [JsonPropertyName("label")] 11 | public string Label { get; set; } 12 | 13 | [JsonPropertyName("desc")] 14 | public string Description { get; set; } 15 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Tetrio.Foxhole.Network.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | Linux 8 | Tetrio.Foxhole.Network 9 | 10 | 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.dockerignore 2 | **/.env 3 | **/.git 4 | **/.gitignore 5 | **/.project 6 | **/.settings 7 | **/.toolstarget 8 | **/.vs 9 | **/.vscode 10 | **/.idea 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | **/.angular 27 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Entities/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Database.Entities; 2 | 3 | public class BaseEntity 4 | { 5 | public Guid Id { get; set; } = Guid.NewGuid(); 6 | // public DateTime CreatedAt { get; set; } = DateTime.UtcNow; 7 | // public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; 8 | } 9 | 10 | public class CreationTimeEntity : BaseEntity 11 | { 12 | public DateTime CreatedAt { get; set; } = DateTime.Now; 13 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Configurations/BaseConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Tetrio.Foxhole.Database.Entities; 4 | 5 | namespace Tetrio.Foxhole.Database.Configurations; 6 | 7 | public class BaseConfiguration : IEntityTypeConfiguration where T : BaseEntity 8 | { 9 | public virtual void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasKey(x => x.Id); 12 | } 13 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Entities/CommunityContribution.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Database.Entities; 2 | 3 | public class CommunityContribution : CreationTimeEntity 4 | { 5 | public double Amount { get; set; } 6 | public bool IsLate { get; set; } = false; 7 | 8 | public Guid UserId { get; set; } 9 | public Guid CommunityChallengeId { get; set; } 10 | 11 | public virtual User User { get; set; } 12 | public virtual CommunityChallenge CommunityChallenge { get; set; } 13 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Entities/Challenge.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Database.Entities; 2 | 3 | public class Challenge : BaseEntity 4 | { 5 | public DateOnly Date { get; set; } 6 | public string Mods { get; set; } 7 | public byte Points { get; set; } 8 | 9 | public virtual ISet Users { get; set; } = new HashSet(); 10 | public virtual ISet Runs { get; set; } = new HashSet(); 11 | public virtual ISet Conditions { get; set; } = new HashSet(); 12 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Configurations/ConditionRangeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | using Tetrio.Foxhole.Database.Entities; 3 | 4 | namespace Tetrio.Foxhole.Database.Configurations; 5 | 6 | public class ConditionRangeConfiguration : BaseConfiguration 7 | { 8 | public override void Configure(EntityTypeBuilder builder) 9 | { 10 | base.Configure(builder); 11 | 12 | builder.HasIndex(x => new { x.ConditionType, x.Difficulty }).IsUnique(); 13 | } 14 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Configurations/ZenithSplitConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | using Tetrio.Foxhole.Database.Entities; 3 | 4 | namespace Tetrio.Foxhole.Database.Configurations; 5 | 6 | public class ZenithSplitConfiguration : BaseConfiguration 7 | { 8 | public override void Configure(EntityTypeBuilder builder) 9 | { 10 | base.Configure(builder); 11 | 12 | builder.HasIndex(x => x.TetrioId).IsUnique(); 13 | 14 | builder.HasOne(x => x.User).WithMany(x => x.Splits); 15 | } 16 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Configurations/RunConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | using Tetrio.Foxhole.Database.Entities; 3 | 4 | namespace Tetrio.Foxhole.Database.Configurations; 5 | 6 | public class RunConfiguration : BaseConfiguration 7 | { 8 | public override void Configure(EntityTypeBuilder builder) 9 | { 10 | base.Configure(builder); 11 | 12 | builder.HasIndex(x => x.TetrioId).IsUnique(); 13 | 14 | builder.HasOne(x => x.User).WithMany(x => x.Runs); 15 | builder.HasMany(x => x.Challenges).WithMany(x => x.Runs); 16 | } 17 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Discord/Models/DiscordUserResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Tetrio.Foxhole.Network.Api.Discord.Models; 4 | 5 | public class DiscordUserResponse 6 | { 7 | [JsonPropertyName("id")] 8 | public string Id { get; set; } 9 | 10 | [JsonPropertyName("username")] 11 | public string Username { get; set; } 12 | 13 | [JsonPropertyName("discriminator")] 14 | public string Discriminator { get; set; } 15 | 16 | [JsonPropertyName("avatar")] 17 | public string Avatar { get; set; } 18 | 19 | public string? ErrorMessage { get; set; } 20 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Configurations/ChallengeConditionConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | using Tetrio.Foxhole.Database.Entities; 3 | 4 | namespace Tetrio.Foxhole.Database.Configurations; 5 | 6 | public class ChallengeConditionConfiguration : BaseConfiguration 7 | { 8 | public override void Configure(EntityTypeBuilder builder) 9 | { 10 | base.Configure(builder); 11 | 12 | builder.HasIndex(x => x.ChallengeId); 13 | 14 | builder.HasOne(x => x.Challenge).WithMany(x => x.Conditions).HasForeignKey(x => x.ChallengeId); 15 | } 16 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Configurations/MasteryChallengeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | using Tetrio.Foxhole.Database.Entities; 3 | 4 | namespace Tetrio.Foxhole.Database.Configurations; 5 | 6 | public class MasteryChallengeConfiguration : BaseConfiguration 7 | { 8 | public override void Configure(EntityTypeBuilder builder) 9 | { 10 | base.Configure(builder); 11 | 12 | builder.HasIndex(x => x.Date).IsUnique(); 13 | 14 | builder.HasMany(x => x.Conditions).WithOne(x => x.MasteryChallenge).HasForeignKey(x => x.ChallengeId); 15 | } 16 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Tetrio/Models/Summary.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Tetrio.Foxhole.Network.Api.Tetrio.Models; 4 | 5 | public class Summary 6 | { 7 | [JsonPropertyName("league")] 8 | public TetraLeague? TetraLeague { get; set; } 9 | 10 | [JsonPropertyName("zenith")] 11 | public QuickPlay? Zenith { get; set; } 12 | 13 | [JsonPropertyName("zenithex")] 14 | public QuickPlay? ZenithExpert { get; set; } 15 | 16 | [JsonPropertyName("40l")] 17 | public Sprint? Sprint { get; set; } 18 | 19 | [JsonPropertyName("blitz")] 20 | public Blitz? Blitz { get; set; } 21 | 22 | 23 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Configurations/ChallengeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Tetrio.Foxhole.Database.Entities; 4 | 5 | namespace Tetrio.Foxhole.Database.Configurations; 6 | 7 | public class ChallengeConfiguration : BaseConfiguration 8 | { 9 | public override void Configure(EntityTypeBuilder builder) 10 | { 11 | base.Configure(builder); 12 | 13 | builder.HasIndex(x => new {x.Date, x.Points}).IsUnique(); 14 | 15 | builder.HasMany(x => x.Conditions).WithOne(x => x.Challenge).OnDelete(DeleteBehavior.Cascade); 16 | } 17 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Configurations/CommunityChallengeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | using Tetrio.Foxhole.Database.Entities; 3 | 4 | namespace Tetrio.Foxhole.Database.Configurations; 5 | 6 | public class CommunityChallengeConfiguration : BaseConfiguration 7 | { 8 | public override void Configure(EntityTypeBuilder builder) 9 | { 10 | base.Configure(builder); 11 | 12 | builder.HasIndex(x => x.StartDate).IsUnique(); 13 | builder.HasMany(x => x.Contributions).WithOne(x => x.CommunityChallenge).HasForeignKey(x => x.CommunityChallengeId); 14 | } 15 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/ApiBase.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Network.Api; 2 | 3 | public abstract class ApiBase 4 | { 5 | protected const string ApiBaseUrl = "https://ch.tetr.io/api/"; 6 | 7 | protected async Task GetString(string url) 8 | { 9 | try 10 | { 11 | using var client = new HttpClient(); 12 | 13 | var uri = new Uri(url); 14 | 15 | client.DefaultRequestHeaders.Add("X-Session-ID", Guid.NewGuid().ToString()); 16 | 17 | return await client.GetStringAsync(uri); 18 | } 19 | catch (Exception _) 20 | { 21 | return null; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/slide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 40 LINES OVERLAY 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Configurations/MasteryAttemptConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | using Tetrio.Foxhole.Database.Entities; 3 | 4 | namespace Tetrio.Foxhole.Database.Configurations; 5 | 6 | public class MasteryAttemptConfiguration : BaseConfiguration 7 | { 8 | public override void Configure(EntityTypeBuilder builder) 9 | { 10 | base.Configure(builder); 11 | 12 | builder.HasOne(x => x.User).WithMany(x => x.MasteryAttempts).HasForeignKey(x => x.UserId); 13 | builder.HasOne(x => x.MasteryChallenge).WithMany(x => x.MasteryAttempts).HasForeignKey(x => x.MasteryChallengeId); 14 | } 15 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Overlay/Tetrio.Foxhole.Overlay.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Configurations/CommunityContributionConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | using Tetrio.Foxhole.Database.Entities; 3 | 4 | namespace Tetrio.Foxhole.Database.Configurations; 5 | 6 | public class CommunityContributionConfiguration : BaseConfiguration 7 | { 8 | public override void Configure(EntityTypeBuilder builder) 9 | { 10 | base.Configure(builder); 11 | 12 | builder.HasOne(x => x.User).WithMany(x => x.CommunityContributions).HasForeignKey(x => x.UserId); 13 | builder.HasOne(x => x.CommunityChallenge).WithMany(x => x.Contributions).HasForeignKey(x => x.CommunityChallengeId); 14 | } 15 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Discord/Models/DiscordTokenResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Tetrio.Foxhole.Network.Api.Discord.Models; 4 | 5 | public class DiscordTokenResponse 6 | { 7 | [JsonPropertyName("access_token")] 8 | public string AccessToken { get; set; } 9 | 10 | [JsonPropertyName("refresh_token")] 11 | public string RefreshToken { get; set; } 12 | 13 | [JsonPropertyName("token_type")] 14 | public string TokenType { get; set; } 15 | 16 | [JsonPropertyName("expires_in")] 17 | public uint ExpiresIn { get; set; } 18 | 19 | [JsonPropertyName("scope")] 20 | public string Scope { get; set; } 21 | 22 | public string? ErrorMessage { get; set; } 23 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Tetrio/ApiResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Tetrio.Foxhole.Network.Api.Tetrio; 4 | 5 | public class ApiResponse 6 | { 7 | public bool Success { get; set; } 8 | public object Error { get; set; } 9 | [JsonPropertyName("cache")] 10 | public CacheResponse Cache { get; set; } 11 | [JsonPropertyName("data")] 12 | public T? Data { get; set; } 13 | } 14 | 15 | public class CacheResponse 16 | { 17 | [JsonPropertyName("status")] 18 | public string Status { get; set; } 19 | [JsonPropertyName("cached_at")] 20 | public long CachedAt { get; set; } 21 | [JsonPropertyName("cached_until")] 22 | public long CacheUntil { get; set; } 23 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Base/Tetrio.Foxhole.Backend.Base.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.ZenithDailyChallenge/Tetrio.Foxhole.ZenithDailyChallenge.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | Tetrio.Zenith.DailyChallenge 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Entities/ZenithSplit.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Database.Entities; 2 | 3 | public class ZenithSplit : CreationTimeEntity 4 | { 5 | public string TetrioId { get; set; } 6 | public DateTime? DatePlayed { get; set; } 7 | 8 | public uint HotelReachedAt { get; set; } 9 | public uint CasinoReachedAt { get; set; } 10 | public uint ArenaReachedAt { get; set; } 11 | public uint MuseumReachedAt { get; set; } 12 | public uint OfficesReachedAt { get; set; } 13 | public uint LaboratoryReachedAt { get; set; } 14 | public uint CoreReachedAt { get; set; } 15 | public uint CorruptionReachedAt { get; set; } 16 | public uint PlatformOfTheGodsReachedAt { get; set; } 17 | 18 | public string? Mods { get; set; } 19 | 20 | public virtual User User { get; set; } 21 | 22 | public DateTime GetSplitDate() => DatePlayed ?? CreatedAt; 23 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20250727150543_TetrioRank.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Tetrio.Overlay.Database.Migrations 6 | { 7 | /// 8 | public partial class TetrioRank : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "TetrioRank", 15 | table: "Users", 16 | type: "TEXT", 17 | nullable: true); 18 | } 19 | 20 | /// 21 | protected override void Down(MigrationBuilder migrationBuilder) 22 | { 23 | migrationBuilder.DropColumn( 24 | name: "TetrioRank", 25 | table: "Users"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20250902105324_SplitsMods.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Tetrio.Overlay.Database.Migrations 6 | { 7 | /// 8 | public partial class SplitsMods : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "Mods", 15 | table: "ZenithSplits", 16 | type: "TEXT", 17 | nullable: true); 18 | } 19 | 20 | /// 21 | protected override void Down(MigrationBuilder migrationBuilder) 22 | { 23 | migrationBuilder.DropColumn( 24 | name: "Mods", 25 | table: "ZenithSplits"); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20250922123258_ZenithSplitChanges.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Tetrio.Overlay.Database.Migrations 7 | { 8 | /// 9 | public partial class ZenithSplitChanges : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.AddColumn( 15 | name: "DatePlayed", 16 | table: "ZenithSplits", 17 | type: "TEXT", 18 | nullable: true); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "DatePlayed", 26 | table: "ZenithSplits"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Entities/CommunityChallenge.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Tetrio.Foxhole.Database.Enums; 3 | 4 | namespace Tetrio.Foxhole.Database.Entities; 5 | 6 | public class CommunityChallenge : BaseEntity 7 | { 8 | public DateTime StartDate { get; set; } 9 | public DateTime EndDate { get; set; } 10 | 11 | public ConditionType ConditionType { get; set; } 12 | public double TargetValue { get; set; } 13 | public double Value { get; set; } = 0; 14 | public bool Finished { get; set; } 15 | 16 | public string? Mods { get; set; } = null; 17 | public bool RequireAllMods { get; set; } = true; 18 | public bool ShowMods { get; set; } = true; 19 | 20 | [MaxLength(256)] 21 | public string? Name { get; set; } 22 | [MaxLength(4096)] 23 | public string? Description { get; set; } 24 | 25 | public virtual ISet Contributions { get; set; } = new HashSet(); 26 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20250418141239_LateContributions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Tetrio.Overlay.Database.Migrations 6 | { 7 | /// 8 | public partial class LateContributions : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "IsLate", 15 | table: "CommunityContributions", 16 | type: "INTEGER", 17 | nullable: false, 18 | defaultValue: false); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "IsLate", 26 | table: "CommunityContributions"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20251219080948_CC-OptionalMods.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Tetrio.Overlay.Database.Migrations 6 | { 7 | /// 8 | public partial class CCOptionalMods : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "RequireAllMods", 15 | table: "CommunityChallenges", 16 | type: "INTEGER", 17 | nullable: false, 18 | defaultValue: false); 19 | } 20 | 21 | /// 22 | protected override void Down(MigrationBuilder migrationBuilder) 23 | { 24 | migrationBuilder.DropColumn( 25 | name: "RequireAllMods", 26 | table: "CommunityChallenges"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Configurations/UserConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Tetrio.Foxhole.Database.Entities; 4 | 5 | namespace Tetrio.Foxhole.Database.Configurations; 6 | 7 | public class UserConfiguration : BaseConfiguration 8 | { 9 | public override void Configure(EntityTypeBuilder builder) 10 | { 11 | base.Configure(builder); 12 | 13 | builder.HasMany(x => x.Splits).WithOne(x => x.User).OnDelete(DeleteBehavior.Cascade); 14 | builder.HasMany(x => x.Runs).WithOne(x => x.User).OnDelete(DeleteBehavior.Cascade); 15 | builder.HasMany(x => x.MasteryAttempts).WithOne(x => x.User).OnDelete(DeleteBehavior.Cascade); 16 | builder.HasMany(x => x.CommunityContributions).WithOne(x => x.User).OnDelete(DeleteBehavior.SetNull); 17 | 18 | builder.HasIndex(x => x.SessionToken).IsUnique(); 19 | builder.HasIndex(x => x.TetrioId).IsUnique(); 20 | builder.HasIndex(x => x.DiscordId).IsUnique(); 21 | } 22 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/css/blitz.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | 6 | .modeContainer{ 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | p{ 12 | margin: 2px; 13 | text-align: center; 14 | } 15 | 16 | .statContainer{ 17 | display: flex; 18 | flex-direction: row; 19 | align-items: center; 20 | justify-content: center; 21 | } 22 | 23 | .stat{ 24 | display: flex; 25 | flex-direction: row; 26 | align-items: center; 27 | justify-content: center; 28 | 29 | margin-left: 10px; 30 | margin-right: 10px; 31 | } 32 | 33 | .prefix{ 34 | margin-right: 10px; 35 | } 36 | 37 | .suffix{ 38 | margin-left: 10px; 39 | } 40 | 41 | #finalScore, #username{ 42 | font-size: 48pt; 43 | } 44 | 45 | .background{ 46 | /*background: rgba(0,0,0,0.25);*/ 47 | } 48 | 49 | .countryImage{ 50 | width: 32px; 51 | height: 32px; 52 | border-radius: 50%; 53 | margin-right: 10px; 54 | 55 | filter: drop-shadow(2px 2px 3px black); 56 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/css/sprint.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | 6 | .modeContainer{ 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | p{ 12 | margin: 2px; 13 | text-align: center; 14 | } 15 | 16 | .statContainer{ 17 | display: flex; 18 | flex-direction: row; 19 | align-items: center; 20 | justify-content: center; 21 | } 22 | 23 | .stat{ 24 | display: flex; 25 | flex-direction: row; 26 | align-items: center; 27 | justify-content: center; 28 | 29 | margin-left: 10px; 30 | margin-right: 10px; 31 | } 32 | 33 | .prefix{ 34 | margin-right: 10px; 35 | } 36 | 37 | .suffix{ 38 | margin-left: 10px; 39 | } 40 | 41 | #finalTime, #username{ 42 | font-size: 48pt; 43 | } 44 | 45 | .background{ 46 | /*background: rgba(0,0,0,0.25);*/ 47 | } 48 | 49 | .countryImage{ 50 | width: 32px; 51 | height: 32px; 52 | border-radius: 50%; 53 | margin-right: 10px; 54 | 55 | filter: drop-shadow(2px 2px 3px black); 56 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/css/achievement.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | display: flex; 3 | flex-direction: row; 4 | } 5 | 6 | .modeContainer{ 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | p{ 12 | margin: 2px; 13 | text-align: center; 14 | } 15 | 16 | .statContainer{ 17 | display: flex; 18 | flex-direction: row; 19 | align-items: center; 20 | justify-content: center; 21 | } 22 | 23 | .stat{ 24 | display: flex; 25 | flex-direction: row; 26 | align-items: center; 27 | justify-content: center; 28 | 29 | margin-left: 10px; 30 | margin-right: 10px; 31 | } 32 | 33 | .prefix{ 34 | margin-right: 10px; 35 | } 36 | 37 | .suffix{ 38 | margin-left: 10px; 39 | } 40 | 41 | #finalTime, #username{ 42 | font-size: 48pt; 43 | } 44 | 45 | .background{ 46 | /*background: rgba(0,0,0,0.25);*/ 47 | } 48 | 49 | .countryImage{ 50 | width: 32px; 51 | height: 32px; 52 | border-radius: 50%; 53 | margin-right: 10px; 54 | 55 | filter: drop-shadow(2px 2px 3px black); 56 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/css/zenith.css: -------------------------------------------------------------------------------- 1 | .mod{ 2 | max-width: 42px; 3 | max-height: 42px; 4 | margin: 5px; 5 | } 6 | 7 | .modsContainer{ 8 | width: 100%; 9 | display: flex; 10 | flex-direction: row; 11 | justify-content: center; 12 | align-items: end; 13 | } 14 | 15 | .container{ 16 | display: flex; 17 | } 18 | 19 | .modeText{ 20 | font-size: 24pt; 21 | margin-bottom: 2px; 22 | } 23 | 24 | .scoreText{ 25 | font-size: 48pt; 26 | } 27 | 28 | .background{ 29 | background: rgba(0,0,0,0.25); 30 | } 31 | 32 | p{ 33 | margin: 0; 34 | } 35 | 36 | .normalContainer, .expertContainer{ 37 | text-align: center; 38 | margin: 0px 10px 0px 10px; 39 | } 40 | 41 | table{ 42 | width: 100%; 43 | font-size: 24pt; 44 | border-spacing: 0; 45 | margin-top: 2px; 46 | margin-bottom: 2px; 47 | } 48 | 49 | td{ 50 | padding-left: 10px; 51 | padding-right: 10px; 52 | padding-top: 5px; 53 | padding-bottom: 5px; 54 | 55 | margin: 0; 56 | 57 | background: rgba(0,0,0,0.25); 58 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Tetrio.Foxhole.Database.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | Tetrio.Foxhole.Database 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20250426094609_UpdatedExpertValues.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Tetrio.Overlay.Database.Migrations 7 | { 8 | /// 9 | public partial class UpdatedExpertValues : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.UpdateData( 15 | table: "ConditionRanges", 16 | keyColumn: "Id", 17 | keyValue: new Guid("11111111-1111-1111-1111-111111111401"), 18 | column: "Max", 19 | value: 1100.0); 20 | } 21 | 22 | /// 23 | protected override void Down(MigrationBuilder migrationBuilder) 24 | { 25 | migrationBuilder.UpdateData( 26 | table: "ConditionRanges", 27 | keyColumn: "Id", 28 | keyValue: new Guid("11111111-1111-1111-1111-111111111401"), 29 | column: "Max", 30 | value: 1750.0); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Tetrio/Models/TetrioUser.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Tetrio.Foxhole.Network.Api.Tetrio.Models; 4 | 5 | public class TetrioUser 6 | { 7 | [JsonPropertyName("_id")] 8 | public string Id { get; set; } 9 | 10 | [JsonPropertyName("username")] 11 | public string Username { get; set; } 12 | 13 | [JsonPropertyName("role")] 14 | public string Role { get; set; } 15 | 16 | [JsonPropertyName("xp")] 17 | public double Xp { get; set; } 18 | 19 | [JsonPropertyName("gamesplayed")] 20 | public int? GamesPlayed { get; set; } 21 | 22 | [JsonPropertyName("gameswon")] 23 | public int? GamesWon { get; set; } 24 | 25 | [JsonPropertyName("country")] 26 | public string? Country { get; set; } 27 | 28 | [JsonPropertyName("avatar_revision")] 29 | public double? Avatar { get; set; } 30 | 31 | [JsonPropertyName("banner_revision")] 32 | public double? Banner { get; set; } 33 | 34 | [JsonPropertyName("supporter")] 35 | public bool Supporter { get; set; } 36 | 37 | [JsonPropertyName("supportertier")] 38 | public int? SupporterTier { get; set; } 39 | 40 | [JsonPropertyName("badges")] 41 | public List? Badges { get; set; } 42 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:12288", 8 | "sslPort": 44364 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5277", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7053;http://localhost:5277", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20250730123819_SessionTokenNullable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Tetrio.Overlay.Database.Migrations 7 | { 8 | /// 9 | public partial class SessionTokenNullable : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.AlterColumn( 15 | name: "SessionToken", 16 | table: "Users", 17 | type: "TEXT", 18 | nullable: true, 19 | oldClrType: typeof(Guid), 20 | oldType: "TEXT"); 21 | } 22 | 23 | /// 24 | protected override void Down(MigrationBuilder migrationBuilder) 25 | { 26 | migrationBuilder.AlterColumn( 27 | name: "SessionToken", 28 | table: "Users", 29 | type: "TEXT", 30 | nullable: false, 31 | defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), 32 | oldClrType: typeof(Guid), 33 | oldType: "TEXT", 34 | oldNullable: true); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base 2 | USER $APP_UID 3 | WORKDIR /app 4 | EXPOSE 8080 5 | EXPOSE 8081 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build 8 | ARG BUILD_CONFIGURATION=Release 9 | WORKDIR /src 10 | COPY ["Tetrio.Foxhole.Backend.Runtime/Tetrio.Foxhole.Backend.Runtime.csproj", "./"] 11 | #COPY ["Tetrio.Network/Tetrio.Network.csproj", "./Network"] 12 | #COPY ["Tetrio.ZenithDaily.Database/Tetrio.ZenithDaily.Database.csproj", "./Database"] 13 | #COPY ["Tetrio.ZenithDaily.Challenge/Tetrio.ZenithDaily.Challenge.csproj", "./DailyChallenge"] 14 | RUN dotnet restore "Tetrio.Foxhole.Backend.Runtime.csproj" 15 | COPY . . 16 | WORKDIR "/src/" 17 | RUN dotnet build "Tetrio.Foxhole.Backend.Runtime/Tetrio.Foxhole.Backend.Runtime.csproj" -c $BUILD_CONFIGURATION -o /app/build 18 | 19 | FROM build AS publish 20 | ARG BUILD_CONFIGURATION=Release 21 | RUN dotnet publish "Tetrio.Foxhole.Backend.Runtime/Tetrio.Foxhole.Backend.Runtime.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false -r linux-x64 22 | 23 | FROM base AS final 24 | 25 | USER root 26 | 27 | #RUN apt-get update && \ 28 | # apt-get install -y libfontconfig1 libfreetype6 fontconfig && \ 29 | # apt-get clean 30 | 31 | WORKDIR /app 32 | COPY --from=publish /app/publish . 33 | 34 | ENTRYPOINT ["dotnet", "Tetrio.Foxhole.Backend.Runtime.dll"] -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20251029091747_UpdateComminutyChallenges.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Tetrio.Overlay.Database.Migrations 6 | { 7 | /// 8 | public partial class UpdateComminutyChallenges : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "Mods", 15 | table: "CommunityChallenges", 16 | type: "TEXT", 17 | nullable: true); 18 | 19 | migrationBuilder.AddColumn( 20 | name: "ShowMods", 21 | table: "CommunityChallenges", 22 | type: "INTEGER", 23 | nullable: false, 24 | defaultValue: false); 25 | } 26 | 27 | /// 28 | protected override void Down(MigrationBuilder migrationBuilder) 29 | { 30 | migrationBuilder.DropColumn( 31 | name: "Mods", 32 | table: "CommunityChallenges"); 33 | 34 | migrationBuilder.DropColumn( 35 | name: "ShowMods", 36 | table: "CommunityChallenges"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Tetrio/Models/Achievement.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Tetrio.Foxhole.Network.Api.Tetrio.Models; 4 | 5 | public class Achievement : ApiRecord 6 | { 7 | [JsonPropertyName("achievement")] public AchievementInfo AchievementInfo { get; set; } 8 | [JsonPropertyName("leaderboard")] public List Leaderboard { get; set; } 9 | [JsonPropertyName("cutoffs")] public AchievementInfo Cuttoffs { get; set; } 10 | } 11 | 12 | public class AchievementLeaderboardEntry 13 | { 14 | [JsonPropertyName("u")] public AchievementUser User { get; set; } 15 | [JsonPropertyName("v")] public float Value { get; set; } 16 | [JsonPropertyName("a")] public float? AdditionalValue { get; set; } 17 | [JsonPropertyName("t")] public string TimeUpdated { get; set; } 18 | } 19 | 20 | public class AchievementUser 21 | { 22 | [JsonPropertyName("_id")] public string Id { get; set; } 23 | [JsonPropertyName("username")] public string Username { get; set; } 24 | [JsonPropertyName("role")] public string Role { get; set; } 25 | [JsonPropertyName("supporter")] public bool Supporter { get; set; } 26 | [JsonPropertyName("country")] public string? Country { get; set; } 27 | } 28 | 29 | public class AchievementInfo 30 | { 31 | [JsonPropertyName("name")] public string Name { get; set; } 32 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/css/splits.css: -------------------------------------------------------------------------------- 1 | .normalContainer{ 2 | display: flex; 3 | /*flex-wrap: wrap;*/ 4 | } 5 | 6 | .floorBox{ 7 | padding: 5px; 8 | margin: 5px; 9 | width: 100%; 10 | } 11 | 12 | .floorBoxWrapper{ 13 | background: rgba(0,0,0,0.50); 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | width: 100%; 19 | min-height: 100px; 20 | } 21 | 22 | .floorName{ 23 | font-size: 16pt; 24 | } 25 | 26 | .floorTime{ 27 | font-size: 24pt; 28 | } 29 | 30 | .floorDifference{ 31 | font-size: 16pt; 32 | } 33 | 34 | .faster{ 35 | color: #00ff00; 36 | } 37 | 38 | .slower{ 39 | color: #ff0062; 40 | } 41 | 42 | .notReached{ 43 | color: #A9A9A9; 44 | } 45 | 46 | .hotel{ 47 | background: rgb(253, 230, 146) 48 | } 49 | 50 | .casino{ 51 | background: rgb(255, 199, 136) 52 | } 53 | 54 | .arena{ 55 | background: rgb(255, 183, 194) 56 | } 57 | 58 | .museum{ 59 | background: rgb(255, 186, 67) 60 | } 61 | 62 | .offices{ 63 | background: rgb(255, 145, 123) 64 | } 65 | 66 | .laboratory{ 67 | background: rgb(0, 221, 255) 68 | } 69 | 70 | .core{ 71 | background: rgb(255, 0, 111) 72 | } 73 | 74 | .corruption{ 75 | background: rgb(152, 255, 178) 76 | } 77 | 78 | .potg{ 79 | background: rgb(214, 119, 255) 80 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Entities/MasteryAttempt.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Database.Entities; 2 | 3 | public class MasteryAttempt : BaseEntity 4 | { 5 | public bool ExpertCompleted { get; set; } = false; 6 | public bool NoHoldCompleted { get; set; } = false; 7 | public bool MessyCompleted { get; set; } = false; 8 | public bool GravityCompleted { get; set; } = false; 9 | public bool VolatileCompleted { get; set; } = false; 10 | public bool DoubleHoleCompleted { get; set; } = false; 11 | public bool InvisibleCompleted { get; set; } = false; 12 | public bool AllSpinCompleted { get; set; } = false; 13 | 14 | public bool ExpertReversedCompleted { get; set; } = false; 15 | public bool NoHoldReversedCompleted { get; set; } = false; 16 | public bool MessyReversedCompleted { get; set; } = false; 17 | public bool GravityReversedCompleted { get; set; } = false; 18 | public bool VolatileReversedCompleted { get; set; } = false; 19 | public bool DoubleHoleReversedCompleted { get; set; } = false; 20 | public bool InvisibleReversedCompleted { get; set; } = false; 21 | public bool AllSpinReversedCompleted { get; set; } = false; 22 | 23 | public Guid UserId { get; set; } 24 | public Guid MasteryChallengeId { get; set; } 25 | 26 | public virtual User? User { get; set; } 27 | public virtual MasteryChallenge? MasteryChallenge { get; set; } 28 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Overlay/Controllers/UserController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Tetrio.Foxhole.Backend.Base.Controllers; 3 | using Tetrio.Foxhole.Database; 4 | using Tetrio.Foxhole.Network.Api.Tetrio; 5 | 6 | namespace Tetrio.Foxhole.Overlay.Controllers; 7 | 8 | public class UserController(TetrioApi api, TetrioContext context) : BaseController(api) 9 | { 10 | [HttpGet] 11 | [Route("{username}/stats")] 12 | public async Task Stats(string? username) 13 | { 14 | if (string.IsNullOrWhiteSpace(username)) return BadRequest(); 15 | 16 | var userData = await Api.GetUserInformation(username); 17 | var userSummaryData = await Api.GetUserSummaries(username); 18 | 19 | var data = new 20 | { 21 | Badges = userData?.Badges?.Select(x => x.Id), 22 | SummaryData = userSummaryData 23 | }; 24 | 25 | return Ok(data); 26 | } 27 | 28 | [HttpGet] 29 | [Route("{username}")] 30 | public async Task View(string? username) 31 | { 32 | if (string.IsNullOrWhiteSpace(username)) return BadRequest(); 33 | 34 | username = username.ToLower(); 35 | 36 | var html = await System.IO.File.ReadAllTextAsync("wwwroot/web/user.html"); 37 | 38 | html = html.Replace("{username}", username); 39 | 40 | return Content(html, "text/html"); 41 | } 42 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/achievement.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ACHIEVEMENT OVERLAY 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

17 |

18 |
19 | # 20 | 0 21 | with 22 | 0 23 |
24 |
25 |
26 | 0 27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Entities/ChallengeCondition.cs: -------------------------------------------------------------------------------- 1 | using Tetrio.Foxhole.Database.Enums; 2 | 3 | namespace Tetrio.Foxhole.Database.Entities; 4 | 5 | public abstract class ChallengeConditionBase : BaseEntity 6 | { 7 | public Guid ChallengeId { get; set; } 8 | public ConditionType Type { get; set; } 9 | public double Value { get; set; } 10 | 11 | public override string ToString() 12 | { 13 | switch (Type) 14 | { 15 | case ConditionType.Height: 16 | return $"REACH {Value} M"; 17 | case ConditionType.Spins: 18 | return $"DO {Value} SPINS"; 19 | case ConditionType.AllClears: 20 | return $"DO {Value} ALL CLEARS"; 21 | case ConditionType.KOs: 22 | return $"DO {Value} KO'S"; 23 | case ConditionType.Quads: 24 | return $"DO {Value} QUADS"; 25 | case ConditionType.Apm: 26 | return $"DO {Value} APM"; 27 | case ConditionType.Pps: 28 | return $"DO {Value} PPS"; 29 | case ConditionType.Vs: 30 | return $"DO {Value} VS"; 31 | case ConditionType.Finesse: 32 | return $"DO {Value} % FINESSE"; 33 | default: 34 | return base.ToString(); 35 | 36 | } 37 | } 38 | } 39 | 40 | public class ChallengeCondition : ChallengeConditionBase 41 | { 42 | public virtual Challenge? Challenge { get; set; } 43 | } 44 | 45 | public class MasteryChallengeCondition : ChallengeConditionBase 46 | { 47 | public bool IsReverse { get; set; } = false; 48 | 49 | public virtual MasteryChallenge? MasteryChallenge { get; set; } 50 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Base/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Tetrio.Foxhole.Network.Api.Tetrio; 3 | using Tetrio.Foxhole.Network.Api.Tetrio.Models; 4 | 5 | namespace Tetrio.Foxhole.Backend.Base.Controllers; 6 | 7 | public class BaseController(TetrioApi api) : MinBaseController(api) 8 | { 9 | [HttpGet] 10 | [Route("{username}/web")] 11 | public async Task Web(string username, string? textcolor = null, string? backgroundColor = null, bool displayUsername = true) 12 | { 13 | username = username.ToLower(); 14 | 15 | var html = await System.IO.File.ReadAllTextAsync("Web/overlay.html"); 16 | 17 | html = html.Replace("{mode}", ControllerContext.ActionDescriptor.ControllerName); 18 | 19 | html = html.Replace("{username}", username); 20 | html = html.Replace("{textColor}", textcolor ?? "FFFFFF"); 21 | html = html.Replace("{backgroundColor}", backgroundColor ?? "00FFFFFF"); 22 | html = html.Replace("{displayUsername}", displayUsername.ToString()); 23 | 24 | return Content(html, "text/html"); 25 | } 26 | 27 | protected async Task GetTetrioUserInformation(string username) 28 | { 29 | var user = await Api.GetUserInformation(username); 30 | 31 | if(user == default) return null; 32 | 33 | return new SlimUserInfo 34 | { 35 | Username = user.Username, 36 | Avatar = $"https://tetr.io/user-content/avatars/{user.Id}.jpg?rv={user.Avatar}", 37 | AvatarRevision = user.Avatar, 38 | Banner = $"https://tetr.io/user-content/banners/{user.Id}.jpg?rv={user.Banner}", 39 | BannerRevision = user.Banner, 40 | }; 41 | } 42 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.ZenithDailyChallenge/ChallengeGeneration/ChallengeGenerator.cs: -------------------------------------------------------------------------------- 1 | using Tetrio.Foxhole.Database; 2 | using Tetrio.Foxhole.Database.Entities; 3 | using Tetrio.Foxhole.Database.Enums; 4 | 5 | namespace Tetrio.Zenith.DailyChallenge.ChallengeGeneration.Daily; 6 | 7 | public class ChallengeGenerator 8 | { 9 | private readonly Random _random; 10 | private readonly DateTime _day; 11 | 12 | public ChallengeGenerator() 13 | { 14 | _day = DateTime.UtcNow; 15 | 16 | var seed = int.Parse(_day.ToString("yyyyMMdd")); 17 | 18 | _random = new(seed); 19 | } 20 | 21 | public ChallengeGenerator(int seed) 22 | { 23 | _day = DateTime.UtcNow; 24 | 25 | _random = new(seed); 26 | } 27 | 28 | public async Task> GenerateChallengesForDay(TetrioContext ctx) 29 | { 30 | var dailyGenerator = new DailyChallengeGeneator(_random, _day); 31 | var reverseGenerator = new ReverseChallengeGenerator(_random, _day); 32 | 33 | List challenges = 34 | [ 35 | await dailyGenerator.GenerateChallenge(Difficulty.Easy, ctx), 36 | await dailyGenerator.GenerateChallenge(Difficulty.Normal, ctx), 37 | await dailyGenerator.GenerateChallenge(Difficulty.Hard, ctx), 38 | await dailyGenerator.GenerateChallenge(Difficulty.Expert, ctx), 39 | await reverseGenerator.GenerateReverseChallenge(ctx) 40 | ]; 41 | 42 | return challenges; 43 | } 44 | 45 | public async Task GenerateMasteryChallengesForDay(TetrioContext ctx) 46 | { 47 | var dailyGenerator = new DailyChallengeGeneator(_random, _day); 48 | 49 | return await dailyGenerator.GenerateMasteryChallenge(ctx); 50 | } 51 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/css/base.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'CB'; 3 | /*src: url('fonts/cb.ttf');*/ 4 | src: url('/web/res/fonts/cb.ttf'); 5 | } 6 | 7 | html{ 8 | font-family: 'CB'; 9 | font-size: 24pt; 10 | 11 | /*background: gray;*/ 12 | } 13 | 14 | .hidden { 15 | position: absolute; 16 | color: transparent; 17 | left: 0; 18 | top: 0; 19 | } 20 | 21 | body { 22 | background: transparent; 23 | color:white; 24 | overflow: hidden;; 25 | } 26 | 27 | img { 28 | image-rendering: high-quality; 29 | aspect-ratio: 1 / 1; 30 | object-fit: cover; 31 | } 32 | 33 | .container { 34 | text-shadow: 2px 2px 3px black; 35 | } 36 | 37 | .progressContainer{ 38 | width: 100%; 39 | display: flex; 40 | flex-direction: row; 41 | align-items: center; 42 | } 43 | 44 | .box{ 45 | display: block; 46 | height: 10px; 47 | } 48 | 49 | .progressBarContainer { 50 | position: relative; 51 | width: 100%; /* Adjust the width as needed */ 52 | height: 10px; /* Ensure the height matches the height of the boxes */ 53 | margin: 10px; 54 | } 55 | 56 | .progressBar, 57 | .progressBarAccent { 58 | position: absolute; 59 | top: 0; 60 | left: 0; 61 | width: 100%; /* Adjust as necessary */ 62 | } 63 | 64 | .progressBar { 65 | background-color: rgba(0, 0, 0, 0.75); 66 | z-index: 1; 67 | } 68 | 69 | .progressBarAccent { 70 | background-color: rgba(255, 255, 255, 1); 71 | transition: width 1s ease-in-out; 72 | width: 0; 73 | z-index: 2; 74 | } 75 | 76 | 77 | .centeredText{ 78 | text-align: center; 79 | } 80 | 81 | .fadeElement { 82 | opacity: 0; 83 | transition: opacity 1s ease-in-out; 84 | } 85 | 86 | .fadeElement.show { 87 | opacity: 1; 88 | visibility: visible; 89 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Tetrio/Models/TetraLeague.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Tetrio.Foxhole.Network.Api.Tetrio.Models; 4 | 5 | public class TetraLeague 6 | { 7 | [JsonPropertyName("gamesplayed")] 8 | public int? Gamesplayed { get; set; } 9 | 10 | [JsonPropertyName("gameswon")] 11 | public int? Gameswon { get; set; } 12 | 13 | [JsonPropertyName("glicko")] 14 | public double? Glicko { get; set; } 15 | 16 | [JsonPropertyName("rd")] 17 | public double? Rd { get; set; } 18 | 19 | [JsonPropertyName("tr")] 20 | public double? Tr { get; set; } 21 | 22 | [JsonPropertyName("gxe")] 23 | public double? Gxe { get; set; } 24 | 25 | [JsonPropertyName("rank")] 26 | public string? Rank { get; set; } 27 | 28 | [JsonPropertyName("bestrank")] 29 | public string? TopRank { get; set; } 30 | 31 | [JsonPropertyName("apm")] 32 | public double? Apm { get; set; } 33 | 34 | [JsonPropertyName("pps")] 35 | public double? Pps { get; set; } 36 | 37 | [JsonPropertyName("vs")] 38 | public double? Vs { get; set; } 39 | 40 | [JsonPropertyName("decaying")] 41 | public bool? Decaying { get; set; } 42 | 43 | [JsonPropertyName("past")] 44 | public dynamic? Past { get; set; } 45 | 46 | [JsonPropertyName("standing_local")] 47 | public int? StandingLocal { get; set; } 48 | 49 | [JsonPropertyName("standing")] 50 | public int? StandingGlobal { get; set; } 51 | 52 | [JsonPropertyName("prev_rank")] 53 | public string? PrevRank { get; set; } 54 | 55 | [JsonPropertyName("prev_at")] 56 | public int? PrevAt { get; set; } 57 | 58 | [JsonPropertyName("next_rank")] 59 | public string? NextRank { get; set; } 60 | 61 | [JsonPropertyName("Next_at")] 62 | public int? NextAt { get; set; } 63 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/blitz.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BLITZ OVERLAY 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

16 |

0

17 |
18 |
19 | 0 20 | PPS 21 |
22 |
23 | 0 24 | KPP 25 |
26 |
27 | 0 28 | SPS 29 |
30 |
31 | 0 32 | F 33 |
34 |
35 |
36 |
37 | # 38 | 0 39 |
40 |
41 | 42 | # 43 | 0 44 |
45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/sprint.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 40 LINES OVERLAY 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

16 |

00:00.000

17 |
18 |
19 | 0 20 | PPS 21 |
22 |
23 | 0 24 | KPP 25 |
26 |
27 | 0 28 | KPS 29 |
30 |
31 | 0 32 | F 33 |
34 |
35 |
36 |
37 | # 38 | 0 39 |
40 |
41 | 42 | # 43 | 0 44 |
45 |
46 |
47 |
48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.ZenithDailyChallenge/ChallengeGeneration/Community/CommunityChallengeGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Tetrio.Foxhole.Database; 3 | using Tetrio.Foxhole.Database.Entities; 4 | using Tetrio.Foxhole.Database.Enums; 5 | 6 | namespace Tetrio.Zenith.DailyChallenge.ChallengeGeneration.Community; 7 | 8 | public class CommunityChallengeGenerator 9 | { 10 | private readonly Random _random; 11 | private readonly DateTime _day; 12 | 13 | public CommunityChallengeGenerator() 14 | { 15 | _day = DateTime.Now; 16 | 17 | var seed = int.Parse(_day.ToString("yyyyMMdd")); 18 | 19 | _random = new(seed); 20 | } 21 | 22 | public async Task GenerateCommunityChallenge(TetrioContext context) 23 | { 24 | var communityChallenge = new CommunityChallenge(); 25 | 26 | var allConditions = Enum.GetValues().ToList(); 27 | allConditions = allConditions.OrderBy(_ => _random.Next()).ToList(); 28 | 29 | ConditionType? selectedCondition = null; 30 | 31 | while (selectedCondition == null || selectedCondition == ConditionType.Finesse) 32 | { 33 | selectedCondition = allConditions.OrderBy(_ => _random.Next()).FirstOrDefault(); 34 | } 35 | 36 | var conditionValues = await context.ConditionRanges.Where(x => x.Difficulty == Difficulty.Community && x.ConditionType == selectedCondition).FirstAsync(); 37 | 38 | var targetValue = _random.Next((int) conditionValues.Min, (int) conditionValues.Max); 39 | 40 | targetValue = (targetValue / 1000) * 1000; 41 | 42 | communityChallenge.ConditionType = selectedCondition.Value; 43 | communityChallenge.TargetValue = targetValue; 44 | communityChallenge.StartDate = new DateTime(_day.Year, _day.Month, _day.Day, 0 , 0 , 0); 45 | communityChallenge.EndDate = communityChallenge.StartDate.AddDays(7).AddSeconds(-1); 46 | 47 | return communityChallenge; 48 | } 49 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/zenith.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tetra League Overlay Web 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

NORMAL

16 |
17 | WEEK 0 18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
000
29 | 30 |
31 | PB 0 32 |
33 | 34 |
35 |
36 |

EXPERT

37 | 38 |
39 | WEEK 0 40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
000
52 | 53 |
54 | PB 0 55 |
56 | 57 |
58 |
59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.ZenithDailyChallenge/Models/ZenithSplitResult.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Zenith.DailyChallenge.Models; 2 | 3 | public class ZenithSplitResult 4 | { 5 | public string Mods { get; set; } 6 | public DateTime? DateAchieved { get; set; } 7 | public uint GoldSplitTime { get; set; } 8 | public double AverageSplitTime { get; set; } 9 | 10 | public ZenithSplitResult() 11 | { 12 | 13 | } 14 | 15 | public ZenithSplitResult(string mods, DateTime? dateAchieved, uint goldSplitTime, double? averageSplitTime) : this() 16 | { 17 | this.Mods = mods; 18 | this.DateAchieved = dateAchieved; 19 | this.GoldSplitTime = goldSplitTime; 20 | this.AverageSplitTime = averageSplitTime ?? 0; 21 | } 22 | 23 | public string ToAverageTimeString() => TimeSpan.FromMilliseconds(this.AverageSplitTime).ToString(@"mm\:ss\.fff"); 24 | public string ToGoldTimeString() => TimeSpan.FromMilliseconds(this.GoldSplitTime).ToString(@"mm\:ss\.fff"); 25 | 26 | public string ToDateAchievedString() 27 | { 28 | if (DateAchieved == null) return "a long time ago"; 29 | 30 | var now = DateTime.UtcNow; 31 | var span = now - DateAchieved.Value; 32 | 33 | if (span.TotalSeconds < 0) span = TimeSpan.Zero; 34 | 35 | if (span.TotalSeconds < 5) return "just now"; 36 | if (span.TotalSeconds < 60) return $"{(int)span.TotalSeconds} seconds ago"; 37 | 38 | if (span.TotalMinutes < 2) return "a minute ago"; 39 | if (span.TotalMinutes < 60) return $"{(int)span.TotalMinutes} minutes ago"; 40 | 41 | if (span.TotalHours < 2) return "an hour ago"; 42 | if (span.TotalHours < 24) return $"{(int)span.TotalHours} hours ago"; 43 | 44 | if (span.TotalDays < 2) return "yesterday"; 45 | if (span.TotalDays < 30) return $"{(int)span.TotalDays} days ago"; 46 | 47 | var months = (int)(span.TotalDays / 30); 48 | if (months < 2) return "a month ago"; 49 | if (months < 12) return $"{months} months ago"; 50 | 51 | var years = (int)(span.TotalDays / 365); 52 | return years < 2 ? "a year ago" : $"{years} years ago"; 53 | } 54 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Overlay/Controllers/BlitzController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Tetrio.Foxhole.Backend.Base.Controllers; 3 | using Tetrio.Foxhole.Network.Api.Tetrio; 4 | 5 | namespace Tetrio.Foxhole.Overlay.Controllers; 6 | 7 | [Route("[controller]")] 8 | public class BlitzController(TetrioApi api) : BaseController(api) 9 | { 10 | [HttpGet] 11 | public ActionResult Get() 12 | { 13 | return Ok("This Endpoint is for Blitz Overlays"); 14 | } 15 | 16 | [HttpGet] 17 | [Route("{username}")] 18 | public async Task Web(string username) 19 | { 20 | username = username.ToLower(); 21 | 22 | var html = await System.IO.File.ReadAllTextAsync("wwwroot/web/blitz.html"); 23 | 24 | html = html.Replace("{mode}", ControllerContext.ActionDescriptor.ControllerName); 25 | 26 | html = html.Replace("{username}", username); 27 | 28 | return Content(html, "text/html"); 29 | } 30 | 31 | [HttpGet] 32 | [Route("{username}/stats")] 33 | public ActionResult GetStats(string username) 34 | { 35 | username = username.ToLower(); 36 | 37 | var userStats = Api.GetUserInformation(username); 38 | var stats = Api.GetBlitzStats(username); 39 | 40 | if(userStats?.Result == null) return NotFound("User Stats could not be fetched from the TETR.IO API"); 41 | if(stats?.Result?.Record?.Results?.Stats?.Finesse == null) return NotFound("Blitz stats could not be fetched from the TETR.IO API"); 42 | 43 | return Ok(new 44 | { 45 | Country = userStats.Result.Country, 46 | Score = stats.Result.Record.Results.Stats.Score, 47 | Pps = stats.Result.Record.Results.Aggregatestats.Pps, 48 | Kpp = (double)stats.Result.Record.Results.Stats.Inputs! / (double)stats.Result.Record.Results.Stats.Piecesplaced!, 49 | Sps = (double)stats.Result.Record.Results.Stats.Score! / (double)stats.Result.Record.Results.Stats.Piecesplaced!, 50 | Finesse = stats.Result.Record.Results.Stats.Finesse!.Faults, 51 | GlobalRank = stats.Result.Rank, 52 | LocalRank = stats.Result.RankLocal 53 | }); 54 | } 55 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Overlay/Controllers/SprintController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Tetrio.Foxhole.Backend.Base.Controllers; 3 | using Tetrio.Foxhole.Network.Api.Tetrio; 4 | 5 | namespace Tetrio.Foxhole.Overlay.Controllers; 6 | 7 | [Route("[controller]")] 8 | public class SprintController(TetrioApi api) : BaseController(api) 9 | { 10 | [HttpGet] 11 | public ActionResult Get() 12 | { 13 | return Ok("This Endpoint is for 40L Overlays"); 14 | } 15 | 16 | [HttpGet] 17 | [Route("{username}")] 18 | public async Task Web(string username) 19 | { 20 | username = username.ToLower(); 21 | 22 | var html = await System.IO.File.ReadAllTextAsync("wwwroot/web/sprint.html"); 23 | 24 | html = html.Replace("{mode}", ControllerContext.ActionDescriptor.ControllerName); 25 | 26 | html = html.Replace("{username}", username); 27 | 28 | return Content(html, "text/html"); 29 | } 30 | 31 | [HttpGet] 32 | [Route("{username}/stats")] 33 | public async Task GetStats(string username) 34 | { 35 | username = username.ToLower(); 36 | 37 | var userStats = await Api.GetUserInformation(username); 38 | var stats = await Api.GetSprintStats(username); 39 | 40 | if(userStats == null) return NotFound("User Stats could not be fetched from the TETR.IO API"); 41 | if(stats?.Record?.Results.Stats == null) return NotFound("Blitz stats could not be fetched from the TETR.IO API"); 42 | 43 | return Ok(new 44 | { 45 | Country = userStats.Country, 46 | Time = stats.Record.Results.Stats.Finaltime, 47 | TimeString = TimeSpan.FromMilliseconds(stats.Record.Results.Stats.Finaltime!.Value).ToString(@"mm\:ss\.fff"), 48 | Pps = stats.Record.Results.Aggregatestats.Pps, 49 | Kpp = (double)stats.Record.Results.Stats.Inputs! / (double)stats.Record.Results.Stats.Piecesplaced!, 50 | kps = (stats.Record.Results.Stats.Inputs / (stats.Record.Results.Stats.Finaltime / 1000)), 51 | Finesse = stats.Record.Results.Stats.Finesse!.Faults, 52 | GlobalRank = stats.Rank, 53 | LocalRank = stats.RankLocal 54 | }); 55 | } 56 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20250409173503_NewDefaultValues.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Tetrio.Overlay.Database.Migrations 7 | { 8 | /// 9 | public partial class NewDefaultValues : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.UpdateData( 15 | table: "ConditionRanges", 16 | keyColumn: "Id", 17 | keyValue: new Guid("11111111-1111-1111-1111-111111111306"), 18 | column: "Max", 19 | value: 100.0); 20 | 21 | migrationBuilder.UpdateData( 22 | table: "ConditionRanges", 23 | keyColumn: "Id", 24 | keyValue: new Guid("11111111-1111-1111-1111-111111111307"), 25 | column: "Max", 26 | value: 2.1000000000000001); 27 | 28 | migrationBuilder.UpdateData( 29 | table: "ConditionRanges", 30 | keyColumn: "Id", 31 | keyValue: new Guid("11111111-1111-1111-1111-111111111308"), 32 | column: "Max", 33 | value: 175.0); 34 | } 35 | 36 | /// 37 | protected override void Down(MigrationBuilder migrationBuilder) 38 | { 39 | migrationBuilder.UpdateData( 40 | table: "ConditionRanges", 41 | keyColumn: "Id", 42 | keyValue: new Guid("11111111-1111-1111-1111-111111111306"), 43 | column: "Max", 44 | value: 85.0); 45 | 46 | migrationBuilder.UpdateData( 47 | table: "ConditionRanges", 48 | keyColumn: "Id", 49 | keyValue: new Guid("11111111-1111-1111-1111-111111111307"), 50 | column: "Max", 51 | value: 2.0); 52 | 53 | migrationBuilder.UpdateData( 54 | table: "ConditionRanges", 55 | keyColumn: "Id", 56 | keyValue: new Guid("11111111-1111-1111-1111-111111111308"), 57 | column: "Max", 58 | value: 200.0); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Network/Api/Discord/DiscordApi.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using System.Text.Json; 3 | using Tetrio.Foxhole.Network.Api.Discord.Models; 4 | 5 | namespace Tetrio.Foxhole.Network.Api.Discord; 6 | 7 | public class DiscordApi 8 | { 9 | public async Task GetDiscordToken(Dictionary values) 10 | { 11 | using var client = new HttpClient(); 12 | 13 | var content = new FormUrlEncodedContent(values); 14 | 15 | var tokenResponse = await client.PostAsync("https://discord.com/api/oauth2/token", content); 16 | 17 | if (!tokenResponse.IsSuccessStatusCode) 18 | { 19 | return new() { ErrorMessage = $"Failed to exchange authorization code.{Environment.NewLine}{Environment.NewLine}{tokenResponse.ReasonPhrase}{Environment.NewLine}{Environment.NewLine}{await tokenResponse.Content.ReadAsStringAsync()}" }; 20 | } 21 | 22 | var tokenContent = await tokenResponse.Content.ReadAsStringAsync(); 23 | var tokenResult = JsonSerializer.Deserialize(tokenContent); 24 | 25 | if (tokenResult == null) 26 | { 27 | return new() { ErrorMessage = $"Failed to retrieve Discord token.{Environment.NewLine}{Environment.NewLine}{tokenResponse.ReasonPhrase}" }; 28 | } 29 | 30 | return tokenResult; 31 | } 32 | 33 | public async Task GetDiscordUser(string accessToken) 34 | { 35 | using var client = new HttpClient(); 36 | 37 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); 38 | var userResponse = await client.GetAsync("https://discord.com/api/users/@me"); 39 | 40 | if (!userResponse.IsSuccessStatusCode) 41 | { 42 | return new() { ErrorMessage = $"Failed to fetch user information.{Environment.NewLine}{Environment.NewLine}{userResponse.ReasonPhrase}" }; 43 | } 44 | 45 | var userContent = await userResponse.Content.ReadAsStringAsync(); 46 | var discordUser = JsonSerializer.Deserialize(userContent); 47 | 48 | if (discordUser == null) 49 | { 50 | return new() { ErrorMessage = $"Failed to retrieve Discord user.{Environment.NewLine}{Environment.NewLine}{userResponse.ReasonPhrase}" }; 51 | } 52 | 53 | return discordUser; 54 | } 55 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/js/blitz.js: -------------------------------------------------------------------------------- 1 | let usernameInfo = document.getElementById("usernameInfo").innerText; 2 | 3 | let username = document.getElementById("username"); 4 | let finalScore = document.getElementById("finalScore"); 5 | let pps = document.getElementById("pps"); 6 | let kpp = document.getElementById("kpp"); 7 | let sps = document.getElementById("sps"); 8 | let finesse = document.getElementById("finesse"); 9 | let globalRank = document.getElementById("globalRank"); 10 | let localRank = document.getElementById("localRank"); 11 | let countryImage = document.getElementById("countryImage"); 12 | 13 | username.innerText = usernameInfo.toUpperCase(); 14 | 15 | function updateStats() { 16 | let url = `${baseUrl}/blitz/${usernameInfo}/stats` 17 | 18 | console.log(url) 19 | 20 | fetch(url) 21 | .then(response => { 22 | if (!response.ok) { 23 | throw new Error('Network response was not ok'); 24 | } 25 | 26 | return response.json(); 27 | }) 28 | .then(data => { 29 | console.log(data); 30 | 31 | animateValue(finalScore, parseInt(finalScore.innerText), data.score, animationDuration, 1, "", "") 32 | 33 | animateValue(pps, parseFloat(pps.innerText), data.pps, animationDuration, 0, "", ""); 34 | animateValue(kpp, parseFloat(kpp.innerText), data.kpp, animationDuration, 0, "", ""); 35 | animateValue(sps, parseFloat(sps.innerText), data.sps, animationDuration, 0, "", ""); 36 | animateValue(finesse, parseFloat(finesse.innerText), data.finesse, animationDuration, 0, "", ""); 37 | 38 | animateValue(globalRank, parseInt(globalRank.innerText), data.globalRank, animationDuration, 1, "", ""); 39 | animateValue(localRank, parseInt(localRank.innerText), data.localRank, animationDuration, 1, "", ""); 40 | 41 | if (data.country == null) 42 | countryImage.style.display = "none"; 43 | else { 44 | countryImage.style.display = "block"; 45 | countryImage.src = `https://tetr.io/res/flags/${data.country.toLowerCase()}.png`; 46 | } 47 | }) 48 | .catch(error => { 49 | console.error('There has been a problem with your fetch operation:', error); 50 | }); 51 | } 52 | 53 | updateStats(); 54 | 55 | setInterval(updateStats, 5000 * 60); -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/js/sprint.js: -------------------------------------------------------------------------------- 1 | let usernameInfo = document.getElementById("usernameInfo").innerText; 2 | 3 | let username = document.getElementById("username"); 4 | let finalTime = document.getElementById("finalTime"); 5 | let pps = document.getElementById("pps"); 6 | let kpp = document.getElementById("kpp"); 7 | let kps = document.getElementById("kps"); 8 | let finesse = document.getElementById("finesse"); 9 | let globalRank = document.getElementById("globalRank"); 10 | let localRank = document.getElementById("localRank"); 11 | let countryImage = document.getElementById("countryImage"); 12 | 13 | username.innerText = usernameInfo.toUpperCase(); 14 | 15 | function updateStats() { 16 | let url = `${baseUrl}/sprint/${usernameInfo}/stats` 17 | 18 | console.log(url) 19 | 20 | fetch(url) 21 | .then(response => { 22 | if (!response.ok) { 23 | throw new Error('Network response was not ok'); 24 | } 25 | 26 | return response.json(); 27 | }) 28 | .then(data => { 29 | console.log(data); 30 | 31 | animateValue(finalTime, finalTime.innerText, data.timeString, animationDuration, 2, "", "") 32 | 33 | animateValue(pps, parseFloat(pps.innerText), data.pps, animationDuration, 0, "", ""); 34 | animateValue(kpp, parseFloat(kpp.innerText), data.kpp, animationDuration, 0, "", ""); 35 | animateValue(kps, parseFloat(kps.innerText), data.kps, animationDuration, 0, "", ""); 36 | animateValue(finesse, parseFloat(finesse.innerText), data.finesse, animationDuration, 0, "", ""); 37 | 38 | animateValue(globalRank, parseFloat(globalRank.innerText), data.globalRank, animationDuration, 1, "", ""); 39 | animateValue(localRank, parseFloat(localRank.innerText), data.localRank, animationDuration, 1, "", ""); 40 | 41 | if (data.country == null) 42 | countryImage.style.display = "none"; 43 | else { 44 | countryImage.style.display = "block"; 45 | countryImage.src = `https://tetr.io/res/flags/${data.country.toLowerCase()}.png`; 46 | } 47 | }) 48 | .catch(error => { 49 | console.error('There has been a problem with your fetch operation:', error); 50 | }); 51 | } 52 | 53 | updateStats(); 54 | 55 | setInterval(updateStats, 5000 * 60); -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/TetrioContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Tetrio.Foxhole.Database.Configurations; 3 | using Tetrio.Foxhole.Database.Entities; 4 | 5 | namespace Tetrio.Foxhole.Database; 6 | 7 | public class TetrioContext : DbContext 8 | { 9 | public DbSet Users { get; set; } 10 | public DbSet Challenges { get; set; } 11 | public DbSet ZenithSplits { get; set; } 12 | public DbSet ConditionRanges { get; set; } 13 | public DbSet ChallengeConditions { get; set; } 14 | public DbSet CommunityChallenges { get; set; } 15 | public DbSet CommunityContributions { get; set; } 16 | public DbSet MasteryAttempts { get; set; } 17 | public DbSet MasteryChallenges { get; set; } 18 | public DbSet Mods { get; set; } 19 | public DbSet Runs { get; set; } 20 | 21 | public TetrioContext() 22 | { 23 | 24 | } 25 | 26 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 27 | { 28 | base.OnConfiguring(optionsBuilder); 29 | 30 | optionsBuilder.UseSqlite("Data Source=database.db"); 31 | optionsBuilder.UseLazyLoadingProxies(); 32 | 33 | optionsBuilder.EnableSensitiveDataLogging().LogTo(Console.WriteLine); 34 | optionsBuilder.LogTo(Console.WriteLine); 35 | optionsBuilder.LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Error); 36 | } 37 | 38 | protected override void OnModelCreating(ModelBuilder builder) 39 | { 40 | builder.Seed(); 41 | 42 | builder.ApplyConfiguration(new UserConfiguration()); 43 | builder.ApplyConfiguration(new ZenithSplitConfiguration()); 44 | builder.ApplyConfiguration(new ChallengeConfiguration()); 45 | builder.ApplyConfiguration(new ChallengeConditionConfiguration()); 46 | builder.ApplyConfiguration(new ConditionRangeConfiguration()); 47 | builder.ApplyConfiguration(new ModConfiguration()); 48 | builder.ApplyConfiguration(new RunConfiguration()); 49 | builder.ApplyConfiguration(new CommunityChallengeConfiguration()); 50 | builder.ApplyConfiguration(new CommunityContributionConfiguration()); 51 | builder.ApplyConfiguration(new MasteryChallengeConfiguration()); 52 | builder.ApplyConfiguration(new MasteryAttemptConfiguration()); 53 | } 54 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/js/achievement.js: -------------------------------------------------------------------------------- 1 | let usernameInfo = document.getElementById("usernameInfo").innerText; 2 | let achievementInfo = document.getElementById("achievementInfo").innerText; 3 | 4 | let username = document.getElementById("username"); 5 | let achievementName = document.getElementById("achievementName"); 6 | let placement = document.getElementById("placement"); 7 | let achievementValue = document.getElementById("achievementValue"); 8 | let achievementAdditionalValue = document.getElementById("achievementAdditionalValue"); 9 | let globalRank = document.getElementById("globalRank"); 10 | let countryImage = document.getElementById("countryImage"); 11 | 12 | username.innerText = usernameInfo.toUpperCase(); 13 | 14 | function updateStats() { 15 | let url = `${baseUrl}/achievement/${achievementInfo}/${usernameInfo}/data` 16 | 17 | console.log(url) 18 | 19 | fetch(url) 20 | .then(response => { 21 | if (!response.ok) { 22 | throw new Error('Network response was not ok'); 23 | } 24 | 25 | return response.json(); 26 | }) 27 | .then(data => { 28 | console.log(data); 29 | 30 | achievementName.innerText = data.achievementName.toUpperCase(); 31 | 32 | animateValue(achievementValue, parseFloat(achievementValue.innerText), data.user.value, animationDuration, 0, "", "") 33 | 34 | if((data.user.additionalData ?? 0) > 0){ 35 | achievementAdditionalValue.style.display = "block"; 36 | animateValue(achievementAdditionalValue, parseFloat(achievementAdditionalValue.innerText), data.user.additonalValue ?? 0, animationDuration, 0, "", "") 37 | }else{ 38 | achievementAdditionalValue.style.display = "none"; 39 | } 40 | 41 | animateValue(globalRank, parseFloat(globalRank.innerText), data.user.rank, animationDuration, 1, "", ""); 42 | 43 | if (data.country == null) 44 | countryImage.style.display = "none"; 45 | else { 46 | countryImage.style.display = "block"; 47 | countryImage.src = `https://tetr.io/res/flags/${data.user.country.toLowerCase()}.png`; 48 | } 49 | }) 50 | .catch(error => { 51 | console.error('There has been a problem with your fetch operation:', error); 52 | }); 53 | } 54 | 55 | updateStats(); 56 | 57 | setInterval(updateStats, 5000 * 60); -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/EncryptionService.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | 4 | namespace Tetrio.Foxhole.Database; 5 | 6 | public class EncryptionService 7 | { 8 | private readonly string _encryptionKey; 9 | 10 | public EncryptionService() 11 | { 12 | if (File.Exists("/run/secrets/TETRIO_OVERLAY_ENCRYPTION_KEY")) 13 | { 14 | _encryptionKey = File.ReadAllText("/run/secrets/TETRIO_OVERLAY_ENCRYPTION_KEY"); 15 | 16 | Console.WriteLine("loaded encryption key from secrets"); 17 | 18 | return; 19 | } 20 | 21 | var encryptionKey = Environment.GetEnvironmentVariable("TETRIO_OVERLAY_ENCRYPTION_KEY"); 22 | 23 | if (string.IsNullOrEmpty(encryptionKey)) 24 | { 25 | throw new ArgumentException("TETRIO_OVERLAY_ENCRYPTION_KEY environment variable is not set."); 26 | } 27 | 28 | _encryptionKey = encryptionKey; 29 | 30 | Console.WriteLine("loaded encryption key from environment variable"); 31 | } 32 | 33 | public string EncryptWithIv(string plaintext) 34 | { 35 | using var aes = Aes.Create(); 36 | 37 | var key = Convert.FromBase64String(_encryptionKey); 38 | 39 | aes.Key = key; 40 | aes.GenerateIV(); 41 | var iv = aes.IV; 42 | 43 | using var encryptor = aes.CreateEncryptor(aes.Key, iv); 44 | 45 | var inputBytes = Encoding.UTF8.GetBytes(plaintext); 46 | var encrypted = encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length); 47 | 48 | var result = new byte[iv.Length + encrypted.Length]; 49 | iv.CopyTo(result, 0); 50 | encrypted.CopyTo(result, iv.Length); 51 | return Convert.ToBase64String(result); 52 | } 53 | 54 | public string DecryptWithIv(string encryptedData) 55 | { 56 | var fullCipher = Convert.FromBase64String(encryptedData); 57 | var iv = new byte[16]; 58 | var cipherText = new byte[fullCipher.Length - iv.Length]; 59 | 60 | Array.Copy(fullCipher, iv, iv.Length); 61 | Array.Copy(fullCipher, iv.Length, cipherText, 0, cipherText.Length); 62 | 63 | using var aes = Aes.Create(); 64 | 65 | aes.Key = Convert.FromBase64String(_encryptionKey); 66 | aes.IV = iv; 67 | 68 | using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV); 69 | 70 | var decrypted = decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length); 71 | return Encoding.UTF8.GetString(decrypted); 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/css/user.css: -------------------------------------------------------------------------------- 1 | .profilePicture { 2 | margin: 10px; 3 | border-radius: 50%; 4 | border: 5px solid white; 5 | filter: drop-shadow(2px 2px 3px black); 6 | } 7 | 8 | .profileContainer { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | justify-content: center; 13 | } 14 | 15 | .verticalContainer { 16 | display: flex; 17 | flex-direction: column; 18 | min-width: 450px; 19 | } 20 | 21 | .gamemodesContainer { 22 | display: flex; 23 | flex-direction: column; 24 | } 25 | 26 | .gamemodeRecordContainer { 27 | display: flex; 28 | justify-content: space-between; 29 | align-items: center; /* Ensures vertical alignment if the text size differs */ 30 | width: 100%; /* Ensures the container spans the full width */ 31 | } 32 | 33 | .bigRank { 34 | margin: 10px; 35 | max-width: 88px; 36 | max-height: 88px; 37 | filter: drop-shadow(2px 2px 3px black); 38 | } 39 | 40 | .smallRank { 41 | max-width: 32px; 42 | max-height: 32px; 43 | filter: drop-shadow(2px 2px 3px black); 44 | } 45 | 46 | .smallText { 47 | font-size: 16pt; 48 | } 49 | 50 | .bigText { 51 | font-size: 2em; 52 | margin: 0; 53 | } 54 | .username{ 55 | text-align: center; 56 | } 57 | 58 | .gamemodeText { 59 | text-align: left; 60 | } 61 | 62 | .gamemodeRecord { 63 | text-align: right; 64 | } 65 | 66 | .statsContainer { 67 | display: flex; 68 | flex-direction: row; 69 | align-items: center; 70 | justify-content: space-between; 71 | } 72 | 73 | .leagueStatsContainer { 74 | text-align: right; 75 | display: flex; 76 | flex-direction: column; 77 | } 78 | 79 | .oldSeasonContainer { 80 | display: flex; 81 | flex-direction: row; 82 | align-items: center; 83 | gap: 5px; 84 | } 85 | 86 | .mod{ 87 | max-width: 24px; 88 | max-height: 24px; 89 | margin: 5px; 90 | } 91 | 92 | .badge{ 93 | /*max-width: 32px;*/ 94 | max-height: 32px; 95 | margin: 5px; 96 | object-fit: contain; 97 | } 98 | 99 | .modsContainer{ 100 | margin-top: 2px; 101 | display: flex; 102 | flex-direction: row; 103 | justify-content: center; 104 | align-items: center; 105 | } 106 | 107 | .badgesContainer{ 108 | margin-top: 2px; 109 | display: flex; 110 | flex-wrap: wrap; 111 | flex-direction: row; 112 | justify-content: center; 113 | align-items: center; 114 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/Web/overlay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Live for {username} 6 | 7 | 23 | 24 | 25 | {username} {mode} stats 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 73 |
74 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Entities/User.cs: -------------------------------------------------------------------------------- 1 | namespace Tetrio.Foxhole.Database.Entities; 2 | 3 | /// 4 | /// Represents a user for Zenith Daily Challenge 5 | /// 6 | public class User : BaseEntity 7 | { 8 | /// 9 | /// The users TETR.IO unique ID 10 | /// 11 | public string TetrioId { get; set; } 12 | /// 13 | /// The users TETR.IO username 14 | /// 15 | public string Username { get; set; } 16 | 17 | /// 18 | /// The users current SessionToken 19 | /// 20 | public Guid? SessionToken { get; set; } 21 | 22 | /// 23 | /// The timestamp when the user request a submission of scores 24 | /// 25 | public DateTime? LastSubmission { get; set; } 26 | 27 | /// 28 | /// If the user is restricted? If so, they can't submit runs 29 | /// 30 | public bool IsRestricted { get; set; } = false; 31 | 32 | /// 33 | /// There current Tetra League Rank 34 | /// 35 | public string? TetrioRank { get; set; } 36 | 37 | /// 38 | /// The users ZDC Score 39 | /// 40 | public uint Score { get; set; } = 0; 41 | 42 | /// 43 | /// The users ZDC Score before it got replaced by new scoring 44 | /// 45 | public uint LegacyScore { get; set; } = 0; 46 | 47 | #region Discord Auth related Stuff 48 | 49 | /// 50 | /// The users discord ID linked to their TETR.IO account 51 | /// 52 | public string DiscordId { get; set; } 53 | 54 | /// 55 | /// The users discord AccessToken 56 | /// 57 | public string? AccessToken { get; set; } 58 | 59 | /// 60 | /// The users discord RefreshToken 61 | /// 62 | public string? RefreshToken { get; set; } 63 | 64 | /// 65 | /// When the users discord tokens expire 66 | /// 67 | public DateTimeOffset? ExpiresAt { get; set; } 68 | 69 | #endregion 70 | 71 | public virtual ISet Challenges { get; set; } = new HashSet(); 72 | public virtual ISet Splits { get; set; } = new HashSet(); 73 | public virtual ISet Runs { get; set; } = new HashSet(); 74 | public virtual ISet CommunityContributions { get; set; } = new HashSet(); 75 | public virtual ISet MasteryAttempts { get; set; } = new HashSet(); 76 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Overlay/Controllers/TetraLeagueController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Tetrio.Foxhole.Backend.Base.Controllers; 3 | using Tetrio.Foxhole.Network.Api.Tetrio; 4 | 5 | namespace Tetrio.Foxhole.Overlay.Controllers; 6 | 7 | public class TetraLeagueController(TetrioApi api) : BaseController(api) 8 | { 9 | [HttpGet] 10 | public ActionResult Get() 11 | { 12 | return Ok("This Endpoint is for Tetra League Overlays"); 13 | } 14 | 15 | [HttpGet] 16 | [Route("stats/{username}")] 17 | public async Task Stats(string username, string? textcolor = null, string? backgroundColor = null, bool displayUsername = true) 18 | { 19 | return await StatsNew(username, textcolor, backgroundColor, displayUsername); 20 | } 21 | 22 | [HttpGet] 23 | [Route("{username}")] 24 | public async Task StatsNew(string username, string? textcolor = null, string? backgroundColor = null, bool displayUsername = true) 25 | { 26 | username = username.ToLower(); 27 | 28 | var html = await System.IO.File.ReadAllTextAsync("wwwroot/web/league.html"); 29 | 30 | html = html.Replace("{username}", username); 31 | 32 | return Content(html, "text/html"); 33 | } 34 | 35 | [HttpGet] 36 | [Route("stats/{username}/web")] 37 | public async Task WebAlt(string username, string? textcolor = null, string? backgroundColor = null) 38 | { 39 | return await StatsNew(username, textcolor, backgroundColor); 40 | } 41 | 42 | [HttpGet] 43 | [Route("{username}/stats")] 44 | public async Task GetStats(string username) 45 | { 46 | username = username.ToLower(); 47 | 48 | var user = await Api.GetUserInformation(username); 49 | var stats = await Api.GetTetraLeagueStats(username); 50 | 51 | if(user == null || stats == null) return NotFound(); 52 | 53 | return Ok(new 54 | { 55 | Username= user.Username, 56 | Country = user.Country, 57 | Tr = stats.Tr, 58 | Rank = stats.Rank, 59 | Apm = stats.Apm, 60 | Pps = stats.Pps, 61 | Vs = stats.Vs, 62 | GlobalRank = stats.StandingGlobal, 63 | CountryRank = stats.StandingLocal, 64 | TopRank = stats.TopRank, 65 | PrevRank = stats.PrevRank, 66 | PrevAt = stats.PrevAt, 67 | NextRank = stats.NextRank, 68 | NextAt = stats.NextAt, 69 | GamesPlayed = stats.Gamesplayed 70 | }); 71 | } 72 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/league.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tetra League Overlay Web 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |

21 |

0

22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
0APMGLOBAL0
0PPS0
0VSTOP RANK
44 |
45 |
46 |
47 |
48 | 0 49 | 50 |
51 |
52 |
53 |
54 |
55 | 56 |
57 |
58 | 59 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/css/league.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'CB'; 3 | /*src: url('fonts/cb.ttf');*/ 4 | src: url('/web/res/fonts/cb.ttf'); 5 | } 6 | 7 | html{ 8 | font-family: 'CB'; 9 | font-size: 24pt; 10 | 11 | /*background: gray;*/ 12 | } 13 | 14 | body { 15 | background: transparent; 16 | color:white; 17 | } 18 | 19 | img { 20 | image-rendering: high-quality; 21 | aspect-ratio: 1 / 1; 22 | object-fit: cover; 23 | } 24 | 25 | .bigRank { 26 | max-width: 256px; 27 | max-height: 256px; 28 | filter: drop-shadow(2px 2px 3px black); 29 | } 30 | 31 | .hidden { 32 | position: absolute; 33 | color: transparent; 34 | left: 0; 35 | top: 0; 36 | } 37 | 38 | .container { 39 | display: inline-grid; 40 | flex-direction: row; 41 | } 42 | 43 | .statsContainer{ 44 | width: 100%; 45 | display: flex; 46 | } 47 | 48 | .imageContainer { 49 | display: flex; 50 | align-items: center; 51 | } 52 | 53 | .statsContainer{ 54 | text-shadow: 2px 2px 3px black; 55 | } 56 | 57 | .prevRankContainer{ 58 | display: flex; 59 | } 60 | 61 | #lastRank{ 62 | font-size: 16pt; 63 | text-shadow: 2px 2px 2px black; 64 | } 65 | 66 | .bigText{ 67 | font-size: 2em; 68 | margin: 0; 69 | } 70 | 71 | table { 72 | width: auto; 73 | margin: 0; 74 | display: flex; 75 | } 76 | 77 | td:first-child{ 78 | text-align: left; 79 | } 80 | 81 | .td{ 82 | vertical-align: middle; 83 | } 84 | 85 | .rankImage{ 86 | width: 32px; 87 | height: 32px; 88 | align-self: center; 89 | filter: drop-shadow(2px 2px 3px black); 90 | } 91 | 92 | .countryImage{ 93 | width: 32px; 94 | height: 32px; 95 | border-radius: 50%; 96 | filter: drop-shadow(2px 2px 3px black); 97 | } 98 | 99 | td img.rankImage, td img.countryImage { 100 | display: block; 101 | margin: 0 auto; 102 | } 103 | 104 | .progressContainer{ 105 | width: 100%; 106 | max-width: 800px; 107 | display: flex; 108 | flex-direction: row; 109 | align-items: center; 110 | } 111 | 112 | .box{ 113 | display: block; 114 | height: 10px; 115 | } 116 | 117 | .progressBarContainer { 118 | position: relative; 119 | width: 100%; /* Adjust the width as needed */ 120 | height: 10px; /* Ensure the height matches the height of the boxes */ 121 | margin: 10px; 122 | } 123 | 124 | .progressBar, 125 | .progressBarAccent { 126 | position: absolute; 127 | top: 0; 128 | left: 0; 129 | width: 100%; /* Adjust as necessary */ 130 | } 131 | 132 | .progressBar { 133 | background-color: rgba(0, 0, 0, 0.75); 134 | z-index: 1; 135 | } 136 | 137 | .progressBarAccent { 138 | background-color: rgba(255, 255, 255, 1); 139 | transition: width 1s ease-in-out; 140 | width: 0; 141 | z-index: 2; 142 | } 143 | 144 | table tr td:nth-child(2) { 145 | padding-left: 0.5rem; 146 | padding-right: 0.5rem; 147 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.Json.Serialization; 3 | using Microsoft.EntityFrameworkCore; 4 | using Tetrio.Foxhole.Database; 5 | using Tetrio.Foxhole.Network.Api.Discord; 6 | using Tetrio.Foxhole.Network.Api.Tetrio; 7 | using Tetrio.Foxhole.Overlay.Controllers; 8 | using Tetrio.Zenith.DailyChallenge.Controllers; 9 | 10 | Console.WriteLine("Starting Tetrio.Foxhole.Backend.Runtime..."); 11 | 12 | var builder = WebApplication.CreateBuilder(args); 13 | 14 | var dailyAssembly = typeof(DailyController).Assembly; 15 | var overlayAssembly = typeof(TetraLeagueController).Assembly; 16 | 17 | // Services 18 | Console.WriteLine("Adding services..."); 19 | builder.Services.AddSwaggerGen(); 20 | builder.Services.AddControllers() 21 | .AddApplicationPart(dailyAssembly) 22 | .AddApplicationPart(overlayAssembly) 23 | .AddJsonOptions(o => 24 | { 25 | o.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; 26 | }); 27 | builder.Services.AddScoped(); 28 | builder.Services.AddScoped(); 29 | builder.Services.AddScoped(); 30 | builder.Services.AddEndpointsApiExplorer(); 31 | 32 | builder.Services.AddDbContext(); 33 | 34 | // CORS config 35 | Console.WriteLine("Adding CORS Rules..."); 36 | builder.Services.AddCors(options => 37 | { 38 | #if DEBUG 39 | Console.WriteLine("Adding CORS Policy: AllowDev"); 40 | 41 | options.AddPolicy("AllowDev", b => 42 | { 43 | b.WithOrigins("http://localhost:8080") 44 | .AllowAnyMethod() 45 | .AllowAnyHeader() 46 | .AllowCredentials(); 47 | }); 48 | #else 49 | Console.WriteLine("Adding CORS Policy: AllowFounntainDev"); 50 | 51 | options.AddPolicy("AllowFounntainDev", policy => 52 | { 53 | policy.SetIsOriginAllowed(origin => 54 | origin.EndsWith(".founntain.dev") || origin == "https://founntain.dev") 55 | .AllowAnyMethod() 56 | .AllowAnyHeader() 57 | .AllowCredentials(); 58 | }); 59 | #endif 60 | }); 61 | 62 | Console.WriteLine("Building app..."); 63 | var app = builder.Build(); 64 | 65 | // Culture 66 | Console.WriteLine("Setting culture..."); 67 | var cultureInfo = new CultureInfo("en-US"); 68 | CultureInfo.DefaultThreadCurrentCulture = cultureInfo; 69 | CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; 70 | 71 | // Swagger for dev 72 | if (app.Environment.IsDevelopment()) 73 | { 74 | Console.WriteLine("Adding Swagger..."); 75 | app.UseSwagger(); 76 | app.UseSwaggerUI(); 77 | } 78 | 79 | using (var scope = app.Services.CreateScope()) 80 | { 81 | var db = scope.ServiceProvider.GetRequiredService(); 82 | await db.Database.MigrateAsync(); 83 | } 84 | 85 | #if DEBUG 86 | Console.WriteLine("Applying CORS Policy: AllowDev"); 87 | app.UseCors("AllowDev"); 88 | #else 89 | Console.WriteLine("Applying CORS Policy: AllowFounntainDev"); 90 | app.UseCors("AllowFounntainDev"); 91 | #endif 92 | 93 | Console.WriteLine("Adding middleware..."); 94 | app.UseHttpsRedirection(); 95 | app.UseStaticFiles(); 96 | app.MapControllers(); 97 | app.Run(); -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Base/Controllers/MinBaseController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.EntityFrameworkCore; 4 | using Tetrio.Foxhole.Database; 5 | using Tetrio.Foxhole.Database.Entities; 6 | using Tetrio.Foxhole.Network.Api.Tetrio; 7 | 8 | namespace Tetrio.Foxhole.Backend.Base.Controllers; 9 | 10 | [ApiController] 11 | [Produces("application/json")] 12 | [Route("[controller]")] 13 | public class MinBaseController(TetrioApi api) : ControllerBase 14 | { 15 | protected readonly TetrioApi Api = api; 16 | 17 | protected async Task<(bool IsAuthorized, int StatusCode, string ResponseText, User? User)> CheckIfAuthorized(TetrioContext? context) 18 | { 19 | if(context == null) return (false, StatusCodes.Status401Unauthorized, string.Empty, null); 20 | 21 | // Retrieve the session token from the cookie 22 | if (!HttpContext.Request.Cookies.TryGetValue("session_token", out string? sessionToken)) 23 | { 24 | return (false, StatusCodes.Status401Unauthorized, "Session token not found.", null); 25 | } 26 | 27 | var token = Guid.Parse(sessionToken); 28 | var date = DateTimeOffset.UtcNow; 29 | 30 | var user = await context.Users.FirstOrDefaultAsync(x => x.SessionToken == token); 31 | 32 | if (user == null) 33 | { 34 | return (false, StatusCodes.Status401Unauthorized, "Invalid session token.", null); 35 | } 36 | 37 | if (user.ExpiresAt < date) 38 | { 39 | return (false, StatusCodes.Status401Unauthorized, "Session token expired.", null); 40 | } 41 | 42 | return (true, StatusCodes.Status200OK, $"User {user.Username} authorized successfully", user); 43 | } 44 | 45 | protected void ResetCookies() 46 | { 47 | #if DEBUG 48 | HttpContext.Response.Cookies.Append("username",string.Empty, new CookieOptions 49 | { 50 | Path = "/", 51 | HttpOnly = false, 52 | Secure = false, 53 | SameSite = SameSiteMode.Strict, 54 | Expires = DateTime.UtcNow.AddDays(-3), 55 | }); 56 | 57 | HttpContext.Response.Cookies.Append("session_token", string.Empty, new CookieOptions 58 | { 59 | Path = "/", 60 | HttpOnly = true, 61 | Secure = true, 62 | SameSite = SameSiteMode.None, 63 | Expires = DateTime.UtcNow.AddDays(-3), 64 | }); 65 | #else 66 | HttpContext.Response.Cookies.Append("username", string.Empty, new CookieOptions 67 | { 68 | Path = "/", 69 | HttpOnly = false, 70 | Secure = true, 71 | SameSite = SameSiteMode.None, 72 | Domain = ".founntain.dev", 73 | Expires = DateTime.UtcNow.AddDays(-3), 74 | }); 75 | 76 | HttpContext.Response.Cookies.Append("session_token", string.Empty, new CookieOptions 77 | { 78 | Path = "/", 79 | HttpOnly = true, 80 | Secure = true, 81 | SameSite = SameSiteMode.None, 82 | Domain = ".founntain.dev", 83 | Expires = DateTime.UtcNow.AddDays(-3) 84 | }); 85 | #endif 86 | } 87 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/js/splits.js: -------------------------------------------------------------------------------- 1 | let usernameInfo = document.getElementById("usernameInfo").innerText; 2 | let expertInfo = document.getElementById("expertInfo").innerText; 3 | 4 | let containers = document.getElementsByClassName("floorBoxWrapper") 5 | 6 | function updateStats() { 7 | let url = `${baseUrl}/zenith/splits/${usernameInfo}/stats?expert=${expertInfo}` 8 | 9 | console.log(url) 10 | 11 | fetch(url) 12 | .then(response => { 13 | if (!response.ok) { 14 | throw new Error('Network response was not ok'); 15 | } 16 | 17 | return response.json(); 18 | }) 19 | .then(data => { 20 | console.log(data); 21 | let notReached = false; 22 | let endReached = false; 23 | 24 | for (let i = 0; i < containers.length; i++) { 25 | 26 | let floorBox = containers[i]; 27 | let floorData = data[i]; 28 | 29 | 30 | if(endReached){ 31 | floorBox.parentElement.style.display = "none"; 32 | }else{ 33 | floorBox.parentElement.style.display = "flex"; 34 | } 35 | 36 | let floorTime = floorBox.getElementsByClassName("floorTime")[0]; 37 | let floorDifference = floorBox.getElementsByClassName("floorDifference")[0]; 38 | 39 | if(floorData.split === 0){ 40 | floorTime.innerText = "DNF"; 41 | endReached = true; 42 | }else{ 43 | floorTime.innerText = floorData.splitTime; 44 | } 45 | 46 | if(floorData.notReached === true){ 47 | if(notReached === false) 48 | floorDifference.innerText = "NOT REACHED" 49 | else{ 50 | floorDifference.innerText = " " 51 | } 52 | floorDifference.classList.add("notReached"); 53 | notReached = true; 54 | continue; 55 | }else{ 56 | floorDifference.classList.remove("notReached"); 57 | } 58 | 59 | var prefix = ""; 60 | 61 | if(floorData.isSlower){ 62 | prefix = "+"; 63 | 64 | floorDifference.classList.remove("faster"); 65 | floorDifference.classList.add("slower"); 66 | }else{ 67 | prefix = "-"; 68 | 69 | floorDifference.classList.remove("slower"); 70 | floorDifference.classList.add("faster"); 71 | } 72 | 73 | if(floorData.differenceToGold === 0 && floorData.differenceToSecondGold > 0){ 74 | floorDifference.innerText = prefix + floorData.timeDifferenceToSecondGold; 75 | }else{ 76 | floorDifference.innerText = prefix + floorData.timeDifferenceToGold; 77 | } 78 | } 79 | }) 80 | .catch(error => { 81 | console.error('There has been a problem with your fetch operation:', error); 82 | }); 83 | } 84 | 85 | updateStats(); 86 | 87 | setInterval(updateStats, 10000); -------------------------------------------------------------------------------- /Tetrio.Foxhole.Overlay/Controllers/AchievementController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Tetrio.Foxhole.Backend.Base.Controllers; 3 | using Tetrio.Foxhole.Network.Api.Tetrio; 4 | using Tetrio.Foxhole.Network.Api.Tetrio.Models; 5 | 6 | namespace Tetrio.Foxhole.Overlay.Controllers; 7 | 8 | public class AchievementController(TetrioApi api) : BaseController(api) 9 | { 10 | [HttpGet] 11 | [Route("{id}/{username}")] 12 | public async Task Web(string id, string username) 13 | { 14 | username = username.ToLower(); 15 | 16 | var html = await System.IO.File.ReadAllTextAsync("wwwroot/web/achievement.html"); 17 | 18 | html = html.Replace("{mode}", ControllerContext.ActionDescriptor.ControllerName); 19 | 20 | html = html.Replace("{username}", username); 21 | html = html.Replace("{achievement}", id); 22 | 23 | return Content(html, "text/html"); 24 | } 25 | 26 | [HttpGet] 27 | [Route("{id}/{username}/data")] 28 | public async Task GetAchievement([FromRoute] string id, [FromRoute] string username) 29 | { 30 | username = username.ToLower(); 31 | 32 | var achievement = await Api.GetAchievement(id); 33 | 34 | if (achievement == null) 35 | { 36 | return NotFound(); 37 | } 38 | 39 | var userIndex = achievement.Leaderboard.FindIndex(x => x.User.Username == username); 40 | 41 | if (userIndex == -1) 42 | { 43 | return NotFound(); 44 | } 45 | 46 | AchievementLeaderboardEntry? leaderBoardEntryBefore = null; 47 | AchievementLeaderboardEntry userEntry = achievement.Leaderboard[userIndex]; 48 | AchievementLeaderboardEntry? leaderBoardEntryAfter = null; 49 | 50 | if (userIndex != 0 && achievement.Leaderboard.Count > 1) 51 | { 52 | leaderBoardEntryBefore = achievement.Leaderboard[userIndex - 1]; 53 | } 54 | 55 | if (userIndex != achievement.Leaderboard.Count - 1) 56 | { 57 | leaderBoardEntryAfter = achievement.Leaderboard[userIndex + 1]; 58 | } 59 | 60 | return Ok(new 61 | { 62 | AchievementName = achievement.AchievementInfo.Name, 63 | Before = new 64 | { 65 | Username = leaderBoardEntryBefore?.User.Username, 66 | Value = leaderBoardEntryBefore?.Value, 67 | AdditionalValue = leaderBoardEntryBefore?.AdditionalValue, 68 | Country = leaderBoardEntryBefore?.User.Country, 69 | Rank = userIndex 70 | }, 71 | User = new 72 | { 73 | Username = userEntry.User.Username, 74 | Value = userEntry.Value, 75 | AdditionalValue = userEntry.AdditionalValue, 76 | Country = userEntry.User.Country, 77 | Rank = userIndex + 1 78 | }, 79 | After = new 80 | { 81 | Username = leaderBoardEntryAfter?.User.Username, 82 | Value = leaderBoardEntryAfter?.Value, 83 | AdditionalValue = leaderBoardEntryAfter?.AdditionalValue, 84 | Country = leaderBoardEntryAfter?.User.Country, 85 | Rank = userIndex + 2 86 | }, 87 | }); 88 | } 89 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/splits.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tetra League Overlay Web 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 | HOTEL 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 | CASINO 27 | 0:36.917 28 | 29 |
30 |
31 | 32 |
33 |
34 | ARENA 35 | 36 | 37 |
38 |
39 | 40 |
41 |
42 | MUSEUM 43 | 44 | 45 |
46 |
47 | 48 |
49 |
50 | OFFICES 51 | 52 | 53 |
54 |
55 | 56 |
57 |
58 | LABORATORY 59 | 60 | 61 |
62 |
63 | 64 |
65 |
66 | THE CORE 67 | 0:36.917 68 | -0.245 69 |
70 |
71 | 72 |
73 |
74 | CORRUPTION 75 | 0:36.917 76 | -0.245 77 |
78 |
79 | 80 |
81 |
82 | POTG 83 | 0:36.917 84 | -0.245 85 |
86 |
87 |
88 |
89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {username} 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 |
15 | 16 |

17 | 18 |
19 | 20 |
21 | 22 |
23 | 0 24 |
25 | 26 | 0 27 |
28 |
29 |
30 | 31 |
32 |
33 | 0 34 | 35 |
36 |
37 |
38 |
39 |
40 | 41 |
42 | 43 |
44 |
45 | 40 LINES 46 | 00:00.000 47 |
48 |
49 | BLITZ 50 | 0 51 |
52 |
53 | QP 54 |
55 |
56 | 0 57 |
58 |
59 |
60 |
61 |
62 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20250727134226_AddBack2BackCondition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | #pragma warning disable CA1814 // Prefer jagged arrays over multidimensional 7 | 8 | namespace Tetrio.Overlay.Database.Migrations 9 | { 10 | /// 11 | public partial class AddBack2BackCondition : Migration 12 | { 13 | /// 14 | protected override void Up(MigrationBuilder migrationBuilder) 15 | { 16 | migrationBuilder.AddColumn( 17 | name: "Back2Back", 18 | table: "Runs", 19 | type: "INTEGER", 20 | nullable: false, 21 | defaultValue: (ushort)0); 22 | 23 | migrationBuilder.InsertData( 24 | table: "ConditionRanges", 25 | columns: new[] { "Id", "ConditionType", "CreatedAt", "Difficulty", "Max", "Min", "UpdatedAt" }, 26 | values: new object[,] 27 | { 28 | { new Guid("11111111-1111-1111-1111-111111111010"), 9, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, 1000000.0, 250000.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) }, 29 | { new Guid("11111111-1111-1111-1111-111111111110"), 9, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 2, 7.0, 3.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) }, 30 | { new Guid("11111111-1111-1111-1111-111111111210"), 9, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 3, 25.0, 7.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) }, 31 | { new Guid("11111111-1111-1111-1111-111111111310"), 9, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 5, 50.0, 25.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) }, 32 | { new Guid("11111111-1111-1111-1111-111111111410"), 9, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 8, 80.0, 20.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) } 33 | }); 34 | } 35 | 36 | /// 37 | protected override void Down(MigrationBuilder migrationBuilder) 38 | { 39 | migrationBuilder.DeleteData( 40 | table: "ConditionRanges", 41 | keyColumn: "Id", 42 | keyValue: new Guid("11111111-1111-1111-1111-111111111010")); 43 | 44 | migrationBuilder.DeleteData( 45 | table: "ConditionRanges", 46 | keyColumn: "Id", 47 | keyValue: new Guid("11111111-1111-1111-1111-111111111110")); 48 | 49 | migrationBuilder.DeleteData( 50 | table: "ConditionRanges", 51 | keyColumn: "Id", 52 | keyValue: new Guid("11111111-1111-1111-1111-111111111210")); 53 | 54 | migrationBuilder.DeleteData( 55 | table: "ConditionRanges", 56 | keyColumn: "Id", 57 | keyValue: new Guid("11111111-1111-1111-1111-111111111310")); 58 | 59 | migrationBuilder.DeleteData( 60 | table: "ConditionRanges", 61 | keyColumn: "Id", 62 | keyValue: new Guid("11111111-1111-1111-1111-111111111410")); 63 | 64 | migrationBuilder.DropColumn( 65 | name: "Back2Back", 66 | table: "Runs"); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.ZenithDailyChallenge/ChallengeGeneration/Daily/ReverseChallengeGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Tetrio.Foxhole.Database; 3 | using Tetrio.Foxhole.Database.Entities; 4 | using Tetrio.Foxhole.Database.Enums; 5 | 6 | namespace Tetrio.Zenith.DailyChallenge.ChallengeGeneration.Daily; 7 | 8 | public class ReverseChallengeGenerator : BaseChallengeGenerator 9 | { 10 | public ReverseChallengeGenerator(Random random, DateTime day) : base(random, day) 11 | { 12 | 13 | } 14 | 15 | public async Task GenerateReverseChallenge(TetrioContext ctx) 16 | { 17 | var challengeConditions = new List(); 18 | var height = 150; 19 | var yesterdayChallenge = await ctx.Challenges.Where(x => x.Points == (byte)Difficulty.Reverse).OrderByDescending(x => x.Date).FirstOrDefaultAsync(); 20 | var randomMod = GetRandomReverseMod(yesterdayChallenge?.Mods);; 21 | 22 | height += randomMod.HeightModifier; 23 | 24 | challengeConditions.Add(new () { Type = ConditionType.Height, Value = height}); 25 | 26 | return new Challenge 27 | { 28 | Date = DateOnly.FromDateTime(_day.Date), 29 | Points = (byte) Difficulty.Reverse, 30 | Mods = randomMod.Mod, 31 | Conditions = challengeConditions.ToHashSet() 32 | }; 33 | } 34 | 35 | private (string Mod, int HeightModifier) GetRandomReverseMod(string? challenge) 36 | { 37 | var yesterdaysReverseMod = challenge; 38 | 39 | (string, int)? selectedMod = null; 40 | 41 | var tries = 0; 42 | 43 | // If after 300 tries we still don't find a mod that is different from yesterday's one, we just use the one rolled last 44 | while (tries <= 300) 45 | { 46 | tries++; 47 | 48 | var mod = _random.Next(0, 9); 49 | 50 | switch (mod) 51 | { 52 | case 0: 53 | selectedMod = ("expert_reversed", _random.Next(0, 50)); break; 54 | case 1: 55 | selectedMod = ("nohold_reversed", _random.Next(0, 150)); break; 56 | case 2: 57 | selectedMod = ("messy_reversed", _random.Next(0, 200)); break; 58 | case 3: 59 | selectedMod = ("gravity_reversed", _random.Next(0, 200)); break; 60 | case 4: 61 | selectedMod = ("volatile_reversed", _random.Next(0, 400)); break; 62 | case 5: 63 | selectedMod = ("doublehole_reversed", _random.Next(0, 100)); break; 64 | case 6: 65 | selectedMod = ("invisible_reversed", _random.Next(0, 50)); break; 66 | case 7: 67 | selectedMod = ("allspin_reversed", _random.Next(0, 200)); break; 68 | case 8: 69 | selectedMod = ("snowman_reversed", _random.Next(0, 150)); break; 70 | // We default to reverse volatile, as it is the easiest for most. 71 | // However, the default case should never trigger. 72 | default: selectedMod = ("volatile_reversed", _random.Next(0, 400)); break; 73 | } 74 | 75 | // We generate mods until we find a reverse mod that is different from yesterdays one 76 | if (selectedMod.Value.Item1 != yesterdaysReverseMod) break; 77 | } 78 | 79 | // Just as a fallback, if selectedMod is still null, we just use the default case. 80 | selectedMod ??= ("volatile_reversed", _random.Next(0, 400)); 81 | 82 | return selectedMod.Value; 83 | } 84 | } -------------------------------------------------------------------------------- /Tetrio.Overlay.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tetrio.Foxhole.Backend.Runtime", "Tetrio.Foxhole.Backend.Runtime\Tetrio.Foxhole.Backend.Runtime.csproj", "{E5CE37FD-F7A5-45B8-B8D3-EDD20241300A}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tetrio.Foxhole.Network", "Tetrio.Foxhole.Network\Tetrio.Foxhole.Network.csproj", "{CCD2ED82-68D5-476E-BD4F-19B701207533}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tetrio.Foxhole.Database", "Tetrio.Foxhole.Database\Tetrio.Foxhole.Database.csproj", "{7F8F9954-9D94-468E-8429-0B8AFC392C83}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tetrio.Foxhole.ZenithDailyChallenge", "Tetrio.Foxhole.ZenithDailyChallenge\Tetrio.Foxhole.ZenithDailyChallenge.csproj", "{2251E4BB-1084-47AB-945D-F35B0EF7E0FB}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tetrio.Foxhole.Overlay", "Tetrio.Foxhole.Overlay\Tetrio.Foxhole.Overlay.csproj", "{C4C8E4FB-3031-4202-8846-DA9CD590F4B6}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tetrio.Foxhole.Backend.Base", "Tetrio.Foxhole.Backend.Base\Tetrio.Foxhole.Backend.Base.csproj", "{03D76D8F-FA28-4989-A3FA-710BD8470FCD}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {E5CE37FD-F7A5-45B8-B8D3-EDD20241300A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {E5CE37FD-F7A5-45B8-B8D3-EDD20241300A}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {E5CE37FD-F7A5-45B8-B8D3-EDD20241300A}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {E5CE37FD-F7A5-45B8-B8D3-EDD20241300A}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {CCD2ED82-68D5-476E-BD4F-19B701207533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {CCD2ED82-68D5-476E-BD4F-19B701207533}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {CCD2ED82-68D5-476E-BD4F-19B701207533}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {CCD2ED82-68D5-476E-BD4F-19B701207533}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {7F8F9954-9D94-468E-8429-0B8AFC392C83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {7F8F9954-9D94-468E-8429-0B8AFC392C83}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {7F8F9954-9D94-468E-8429-0B8AFC392C83}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {7F8F9954-9D94-468E-8429-0B8AFC392C83}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {2251E4BB-1084-47AB-945D-F35B0EF7E0FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {2251E4BB-1084-47AB-945D-F35B0EF7E0FB}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {2251E4BB-1084-47AB-945D-F35B0EF7E0FB}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {2251E4BB-1084-47AB-945D-F35B0EF7E0FB}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {C4C8E4FB-3031-4202-8846-DA9CD590F4B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {C4C8E4FB-3031-4202-8846-DA9CD590F4B6}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {C4C8E4FB-3031-4202-8846-DA9CD590F4B6}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {C4C8E4FB-3031-4202-8846-DA9CD590F4B6}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {03D76D8F-FA28-4989-A3FA-710BD8470FCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {03D76D8F-FA28-4989-A3FA-710BD8470FCD}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {03D76D8F-FA28-4989-A3FA-710BD8470FCD}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {03D76D8F-FA28-4989-A3FA-710BD8470FCD}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(NestedProjects) = preSolution 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/Tetrio.Foxhole.Backend.Runtime.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | Linux 8 | TetraLeague.Overlay 9 | Tetrio.Foxhole.Backend.Runtime 10 | Tetrio.Foxhole.Backend.Runtime 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | PreserveNewest 25 | 26 | 27 | 28 | PreserveNewest 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | PreserveNewest 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/Web/slide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Live for {username} 6 | 7 | 41 | 42 | 43 | {username} tetraLeague stats 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | 110 |
111 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20251214160108_ReverseWOM.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace Tetrio.Overlay.Database.Migrations 6 | { 7 | /// 8 | public partial class ReverseWOM : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.AddColumn( 14 | name: "IsReverse", 15 | table: "MasteryChallengeCondition", 16 | type: "INTEGER", 17 | nullable: false, 18 | defaultValue: false); 19 | 20 | migrationBuilder.AddColumn( 21 | name: "AllSpinReversedCompleted", 22 | table: "MasteryAttempts", 23 | type: "INTEGER", 24 | nullable: false, 25 | defaultValue: false); 26 | 27 | migrationBuilder.AddColumn( 28 | name: "DoubleHoleReversedCompleted", 29 | table: "MasteryAttempts", 30 | type: "INTEGER", 31 | nullable: false, 32 | defaultValue: false); 33 | 34 | migrationBuilder.AddColumn( 35 | name: "ExpertReversedCompleted", 36 | table: "MasteryAttempts", 37 | type: "INTEGER", 38 | nullable: false, 39 | defaultValue: false); 40 | 41 | migrationBuilder.AddColumn( 42 | name: "GravityReversedCompleted", 43 | table: "MasteryAttempts", 44 | type: "INTEGER", 45 | nullable: false, 46 | defaultValue: false); 47 | 48 | migrationBuilder.AddColumn( 49 | name: "InvisibleReversedCompleted", 50 | table: "MasteryAttempts", 51 | type: "INTEGER", 52 | nullable: false, 53 | defaultValue: false); 54 | 55 | migrationBuilder.AddColumn( 56 | name: "MessyReversedCompleted", 57 | table: "MasteryAttempts", 58 | type: "INTEGER", 59 | nullable: false, 60 | defaultValue: false); 61 | 62 | migrationBuilder.AddColumn( 63 | name: "NoHoldReversedCompleted", 64 | table: "MasteryAttempts", 65 | type: "INTEGER", 66 | nullable: false, 67 | defaultValue: false); 68 | 69 | migrationBuilder.AddColumn( 70 | name: "VolatileReversedCompleted", 71 | table: "MasteryAttempts", 72 | type: "INTEGER", 73 | nullable: false, 74 | defaultValue: false); 75 | } 76 | 77 | /// 78 | protected override void Down(MigrationBuilder migrationBuilder) 79 | { 80 | migrationBuilder.DropColumn( 81 | name: "IsReverse", 82 | table: "MasteryChallengeCondition"); 83 | 84 | migrationBuilder.DropColumn( 85 | name: "AllSpinReversedCompleted", 86 | table: "MasteryAttempts"); 87 | 88 | migrationBuilder.DropColumn( 89 | name: "DoubleHoleReversedCompleted", 90 | table: "MasteryAttempts"); 91 | 92 | migrationBuilder.DropColumn( 93 | name: "ExpertReversedCompleted", 94 | table: "MasteryAttempts"); 95 | 96 | migrationBuilder.DropColumn( 97 | name: "GravityReversedCompleted", 98 | table: "MasteryAttempts"); 99 | 100 | migrationBuilder.DropColumn( 101 | name: "InvisibleReversedCompleted", 102 | table: "MasteryAttempts"); 103 | 104 | migrationBuilder.DropColumn( 105 | name: "MessyReversedCompleted", 106 | table: "MasteryAttempts"); 107 | 108 | migrationBuilder.DropColumn( 109 | name: "NoHoldReversedCompleted", 110 | table: "MasteryAttempts"); 111 | 112 | migrationBuilder.DropColumn( 113 | name: "VolatileReversedCompleted", 114 | table: "MasteryAttempts"); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Entities/Run.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using Tetrio.Foxhole.Network.Api.Tetrio.Models; 3 | 4 | namespace Tetrio.Foxhole.Database.Entities; 5 | 6 | public class Run : BaseEntity 7 | { 8 | public string TetrioId { get; set; } 9 | public DateTime? PlayedAt { get; set; } 10 | 11 | // Run Data 12 | public double Altitude { get; set; } = 0; 13 | public byte KOs { get; set; } = 0; 14 | public ushort AllClears { get; set; } = 0; 15 | public ushort Quads { get; set; } = 0; 16 | public ushort Spins { get; set; } = 0; 17 | public string Mods { get; set; } = string.Empty; 18 | 19 | // Speedrun 20 | public bool SpeedrunSeen { get; set; } = false; 21 | public bool SpeedrunCompleted { get; set; } = false; 22 | 23 | //Other Stats 24 | public double Apm { get; set; } = 0; 25 | public double Pps { get; set; } = 0; 26 | public double Vs { get; set; } = 0; 27 | public double Finesse { get; set; } = 0; 28 | public ushort Back2Back { get; set; } = 0; 29 | public double TotalBonus { get; set; } = 0; 30 | 31 | // Meta data 32 | public double TargetingFactor { get; set; } = 0; 33 | public double TargetingGrace { get; set; } = 0; 34 | public double Rank { get; set; } = 0; 35 | public double PeakRank { get; set; } = 0; 36 | public double AverageRankPoints { get; set; } = 0; 37 | public uint LinesCleared { get; set; } = 0; 38 | public uint Inputs { get; set; } = 0; 39 | public uint Holds { get; set; } = 0; 40 | public byte TopCombo { get; set; } = 0; 41 | 42 | // Garbage 43 | public uint GarbageSent { get; set; } = 0; 44 | public uint GarbageSendNoMult { get; set; } = 0; 45 | public uint GarbageMaxSpike { get; set; } = 0; 46 | public uint GarbageMaxSpikeNoMult { get; set; } = 0; 47 | public uint GarbageReceived { get; set; } = 0; 48 | public uint GarbageAttack { get; set; } = 0; 49 | public uint GarbageCleared { get; set; } = 0; 50 | 51 | public int TotalTime { get; set; } = 0; 52 | 53 | public virtual ISet? Challenges { get; set; } = new HashSet(); 54 | 55 | [ForeignKey("UserId")] 56 | public Guid UserId { get; set; } 57 | public virtual User User { get; set; } 58 | 59 | public static Run Create(User user, Record record, Stats stats, Clears clears, double finesse, double? totalSpins, string[] mods) 60 | { 61 | return new Run 62 | { 63 | User = user, 64 | TetrioId = record.Id, 65 | PlayedAt = record.Ts, 66 | 67 | Altitude = stats.Zenith.Altitude ?? 0, 68 | KOs = (byte?)stats.Kills ?? 0, 69 | AllClears = (ushort?)clears.AllClear ?? 0, 70 | Quads = (ushort?)clears.Quads ?? 0, 71 | Spins = (ushort?)totalSpins ?? 0, 72 | Mods = string.Join(" ", mods), 73 | 74 | SpeedrunSeen = stats.Zenith.SpeedrunSeen ?? false, 75 | SpeedrunCompleted = stats.Zenith.Speedrun ?? false, 76 | 77 | Apm = record.Results.Aggregatestats.Apm ?? 0, 78 | Pps = record.Results.Aggregatestats.Pps ?? 0, 79 | Vs = record.Results.Aggregatestats.Vsscore ?? 0, 80 | Finesse = finesse, 81 | Back2Back = (ushort?)stats.Topbtb ?? 0, 82 | TotalBonus = stats.Zenith.Totalbonus ?? 0, 83 | 84 | TargetingFactor = stats.Zenith.Targetingfactor ?? 0, 85 | TargetingGrace = stats.Zenith.Targetinggrace ?? 0, 86 | Rank = stats.Zenith.Rank ?? 0, 87 | PeakRank = stats.Zenith.Peakrank ?? 0, 88 | AverageRankPoints = stats.Zenith.Avgrankpts ?? 0, 89 | LinesCleared = stats.Lines ?? 0, 90 | Inputs = stats.Inputs ?? 0, 91 | Holds = stats.Holds ?? 0, 92 | TopCombo = (byte)(stats.Topcombo ?? 0), 93 | 94 | GarbageSent = stats.Garbage.Sent ?? 0, 95 | GarbageSendNoMult = stats.Garbage.SentNomult ?? 0, 96 | GarbageMaxSpike = stats.Garbage.Maxspike ?? 0, 97 | GarbageMaxSpikeNoMult = stats.Garbage.MaxspikeNomult ?? 0, 98 | GarbageReceived = stats.Garbage.Received ?? 0, 99 | GarbageAttack = stats.Garbage.Attack ?? 0, 100 | GarbageCleared = stats.Garbage.Cleared ?? 0, 101 | 102 | TotalTime = (int)Math.Round(stats.Finaltime ?? 0, 0) 103 | }; 104 | } 105 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.ZenithDailyChallenge/ChallengeGeneration/BaseChallengeGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Tetrio.Foxhole.Database; 3 | using Tetrio.Foxhole.Database.Enums; 4 | 5 | namespace Tetrio.Zenith.DailyChallenge.ChallengeGeneration; 6 | 7 | public abstract class BaseChallengeGenerator 8 | { 9 | protected Random _random; 10 | protected DateTime _day; 11 | 12 | public BaseChallengeGenerator(Random random, DateTime day) 13 | { 14 | _random = random; 15 | _day = day; 16 | } 17 | 18 | protected List GetRandomConditions(string mods) 19 | { 20 | var allConditions = Enum.GetValues().ToList(); 21 | 22 | allConditions.RemoveAt(0); 23 | // Removed finesse for challenge generator because of popular request 24 | allConditions.Remove(ConditionType.Finesse); 25 | allConditions.Remove(ConditionType.TotalBonus); // Removed for now as balancing is not done yet 26 | 27 | // If no hold was selected as mod, remove all clears from the condition roll 28 | if(mods.Contains("nohold")) allConditions.Remove(ConditionType.AllClears); 29 | 30 | allConditions = allConditions.OrderBy(_ => _random.Next()).ToList(); 31 | 32 | var selectedConditions = allConditions.Take(_random.Next(2, 4)).ToList(); 33 | 34 | return selectedConditions; 35 | } 36 | 37 | protected List GetRandomConditionsWithoutAllClears() 38 | { 39 | var allConditions = Enum.GetValues().ToList(); 40 | 41 | allConditions.RemoveAt(0); 42 | allConditions.Remove(ConditionType.Finesse); 43 | 44 | allConditions.Remove(ConditionType.AllClears); 45 | 46 | allConditions = allConditions.OrderBy(_ => _random.Next()).ToList(); 47 | 48 | var selectedConditions = allConditions.Take(_random.Next(1, 3)).ToList(); 49 | 50 | return selectedConditions; 51 | } 52 | 53 | protected async Task<(double min, double max)> GetRangeForConditionAndDifficulty(TetrioContext context, ConditionType condition, Difficulty difficulty) 54 | { 55 | var range = await context.ConditionRanges.FirstOrDefaultAsync(x => x.ConditionType == condition && x.Difficulty == difficulty); 56 | 57 | if(range == null) 58 | return (0, 0); 59 | 60 | return (range.Min, range.Max); 61 | } 62 | 63 | protected (double Alitude, double Vs, double Apm) CalculateNerfAdjustmentFactors(string[] mods) 64 | { 65 | var modCount = mods.Length; 66 | var altitudeAdjustment = 1d; 67 | var vsAdjustment = 1d; 68 | var apmAdjustment = 1d; 69 | 70 | if (mods.Contains("nohold")) 71 | { 72 | if (modCount == 1) 73 | { 74 | // 10% reduction 75 | altitudeAdjustment = 0.9d; 76 | vsAdjustment = 0.9d; 77 | apmAdjustment = 0.9d; 78 | } 79 | 80 | if (modCount == 2) 81 | { 82 | // Base Adjustment | 15% reduction 83 | altitudeAdjustment = 0.85d; 84 | vsAdjustment = 0.85d; 85 | apmAdjustment = 0.85d; 86 | 87 | if (mods.Contains("doublehole")) 88 | { 89 | // 20% reduction for altitude and APM | 30% reduction for VS 90 | if(altitudeAdjustment > 0.8d) altitudeAdjustment = 0.8d; 91 | if(vsAdjustment > 0.70d) vsAdjustment = 0.70d; 92 | if(apmAdjustment > 0.8d) apmAdjustment = 0.8d; 93 | } 94 | 95 | if (mods.Contains("gravity")) 96 | { 97 | // 15% reduction for altitude | 20% reduction for APM & VS 98 | if(altitudeAdjustment > 0.8d) altitudeAdjustment = 0.85d; 99 | if(vsAdjustment > 0.8d) vsAdjustment = 0.8d; 100 | if(apmAdjustment > 0.8d) apmAdjustment = 0.8d; 101 | } 102 | } 103 | 104 | if (modCount >= 3) 105 | { 106 | // 25% reduction 107 | if(altitudeAdjustment > 0.75d) altitudeAdjustment = 0.75d; 108 | if(vsAdjustment > 0.75d) vsAdjustment = 0.75d; 109 | if(apmAdjustment > 0.75d) apmAdjustment = 0.75d; 110 | } 111 | } 112 | 113 | return (altitudeAdjustment, vsAdjustment, apmAdjustment); 114 | } 115 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/js/base.js: -------------------------------------------------------------------------------- 1 | const { protocol, hostname, port } = window.location; 2 | const baseUrl = `${protocol}//${hostname}${port ? `:${port}` : ''}`; 3 | const animationDuration = 500; 4 | const imgUrl = "/web/res/img/"; 5 | 6 | function lerp(a, b, t) { 7 | return a + (b - a) * t; 8 | } 9 | 10 | function lerpInt(a, b, t) { 11 | return (a + (b - a) * t).toFixed(0); 12 | } 13 | 14 | function lerpText(start, end, t) { 15 | const length = Math.floor(start.length + (end.length - start.length) * t); 16 | let result = ''; 17 | 18 | for (let i = 0; i < length; i++) { 19 | const startCharCode = i < start.length ? start.charCodeAt(i) : ' '.charCodeAt(0); 20 | const endCharCode = i < end.length ? end.charCodeAt(i) : ' '.charCodeAt(0); 21 | 22 | const charCode = Math.floor(startCharCode + (endCharCode - startCharCode) * t); 23 | result += String.fromCharCode(charCode); 24 | } 25 | 26 | return result; 27 | } 28 | 29 | function formatTime(milliseconds) { 30 | const minutes = Math.floor(milliseconds / 60000); 31 | const seconds = Math.floor((milliseconds % 60000) / 1000); 32 | const ms = Math.floor(milliseconds % 1000); 33 | 34 | const formattedMinutes = String(minutes).padStart(2, '0'); 35 | const formattedSeconds = String(seconds).padStart(2, '0'); 36 | const formattedMilliseconds = String(ms).padStart(3, '0'); 37 | 38 | return `${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`; 39 | } 40 | 41 | function timeToMilliseconds(timeString) { 42 | const [minutes, seconds] = timeString.split(':'); 43 | const [sec, ms] = seconds.split('.'); 44 | 45 | const minutesMs = parseInt(minutes, 10) * 60000; 46 | const secondsMs = parseInt(sec, 10) * 1000; 47 | const milliseconds = parseInt(ms, 10); 48 | 49 | return minutesMs + secondsMs + milliseconds; 50 | } 51 | 52 | 53 | function animateValue(element, start, end, duration, lerpType = 0, prefix = "", suffix = "", formatAsTime = false) { 54 | let currentValue; 55 | 56 | if(start === end){ 57 | 58 | switch (lerpType) { 59 | case 0: 60 | if(formatAsTime) 61 | currentValue = formatTime(start); 62 | else 63 | currentValue = parseFloat(start.toFixed(2)).toLocaleString('en-US'); 64 | 65 | break; 66 | case 1: 67 | if(formatAsTime) 68 | currentValue = formatTime(start); 69 | else 70 | currentValue = parseInt(start).toLocaleString('en-US'); 71 | 72 | break; 73 | default: 74 | if(formatAsTime) 75 | currentValue = formatTime(start); 76 | else 77 | currentValue = start; 78 | 79 | break; 80 | } 81 | 82 | element.innerText = `${prefix}${currentValue}${suffix}` 83 | return; 84 | } 85 | 86 | let startTime = null; 87 | 88 | function animation(currentTime) { 89 | if (!startTime) startTime = currentTime; 90 | const elapsed = currentTime - startTime; 91 | 92 | const progress = Math.min(elapsed / duration, 1); 93 | 94 | switch (lerpType) { 95 | case 0: 96 | if(formatAsTime) 97 | currentValue = formatTime(parseFloat(lerp(start, end, progress).toFixed(2))); 98 | else 99 | currentValue = parseFloat(lerp(start, end, progress).toFixed(2)).toLocaleString('en-US'); 100 | 101 | break; 102 | case 1: 103 | currentValue = parseInt(lerpInt(start, end, progress)).toLocaleString('en-US'); 104 | 105 | break; 106 | case 2: 107 | currentValue = lerpText(start, end, progress); 108 | 109 | break; 110 | } 111 | 112 | element.innerText = `${prefix}${currentValue}${suffix}` 113 | 114 | if (progress < 1) { 115 | requestAnimationFrame(animation); 116 | } 117 | } 118 | 119 | requestAnimationFrame(animation); 120 | } 121 | 122 | 123 | function fadeIn(element) { 124 | element.style.display = "block"; 125 | 126 | setTimeout(() => { 127 | element.style.opacity = "1"; 128 | }, 10); 129 | } 130 | 131 | function fadeOut(element) { 132 | element.style.display = "block"; 133 | 134 | setTimeout(() => { 135 | element.style.opacity = "0"; 136 | }, 10); 137 | } -------------------------------------------------------------------------------- /Tetrio.Foxhole.Backend.Runtime/wwwroot/web/res/js/zenith.js: -------------------------------------------------------------------------------- 1 | let usernameInfo = document.getElementById("usernameInfo").innerText; 2 | 3 | //get all fields 4 | let thisWeeksNormalScore = document.getElementById("thisWeeksNormalScore"); 5 | let pps = document.getElementById("pps"); 6 | let apm = document.getElementById("apm"); 7 | let vs = document.getElementById("vs"); 8 | let normalPersonalBest = document.getElementById("normalPersonalBest"); 9 | let mods = document.getElementById("mods"); 10 | 11 | let thisWeeksExpertScore = document.getElementById("thisWeeksExpertScore"); 12 | let ppsExpert = document.getElementById("ppsExpert"); 13 | let apmExpert = document.getElementById("apmExpert"); 14 | let vsExpert = document.getElementById("vsExpert"); 15 | let expertPersonalBest = document.getElementById("expertPersonalBest"); 16 | let modsExpert = document.getElementById("modsExpert"); 17 | 18 | let expertContainer = document.getElementById("expertContainer"); 19 | 20 | function updateStats() { 21 | let url = `${baseUrl}/zenith/${usernameInfo}/stats` 22 | 23 | console.log(url) 24 | 25 | fetch(url) 26 | .then(response => { 27 | if (!response.ok) { 28 | throw new Error('Network response was not ok'); 29 | } 30 | 31 | return response.json(); 32 | }) 33 | .then(data => { 34 | console.log(data); 35 | 36 | // NORMAL 37 | 38 | 39 | let normalString = thisWeeksNormalScore.innerText; 40 | let cleanNormalString = normalString.replace(/[^0-9.]/g, ''); 41 | cleanNormalString = cleanNormalString.replace(/,/g, ''); 42 | 43 | animateValue(thisWeeksNormalScore, parseFloat(cleanNormalString), data.zenith.altitude, animationDuration, 0, "", " M"); 44 | 45 | animateValue(pps, parseFloat(pps.innerText), data.zenith.pps, animationDuration, 0, "", " PPS"); 46 | animateValue(apm, parseFloat(apm.innerText), data.zenith.apm, animationDuration, 0, "", " APM"); 47 | animateValue(vs, parseFloat(vs.innerText), data.zenith.vs, animationDuration, 0, "", " VS"); 48 | 49 | normalString = normalPersonalBest.innerText; 50 | cleanNormalString = normalString.replace(/[^0-9.]/g, ''); 51 | cleanNormalString = cleanNormalString.replace(/,/g, ''); 52 | 53 | animateValue(normalPersonalBest, parseFloat(cleanNormalString), data.zenith.best, animationDuration, 0, "", " M"); 54 | 55 | mods.innerHTML = ""; 56 | 57 | data.zenith.mods?.forEach(mod => { 58 | const img = document.createElement('img'); 59 | img.classList.add("mod"); 60 | img.src = `${imgUrl}${mod}.png`; 61 | mods.appendChild(img); 62 | }); 63 | 64 | // EXPERT 65 | if(data.expertPlayed == false){ 66 | expertContainer.style.display = "none"; 67 | }else{ 68 | expertContainer.style.display = "block"; 69 | 70 | let expertString = thisWeeksExpertScore.innerText; 71 | let cleanExpertString = expertString.replace(/[^0-9.]/g, ''); 72 | cleanExpertString = cleanExpertString.replace(/,/g, ''); 73 | 74 | animateValue(thisWeeksExpertScore, parseFloat(cleanExpertString), data.zenithExpert.altitude, animationDuration, 0, "", " M"); 75 | 76 | animateValue(ppsExpert, parseFloat(ppsExpert.innerText), data.zenithExpert.pps, animationDuration, 0, "", " PPS"); 77 | animateValue(apmExpert, parseFloat(apmExpert.innerText), data.zenithExpert.apm, animationDuration, 0, "", " APM"); 78 | animateValue(vsExpert, parseFloat(vsExpert.innerText), data.zenithExpert.vs, animationDuration, 0, "", " VS"); 79 | 80 | expertString = expertPersonalBest.innerText; 81 | cleanExpertString = expertString.replace(/[^0-9.]/g, ''); 82 | cleanExpertString = cleanExpertString.replace(/,/g, ''); 83 | 84 | animateValue(expertPersonalBest, parseFloat(cleanExpertString), data.zenithExpert.best, animationDuration, 0, "", " M"); 85 | 86 | modsExpert.innerHTML = ""; 87 | 88 | data.zenithExpert.mods.forEach(mod => { 89 | const img = document.createElement('img'); 90 | img.classList.add("mod"); 91 | img.src = `${imgUrl}${mod}.png`; 92 | modsExpert.appendChild(img); 93 | }); 94 | } 95 | }) 96 | .catch(error => { 97 | console.error('There has been a problem with your fetch operation:', error); 98 | }); 99 | } 100 | 101 | updateStats(); 102 | 103 | setInterval(updateStats, 30000); -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20250415181219_UpdatedConditions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Tetrio.Overlay.Database.Migrations 7 | { 8 | /// 9 | public partial class UpdatedConditions : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.UpdateData( 15 | table: "ConditionRanges", 16 | keyColumn: "Id", 17 | keyValue: new Guid("11111111-1111-1111-1111-111111111401"), 18 | columns: new[] { "Max", "Min" }, 19 | values: new object[] { 1750.0, 850.0 }); 20 | 21 | migrationBuilder.UpdateData( 22 | table: "ConditionRanges", 23 | keyColumn: "Id", 24 | keyValue: new Guid("11111111-1111-1111-1111-111111111402"), 25 | column: "Min", 26 | value: 2.0); 27 | 28 | migrationBuilder.UpdateData( 29 | table: "ConditionRanges", 30 | keyColumn: "Id", 31 | keyValue: new Guid("11111111-1111-1111-1111-111111111403"), 32 | columns: new[] { "Max", "Min" }, 33 | values: new object[] { 1.0, 0.0 }); 34 | 35 | migrationBuilder.UpdateData( 36 | table: "ConditionRanges", 37 | keyColumn: "Id", 38 | keyValue: new Guid("11111111-1111-1111-1111-111111111406"), 39 | columns: new[] { "Max", "Min" }, 40 | values: new object[] { 80.0, 50.0 }); 41 | 42 | migrationBuilder.UpdateData( 43 | table: "ConditionRanges", 44 | keyColumn: "Id", 45 | keyValue: new Guid("11111111-1111-1111-1111-111111111407"), 46 | columns: new[] { "Max", "Min" }, 47 | values: new object[] { 1.75, 1.3999999999999999 }); 48 | 49 | migrationBuilder.UpdateData( 50 | table: "ConditionRanges", 51 | keyColumn: "Id", 52 | keyValue: new Guid("11111111-1111-1111-1111-111111111408"), 53 | columns: new[] { "Max", "Min" }, 54 | values: new object[] { 125.0, 80.0 }); 55 | 56 | migrationBuilder.UpdateData( 57 | table: "ConditionRanges", 58 | keyColumn: "Id", 59 | keyValue: new Guid("11111111-1111-1111-1111-111111111409"), 60 | columns: new[] { "Max", "Min" }, 61 | values: new object[] { 80.0, 60.0 }); 62 | } 63 | 64 | /// 65 | protected override void Down(MigrationBuilder migrationBuilder) 66 | { 67 | migrationBuilder.UpdateData( 68 | table: "ConditionRanges", 69 | keyColumn: "Id", 70 | keyValue: new Guid("11111111-1111-1111-1111-111111111401"), 71 | columns: new[] { "Max", "Min" }, 72 | values: new object[] { 2500.0, 1350.0 }); 73 | 74 | migrationBuilder.UpdateData( 75 | table: "ConditionRanges", 76 | keyColumn: "Id", 77 | keyValue: new Guid("11111111-1111-1111-1111-111111111402"), 78 | column: "Min", 79 | value: 3.0); 80 | 81 | migrationBuilder.UpdateData( 82 | table: "ConditionRanges", 83 | keyColumn: "Id", 84 | keyValue: new Guid("11111111-1111-1111-1111-111111111403"), 85 | columns: new[] { "Max", "Min" }, 86 | values: new object[] { 6.0, 3.0 }); 87 | 88 | migrationBuilder.UpdateData( 89 | table: "ConditionRanges", 90 | keyColumn: "Id", 91 | keyValue: new Guid("11111111-1111-1111-1111-111111111406"), 92 | columns: new[] { "Max", "Min" }, 93 | values: new object[] { 130.0, 85.0 }); 94 | 95 | migrationBuilder.UpdateData( 96 | table: "ConditionRanges", 97 | keyColumn: "Id", 98 | keyValue: new Guid("11111111-1111-1111-1111-111111111407"), 99 | columns: new[] { "Max", "Min" }, 100 | values: new object[] { 2.5, 2.0 }); 101 | 102 | migrationBuilder.UpdateData( 103 | table: "ConditionRanges", 104 | keyColumn: "Id", 105 | keyValue: new Guid("11111111-1111-1111-1111-111111111408"), 106 | columns: new[] { "Max", "Min" }, 107 | values: new object[] { 200.0, 150.0 }); 108 | 109 | migrationBuilder.UpdateData( 110 | table: "ConditionRanges", 111 | keyColumn: "Id", 112 | keyValue: new Guid("11111111-1111-1111-1111-111111111409"), 113 | columns: new[] { "Max", "Min" }, 114 | values: new object[] { 100.0, 80.0 }); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20250411134524_Scaling.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Tetrio.Overlay.Database.Migrations 7 | { 8 | /// 9 | public partial class Scaling : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.AddColumn( 15 | name: "Scaling", 16 | table: "Mods", 17 | type: "REAL", 18 | nullable: false, 19 | defaultValue: 0.0); 20 | 21 | migrationBuilder.UpdateData( 22 | table: "ConditionRanges", 23 | keyColumn: "Id", 24 | keyValue: new Guid("11111111-1111-1111-1111-111111111407"), 25 | columns: new[] { "Max", "Min" }, 26 | values: new object[] { 2.5, 2.0 }); 27 | 28 | migrationBuilder.UpdateData( 29 | table: "ConditionRanges", 30 | keyColumn: "Id", 31 | keyValue: new Guid("11111111-1111-1111-1111-111111111408"), 32 | column: "Min", 33 | value: 150.0); 34 | 35 | migrationBuilder.UpdateData( 36 | table: "ConditionRanges", 37 | keyColumn: "Id", 38 | keyValue: new Guid("11111111-1111-1111-1111-111111111409"), 39 | column: "Min", 40 | value: 80.0); 41 | 42 | migrationBuilder.UpdateData( 43 | table: "Mods", 44 | keyColumn: "Id", 45 | keyValue: new Guid("11111111-1111-1111-1111-111111111101"), 46 | columns: new[] { "Scaling", "Weight" }, 47 | values: new object[] { 0.75, (byte)60 }); 48 | 49 | migrationBuilder.UpdateData( 50 | table: "Mods", 51 | keyColumn: "Id", 52 | keyValue: new Guid("11111111-1111-1111-1111-111111111102"), 53 | column: "Scaling", 54 | value: 0.90000000000000002); 55 | 56 | migrationBuilder.UpdateData( 57 | table: "Mods", 58 | keyColumn: "Id", 59 | keyValue: new Guid("11111111-1111-1111-1111-111111111103"), 60 | column: "Scaling", 61 | value: 1.0); 62 | 63 | migrationBuilder.UpdateData( 64 | table: "Mods", 65 | keyColumn: "Id", 66 | keyValue: new Guid("11111111-1111-1111-1111-111111111104"), 67 | column: "Scaling", 68 | value: 1.0); 69 | 70 | migrationBuilder.UpdateData( 71 | table: "Mods", 72 | keyColumn: "Id", 73 | keyValue: new Guid("11111111-1111-1111-1111-111111111105"), 74 | column: "Scaling", 75 | value: 1.0); 76 | 77 | migrationBuilder.UpdateData( 78 | table: "Mods", 79 | keyColumn: "Id", 80 | keyValue: new Guid("11111111-1111-1111-1111-111111111106"), 81 | column: "Scaling", 82 | value: 1.0); 83 | 84 | migrationBuilder.UpdateData( 85 | table: "Mods", 86 | keyColumn: "Id", 87 | keyValue: new Guid("11111111-1111-1111-1111-111111111107"), 88 | column: "Scaling", 89 | value: 1.0); 90 | 91 | migrationBuilder.UpdateData( 92 | table: "Mods", 93 | keyColumn: "Id", 94 | keyValue: new Guid("11111111-1111-1111-1111-111111111108"), 95 | column: "Scaling", 96 | value: 1.0); 97 | } 98 | 99 | /// 100 | protected override void Down(MigrationBuilder migrationBuilder) 101 | { 102 | migrationBuilder.DropColumn( 103 | name: "Scaling", 104 | table: "Mods"); 105 | 106 | migrationBuilder.UpdateData( 107 | table: "ConditionRanges", 108 | keyColumn: "Id", 109 | keyValue: new Guid("11111111-1111-1111-1111-111111111407"), 110 | columns: new[] { "Max", "Min" }, 111 | values: new object[] { 2.25, 1.75 }); 112 | 113 | migrationBuilder.UpdateData( 114 | table: "ConditionRanges", 115 | keyColumn: "Id", 116 | keyValue: new Guid("11111111-1111-1111-1111-111111111408"), 117 | column: "Min", 118 | value: 100.0); 119 | 120 | migrationBuilder.UpdateData( 121 | table: "ConditionRanges", 122 | keyColumn: "Id", 123 | keyValue: new Guid("11111111-1111-1111-1111-111111111409"), 124 | column: "Min", 125 | value: 75.0); 126 | 127 | migrationBuilder.UpdateData( 128 | table: "Mods", 129 | keyColumn: "Id", 130 | keyValue: new Guid("11111111-1111-1111-1111-111111111101"), 131 | column: "Weight", 132 | value: (byte)50); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20250411220903_CommunityChallenges.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Tetrio.Overlay.Database.Migrations 7 | { 8 | /// 9 | public partial class CommunityChallenges : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "CommunityChallenges", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "TEXT", nullable: false), 19 | StartDate = table.Column(type: "TEXT", nullable: false), 20 | EndDate = table.Column(type: "TEXT", nullable: false), 21 | ConditionType = table.Column(type: "INTEGER", nullable: false), 22 | TargetValue = table.Column(type: "REAL", nullable: false), 23 | Value = table.Column(type: "REAL", nullable: false), 24 | Finished = table.Column(type: "INTEGER", nullable: false), 25 | CreatedAt = table.Column(type: "TEXT", nullable: false), 26 | UpdatedAt = table.Column(type: "TEXT", nullable: false) 27 | }, 28 | constraints: table => 29 | { 30 | table.PrimaryKey("PK_CommunityChallenges", x => x.Id); 31 | }); 32 | 33 | migrationBuilder.CreateTable( 34 | name: "CommunityContributions", 35 | columns: table => new 36 | { 37 | Id = table.Column(type: "TEXT", nullable: false), 38 | Amount = table.Column(type: "REAL", nullable: false), 39 | UserId = table.Column(type: "TEXT", nullable: false), 40 | CommunityChallengeId = table.Column(type: "TEXT", nullable: false), 41 | CreatedAt = table.Column(type: "TEXT", nullable: false), 42 | UpdatedAt = table.Column(type: "TEXT", nullable: false) 43 | }, 44 | constraints: table => 45 | { 46 | table.PrimaryKey("PK_CommunityContributions", x => x.Id); 47 | table.ForeignKey( 48 | name: "FK_CommunityContributions_CommunityChallenges_CommunityChallengeId", 49 | column: x => x.CommunityChallengeId, 50 | principalTable: "CommunityChallenges", 51 | principalColumn: "Id", 52 | onDelete: ReferentialAction.Cascade); 53 | table.ForeignKey( 54 | name: "FK_CommunityContributions_Users_UserId", 55 | column: x => x.UserId, 56 | principalTable: "Users", 57 | principalColumn: "Id", 58 | onDelete: ReferentialAction.Cascade); 59 | }); 60 | 61 | migrationBuilder.UpdateData( 62 | table: "ConditionRanges", 63 | keyColumn: "Id", 64 | keyValue: new Guid("11111111-1111-1111-1111-111111111207"), 65 | column: "Max", 66 | value: 1.6499999999999999); 67 | 68 | migrationBuilder.UpdateData( 69 | table: "ConditionRanges", 70 | keyColumn: "Id", 71 | keyValue: new Guid("11111111-1111-1111-1111-111111111307"), 72 | column: "Min", 73 | value: 1.6499999999999999); 74 | 75 | migrationBuilder.CreateIndex( 76 | name: "IX_CommunityChallenges_StartDate", 77 | table: "CommunityChallenges", 78 | column: "StartDate", 79 | unique: true); 80 | 81 | migrationBuilder.CreateIndex( 82 | name: "IX_CommunityContributions_CommunityChallengeId", 83 | table: "CommunityContributions", 84 | column: "CommunityChallengeId"); 85 | 86 | migrationBuilder.CreateIndex( 87 | name: "IX_CommunityContributions_UserId", 88 | table: "CommunityContributions", 89 | column: "UserId"); 90 | } 91 | 92 | /// 93 | protected override void Down(MigrationBuilder migrationBuilder) 94 | { 95 | migrationBuilder.DropTable( 96 | name: "CommunityContributions"); 97 | 98 | migrationBuilder.DropTable( 99 | name: "CommunityChallenges"); 100 | 101 | migrationBuilder.UpdateData( 102 | table: "ConditionRanges", 103 | keyColumn: "Id", 104 | keyValue: new Guid("11111111-1111-1111-1111-111111111207"), 105 | column: "Max", 106 | value: 1.75); 107 | 108 | migrationBuilder.UpdateData( 109 | table: "ConditionRanges", 110 | keyColumn: "Id", 111 | keyValue: new Guid("11111111-1111-1111-1111-111111111307"), 112 | column: "Min", 113 | value: 1.75); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.ZenithDailyChallenge/Controllers/ArchiveController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.EntityFrameworkCore; 3 | using Tetrio.Foxhole.Backend.Base.Controllers; 4 | using Tetrio.Foxhole.Database; 5 | using Tetrio.Foxhole.Network.Api.Tetrio; 6 | 7 | namespace Tetrio.Zenith.DailyChallenge.Controllers; 8 | 9 | public class ArchiveController(TetrioApi api, TetrioContext context) : MinBaseController(api) 10 | { 11 | [HttpGet] 12 | [Route("community")] 13 | public async Task GetPastCommunityChallenges(Guid? id) 14 | { 15 | var baseQuery = context.CommunityChallenges.AsNoTracking().Where(x => x.EndDate < DateTime.UtcNow); 16 | 17 | if (id != null) 18 | { 19 | baseQuery = baseQuery.Where(x => x.Id == id); 20 | } 21 | 22 | var communityChallenge = await baseQuery 23 | .OrderByDescending(x => x.StartDate) 24 | .Select(x => new { x.Id, x.StartDate, x.EndDate, x.Value, x.TargetValue, x.ConditionType, x.Name, x.Description, x.Mods }) 25 | .FirstOrDefaultAsync(); 26 | 27 | if (communityChallenge == null) return NotFound(); 28 | 29 | var challengeId = communityChallenge.Id; 30 | 31 | var groupedContributions = await context.CommunityContributions 32 | .AsNoTracking() 33 | .Where(x => x.CommunityChallengeId == challengeId && !x.IsLate) 34 | .GroupBy(x => new { x.CommunityChallengeId, x.UserId, x.User.Username }) 35 | .Select(g => new 36 | { 37 | ChallengeId = g.Key.CommunityChallengeId, 38 | Name = g.Key.Username, 39 | Contributions = Math.Round(g.Sum(x => x.Amount), 2) 40 | }).ToListAsync(); 41 | 42 | var previousChallenge = context.CommunityChallenges.AsNoTracking().Where(x => x.StartDate < communityChallenge.StartDate).OrderByDescending(x => x.StartDate).FirstOrDefault(); 43 | var nextChallenge = context.CommunityChallenges.AsNoTracking().Where(x => x.StartDate > communityChallenge.StartDate && x.EndDate < DateTime.UtcNow && x.Id != communityChallenge.Id).OrderBy(x => x.StartDate).FirstOrDefault(); 44 | 45 | var archiveData = new 46 | { 47 | CommunityChallengeId = communityChallenge.Id, 48 | PreviousChallengeId = previousChallenge?.Id, 49 | NextChallengeId = nextChallenge?.Id, 50 | StartDate = communityChallenge.StartDate.ToLongDateString(), 51 | EndDate = communityChallenge.EndDate.ToLongDateString(), 52 | Name = communityChallenge.Name, 53 | Description = communityChallenge.Description, 54 | Value = Math.Round(communityChallenge.Value, 2), 55 | Mods = communityChallenge.Mods?.Split(" ", StringSplitOptions.RemoveEmptyEntries), 56 | 57 | TargetValue = Math.Round(communityChallenge.TargetValue, 2), 58 | ConditionType = communityChallenge.ConditionType, 59 | Participants = groupedContributions 60 | .OrderByDescending(y => y.Contributions) 61 | .Select(y => new { y.Name, y.Contributions }) 62 | .ToArray() 63 | }; 64 | 65 | return Ok(archiveData); 66 | } 67 | 68 | [HttpGet] 69 | [Route("daily")] 70 | public async Task GetPastDailyChallenges(DateOnly? date = null) 71 | { 72 | if (date == null) 73 | { 74 | // If no date is provided, find the most recent date with challenges 75 | var latest = await context.Challenges.AsNoTracking() 76 | .OrderByDescending(x => x.Date) 77 | .Select(x => new { x.Date }) 78 | .FirstOrDefaultAsync(); 79 | 80 | if (latest == null) return NotFound(); 81 | 82 | date = latest.Date; 83 | } 84 | 85 | var minDate = await context.Challenges.AsNoTracking().MinAsync(x => x.Date); 86 | var maxDate = await context.Challenges.AsNoTracking().MaxAsync(x => x.Date); 87 | 88 | var rawData = await context.Challenges.AsNoTracking() 89 | .Where(x => x.Date == date) 90 | .OrderBy(x => x.Points) 91 | .Select(x => new 92 | { 93 | x.Date, 94 | x.Points, 95 | x.Mods, 96 | Conditions = x.Conditions.Select(y => new { y.Type, y.Value }), 97 | Runs = x.Runs.Select(r => new { r.User.Username, r.PlayedAt }) 98 | }).ToListAsync(); 99 | 100 | if (rawData.Count == 0) return NotFound($"Nothing found on the given date {date.Value.ToLongDateString()}"); 101 | 102 | var archiveData = rawData.Select(x => new 103 | { 104 | MinDate = minDate, 105 | MaxDate = maxDate, 106 | Date = x.Date.ToString("D"), 107 | Points = x.Points, 108 | Mods = x.Mods?.Split(" ", StringSplitOptions.RemoveEmptyEntries), 109 | Conditions = x.Conditions.OrderBy ( x=> x.Type), 110 | Users = x.Runs 111 | .GroupBy(r => r.Username) 112 | .Select(g => new 113 | { 114 | Username = g.Key, 115 | CompletedAt = g.Select(r => r.PlayedAt).Min()?.ToString("HH:m:s") 116 | }).OrderBy(y => y.CompletedAt) 117 | }); 118 | 119 | return Ok(archiveData); 120 | } 121 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TETR.IO Overlay 2 | 3 | A simple overlay for displaying your Tetra League, Quick Play, 40L, Blitz and much more stats in OBS using a Browser Source. You can choose to display your stats as a static image or a live view that updates every 30 seconds. 4 | 5 | If you find this overlay useful, please consider giving it a ⭐ to show your support, or share it with others who might enjoy it. Thank you for using it! It really means a lot to me 🧡🦊 6 | 7 | > *this project also contains the backend of the [Zenith Daily Challenge](https://github.com/Founntain/Zenith.DailyChallenge.Web) as this project contains almost like 99% of the code that is need anyways* 8 | 9 | ## 🎖️ Usage 10 | 11 | For screenshots and examples look at the [examples section](#examples) at the bottom. 12 | To use the overlay, simply use one of the following URLs: 13 | 14 | ### User Card 15 | - **Live View**: `https://tetrio.founntain.dev/user/` 16 | 17 | ### Tetra League 18 | - **Live View**: `https://tetrio.founntain.dev/tetraleague/` 19 | 20 | ### Quick Play 21 | - **Live View**: `https://tetrio.founntain.dev/zenith/` 22 | 23 | ### 40 Lines 24 | - **Live View**: `https://tetrio.founntain.dev/sprint/` 25 | 26 | ### Blitz 27 | - **Live View**: `https://tetrio.founntain.dev/blitz/` 28 | 29 | ### QP Speedrun Splits 30 | - **Live View**: `https://tetrio.founntain.dev/zenith/splits/` 31 | > for more info check the [splits section](#Splits). 32 | 33 | ### 📽️ OBS Setup 34 | 35 | For OBS, it is recommended to use the live view URL. To set it up: 36 | 37 | 1. Create a new Browser Source in OBS. 38 | 2. Paste the live view URL into the Browser Source settings, replacing `` with your tetr.io username (make sure to remove the `<` and `>`), the same goes for ``. 39 | 3. Make sure the width and height is correct check below what sizes are best for each overlay: 40 | - User Card: 800 x 350 41 | - Tetra League: 800 x 350 42 | - Quick Play: 900 x 350 43 | - 40 Lines: 700 x 225 44 | - Blitz: 700 x 225 45 | - Speedrun splits: 1500 x 200 46 | 47 | > [!NOTE] 48 | > The data is cached and refreshes every 30 seconds. For 40 Lines and Blitz the default cache is used which is **5 minutes**. 49 | 50 | ### 🛠️ Customization Parameters 51 | You can customize a lot, as they are all web-based you can modify the CSS to your liking in OBS. 52 | Here are some common examples for the **user card**: 53 | ```CSS 54 | // Hide the profile picture 55 | .profilePicture { display: none } 56 | 57 | // Hide username 58 | #username { display: none } 59 | 60 | // Hide badges 61 | #badges { display: none } 62 | 63 | // Hide Tetra League Progressbar 64 | #tetraLeagueProgressContainer { display: none } 65 | 66 | // Change the text color for example red 67 | .body { color: #FF0000 } 68 | 69 | // Hide 40L, Blitz or QP 70 | #sprintContainer { display: none } 71 | #blitzContainer { display: none } 72 | #zenithContainer { display: none } 73 | 74 | // Or hide them all together with one line 75 | .gamemodesContainer { display: none } 76 | ``` 77 | 78 | ## Splits 79 | 80 | Ever wanted to know how fast you clear floors without hitting hyperspeed, or not able to hit hyperspeed ever, to see the splits in general? We have you covered. With the splits overlay, you can compare your times with your gold splits from this week. 81 | The splits overlay uses your last 100 games from the current week; all games past that aren't counted. We chose this limit because we don't want to make excessive API calls, and the last 100 games is enough to compare your performance this week. If you haven't played this week yet, we use your career best splits for display. 82 | With the `expert` parameter you can show the splits for expert or normal, default value is `false` 83 | 84 | ![image](https://github.com/user-attachments/assets/5f20844a-9fef-4559-a6b6-48e8852b7ebe) 85 | 86 | > [!CAUTION] 87 | > The splits overlay is still very early and pretty complex. If something does not work as intended, or is not working at all, please let me know so I can look into it! 88 | > ***The splits overlay is not supported in slide mode, because it is quite large. Maybe in the future.*** 89 | 90 | ## 🏠 Running Locally 91 | 92 | If you prefer not to use the hosted version, you have a few alternatives: 93 | 94 | - Clone the repository, build, and run the project locally, then access it via `localhost`. 95 | - Pull the [Docker image](https://hub.docker.com/repository/docker/founntain/tetrio.overlay.api/general) and run it with Docker, accessing it via `localhost` and the assigned port. 96 | 97 | ## 🔨 Contributing 98 | 99 | Contributions are welcome! Feel free to open issues, request features, provide feedback, report bugs, or even contribute code. If you're unsure about anything, let's discuss it. Thanks for your support! 100 | 101 | ## 🧡Special Thanks 102 | 103 | - **[osk](https://tetr.io)**: for creating tetr.io and providing an amazing, well-structured API. 104 | - *all assets like rank and mod icons belong to osk and tetr.io* 105 | - **[Veggie_Dog](https://www.twitch.tv/theveggiedog)**: motivating to make this project a reality, testing and feedback 106 | - **[PixelAtc](https://www.twitch.tv/pixelatc)**: providing feedback, ideas and spreading the word 107 | - **[ZaptorZap](https://zaptorz.app/)**: for giving feedback and some incredible ideas 108 | 109 | ## Examples 110 | > *added a background to all of them so they are visible on github light mode users* 111 | > 112 | 113 | #### User Card 114 | ![image](https://github.com/user-attachments/assets/bddedc0c-d560-444b-9474-6071286dcb17) 115 | 116 | #### Tetra League 117 | ![founntain](https://github.com/user-attachments/assets/b867218b-de57-4a44-85d3-1a5721878720) 118 | 119 | #### Quick Play 120 | ![image](https://github.com/user-attachments/assets/e91f4eb6-0fd8-4b9a-aa6c-8f0fdce8a43d) 121 | 122 | #### Speedrun splits 123 | ![image](https://github.com/user-attachments/assets/314b1ab6-98b7-4a5f-aad7-aa447ceb7f2b) 124 | 125 | #### Sprint 126 | ![image](https://github.com/user-attachments/assets/1eaae399-546e-470a-b108-360d5a34818b) 127 | 128 | #### Blitz 129 | ![image](https://github.com/user-attachments/assets/90fc37f2-2a7d-408b-a60a-f412e38c3378) 130 | -------------------------------------------------------------------------------- /Tetrio.Foxhole.Database/Migrations/20250412220351_CommunityChallengeContributions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | #pragma warning disable CA1814 // Prefer jagged arrays over multidimensional 7 | 8 | namespace Tetrio.Overlay.Database.Migrations 9 | { 10 | /// 11 | public partial class CommunityChallengeContributions : Migration 12 | { 13 | /// 14 | protected override void Up(MigrationBuilder migrationBuilder) 15 | { 16 | migrationBuilder.AddColumn( 17 | name: "IsRestricted", 18 | table: "Users", 19 | type: "INTEGER", 20 | nullable: false, 21 | defaultValue: false); 22 | 23 | migrationBuilder.AddColumn( 24 | name: "TotalTime", 25 | table: "Runs", 26 | type: "INTEGER", 27 | nullable: false, 28 | defaultValue: 0); 29 | 30 | migrationBuilder.InsertData( 31 | table: "ConditionRanges", 32 | columns: new[] { "Id", "ConditionType", "CreatedAt", "Difficulty", "Max", "Min", "UpdatedAt" }, 33 | values: new object[,] 34 | { 35 | { new Guid("11111111-1111-1111-1111-111111111001"), 0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, 5000000.0, 1000000.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) }, 36 | { new Guid("11111111-1111-1111-1111-111111111002"), 1, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, 15000.0, 10000.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) }, 37 | { new Guid("11111111-1111-1111-1111-111111111003"), 4, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, 100000.0, 50000.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) }, 38 | { new Guid("11111111-1111-1111-1111-111111111004"), 2, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, 2000000.0, 1000000.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) }, 39 | { new Guid("11111111-1111-1111-1111-111111111005"), 3, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, 4000000.0, 1000000.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) }, 40 | { new Guid("11111111-1111-1111-1111-111111111006"), 5, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, 1000000.0, 500000.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) }, 41 | { new Guid("11111111-1111-1111-1111-111111111007"), 6, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, 10000.0, 5000.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) }, 42 | { new Guid("11111111-1111-1111-1111-111111111008"), 7, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, 1500000.0, 750000.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) }, 43 | { new Guid("11111111-1111-1111-1111-111111111009"), 8, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, 0.0, 0.0, new DateTime(2020, 3, 22, 0, 0, 0, 0, DateTimeKind.Unspecified) } 44 | }); 45 | 46 | migrationBuilder.UpdateData( 47 | table: "Mods", 48 | keyColumn: "Id", 49 | keyValue: new Guid("11111111-1111-1111-1111-111111111104"), 50 | column: "Weight", 51 | value: (byte)30); 52 | } 53 | 54 | /// 55 | protected override void Down(MigrationBuilder migrationBuilder) 56 | { 57 | migrationBuilder.DeleteData( 58 | table: "ConditionRanges", 59 | keyColumn: "Id", 60 | keyValue: new Guid("11111111-1111-1111-1111-111111111001")); 61 | 62 | migrationBuilder.DeleteData( 63 | table: "ConditionRanges", 64 | keyColumn: "Id", 65 | keyValue: new Guid("11111111-1111-1111-1111-111111111002")); 66 | 67 | migrationBuilder.DeleteData( 68 | table: "ConditionRanges", 69 | keyColumn: "Id", 70 | keyValue: new Guid("11111111-1111-1111-1111-111111111003")); 71 | 72 | migrationBuilder.DeleteData( 73 | table: "ConditionRanges", 74 | keyColumn: "Id", 75 | keyValue: new Guid("11111111-1111-1111-1111-111111111004")); 76 | 77 | migrationBuilder.DeleteData( 78 | table: "ConditionRanges", 79 | keyColumn: "Id", 80 | keyValue: new Guid("11111111-1111-1111-1111-111111111005")); 81 | 82 | migrationBuilder.DeleteData( 83 | table: "ConditionRanges", 84 | keyColumn: "Id", 85 | keyValue: new Guid("11111111-1111-1111-1111-111111111006")); 86 | 87 | migrationBuilder.DeleteData( 88 | table: "ConditionRanges", 89 | keyColumn: "Id", 90 | keyValue: new Guid("11111111-1111-1111-1111-111111111007")); 91 | 92 | migrationBuilder.DeleteData( 93 | table: "ConditionRanges", 94 | keyColumn: "Id", 95 | keyValue: new Guid("11111111-1111-1111-1111-111111111008")); 96 | 97 | migrationBuilder.DeleteData( 98 | table: "ConditionRanges", 99 | keyColumn: "Id", 100 | keyValue: new Guid("11111111-1111-1111-1111-111111111009")); 101 | 102 | migrationBuilder.DropColumn( 103 | name: "IsRestricted", 104 | table: "Users"); 105 | 106 | migrationBuilder.DropColumn( 107 | name: "TotalTime", 108 | table: "Runs"); 109 | 110 | migrationBuilder.UpdateData( 111 | table: "Mods", 112 | keyColumn: "Id", 113 | keyValue: new Guid("11111111-1111-1111-1111-111111111104"), 114 | column: "Weight", 115 | value: (byte)25); 116 | } 117 | } 118 | } 119 | --------------------------------------------------------------------------------