Add credits UI

Lists all GitHub contributors and moderators, alongside the original credits (which were moved from the intro text to here)

The UI itself is controlled with credits.json. This can be regenerated with resources/gencredits.py.
This commit is contained in:
jacob1 2024-10-16 23:24:42 -04:00
parent 9fa0fc45bc
commit 57593fb212
No known key found for this signature in database
GPG Key ID: 4E58A32D510E1995
15 changed files with 412 additions and 25 deletions

1
.gitignore vendored
View File

@ -74,6 +74,7 @@ screenshot_*
/.kdev4
# Other IDEs / misc
/.idea
.vscode/
.vs/
*.sublime-*

1
resources/credits.json Normal file
View File

@ -0,0 +1 @@
{"GitHub": ["simtr", "jacob1", "LBPHacker", "jacksonmj", "Pilihp64", "mniip", "savask", "NoH", "triclops200", "SuperDoxin", "zc00gii", "krawthekrow", "wolfy1339", "QuanTech0", "catsoften", "Catelite", "antb", "moonheart08", "cracker1000", "Huulivoide", "ntoskrnl11", "me4502", "suve", "mmbob", "C7C8", "boxmein", "ief015", "jbot-42", "tridiaq", "perssphere07", "iczero", "SebastianMestre", "nixls", "yareky", "CapacitorSet", "ssccsscc", "JustinShurie", "BlueSyncLine", "Mailaender", "kroq-gar78", "nunom27", "amaank404", "nucular", "jombo23", "gamax92", "Ristovski", "Novocain1", "VelocityRa", "orbitcowboy", "TropicalBastos", "Bowserinator", "n1kolasM", "NoComplaintsEver", "Not-Super-Nova", "Onestay42", "RCAProduction", "SilentSpud", "Rebmiami", "RobertBScott", "ageofadz", "Vgr255", "jebbyk", "avevad", "cppxor2arr", "dxgldotorg", "grufkork", "rfht", "china-richway2", "yangbowen", "dreness", "andrewrk", "super7ramp", "Caeleron", "CanGonenc", "ChromicQuanta", "connor-create", "Departing", "AMDmi3", "EchoHowardLam", "BigWolfy1339", "Jakav-N", "JasonS05", "meyer9", "um3k", "Ksawi999", "Maticzpl", "Mrprocom", "handicraftsman"], "OrigCredits": [{"realname": "Stanislaw K Skowronek", "message": "Designed the original Powder Toy"}, {"realname": "Simon Robertshaw", "message": "Wrote the website, current server owner"}, {"realname": "Skresanov Savely", "message": ""}, {"realname": "Pilihp64", "message": ""}, {"realname": "Catelite", "message": ""}, {"realname": "Victoria Hoyle", "message": ""}, {"realname": "Nathan Cousins", "message": ""}, {"realname": "jacksonmj", "message": ""}, {"realname": "Felix Wallin", "message": ""}, {"realname": "Lieuwe Mosch", "message": ""}, {"realname": "Anthony Boot", "message": ""}, {"realname": "Me4502", "message": ""}, {"realname": "MaksProg", "message": ""}, {"realname": "jacob1", "message": ""}, {"realname": "mniip", "message": ""}, {"realname": "LBPHacker", "message": ""}], "Moderators": [{"username": "jacob1", "role": "Moderator"}, {"username": "LBPHacker", "role": "Moderator"}, {"username": "Sylvi", "role": "Moderator"}, {"username": "CCl2F2", "role": "Moderator"}, {"username": "catsoften", "role": "Moderator"}, {"username": "Denderth", "role": "Moderator"}, {"username": "Simon", "role": "Moderator"}, {"username": "Mrprocom", "role": "Moderator"}, {"username": "jacksonmj", "role": "Former Staff"}, {"username": "Pilihp64", "role": "Former Staff"}, {"username": "Catelite", "role": "Former Staff"}, {"username": "boxmein", "role": "Former Staff"}, {"username": "lolzy", "role": "Former Staff"}, {"username": "Xenocide", "role": "Former Staff"}, {"username": "triclops200", "role": "Former Staff"}, {"username": "devast8a", "role": "Former Staff"}, {"username": "HK6", "role": "Former Staff"}, {"username": "FrankBro", "role": "Former Staff"}, {"username": "doxin", "role": "Former Staff"}, {"username": "ief015", "role": "Former Staff"}, {"username": "ad", "role": "Former Staff"}]}

101
resources/gencredits.py Normal file
View File

@ -0,0 +1,101 @@
import urllib.error
import urllib.request
import json
def get_url(url : str) -> bytes | None:
try:
req = urllib.request.Request(url)
data = urllib.request.urlopen(req, timeout=10)
page = data.read()
return page
except urllib.error.URLError as e:
print(f"{url} - {e}")
return None
def fetch_gh_contributors() -> list[any]:
page = 1
ret = []
while True:
data = get_url(f"https://api.github.com/repos/The-Powder-Toy/The-Powder-Toy/contributors?page={page}")
contributors = json.loads(data)
if not len(contributors):
break
ret.extend(contributors)
page = page + 1
return ret
def get_github_json(github_contributors : list[any]) -> list[str]:
ret = []
for contributor in github_contributors:
ret.append(contributor["login"])
return ret
def get_orig_json() -> list[dict[str, str | int]]:
"""Credits that appeared in intro text in older versions"""
return [
{ "realname" : "Stanislaw K Skowronek", "message" : "Designed the original Powder Toy"},
{ "realname" : "Simon Robertshaw", "message" : "Wrote the website, current server owner"},
{ "realname" : "Skresanov Savely", "message" : ""},
{ "realname" : "Pilihp64", "message" : ""},
{ "realname" : "Catelite", "message" : ""},
{ "realname" : "Victoria Hoyle", "message" : ""},
{ "realname" : "Nathan Cousins", "message" : ""},
{ "realname" : "jacksonmj", "message" : ""},
{ "realname" : "Felix Wallin", "message" : ""},
{ "realname" : "Lieuwe Mosch", "message" : ""},
{ "realname" : "Anthony Boot", "message" : ""},
{ "realname" : "Me4502", "message" : ""},
{ "realname" : "MaksProg", "message" : ""},
{ "realname" : "jacob1", "message" : ""},
{ "realname" : "mniip", "message" : ""},
{ "realname" : "LBPHacker", "message" : ""},
]
def get_moderator_json() -> list[dict[str, str | int]]:
"""Current and former moderators"""
return [
{ "username" : "jacob1", "role" : "Moderator" },
{ "username" : "LBPHacker", "role" : "Moderator" },
{ "username" : "Sylvi", "role" : "Moderator" },
{ "username" : "CCl2F2", "role" : "Moderator" },
{ "username" : "catsoften", "role" : "Moderator" },
{ "username" : "Denderth", "role" : "Moderator" },
{ "username" : "Simon", "role" : "Moderator" },
{ "username" : "Mrprocom", "role" : "Moderator" },
{ "username" : "jacksonmj", "role" : "Former Staff" },
{ "username" : "Pilihp64", "role" : "Former Staff" },
{ "username" : "Catelite", "role" : "Former Staff" },
{ "username" : "boxmein", "role" : "Former Staff" },
{ "username" : "lolzy", "role" : "Former Staff" },
{ "username" : "Xenocide", "role" : "Former Staff" },
{ "username" : "triclops200", "role" : "Former Staff" },
{ "username" : "devast8a", "role" : "Former Staff" },
{ "username" : "HK6", "role" : "Former Staff" },
{ "username" : "FrankBro", "role" : "Former Staff" },
{ "username" : "doxin", "role" : "Former Staff" },
{ "username" : "ief015", "role" : "Former Staff" },
{ "username" : "ad", "role" : "Former Staff" },
]
def process() -> any:
github_contributors = fetch_gh_contributors()
github = get_github_json(github_contributors)
orig = get_orig_json()
mods = get_moderator_json()
data = {
"GitHub" : github,
"OrigCredits" : orig,
"Moderators" : mods,
}
with open("credits.json", "w") as f:
json.dump(data, f)
process()

View File

@ -118,3 +118,4 @@ endif
data_files += to_array.process('save_local.png', extra_args: 'save_local_png')
data_files += to_array.process('save_online.png', extra_args: 'save_online_png')
data_files += to_array.process('font.bz2', extra_args: 'compressed_font_data')
data_files += to_array.process('credits.json', extra_args: 'credits_json')

217
src/gui/credits/Credits.cpp Normal file
View File

@ -0,0 +1,217 @@
#include "Credits.h"
#include <json/json.h>
#include "credits.json.h"
#include "gui/Style.h"
#include "common/platform/Platform.h"
#include "gui/interface/AvatarButton.h"
#include "gui/interface/Button.h"
#include "gui/interface/Engine.h"
#include "gui/interface/Label.h"
#include "gui/interface/RichLabel.h"
#include "gui/interface/ScrollPanel.h"
#include "gui/interface/Separator.h"
Credits::Credits():
ui::Window(ui::Point(-1, -1), ui::Point(WINDOWW, WINDOWH))
{
Json::Value root;
Json::Reader reader;
auto credits = credits_json.AsCharSpan();
if (bool parsed = reader.parse(credits.data(), credits.data() + credits.size(), root, false); !parsed) {
// Failure. Shouldn't ever happen.
return;
}
auto *scrollPanel = new ui::ScrollPanel(ui::Point(0, 0), ui::Point(Size.X, Size.Y - 12));
AddComponent(scrollPanel);
int xPos = 0, yPos = 0, row = 0;
int nextY = 0;
// Organize blocks of components of equal width into rows, and add them to the scroll panel
auto organizeComponents = [&xPos, &yPos, &nextY, &row, &scrollPanel](const auto& components, const int panelWidth) {
ui::Point blockSize = { 0, 0 };
for (const auto &component : components)
{
blockSize.X = std::max(blockSize.X, component->Position.X + component->Size.X);
blockSize.Y = std::max(blockSize.Y, component->Position.Y + component->Size.Y);
}
// New row, offset x position to ensure entire row is centered
if (xPos == 0)
xPos = (panelWidth % blockSize.X) / 2;
for (const auto &component : components)
component->Position += ui::Point({ xPos, yPos });
xPos += blockSize.X;
nextY = std::max(nextY, yPos + blockSize.Y);
if (xPos + blockSize.X > panelWidth)
{
xPos = 0;
yPos = nextY + 8;
row++;
}
for (const auto &component : components)
scrollPanel->AddChild(component);
};
// Add header and separator for each section of credits
auto addHeader = [&xPos, &yPos, &nextY, &row, &scrollPanel](const String &text, const bool addSeparator = true) {
xPos = 0;
yPos = nextY + 10;
row = 0;
if (addSeparator)
{
auto *separator = new ui::Separator(ui::Point(0, yPos), ui::Point(scrollPanel->Size.X, 1));
scrollPanel->AddChild(separator);
yPos += 6;
}
auto *label = new ui::RichLabel(ui::Point(4, yPos), ui::Point(scrollPanel->Size.X, 24), text);
label->SetTextColour(style::Colour::InformationTitle);
label->Appearance.HorizontalAlign = ui::Appearance::AlignCentre;
label->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
scrollPanel->AddChild(label);
yPos += label->Size.Y + 8;
};
addHeader("TPT is an open source project, developed by members of the community.\n"
"We'd like to thank everyone who contributed to our \bt{a:https://github.com/The-Powder-Toy/The-Powder-Toy|GitHub repo}\x0E:", false);
auto GitHub = root["GitHub"];
int grayscale = 255;
for (auto &item : GitHub)
{
ByteString username = item.asString();
auto components = AddCredit(username.FromUtf8(), "", Small, "", false, grayscale);
organizeComponents(components, scrollPanel->Size.X);
if (grayscale > 180)
grayscale--;
}
addHeader("Staff - volunteers that run the community and keep the site running");
auto Moderators = root["Moderators"];
for (auto &item : Moderators)
{
ByteString username = item["username"].asString();
ByteString role = item["role"].asString();
if (role == "Moderator" || role == "HalfMod")
{
auto components = AddCredit(username.FromUtf8(), "", Large, GetProfileUri(username), true);
organizeComponents(components, scrollPanel->Size.X);
}
}
addHeader("Former Staff", false);
for (auto &item : Moderators)
{
ByteString username = item["username"].asString();
ByteString role = item["role"].asString();
if (role == "Former Staff")
{
auto components = AddCredit(username.FromUtf8(), "", Small, "", true);
organizeComponents(components, scrollPanel->Size.X);
}
}
addHeader("The following users have been credited in the intro text from the start.\n"
"Their contributions to the early beginnings of TPT were invaluable in shaping TPT into what it is today.");
auto OrigCredits = root["OrigCredits"];
for (auto &item : OrigCredits)
{
ByteString realname = item["realname"].asString();
ByteString message = item["message"].asString();
auto components = AddCredit(realname.FromUtf8(), message.FromUtf8(), row == 0 ? Half : Small, "");
organizeComponents(components, scrollPanel->Size.X);
}
scrollPanel->InnerSize = ui::Point(scrollPanel->Size.X, nextY);
auto *closeButton = new ui::Button({ 0, Size.Y - 12 }, { Size.X, 12 }, "Close");
closeButton->SetActionCallback({
[this] {
CloseActiveWindow();
} });
AddComponent(closeButton);
}
std::vector<ui::Component *> Credits::AddCredit(const String &name, const String &subheader, const CreditSize size,
const ByteString &uri, const bool includeAvatar, const int grayscale)
{
std::vector<ui::Component *> components;
int creditBlockWidth = size == Small ? 100 : (size == Large ? 155 : 310);
int y = 0;
if (includeAvatar)
{
int avatarWidth = size == Small ? 40 : 64;
int avatarSize = size == Small ? 40 : 256;
auto *avatarButton = new ui::AvatarButton(ui::Point((creditBlockWidth - avatarWidth) / 2, 0), ui::Point(avatarWidth, avatarWidth), name.ToUtf8(), avatarSize);
if (!uri.empty())
{
avatarButton->SetActionCallback({[uri] {
Platform::OpenURI(uri);
} });
}
components.push_back(avatarButton);
y += avatarButton->Size.Y + 2;
}
if (!name.empty())
{
auto labelText = !uri.empty() ? GetRichLabelText(uri, name) : name;
auto *nameLabel = new ui::RichLabel(ui::Point(0, y), ui::Point(creditBlockWidth, 14), labelText);
nameLabel->Appearance.HorizontalAlign = ui::Appearance::AlignCentre;
nameLabel->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
nameLabel->SetTextColour(ui::Colour(grayscale, grayscale, grayscale));
components.push_back(nameLabel);
y += nameLabel->Size.Y + 2;
}
if (!subheader.empty())
{
auto *subheaderLabel = new ui::Label(ui::Point(0, y), ui::Point(creditBlockWidth, 14), subheader);
subheaderLabel->Appearance.HorizontalAlign = ui::Appearance::AlignCentre;
subheaderLabel->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
int col = (int)((float)grayscale * .67f);
subheaderLabel->SetTextColour(ui::Colour(col, col, col));
components.push_back(subheaderLabel);
}
return components;
}
ByteString Credits::GetProfileUri(const ByteString &username)
{
return "https://powdertoy.co.uk/User.html?Name=" + username;
}
String Credits::GetRichLabelText(const ByteString &uri, const String &message)
{
StringBuilder builder;
builder << "{a:" << uri.FromUtf8() << "|" << message << "}";
return builder.Build();
}
void Credits::OnTryExit(ExitMethod method)
{
ui::Engine::Ref().CloseWindow();
}

23
src/gui/credits/Credits.h Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include <vector>
#include "gui/interface/Window.h"
class Credits : public ui::Window
{
enum CreditSize
{
Small,
Large,
Half,
};
static std::vector<ui::Component *> AddCredit(const String &name, const String &subheader, CreditSize size,
const ByteString &uri, bool includeAvatar = false, int grayscale = 255);
static ByteString GetProfileUri(const ByteString &username);
static ByteString GetTptLabelText(const ByteString &tpt, const ByteString &github);
static String GetRichLabelText(const ByteString &uri, const String &message);
public:
Credits();
void OnTryExit(ExitMethod method) override;
};

View File

@ -0,0 +1,3 @@
powder_files += files(
'Credits.cpp',
)

View File

@ -64,10 +64,6 @@ inline ByteString IntroText()
"Use 'S' to save parts of the window as 'stamps'. 'L' loads the most recent stamp, 'K' shows a library of stamps you saved.\n"
"Use 'P' to take a screenshot and save it into the current directory.\n"
"Use 'H' to toggle the HUD. Use 'D' to toggle debug mode in the HUD.\n"
"\n"
"Contributors: \bgStanislaw K Skowronek (Designed the original Powder Toy),\n"
"\bgSimon Robertshaw, Skresanov Savely, Pilihp64, Catelite, Victoria Hoyle, Nathan Cousins, jacksonmj,\n"
"\bgFelix Wallin, Lieuwe Mosch, Anthony Boot, Me4502, MaksProg, jacob1, mniip, LBPHacker\n"
"\n";
if constexpr (BETA)
{

View File

@ -10,9 +10,10 @@
namespace ui {
AvatarButton::AvatarButton(Point position, Point size, ByteString username):
AvatarButton::AvatarButton(Point position, Point size, ByteString username, int avatarSize):
Component(position, size),
name(username),
avatarSize(avatarSize),
tried(false)
{
@ -23,7 +24,14 @@ void AvatarButton::Tick(float dt)
if (!avatar && !tried && name.size() > 0)
{
tried = true;
imageRequest = std::make_unique<http::ImageRequest>(ByteString::Build(STATICSERVER, "/avatars/", name, ".png"), Size);
ByteStringBuilder urlBuilder;
urlBuilder << STATICSERVER << "/avatars/" << name;
if (avatarSize)
{
urlBuilder << "." << avatarSize;
}
urlBuilder << ".png";
imageRequest = std::make_unique<http::ImageRequest>(urlBuilder.Build(), Size);
imageRequest->Start();
}
@ -45,7 +53,7 @@ void AvatarButton::Draw(const Point& screenPos)
{
Graphics * g = GetGraphics();
if(avatar)
if (avatar)
{
auto *tex = avatar.get();
g->BlendImage(tex->Data(), 255, RectSized(screenPos, tex->Size()));

View File

@ -3,7 +3,6 @@
#include "Component.h"
#include "graphics/Graphics.h"
#include "gui/interface/Colour.h"
#include "client/http/ImageRequest.h"
#include <memory>
@ -15,6 +14,7 @@ class AvatarButton : public Component
{
std::unique_ptr<VideoBuffer> avatar;
ByteString name;
int avatarSize;
bool tried;
struct AvatarButtonAction
@ -26,7 +26,7 @@ class AvatarButton : public Component
std::unique_ptr<http::ImageRequest> imageRequest;
public:
AvatarButton(Point position, Point size, ByteString username);
AvatarButton(Point position, Point size, ByteString username, int avatarSize = 0);
virtual ~AvatarButton() = default;
void OnMouseClick(int x, int y, unsigned int button) override;
@ -46,6 +46,6 @@ public:
ByteString GetUsername() { return name; }
inline void SetActionCallback(AvatarButtonAction const &action) { actionCallback = action; };
protected:
bool isMouseInside, isButtonDown;
bool isMouseInside = false, isButtonDown = false;
};
}

View File

@ -0,0 +1,12 @@
#include "Separator.h"
#include "graphics/Graphics.h"
namespace ui
{
void Separator::Draw(const ui::Point& screenPos)
{
GetGraphics()->BlendRect(RectSized(screenPos, Size), 0xFFFFFF_rgb .WithAlpha(180));
}
}

View File

@ -0,0 +1,16 @@
#pragma once
#include "Component.h"
namespace ui
{
class Separator : public Component
{
public:
Separator(ui::Point position, ui::Point size) : Component(position, size)
{ }
void Draw(const ui::Point& screenPos) override;
};
}

View File

@ -23,4 +23,5 @@ powder_files += files(
'AvatarButton.cpp',
'RichLabel.cpp',
'SaveButton.cpp',
'Separator.cpp',
)

View File

@ -4,6 +4,7 @@ gui_files = files(
subdir('colourpicker')
subdir('console')
subdir('credits')
subdir('dialogues')
subdir('elementsearch')
subdir('filebrowser')

View File

@ -10,6 +10,7 @@
#include "simulation/ElementDefs.h"
#include "simulation/SimulationSettings.h"
#include "client/Client.h"
#include "gui/credits/Credits.h"
#include "gui/dialogues/ConfirmPrompt.h"
#include "gui/dialogues/InformationMessage.h"
#include "gui/interface/Button.h"
@ -17,6 +18,7 @@
#include "gui/interface/DropDown.h"
#include "gui/interface/Engine.h"
#include "gui/interface/Label.h"
#include "gui/interface/Separator.h"
#include "gui/interface/Textbox.h"
#include "gui/interface/DirectionSelector.h"
#include "PowderToySDL.h"
@ -41,19 +43,7 @@ OptionsView::OptionsView() : ui::Window(ui::Point(-1, -1), ui::Point(320, 340))
AddComponent(label);
}
class Separator : public ui::Component
{
public:
Separator(ui::Point position, ui::Point size) : Component(position, size){}
virtual ~Separator(){}
void Draw(const ui::Point& screenPos) override
{
GetGraphics()->BlendRect(RectSized(screenPos, Size), 0xFFFFFF_rgb .WithAlpha(180));
}
};
Separator *tmpSeparator = new Separator(ui::Point(0, 22), ui::Point(Size.X, 1));
auto *tmpSeparator = new ui::Separator(ui::Point(0, 22), ui::Point(Size.X, 1));
AddComponent(tmpSeparator);
scrollPanel = new ui::ScrollPanel(ui::Point(1, 23), ui::Point(Size.X-2, Size.Y-39));
@ -103,7 +93,7 @@ OptionsView::OptionsView() : ui::Window(ui::Point(-1, -1), ui::Point(320, 340))
};
auto addSeparator = [this, &currentY]() {
currentY += 6;
auto *separator = new Separator(ui::Point(0, currentY), ui::Point(Size.X, 1));
auto *separator = new ui::Separator(ui::Point(0, currentY), ui::Point(Size.X, 1));
scrollPanel->AddChild(separator);
currentY += 11;
};
@ -180,7 +170,7 @@ OptionsView::OptionsView() : ui::Window(ui::Point(-1, -1), ui::Point(320, 340))
tempLabel->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
AddComponent(tempLabel);
Separator * tempSeparator = new Separator(ui::Point(0, 22), ui::Point(Size.X, 1));
auto * tempSeparator = new ui::Separator(ui::Point(0, 22), ui::Point(Size.X, 1));
AddComponent(tempSeparator);
labelValues = new ui::Label(ui::Point(0, (radius * 5 / 2) + 37), ui::Point(Size.X, 16), String::Build(Format::Precision(1), "X:", x, " Y:", y, " Total:", std::hypot(x, y)));
@ -364,6 +354,22 @@ OptionsView::OptionsView() : ui::Window(ui::Point(-1, -1), ui::Point(320, 340))
}
currentY += 26;
}
{
addSeparator();
auto *creditsButton = new ui::Button(ui::Point(10, currentY), ui::Point(90, 16), "Credits");
creditsButton->SetActionCallback({ [this] {
auto *credits = new Credits();
ui::Engine::Ref().ShowWindow(credits);
} });
scrollPanel->AddChild(creditsButton);
addLabel(5, " - Find out who contributed to TPT");
currentY += 13;
}
{
ui::Button *ok = new ui::Button(ui::Point(0, Size.Y-16), ui::Point(Size.X, 16), "OK");
ok->SetActionCallback({ [this] {