見出し画像

コースIDの仕組み [マリメ2 - #1]

みなさん、こんにちは。Heigrot 99です。 今日は、コース(職人)IDの仕組みを解説します。


0.この記事を読む時の前提知識・能力

・(数学)N進法の知識
・やや高度な読解力

1.そもそもコース(職人)IDに規則はあるのか

1.1 コース(職人)IDとは

ゲーム内の画面より

コース(職人)IDとは、マリオメーカー2のそれぞれのコースに付けられている"サーバーのデータの読み取り用のID"です。マリオメーカー2では、必ず9桁(3桁ごとにハイフンで区切り)です。

このIDを使ってゲーム内でコースを調べることができます。同じIDを持つコースの組みは存在しておらず、一度サーバーから削除されたコースのIDが再び使われることはありません。

サーバーはこのIDをもとにある"サーバー管理番号"を生成して、その番号の位置にあるデータを読み取ります。

この番号は、新しいコース(職人)が投稿|登録されると、世界中のコース(職人)でまだ使われていない番号のもののうち最小のものが付けられます。
したがって、この"番号"はそのコース(職人)がおよそ何番目に投稿|登録されたかを知ることができます。

1.2 IDの番号が職人かコースか

SMM2 Level viewerより

現存する(2024年8月現在)最もIDが示すコースの番号は3000004(ID:4RF-XV8-WCG)です。実は、1番またはそれに近い番号ではありません。

おそらく、1 ~ 3000000までが職人IDで、それ以降はコースIDであり、サーバー管理番号が職人IDとコースIDで重複することはない仕様…. かと思いきや、自分の職人ID(GLC-FNJ-9LF)の番号を後述する方法で調べたところ、7095334でした。

これらのことから、おそらくマリメ2の初代versionでは、1 ~ 3000000までが職人IDで、それ以降はコースID….と管理していたが、職人が増えすぎて仕様変更されて、現在の仕様ではサーバー管理番号はコースと職人で両方あるとの見方が有力です。(確信できる根拠はありませんが…)

実は、IDの一部に、コースか職人かを識別する情報が埋め込まれています。(詳細は後ほど)

1.3 コースIDの仕組みを複雑にする必要性

では、どうしてコースIDを複雑にする必要があるのでしょうか。単純にコースIDを"そのコースが世界に何番目に投稿されたか"でも良い気がします。

例: 世界で10,000,000番目に投稿されたコースのID:010-000-000

理由としては、以下のことが考えられます:
・キリのいい番号に注目が集まってしまい、他のコース(職人)にとって不公平
・コースのIDの生成方法が解明されてしまうと、不正ログイン等に悪用されてしまう
・IDによってユーザーに関係する情報が漏洩しかねない(注1)

自分の考察や調査の範囲では、思いつく理由がこのくらいです。実際、他のサービスでのこうしたIDは、普通は単純でない方法で生成されています。

結論:

コースIDは、サーバーが何番目のデータかを把握するために用いられています。したがって、コースIDから生成される番号は一定のルールにしたがっている必要があります。つまり、規則性はあります。

2. コースIDの仕組み

2.1 概念

この章では、第1章で述べたコースIDが示す"番号"の導出をします。
万一第1章を読み飛ばした場合は、読むことを強く推奨します。

2.2 導出手続き

1.まず、コースIDのもとに次の手続きで得られる"9つの数字"を考えます:
※この9つの数字の順番は重要なのでごっちゃにしないで下さい。

0123456789BCDFGHJKLMNPQRSTVWXY

1つ目の数字:コースIDの1文字目が上の文字列で左から何番目になるか。
例えば、ID(GLC-FNJ-9LF)なら、1つ目の数字は15です。

同様に、
2つ目の数字:コースIDの2文字目が上の文字列で左から何番目になるか。
3つ目の数字:コースIDの3文字目が上の文字列で左から何番目になるか。
とします。


2.先ほど求めた9つの数字をもとに、次の値を計算して、2進数に直す。

(1つ目の数字)+(2つ目の数字)✖️30^1+(3つ目の数字)✖️30^2・・・
(9つ目の数字)✖️30^8


例えば、ID(GLC-FNJ-9LF)なら、上記の値は8929926361454で、
これを2進数に直すと
10000001111100101001000101101011000101101110
になります。

2進数に直した時、44桁であることを確認してください。44桁になっていない場合、計算ミスをしています。


3. 先ほどの44桁の数字を
4桁、6桁、20桁、1桁、1桁、12桁に分離します。
(説明の都合上、それぞれの分離後の数字を①,②,③,④,⑤,⑥とします。)

例えば、ID(GLC-FNJ-9LF)なら
① = 1000
② = 000111
③ = 11001010010001011010
④ = 1
⑤ = 1
⑥ = 000101101110
です。

この時、①  = 1000, ⑤ = 1を確認してください。

そうなっていない場合、そのIDは無効です。

また、④がコースか職人IDの判別です。
④ = 0ならコース,④ = 1なら職人です。


4.先ほど生成した③と⑥を⑥,③の順で連結して、32桁の2進数を生成します。

そして、(10110100000001110000001111100(2進数表記)とその32桁の数値)の2つの値を使って、次の手続きで"サーバー管理番号"の2進数表記の数字を得ます。

2つの数のそれぞれの桁について、ちょうど一方が1の場合、"番号"のその桁は1,そうでない場合は0


例えば、1100と1010にこの手続きを適用した場合、
1    1    0    0
1    0    1    0
-------------
0   1     1    0 (番号)

ID(GLC-FNJ-9LF)なら000000000000000000000011011000100010000100110となります。
10進数表記なら7095334です。

そして、その番号を10進数表記に変えたものが、コースIDに隠された"番号"です。


5.まだ使われていない②はダミーというわけではなく、計算ミスがないかどうかのチェック用です。
(先ほどの番号)-31を64で割ったあまりが②の10進数表記に一致すればOKです。
ID(GLC-FNJ-9LF)なら、
(7095334-31) ÷ 64 = 110864 あまり 7
で、確かに②と一致しています。

2.3 導出手続きをやるプログラム

ここに先ほどの手続きをすべてやるC++のプログラムを書いておきます。

https://www.onlinegdb.com/online_c++_debuggerでプログラムを実行して、出力画面の指示に従えば結果が出ます。


#include "bits/stdc++.h"
using namespace std;
int main(){
cout << "コースIDを入力して下さい(9桁,ハイフン無し)" << endl;
string s; cin >> s;
const string t = "0123456789bcdfghjklmnpqrstvwxy";
if (s.size() != 9){
cout << "Error" << endl;
cout << "IDは9桁(ハイフン無し)で入力してください";
return 0;
}
for (int i = 0; i < 9; i++){
if (s[i] >= 'A' && s[i] <= 'Z') s[i] += ('a'-'A');
}
long long val = 0;
long long mul = 1;
for (int i = 0; i < 9; i++){
for (int j = 0; j < 30; j++){
if (s[i] == t[j]) val += mul*j;
}
mul *= 30;
}

vector<long long> p (6);
p[0] = val/(1ll << 40);
val -= p[0]*(1ll << 40);
if (p[0] != 8){
cout << "Error" << endl;
cout << "IDが無効です";
return 0;
}
p[1] = val/(1ll << 34);
val -= p[1]*(1ll << 34);
p[2] = val/(1ll << 14);
val -= p[2]*(1ll << 14);
p[3] = val/(1ll << 13);
val -= p[3]*(1ll << 13);
p[4] = val/(1ll << 12);
val -= p[4]*(1ll << 12);
if (p[4] != 1){
cout << "Error" << endl;
cout << "IDが無効です";
return 0;
}
p[5] = val;
long long id = p[2]+p[5]*(1ll << 20);
id ^= 377544828ll;
if ((id-31)%64 != p[1]){
cout << "Error" << endl;
cout << "エラーが発生しました。やり直してください。";
return 0;
}
if (p[3] == 0){
cout << "このコースのサーバー管理番号は" << id << "です"; 
} else {
cout << "この職人のサーバー管理番号は" << id << "です"; 
}
}

2.4  サーバーでの番号からIDを求める

先ほどの手順を逆に辿ればいいので、簡単ですが、一つ注意があります。
それは、第1章で述べたように、現在の仕様ではサーバー管理番号はコースと職人で両方ある点です。

例えば、サーバー管理番号7095334では、職人ならGLC-FNJ-9LFですが、コースならDH2-FNJ-9LFです。

これまたC++のコードを載せておきます。このコードはユーザーの場合と職人の場合両方を出力します。

#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const string t = "0123456789BCDFGHJKLMNPQRSTVWXY";
string f (ll val){
string ans = "";
for (int i = 0; i < 9; i++){
ans.push_back(t[val % 30]);
val /= 30;
if (i == 2 || i == 5) ans.push_back('-');
}
return ans;
}
int main(){
cout << "調べたい番号を入力して下さい" << endl;
long long x; cin >> x;
if (x <= 0){
cout << "Error"<< endl;
cout << "番号が無効です" << endl;
return 0;
}
vector <ll> p (6);
p[1] = (x-31)%64;
x ^= 377544828ll;
p[5] = x/(1ll << 20);
x -= p[5]*(1ll << 20);
p[2] = x;
p[0] = 8; p[4] = 1;
p[3] = 1;//職人の場合
ll val = p[0]*(1ll << 40)+p[1]*(1ll << 34)+p[2]*(1ll << 14)+p[3]*(1ll << 13)+p[4]*(1ll << 12)+p[5];
cout << "職人IDの場合: " << f(val) << endl;
cout << "コースIDの場合: " << f(val - (1ll << 13)) << endl;
}

3.終わりに

今回の記事、いかがでしょうか。 

実はコースIDにはこのような秘密が隠されていました。

面白いと思っていただけたら、ぜひ、いいねをお願いします!

最後まで読んでくださってありがとうございます!

引用注:

(※全般)

(注1) ChatGPT へ以下の質問をしてその回答の一部を参考にしました
"Why does SNS's account ID are usually generates with complex means?"
https://chatgpt.com/

前回・次回の記事

[前回の記事(#0)] 

[次回の記事(#2)] : 未公開



いいなと思ったら応援しよう!