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