1#include "community_icons.h"
2
3#include <base/log.h>
4
5#include <engine/engine.h>
6#include <engine/gfx/image_manipulation.h>
7#include <engine/storage.h>
8
9CCommunityIcons::CAbstractCommunityIconJob::CAbstractCommunityIconJob(CCommunityIcons *pCommunityIcons, const char *pCommunityId, int StorageType) :
10 m_pCommunityIcons(pCommunityIcons),
11 m_StorageType(StorageType)
12{
13 str_copy(dst&: m_aCommunityId, src: pCommunityId);
14 str_format(buffer: m_aPath, buffer_size: sizeof(m_aPath), format: "communityicons/%s.png", pCommunityId);
15}
16
17CCommunityIcons::CCommunityIconDownloadJob::CCommunityIconDownloadJob(CCommunityIcons *pCommunityIcons, const char *pCommunityId, const char *pUrl, const SHA256_DIGEST &Sha256) :
18 CHttpRequest(pUrl),
19 CAbstractCommunityIconJob(pCommunityIcons, pCommunityId, IStorage::TYPE_SAVE)
20{
21 WriteToFile(pStorage: pCommunityIcons->Storage(), pDest: m_aPath, StorageType: IStorage::TYPE_SAVE);
22 ExpectSha256(Sha256);
23 Timeout(Timeout: CTimeout{.m_ConnectTimeoutMs: 0, .m_TimeoutMs: 0, .m_LowSpeedLimit: 0, .m_LowSpeedTime: 0});
24 LogProgress(LogProgress: HTTPLOG::FAILURE);
25}
26
27void CCommunityIcons::CCommunityIconLoadJob::Run()
28{
29 m_Success = m_pCommunityIcons->LoadFile(pPath: m_aPath, DirType: m_StorageType, Info&: m_ImageInfo, InfoGrayscale&: m_ImageInfoGrayscale, Sha256&: m_Sha256);
30}
31
32CCommunityIcons::CCommunityIconLoadJob::CCommunityIconLoadJob(CCommunityIcons *pCommunityIcons, const char *pCommunityId, int StorageType) :
33 CAbstractCommunityIconJob(pCommunityIcons, pCommunityId, StorageType)
34{
35 Abortable(Abortable: true);
36}
37
38CCommunityIcons::CCommunityIconLoadJob::~CCommunityIconLoadJob()
39{
40 m_ImageInfo.Free();
41 m_ImageInfoGrayscale.Free();
42}
43
44int CCommunityIcons::FileScan(const char *pName, int IsDir, int DirType, void *pUser)
45{
46 const char *pExtension = ".png";
47 CCommunityIcons *pSelf = static_cast<CCommunityIcons *>(pUser);
48 if(IsDir || !str_endswith(str: pName, suffix: pExtension) || str_length(str: pName) - str_length(str: pExtension) >= (int)CServerInfo::MAX_COMMUNITY_ID_LENGTH)
49 return 0;
50
51 char aCommunityId[CServerInfo::MAX_COMMUNITY_ID_LENGTH];
52 str_truncate(dst: aCommunityId, dst_size: sizeof(aCommunityId), src: pName, truncation_len: str_length(str: pName) - str_length(str: pExtension));
53
54 std::shared_ptr<CCommunityIconLoadJob> pJob = std::make_shared<CCommunityIconLoadJob>(args&: pSelf, args&: aCommunityId, args&: DirType);
55 pSelf->Engine()->AddJob(pJob);
56 pSelf->m_CommunityIconLoadJobs.push_back(x: pJob);
57 return 0;
58}
59
60const CCommunityIcon *CCommunityIcons::Find(const char *pCommunityId)
61{
62 auto Icon = std::find_if(first: m_vCommunityIcons.begin(), last: m_vCommunityIcons.end(), pred: [pCommunityId](const CCommunityIcon &Element) {
63 return str_comp(a: Element.m_aCommunityId, b: pCommunityId) == 0;
64 });
65 return Icon == m_vCommunityIcons.end() ? nullptr : &(*Icon);
66}
67
68bool CCommunityIcons::LoadFile(const char *pPath, int DirType, CImageInfo &Info, CImageInfo &InfoGrayscale, SHA256_DIGEST &Sha256)
69{
70 if(!Graphics()->LoadPng(Image&: Info, pFilename: pPath, StorageType: DirType))
71 {
72 log_error("menus/browser", "Failed to load community icon from '%s'", pPath);
73 return false;
74 }
75 if(Info.m_Format != CImageInfo::FORMAT_RGBA)
76 {
77 Info.Free();
78 log_error("menus/browser", "Failed to load community icon from '%s': must be an RGBA image", pPath);
79 return false;
80 }
81 if(!Storage()->CalculateHashes(pFilename: pPath, Type: DirType, pSha256: &Sha256))
82 {
83 Info.Free();
84 log_error("menus/browser", "Failed to load community icon from '%s': could not calculate hash", pPath);
85 return false;
86 }
87 InfoGrayscale = Info.DeepCopy();
88 ConvertToGrayscale(Image: InfoGrayscale);
89 return true;
90}
91
92void CCommunityIcons::LoadFinish(const char *pCommunityId, CImageInfo &Info, CImageInfo &InfoGrayscale, const SHA256_DIGEST &Sha256)
93{
94 CCommunityIcon CommunityIcon;
95 str_copy(dst&: CommunityIcon.m_aCommunityId, src: pCommunityId);
96 CommunityIcon.m_Sha256 = Sha256;
97 CommunityIcon.m_OrgTexture = Graphics()->LoadTextureRawMove(Image&: Info, Flags: 0, pTexName: pCommunityId);
98 CommunityIcon.m_GreyTexture = Graphics()->LoadTextureRawMove(Image&: InfoGrayscale, Flags: 0, pTexName: pCommunityId);
99
100 auto ExistingIcon = std::find_if(first: m_vCommunityIcons.begin(), last: m_vCommunityIcons.end(), pred: [pCommunityId](const CCommunityIcon &Element) {
101 return str_comp(a: Element.m_aCommunityId, b: pCommunityId) == 0;
102 });
103 if(ExistingIcon == m_vCommunityIcons.end())
104 {
105 m_vCommunityIcons.push_back(x: CommunityIcon);
106 }
107 else
108 {
109 Graphics()->UnloadTexture(pIndex: &ExistingIcon->m_OrgTexture);
110 Graphics()->UnloadTexture(pIndex: &ExistingIcon->m_GreyTexture);
111 *ExistingIcon = CommunityIcon;
112 }
113
114 log_trace("menus/browser", "Loaded community icon '%s'", pCommunityId);
115}
116
117void CCommunityIcons::Render(const CCommunityIcon *pIcon, CUIRect Rect, bool Active)
118{
119 Rect.VMargin(Cut: Rect.w / 2.0f - Rect.h, pOtherRect: &Rect);
120
121 Graphics()->WrapClamp();
122 Graphics()->TextureSet(Texture: Active ? pIcon->m_OrgTexture : pIcon->m_GreyTexture);
123 Graphics()->QuadsBegin();
124 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: Active ? 1.0f : 0.5f);
125 IGraphics::CQuadItem QuadItem(Rect.x, Rect.y, Rect.w, Rect.h);
126 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
127 Graphics()->QuadsEnd();
128 Graphics()->WrapNormal();
129}
130
131void CCommunityIcons::Load()
132{
133 m_vCommunityIcons.clear();
134 Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: "communityicons", pfnCallback: FileScan, pUser: this);
135}
136
137void CCommunityIcons::Shutdown()
138{
139 m_CommunityIconLoadJobs.clear();
140 m_CommunityIconDownloadJobs.clear();
141}
142
143void CCommunityIcons::Update()
144{
145 // Update load jobs (icon is loaded from existing file)
146 if(!m_CommunityIconLoadJobs.empty())
147 {
148 std::shared_ptr<CCommunityIconLoadJob> pJob = m_CommunityIconLoadJobs.front();
149 if(pJob->Done())
150 {
151 if(pJob->Success())
152 {
153 LoadFinish(pCommunityId: pJob->CommunityId(), Info&: pJob->ImageInfo(), InfoGrayscale&: pJob->ImageInfoGrayscale(), Sha256: pJob->Sha256());
154 }
155 m_CommunityIconLoadJobs.pop_front();
156 }
157
158 // Don't start download jobs until all load jobs are done
159 if(!m_CommunityIconLoadJobs.empty())
160 return;
161 }
162
163 // Update download jobs (icon is downloaded and loaded from new file)
164 if(!m_CommunityIconDownloadJobs.empty())
165 {
166 std::shared_ptr<CCommunityIconDownloadJob> pJob = m_CommunityIconDownloadJobs.front();
167 if(pJob->Done())
168 {
169 if(pJob->State() == EHttpState::DONE)
170 {
171 std::shared_ptr<CCommunityIconLoadJob> pLoadJob = std::make_shared<CCommunityIconLoadJob>(args: this, args: pJob->CommunityId(), args: IStorage::TYPE_SAVE);
172 Engine()->AddJob(pJob: pLoadJob);
173 m_CommunityIconLoadJobs.push_back(x: pLoadJob);
174 }
175 m_CommunityIconDownloadJobs.pop_front();
176 }
177 }
178
179 // Rescan for changed communities only when necessary
180 if(!ServerBrowser()->DDNetInfoAvailable() || (m_CommunityIconsInfoSha256 != SHA256_ZEROED && m_CommunityIconsInfoSha256 == ServerBrowser()->DDNetInfoSha256()))
181 return;
182 m_CommunityIconsInfoSha256 = ServerBrowser()->DDNetInfoSha256();
183
184 // Remove icons for removed communities
185 auto RemovalIterator = m_vCommunityIcons.begin();
186 while(RemovalIterator != m_vCommunityIcons.end())
187 {
188 if(ServerBrowser()->Community(pCommunityId: RemovalIterator->m_aCommunityId) == nullptr)
189 {
190 Graphics()->UnloadTexture(pIndex: &RemovalIterator->m_OrgTexture);
191 Graphics()->UnloadTexture(pIndex: &RemovalIterator->m_GreyTexture);
192 RemovalIterator = m_vCommunityIcons.erase(position: RemovalIterator);
193 }
194 else
195 {
196 ++RemovalIterator;
197 }
198 }
199
200 // Find added and updated community icons
201 for(const auto &Community : ServerBrowser()->Communities())
202 {
203 if(str_comp(a: Community.Id(), b: IServerBrowser::COMMUNITY_NONE) == 0)
204 continue;
205 auto ExistingIcon = std::find_if(first: m_vCommunityIcons.begin(), last: m_vCommunityIcons.end(), pred: [Community](const auto &Element) {
206 return str_comp(Element.m_aCommunityId, Community.Id()) == 0;
207 });
208 auto ExistingDownload = std::find_if(first: m_CommunityIconDownloadJobs.begin(), last: m_CommunityIconDownloadJobs.end(), pred: [Community](const auto &Element) {
209 return str_comp(Element->CommunityId(), Community.Id()) == 0;
210 });
211 if(ExistingDownload == m_CommunityIconDownloadJobs.end() && (ExistingIcon == m_vCommunityIcons.end() || ExistingIcon->m_Sha256 != Community.IconSha256()))
212 {
213 std::shared_ptr<CCommunityIconDownloadJob> pJob = std::make_shared<CCommunityIconDownloadJob>(args: this, args: Community.Id(), args: Community.IconUrl(), args: Community.IconSha256());
214 Http()->Run(pRequest: pJob);
215 m_CommunityIconDownloadJobs.push_back(x: pJob);
216 }
217 }
218}
219