| 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 | |
| 9 | CCommunityIcons::CAbstractCommunityIconJob::(CCommunityIcons *, const char *, 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 | |
| 17 | CCommunityIcons::CCommunityIconDownloadJob::(CCommunityIcons *, const char *, 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 | |
| 27 | void CCommunityIcons::CCommunityIconLoadJob::() |
| 28 | { |
| 29 | m_Success = m_pCommunityIcons->LoadFile(pPath: m_aPath, DirType: m_StorageType, Info&: m_ImageInfo, InfoGrayscale&: m_ImageInfoGrayscale, Sha256&: m_Sha256); |
| 30 | } |
| 31 | |
| 32 | CCommunityIcons::CCommunityIconLoadJob::(CCommunityIcons *, const char *, int StorageType) : |
| 33 | CAbstractCommunityIconJob(pCommunityIcons, pCommunityId, StorageType) |
| 34 | { |
| 35 | Abortable(Abortable: true); |
| 36 | } |
| 37 | |
| 38 | CCommunityIcons::CCommunityIconLoadJob::() |
| 39 | { |
| 40 | m_ImageInfo.Free(); |
| 41 | m_ImageInfoGrayscale.Free(); |
| 42 | } |
| 43 | |
| 44 | int CCommunityIcons::(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 [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 | |
| 60 | const CCommunityIcon *CCommunityIcons::(const char *) |
| 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 | |
| 68 | bool CCommunityIcons::(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 | |
| 92 | void CCommunityIcons::(const char *, CImageInfo &Info, CImageInfo &InfoGrayscale, const SHA256_DIGEST &Sha256) |
| 93 | { |
| 94 | CCommunityIcon ; |
| 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 | |
| 117 | void CCommunityIcons::(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 | |
| 131 | void CCommunityIcons::() |
| 132 | { |
| 133 | m_vCommunityIcons.clear(); |
| 134 | Storage()->ListDirectory(Type: IStorage::TYPE_ALL, pPath: "communityicons" , pfnCallback: FileScan, pUser: this); |
| 135 | } |
| 136 | |
| 137 | void CCommunityIcons::() |
| 138 | { |
| 139 | m_CommunityIconLoadJobs.clear(); |
| 140 | m_CommunityIconDownloadJobs.clear(); |
| 141 | } |
| 142 | |
| 143 | void CCommunityIcons::() |
| 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 & : 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 | |