Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions buildconfig/BuildConfig.cpp.in
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Config::Config()
MESHMC_CONFIGFILE = "@MeshMC_ConfigFile@";
MESHMC_GIT = "@MeshMC_Git@";
MESHMC_APPID = "@MeshMC_AppID@";
MESHMC_ENVNAME = "@MeshMC_EnvName@";

USER_AGENT = "@MeshMC_UserAgent@";
USER_AGENT_UNCACHED = USER_AGENT + " (Uncached)";
Expand Down
5 changes: 5 additions & 0 deletions buildconfig/BuildConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ class Config
QString MESHMC_GIT;
QString MESHMC_APPID;

/** Prefix for the launcher's own environment variables, e.g. "MESHMC"
* for MESHMC_DISABLE_GLVULKAN. Configurable rather than hardcoded so a
* rebranded build's variables carry its own name. */
QString MESHMC_ENVNAME;

/// The major version number.
int VERSION_MAJOR;
/// The minor version number.
Expand Down
26 changes: 23 additions & 3 deletions launcher/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,14 @@ set(MINECRAFT_SOURCES
minecraft/services/SkinDelete.cpp
minecraft/services/SkinDelete.h

# Local skin library
minecraft/skins/SkinEntry.cpp
minecraft/skins/SkinEntry.h
minecraft/skins/SkinLibrary.cpp
minecraft/skins/SkinLibrary.h
minecraft/skins/ProfileSkinImport.cpp
minecraft/skins/ProfileSkinImport.h

mojang/PackageManifest.h
mojang/PackageManifest.cpp
)
Expand Down Expand Up @@ -776,6 +784,7 @@ SET(MESHMC_SOURCES
resources/documents/documents.qrc
resources/breeze_dark/breeze_dark.qrc
resources/breeze_light/breeze_light.qrc
resources/shaders/shaders.qrc
${CMAKE_BINARY_DIR}/${MeshMC_AppBinaryName}.qrc

# Icons
Expand Down Expand Up @@ -993,8 +1002,14 @@ SET(MESHMC_SOURCES
ui/dialogs/UpdateAvailableDialog.h
ui/dialogs/VersionSelectDialog.cpp
ui/dialogs/VersionSelectDialog.h
ui/dialogs/SkinUploadDialog.cpp
ui/dialogs/SkinUploadDialog.h
ui/dialogs/skins/SkinManageDialog.cpp
ui/dialogs/skins/SkinManageDialog.h
ui/dialogs/skins/render/CubeMesh.cpp
ui/dialogs/skins/render/CubeMesh.h
ui/dialogs/skins/render/PlayerScene.cpp
ui/dialogs/skins/render/PlayerScene.h
ui/dialogs/skins/render/SkinPreviewSurface.cpp
ui/dialogs/skins/render/SkinPreviewSurface.h
ui/dialogs/DownloadContentDialog.cpp
ui/dialogs/DownloadContentDialog.h
ui/dialogs/DownloadSummaryDialog.cpp
Expand Down Expand Up @@ -1114,7 +1129,7 @@ set(MESHMC_UI_FILES
ui/dialogs/PluginsDialog.ui
ui/dialogs/NewComponentDialog.ui
ui/dialogs/ProfileSelectDialog.ui
ui/dialogs/SkinUploadDialog.ui
ui/dialogs/skins/SkinManageDialog.ui
ui/dialogs/CreateShortcutDialog.ui
ui/dialogs/ExportInstanceDialog.ui
ui/dialogs/ExportPackDialog.ui
Expand Down Expand Up @@ -1176,6 +1191,7 @@ set(MESHMC_QRC_FILES
resources/documents/documents.qrc
resources/breeze_dark/breeze_dark.qrc
resources/breeze_light/breeze_light.qrc
resources/shaders/shaders.qrc
${CMAKE_BINARY_DIR}/${MeshMC_AppBinaryName}.qrc
)

Expand Down Expand Up @@ -1248,6 +1264,10 @@ target_link_libraries(MeshMC_logic
LocalPeer::LocalPeer
)

if(QT_VERSION_MAJOR EQUAL 6)
target_link_libraries(MeshMC_logic Qt6::OpenGL)
endif()

# Plugin system needs dlopen/dlsym on Unix
if(UNIX AND NOT APPLE)
target_link_libraries(MeshMC_logic ${CMAKE_DL_LIBS})
Expand Down
1 change: 1 addition & 0 deletions launcher/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ int main(int argc, char* argv[])
Q_INIT_RESOURCE(backgrounds);
Q_INIT_RESOURCE(documents);
Q_INIT_RESOURCE(meshmc);
Q_INIT_RESOURCE(shaders);

Q_INIT_RESOURCE(pe_dark);
Q_INIT_RESOURCE(pe_light);
Expand Down
4 changes: 2 additions & 2 deletions launcher/minecraft/auth/AccountData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,15 @@ namespace
qWarning() << "cape data is something unexpected";
return MinecraftProfile();
}
out.capes[cape.id] = cape;
out.capes.append(cape);
}
}
// current cape
{
auto capeV = tokenObject.value("cape");
if (capeV.isString()) {
auto currentCape = capeV.toString();
if (out.capes.contains(currentCape)) {
if (out.capeById(currentCape)) {
out.currentCape = currentCape;
}
}
Expand Down
26 changes: 25 additions & 1 deletion launcher/minecraft/auth/AccountData.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#pragma once
#include <QString>
#include <QByteArray>
#include <QList>
#include <QVector>
#include <katabasis/Bits.h>
#include <QJsonObject>
Expand Down Expand Up @@ -51,7 +52,30 @@ struct MinecraftProfile {
QString name;
Skin skin;
QString currentCape;
QMap<QString, Cape> capes;

/* Capes the account owns, in the order the profile service returned
* them.
*
* A list rather than a map keyed by id: the order is meaningful. It is
* the order the cape picker shows them in, and it is the order Mojang
* lists them in -- which a map would silently replace with alphabetical
* order by id, i.e. by opaque UUID, i.e. arbitrary. The stored JSON has
* always been an array, so nothing about the on-disk format changes;
* round-tripping through a list actually preserves it where the map did
* not. */
QList<Cape> capes;

/* Look a cape up by id, or nullptr if the account does not own it. */
const Cape* capeById(const QString& id) const
{
for (const Cape& cape : capes) {
if (cape.id == id) {
return &cape;
}
}
return nullptr;
}

Katabasis::Validity validity = Katabasis::Validity::None;
};

Expand Down
193 changes: 192 additions & 1 deletion launcher/minecraft/auth/Parsers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,9 @@ namespace Parsers
continue;
}

output.capes[capeOut.id] = capeOut;
/* Appended, not keyed: the service's order is the order the cape
* picker shows. */
output.capes.append(capeOut);
}
output.currentCape = currentCape;
output.validity = Katabasis::Validity::Certain;
Expand Down Expand Up @@ -364,4 +366,193 @@ namespace Parsers
return true;
}

namespace
{
/* Skins of the MHF_Steve and MHF_Alex accounts.
*
* The session server omits the SKIN texture entirely for players who
* never set one, so there is no URL to read. These two are the
* canonical default skins, and pointing at them is the only way to
* show such a player as the game shows them. */
const char* const kDefaultSkinUrlClassic =
"https://textures.minecraft.net/texture/"
"1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b";
const char* const kDefaultSkinUrlSlim =
"https://textures.minecraft.net/texture/"
"83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032";

/* Which of the two default skins a player without a custom one gets.
*
* The game decides this from the Java hashCode of the profile UUID:
* even means the classic model, odd means slim. Reproducing it means
* folding the 128-bit id down the same way Java's UUID.hashCode()
* does -- xor the two halves, then xor the halves of that. */
bool defaultsToClassicModel(QString uuid)
{
uuid.remove('-');
if (uuid.size() != 32) {
/* Not a UUID we can read; classic is the safer guess, since
* it is what the game falls back to as well. */
return true;
}

bool okHigh = false;
bool okLow = false;
const quint64 high = uuid.left(16).toULongLong(&okHigh, 16);
const quint64 low = uuid.right(16).toULongLong(&okLow, 16);
if (!okHigh || !okLow) {
return true;
}

const quint64 folded = high ^ low;
const quint32 hashCode = static_cast<quint32>(folded >> 32) ^
static_cast<quint32>(folded);
return hashCode % 2 == 0;
}

/* The session server still hands out plain-http texture URLs. Qt will
* follow them, but there is no reason to fetch a skin in the clear
* when the same host serves it over TLS. */
QString preferHttps(QString textureUrl)
{
return textureUrl.replace(
QLatin1String("http://textures.minecraft.net"),
QLatin1String("https://textures.minecraft.net"));
}

/* Pull the base64 "textures" property out of a session profile. */
QByteArray findTexturePayload(const QJsonArray& properties)
{
for (const QJsonValue& property : properties) {
const QJsonObject entry = property.toObject();
if (entry.value("name").toString() !=
QLatin1String("textures")) {
continue;
}
const QJsonValue value = entry.value("value");
if (!value.isString()) {
continue;
}
const QByteArray decoded = QByteArray::fromBase64(
value.toString().toUtf8(),
QByteArray::AbortOnBase64DecodingErrors);
if (!decoded.isEmpty()) {
return decoded;
}
}
return QByteArray();
}
} // namespace

bool parseMojangSessionProfile(QByteArray& data, MinecraftProfile& output)
{
/* This is the *public* profile endpoint
* (sessionserver.mojang.com/session/minecraft/profile/<id>), not the
* authenticated one parseMinecraftProfile() handles. It describes
* somebody else's account, so it carries no entitlements and no cape
* ids -- only texture URLs, and those are buried in a base64 blob. */
qCDebug(minecraftauthLog) << "Parsing Mojang session profile...";

QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if (jsonError.error) {
qWarning() << "Failed to parse response from sessionserver as "
"JSON:"
<< jsonError.errorString();
return false;
}

QJsonObject obj = doc.object();
if (!getString(obj.value("id"), output.id)) {
qWarning() << "Session profile id is not a string";
return false;
}
if (!getString(obj.value("name"), output.name)) {
qWarning() << "Session profile name is not a string";
return false;
}

const QByteArray texturePayload =
findTexturePayload(obj.value("properties").toArray());
if (texturePayload.isEmpty()) {
qWarning() << "Session profile carries no texture payload";
return false;
}

doc = QJsonDocument::fromJson(texturePayload, &jsonError);
if (jsonError.error) {
qWarning() << "Failed to parse the session texture payload as "
"JSON:"
<< jsonError.errorString();
return false;
}

const QJsonValue textures = doc.object().value("textures");
if (!textures.isObject()) {
qWarning() << "Session texture payload has no textures object";
return false;
}

Skin skinOut;
/* Start from the default this player would be shown with, so that a
* profile with no SKIN entry still yields something displayable. */
const bool classic = defaultsToClassicModel(output.id);
skinOut.variant = classic ? QStringLiteral("CLASSIC")
: QStringLiteral("SLIM");
skinOut.url = classic ? QLatin1String(kDefaultSkinUrlClassic)
: QLatin1String(kDefaultSkinUrlSlim);
/* The endpoint does not expose texture ids at all. Nothing downstream
* of here needs one, so it is left empty rather than invented. */

Cape capeOut;
bool hasCape = false;

const QJsonObject textureObj = textures.toObject();
for (auto it = textureObj.constBegin(); it != textureObj.constEnd();
++it) {
if (!it->isObject()) {
continue;
}
const QJsonObject texture = it->toObject();

if (it.key() == QLatin1String("SKIN")) {
if (!getString(texture.value("url"), skinOut.url)) {
qWarning() << "Session profile skin url is not a string";
return false;
}
skinOut.url = preferHttps(skinOut.url);

/* Present only for slim skins; absent means classic, which
* is already what variant holds unless the UUID said
* otherwise -- so read it, but do not require it. */
const QJsonValue metadata = texture.value("metadata");
if (metadata.isObject()) {
getString(metadata.toObject().value("model"),
skinOut.variant);
}
} else if (it.key() == QLatin1String("CAPE")) {
if (!getString(texture.value("url"), capeOut.url)) {
qWarning() << "Session profile cape url is not a string";
return false;
}
capeOut.url = preferHttps(capeOut.url);
/* No id is published for it either. A stable placeholder is
* enough: this profile is only ever read to copy a look, and
* nothing tries to equip somebody else's cape. */
capeOut.id = QStringLiteral("cape");
capeOut.alias = QStringLiteral("cape");
hasCape = true;
}
}

output.skin = skinOut;
if (hasCape) {
output.capes.clear();
output.capes.append(capeOut);
output.currentCape = capeOut.id;
}
output.validity = Katabasis::Validity::Certain;
return true;
}

} // namespace Parsers
14 changes: 14 additions & 0 deletions launcher/minecraft/auth/Parsers.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ namespace Parsers
bool parseMojangResponse(QByteArray& data, Katabasis::Token& output);

bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output);

/* Parse a *public* profile from
* sessionserver.mojang.com/session/minecraft/profile/<id>.
*
* Unlike parseMinecraftProfile(), which reads the signed-in account's own
* profile, this describes an arbitrary player: only id, name and texture
* URLs come back, and the textures arrive as a base64 blob. Used to copy
* another player's skin into the local library.
*
* Fills output.skin (with the default Steve/Alex texture when the player
* never set one) and, if the player has a cape, a single cape entry under
* a placeholder id -- the endpoint publishes no cape ids. */
bool parseMojangSessionProfile(QByteArray& data, MinecraftProfile& output);

bool parseMinecraftEntitlements(QByteArray& data,
MinecraftEntitlement& output);
bool parseRolloutResponse(QByteArray& data, bool& result);
Expand Down
Loading
Loading