1+ using System . Collections . Concurrent ;
2+ using Intersect . Framework . IO ;
3+ using Intersect . Server . Collections . Indexing ;
4+ using Intersect . Server . Entities ;
5+ using Microsoft . AspNetCore . Authorization ;
6+ using Microsoft . AspNetCore . Mvc ;
7+ using Microsoft . AspNetCore . OutputCaching ;
8+ using SixLabors . ImageSharp . Formats ;
9+
10+ namespace Intersect . Server . Web . Controllers ;
11+
12+ [ AllowAnonymous ]
13+ [ Route ( "avatar" ) ]
14+ [ ResponseCache ( CacheProfileName = nameof ( AvatarController ) ) ]
15+ [ OutputCache ( PolicyName = nameof ( AvatarController ) ) ]
16+ public class AvatarController : IntersectController
17+ {
18+ #if DEBUG
19+ private const int CacheSeconds = 30 ;
20+ #else
21+ private const int CacheSeconds = 900 ;
22+ #endif
23+
24+ public static readonly CacheProfile ResponseCacheProfile = new ( )
25+ {
26+ Duration = CacheSeconds ,
27+ } ;
28+
29+ private record struct CachingResult ( IActionResult ? Result , string ? Checksum , FileInfo ? FileInfo ) ;
30+
31+ public static readonly Action < OutputCachePolicyBuilder > OutputCachePolicy =
32+ builder => builder . Expire ( TimeSpan . FromSeconds ( CacheSeconds ) ) . Tag ( ) ;
33+
34+ private static readonly ConcurrentDictionary < string , Task < CachingResult > > CachingTasks = new ( ) ;
35+
36+ private readonly DirectoryInfo _cacheDirectoryInfo ;
37+ private readonly ILogger < AvatarController > _logger ;
38+
39+ public AvatarController ( ILogger < AvatarController > logger )
40+ {
41+ _cacheDirectoryInfo = new DirectoryInfo ( Path . Combine ( Environment . CurrentDirectory , ".cache" , "avatars" ) ) ;
42+ _logger = logger ;
43+ }
44+
45+ [ HttpGet ( "player/{lookupKey:LookupKey}" ) ]
46+ public async Task < IActionResult > GetPlayerAvatarAsync ( LookupKey lookupKey )
47+ {
48+ DirectoryInfo assetsDirectoryInfo = new ( "assets/editor/resources" ) ;
49+ if ( ! assetsDirectoryInfo . Exists )
50+ {
51+ return InternalServerError ( "Error occurred while fetching the avatar" ) ;
52+ }
53+
54+ if ( lookupKey . IsInvalid )
55+ {
56+ return BadRequest ( $ "Invalid lookup key: { lookupKey } ") ;
57+ }
58+
59+ if ( ! Player . TryFetch ( lookupKey , out var player ) )
60+ {
61+ return NotFound ( $ "Player not found for lookup key '{ lookupKey } '") ;
62+ }
63+
64+ if ( ! player . TryLoadAvatarName ( out var avatarName , out var isFace ) )
65+ {
66+ return NotFound ( $ "Avatar not found for player '{ lookupKey } '") ;
67+ }
68+
69+ var ( result , checksum , fileInfo ) = await ResolveAvatarAsync ( assetsDirectoryInfo , avatarName , isFace ) ;
70+ if ( result == null )
71+ {
72+ if ( fileInfo != null )
73+ {
74+ _logger . LogWarning (
75+ "Avatar '{AvatarName}' was found in {AssetsDirectory} but failed to be loaded and should be located at {AvatarFilePath}" ,
76+ avatarName ,
77+ assetsDirectoryInfo . FullName ,
78+ fileInfo . FullName
79+ ) ;
80+ }
81+ else
82+ {
83+ _logger . LogWarning (
84+ "Avatar '{AvatarName}' was not found in {AssetsDirectory}" ,
85+ avatarName ,
86+ assetsDirectoryInfo . FullName
87+ ) ;
88+ }
89+ return InternalServerError ( $ "Error loading avatar for player '{ lookupKey } '") ;
90+ }
91+
92+ if ( ! string . IsNullOrWhiteSpace ( checksum ) )
93+ {
94+ Response . Headers . ETag = checksum ;
95+ }
96+
97+ return result ;
98+ }
99+
100+ [ HttpGet ( "{lookupKey:LookupKey}" ) ]
101+ public async Task < IActionResult > GetUserAvatarAsync ( LookupKey lookupKey )
102+ {
103+ DirectoryInfo assetsDirectoryInfo = new ( "assets/editor/resources" ) ;
104+ if ( ! assetsDirectoryInfo . Exists )
105+ {
106+ return InternalServerError ( "Error occurred while fetching the avatar" ) ;
107+ }
108+
109+ if ( lookupKey . IsInvalid )
110+ {
111+ return BadRequest ( $ "Invalid lookup key: { lookupKey } ") ;
112+ }
113+
114+ if ( ! Database . PlayerData . User . TryFetch ( lookupKey , out var user ) )
115+ {
116+ return NotFound ( $ "User not found for lookup key '{ lookupKey } '") ;
117+ }
118+
119+ if ( ! user . TryLoadAvatarName ( out _ , out var avatarName , out var isFace ) )
120+ {
121+ return NotFound ( $ "Avatar not found for user '{ lookupKey } '") ;
122+ }
123+
124+ var ( result , checksum , fileInfo ) = await ResolveAvatarAsync ( assetsDirectoryInfo , avatarName , isFace ) ;
125+ if ( result == null )
126+ {
127+ if ( fileInfo != null )
128+ {
129+ _logger . LogWarning (
130+ "Avatar '{AvatarName}' was found in {AssetsDirectory} but failed to be loaded and should be located at {AvatarFilePath}" ,
131+ avatarName ,
132+ assetsDirectoryInfo . FullName ,
133+ fileInfo . FullName
134+ ) ;
135+ }
136+ else
137+ {
138+ _logger . LogWarning (
139+ "Avatar '{AvatarName}' was not found in {AssetsDirectory}" ,
140+ avatarName ,
141+ assetsDirectoryInfo . FullName
142+ ) ;
143+ }
144+ return InternalServerError ( $ "Error loading avatar for user '{ lookupKey } '") ;
145+ }
146+
147+ if ( ! string . IsNullOrWhiteSpace ( checksum ) )
148+ {
149+ Response . Headers . ETag = checksum ;
150+ }
151+
152+ return result ;
153+ }
154+
155+ private async Task < CachingResult > ResolveAvatarAsync (
156+ DirectoryInfo assetsDirectoryInfo ,
157+ string avatarName ,
158+ bool isFace
159+ )
160+ {
161+ var directoryInfos = isFace
162+ ? assetsDirectoryInfo . EnumerateDirectories ( "faces" )
163+ : assetsDirectoryInfo . EnumerateDirectories ( "entities" ) ;
164+
165+ var fileInfos = directoryInfos . SelectMany ( di => di . EnumerateFiles ( ) ) . Where (
166+ f => string . Equals ( f . Name , avatarName , StringComparison . OrdinalIgnoreCase )
167+ ) ;
168+
169+ var avatarFileInfo = fileInfos . FirstOrDefault ( ) ;
170+
171+ if ( avatarFileInfo == default )
172+ {
173+ return default ;
174+ }
175+
176+ if ( isFace )
177+ {
178+ _ = avatarFileInfo . TryComputeChecksum ( out var checksum ) ;
179+ return new CachingResult (
180+ new PhysicalFileResult ( avatarFileInfo . FullName , "image/png" ) ,
181+ checksum ,
182+ avatarFileInfo
183+ ) ;
184+ }
185+
186+ var relativeName = Path . GetRelativePath ( assetsDirectoryInfo . FullName , avatarFileInfo . FullName ) ;
187+ FileInfo cacheFileInfo = new ( Path . Combine ( _cacheDirectoryInfo . FullName , relativeName ) ) ;
188+
189+ var taskKey = cacheFileInfo . FullName ;
190+ Task < CachingResult > task ;
191+ lock ( CachingTasks )
192+ {
193+ task = CachingTasks . GetOrAdd (
194+ taskKey ,
195+ async ( _ , args ) =>
196+ {
197+ string ? checksum ;
198+
199+ var result = new PhysicalFileResult ( args . cacheFileInfo . FullName , "image/png" ) ;
200+ if ( args . cacheFileInfo . Exists )
201+ {
202+ args . cacheFileInfo . TryComputeChecksum ( out checksum ) ;
203+ return new CachingResult ( result , checksum , args . cacheFileInfo ) ;
204+ }
205+
206+ var cacheParentDirectoryInfo = args . cacheFileInfo . Directory ;
207+
208+ if ( cacheParentDirectoryInfo is { Exists : false } )
209+ {
210+ cacheParentDirectoryInfo . Create ( ) ;
211+ }
212+
213+ using var avatarImage = await Image . LoadAsync ( new DecoderOptions ( ) , args . avatarFileInfo . FullName ) ;
214+ var spritesOptions = Options . Instance . Sprites ;
215+ var horizontalFrames = spritesOptions . IdleFrames ;
216+ var verticalFrames = spritesOptions . Directions ;
217+
218+ var minSize = Math . Min ( avatarImage . Width / horizontalFrames , avatarImage . Height / verticalFrames ) ;
219+ avatarImage . Mutate ( i => i . Crop ( minSize , minSize ) ) ;
220+ await avatarImage . SaveAsync ( args . cacheFileInfo . FullName ) ;
221+
222+ args . cacheFileInfo . TryComputeChecksum ( out checksum ) ;
223+ return new CachingResult ( result , checksum , args . cacheFileInfo ) ;
224+ } ,
225+ ( avatarFileInfo , cacheFileInfo )
226+ ) ;
227+ }
228+
229+ var result = await task ;
230+
231+ // ReSharper disable once InconsistentlySynchronizedField
232+ _ = CachingTasks . TryRemove ( taskKey , out _ ) ;
233+
234+ return result ;
235+ }
236+ }
0 commit comments