Gift history
Please access after login.


Wait a moment...

異世界転生したら法定通貨がXYMだった件 速習Symbol参考箇所と実装例 in トマティーナ2022

460
0
2022-09-05 18:02:34
300000 mXYM
(4)


2022年8月31日に開催されたトマティーナイベントに合わせて「異世界転生したら法定通貨がXYMだった件」(RPGツクールMZにて開発)を期間限定仕様でアップデートしました。
アップデートに含まれるSymbolを利用した仕組みの中で速習Symbolの内容を参考にした部分について記します。

途中からコードがずっと続くため序盤に反省点まで書いてしまいます。
コードてんこ盛りでコード内にコメントを入れて補足しているため、全部読みきれないという場合は各コード前の速習Symbolのどの部分を参考にしたという部分だけでも目を通していただけると幸いです。

参考にした部分を先に列挙しておきます。
3. アカウント
3.1 アカウント生成
        秘密鍵からアカウント生成
        公開鍵クラスの生成
        アドレスクラスの生成
    3.2 アカウントへの送信
    3.3 アカウント情報の確認
4. トランザクション
    4.2 トランザクション作成
    4.3 署名とアナウンス
    4.6 アグリゲートトランザクション
5. モザイク
    5.1 モザイク生成
6. ネームスペース
    6.2 レンタル
    6.3 リンク
12. オフライン署名
    12.1 トランザクション作成
    12.2 Bonによる連署
    12.3 Aliceによるアナウンス

自分の中で強く実現したかったことは
・他のトマトと転XYMトマトの交換
・転XYMトマトとゲームコインの交換
・プレイヤー署名無しでのゲームコイン転移
でした。いずれも実現出来たので当該機能に関しては満足できる内容になったと思います。

反省点としては速習Symbolから 7.メタデータ, 8.ロック, 9.マルチシグ, 10.監視 が使えなかったことが悔しいです。
時間があれば無理やり使うということは出来たかもしれませんが、やっぱり自然な形で盛り込みたいなぁと・・・
ちなみにTheTowerは上記4点全て自然な形で使用してると思います。別格ですね😇


TheTowerはゲーム内のトマタウンにいる異界の塔の管理者横のクリスタルを調べることでもリンクが出てくるので、もし未プレイの方は是非プレイしてみてください。

それでは以下コードが続きます。

■モザイク生成と数量変更
ゲームで使用するモザイクを2種類作成しました。
トマトはトマティーナイベントで投げ合うことも可能かつゲーム内で他のトマトモザイクやベットするゲームコインと交換するため転送可で作成。
ベットするゲームコインとして使用するモザイクは第三者への転送不可、後述するゲームコインベットのためゲーム側がプレイヤー側から回収可能なリボーカブルモザイクとして生成しました。
5. モザイク の 5.1 モザイク生成 を参考にしました。モザイク生成と数量変更をアグリゲートトランザクションでまとめて行っています。

モザイク生成
// ゲームコインモザイク特性定義
const supplyMutable = true; //供給量変更の可否
const transferable = false; //第三者への転送可否
const restrictable = true; //制限設定の可否
const revokable = true; //発行者からの還収可否

//モザイク生成トランザクション生成
const nonce = symbol.MosaicNonce.createRandom();
const mosaicDefTx = symbol.MosaicDefinitionTransaction.create(
    symbol.Deadline.create(epochAdjustment), 
    nonce,
    symbol.MosaicId.createFromNonce(nonce, alice.address), //モザイクID
    symbol.MosaicFlags.create(supplyMutable, transferable, restrictable, revokable), // モザイク特性フラグ
    0, //divisibility:可分性
    symbol.UInt64.fromUint(0), //duration:有効期限
    networkType
);

 

数量変更
//モザイク数量変更トランザクション生成
const delta = 1000000000000000; // 数量
const mosaicChangeTx = symbol.MosaicSupplyChangeTransaction.create(
    symbol.Deadline.create(epochAdjustment),
    mosaicDefTx.mosaicId,
    symbol.MosaicSupplyChangeAction.Increase,
    symbol.UInt64.fromUint(delta * Math.pow(10, divisibility)),
    networkType,
);

 

アグリゲートトランザクションでネットワークにアナウンス
const aggregateTx = symbol.AggregateTransaction.createComplete(
    symbol.Deadline.create(epochAdjustment),
    [
      mosaicDefTx.toAggregate(alice.publicAccount), // モザイク生成トランザクション
      mosaicChangeTx.toAggregate(alice.publicAccount), // モザイク数量変更トランザクション
    ],
    networkType,[],
).setMaxFeeForAggregate(100, 0);

signedTx = alice.sign(aggregateTx,generationHash);
await txRepo.announce(signedTx).toPromise();

 

■サブネームスペースのレンタル
ゲームで使用するモザイクに紐付けるサブネームスペースを作成しました。
今回はレンタル済みだったルートネームスペースtenxymのサブネームスペースとして tomato, game-coinを作成しました。
6. ネームスペース の 6.2 レンタル を参考にしました。
const subNamespaceTx = symbol.NamespaceRegistrationTransaction.createSubNamespace(
    symbol.Deadline.create(epochAdjustment),
    "game-coin",  //作成するサブネームスペース
    "tenxym", //紐づけたいルートネームスペース
    networkType,
).setMaxFee(100);
const signedTx = alice.sign(subNamespaceTx,generationHash);
await txRepo.announce(signedTx).toPromise();

■サブネームスペースのモザイクへのリンク
取得したサブネームスペースと作成したモザイクを紐付けました。
6. ネームスペース の 6.3 リンク を参考にしました。
const namespaceId = new symbol.NamespaceId("tenxym.game-coin");
const mosaicId = new symbol.MosaicId("116E2C1423585B81"); // tenxym.game-coinモザイクID
const tx = symbol.AliasTransaction.createForMosaic(
    symbol.Deadline.create(epochAdjustment),
    symbol.AliasAction.Link,
    namespaceId,
    mosaicId,
    networkType
).setMaxFee(100);
const signedTx = alice.sign(tx,generationHash);
await txRepo.announce(signedTx).toPromise();

■まめ・・・トマタウン案内犬
プレイヤーのアカウントが保有するモザイク情報を取得、指定しているモザイクがあればそれを指定アドレスに送るトランザクションを作ってプレイヤーに署名、送信させています。
3.3 アカウント情報の確認 の 所有モザイク一覧の取得 を参考にしました。
// アドレスのアカウント情報を取得
const getAccountInformation = async (rawAddress) => {
    const repositoryFactoryHttp = new symbol.RepositoryFactoryHttp(nodeUrl)
    const accountRepository = repositoryFactoryHttp.createAccountRepository();
    const address = symbol.Address.createFromRawAddress(rawAddress);
    return  await accountRepository.getAccountInfo(address).toPromise();
}

// アドレスのアカウントの所有モザイクからピックアップモザイクを1つランダムに抽出する
const getPickupMosaicRandom = async (rawAddress, pickupMosaics) => {
    // アカウント情報を取得
    const account = await getAccountInformation(rawAddress, pickupMosaics)
    // 所有モザイクの中にピックアップモザイクがあればそのモザイク情報を取得
    const haveMosaics = account.mosaics.filter((mosaic) => {
        return pickupMosaics.includes(mosaic.id.toHex());
    }, pickupMosaics);
    // 所有中のピックアップモザイクからランダムに1つ取り出す
    return haveMosaics[Math.floor(Math.random() * haveMosaics.length)];
}

// モザイクのIDと数量を返す
const getMosaicDetail = (mosaic) => {
        return {
            id: mosaic.id.toHex(),
            amount: Number(mosaic.amount.toString()),
        }
}

(async () => {
    // 送信対象のピックアップモザイクリスト
    const mosaics = [
        '613E6D0FC11F4530', // toshi.tomato
        '5A8F12439B09B33E', // shizui.tomato
        '310378C18A140D1B' // xembook.tomato
    ];
    // SSSに登録されているアカウントの所有モザイクの中からピックアップモザイクを1つランダムに抽出する
    const mosaic = await getPickupMosaicRandom(window.SSS?.activeAddress, mosaics)
    if (mosaic !== undefined) {
        const mosaicDetail = getMosaicDetail(mosaic);
        console.log(`pickup mosaic amount : ${mosaicDetail.amount}`)
    }
})();

 

案内犬(ゲーム側)にモザイクを送る転送トランザクションの生成から送信
4.2 トランザクション作成, 4.3 署名とアナウンスを参考にしました。
// ゲーム側へモザイクを送信するTransferTransaction
const transferTransaction = async (mosaicId) => {
    const repositoryFactory = new symbol.RepositoryFactoryHttp(nodeUrl;
    const networkRepository = repositoryFactory.createNetworkRepository();
    // ゲーム側へモザイクを1枚送信するトランザクションを作成
    let medianFee = null;
    medianFee = (await networkRepository.getTransactionFees().toPromise()).medianFeeMultiplier;
    const tx = symbol.TransferTransaction.create(
        symbol.Deadline.create(EPOCH_ADJUSTMENT),
        symbol.Address.createFromRawAddress(gameRawAddress),
        [new symbol.Mosaic(new symbol.MosaicId(senderMosaicId), symbol.UInt64.fromUint(1))],
        symbol.PlainMessage.create(""),
        networkType,
    ).setMaxFee(medianFee)

    // 作成したトランザクションをSSSにセット
    window.SSS.setTransaction(tx)

    // SSSでの署名を受け取る
    const  signedTx = await window.SSS.requestSign().then(signedTx => {
        return signedTx;
    })

    // 署名済みのトランザクションをアナウンス
    new symbol.TransactionHttp(nodeUrl)
        .announce(signedTx)
        .subscribe((x) => console.log(x), (err) => console.error(err))

}
(async() => {
    // 前処理で抽出したモザイクIDのモザイクをゲーム側アカウントに送る
    await transferTransaction(transferMosaicId).catch((e) => console.log(e));
})();

 

■ゲーム施設(カ・ジーノ)内での定期実行
本処理を定期実行することで画面に表示するコイン残高をチェーン上にあるプレイヤーアカウントのtenxym.game-coin保有数量と同期しました。
ゲーム画面の一部にSSSアクティブアカウントの保有するゲーム用モザイク(コイン)を表示は 3.3 アカウント情報の確認 にある 所有モザイク一覧の取得 を参考にしました。
// 特定のモザイク数量を取得
const getMosaicAmount = async (rawAddress, mosaicId) => {
    const repositoryFactoryHttp = new symbol.RepositoryFactoryHttp(nodeUrl);
    const accountRepository = repositoryFactoryHttp.createAccountRepository();
    const address = symbol.Address.createFromRawAddress(rawAddress);
    const account = await accountRepository.getAccountInfo(address).toPromise();
    const mosaic = account.mosaics.find(mosaic => mosaic.id.toHex() === mosaicId);

    // 持ってなかったら表示用に0をセットしとく
    let amount = 0;
    if (mosaic !== undefined) {
        amount = Number(mosaic.amount.toString());
    }
    return amount;
}
(async () => {
    // ゲームコイン枚数を取得してゲーム画面表示用の変数にセット
    $gameVariables.setValue(501, await getMosaicAmount(window.SSS.activeAddress, $gameVariables.value(35)));
})();

■モザイク交換
プレイヤーが選択したモザイクとモザイク数量を元にアグリゲートトランザクションを作成し、SSSで署名。
署名後生成されたペイロードをサーバに送信し、検証後に署名、ネットワークにアナウンスしました。

3.アカウント生成, 4.トランザクション, 12.オフライン署名 の複数節を参考にしました。

クライアント側 (3.1 アカウント生成 公開鍵クラスの生成, 12.1 トランザクション作成)
// 交換のアグリゲートトランザクションを提起してSSSにセットする
const createExchangeAggregateTransaction = async (playerAddress, cosignerAddress, releaseMosaicId, receiveMosaicId, releaseMosaicAmount, receiveMosaicAmount) => {
    // Repository作成
    const repositoryFactory = new symbol.RepositoryFactoryHttp(NODE_URL);
    const accountRepository = repositoryFactory.createAccountRepository();

    // プレイヤーPublicAccount
    const playerPublicAccount = symbol.PublicAccount.createFromPublicKey(
        window.SSS.activePublicKey,
        networkType,
    );
    // 連署用ゲームAccount
    const cosignerAccountAddress = symbol.Address.createFromRawAddress(cosignerAddress);
    const cosignerAccountInfo = await accountRepository.getAccountInfo(cosignerAccountAddress).toPromise();
    const cosignerPublicAccount = symbol.PublicAccount.createFromPublicKey(
        cosignerAccountInfo.publicKey,
        networkType,
    );

    // プレイヤーからゲームへのトランザクション
    const innerTx1 = symbol.TransferTransaction.create(
        symbol.Deadline.create(epochAdjustment),
        symbol.Address.createFromRawAddress(cosignerAddress),
        [new symbol.Mosaic(new
            symbol.MosaicId(releaseMosaicId),
            symbol.UInt64.fromUint(releaseMosaicAmount))],
        symbol.PlainMessage.create('tenxym exchange'),
        networkType
    );

    // ゲームからプレイヤーへのトランザクション
    const innerTx2 = symbol.TransferTransaction.create(
        symbol.Deadline.create(epochAdjustment),
        symbol.Address.createFromRawAddress(playerAddress),
        [new symbol.Mosaic(new
            symbol.MosaicId(receiveMosaicId),
            symbol.UInt64.fromUint(receiveMosaicAmount))],
        symbol.PlainMessage.create('tenxym exchange'),
        networkType
    );

    // アグリゲートトランザクションを作成
    const aggregateTx = symbol.AggregateTransaction.createComplete(
        symbol.Deadline.create(epochAdjustment),
        [
            innerTx1.toAggregate(playerPublicAccount),
            innerTx2.toAggregate(cosignerPublicAccount)
        ],
        networkType,
        [],
    ).setMaxFeeForAggregate(100, 1);

    // SSSでの署名を求めるトランザクションをSSSにセット
    window.SSS.setTransaction(aggregateTx)
    const signedTx = await window.SSS.requestSign().then(signedTx => {
        return signedTx;
    });

    // 署名済みのペイロードをセット
    const signedPayload = signedTx.payload;

    console.log(signedTx.payload);
}
// ゲーム側が出す交換数量を返す
const calcExchangeGameCoinAmount = (releaseMosaicAmount) => {
    return releaseMosaicAmount * 10 // ゲーム側はプレイヤーが交換に出す数量の10倍を交換に出す
}
(async () => {
    const sssActiveAddress = window.SSS.activeAddress; // SSSのアクティブ
    const cosignerAddress = $gameVariables.value(36); // ゲーム側アドレスを取得 
    const releaseMosaicId = $gameVariables.value(508); // プレイヤーが交換に出すモザイクIDを取得(事前にプレイヤーに入力させたもの)
    const receiveMosaicId = $gameVariables.value(502); // ゲーム側が交換に出すモザイクIDを取得(事前にプレイヤーに選択させたもの)
    const releaseMosaicAmount = $gameVariables.value(59); // プレイヤーが交換に出すモザイク数量を取得(事前にプレイヤーに入力させたもの)
    const receiveMosaicAmount = calcExchangeGameCoinAmount(releaseMosaicAmount); // プレイヤーが交換に出すモザイクからゲーム側が交換に出すモザイク数量を算出(レートが1:1でない場合に使用)

    // 交換のアグリゲートトランザクションを提起してSSSにセットする
    await createExchangeAggregateTransaction(sssActiveAddress, cosignerAddress, releaseMosaicId, receiveMosaicId, releaseMosaicAmount, receiveMosaicAmount).catch((e) => console.log(e));

})()

 

上記で生成されたペイロードとプレイヤーの公開鍵をサーバに渡し、検証後に署名、アナウンスします。

サーバ側(3.1 アカウント生成 秘密鍵からアカウント生成, 12.2 Bobによる連署, 12.3 Aliceによるアナウンス)
    const repositoryFactory = new symbol.RepositoryFactoryHttp(getRandomNodeUrl(type));
    const txRepo = repositoryFactory.createTransactionRepository();

    // ゲーム側秘密鍵からアカウントを復元
    const privateKey = getTenXymMasterAccount(type)
    const tenXymMasterAccount = symbol.Account.createFromPrivateKey(
        privateKey,
        networkType,
    );

    // ペイロードからトランザクションを復元
    const tx = symbol.TransactionMapping.createFromPayload(signedPayload);


 /*******************************************************************/
 /* 
 /* ここで復元したトランザクションが妥当な内容のものか検証する。本記事では省略。
 /* 
 /*******************************************************************/


    const tenXymMasterSignedTx = symbol.CosignatureTransaction.signTransactionPayload(tenXymMasterAccount, signedPayload, GENERATION_HASH);
    const tenXymMasterSignedTxSignature = tenXymMasterSignedTx.signature;
    const tenXymMasterTxSignerPublicKey = tenXymMasterSignedTx.signerPublicKey;

    // プレイヤーの署名済みペイロードにゲーム側アカウントで連署
    signedHash = symbol.Transaction.createTransactionHash(signedPayload,Buffer.from(GENERATION_HASH, 'hex'));
    cosignSignedTxs = [
        new symbol.CosignatureSignedTransaction(signedHash,tenXymMasterSignedTxSignature,tenXymMasterTxSignerPublicKey)
    ];
    recreatedTx = symbol.TransactionMapping.createFromPayload(signedPayload);
    cosignSignedTxs.forEach((cosignedTx) => {
        signedPayload += cosignedTx.version.toHex() + cosignedTx.signerPublicKey + cosignedTx.signature;
    });
    size = `00000000${(signedPayload.length / 2).toString(16)}`;
    formatedSize = size.substr(size.length - 8, size.length);
    littleEndianSize = formatedSize.substr(6, 2) + formatedSize.substr(4, 2) + formatedSize.substr(2, 2) + formatedSize.substr(0, 2);

    signedPayload = littleEndianSize + signedPayload.substr(8, signedPayload.length - 8);
    signedTx = new symbol.SignedTransaction(signedPayload, signedHash, fromAddressPublicKey, recreatedTx.type, recreatedTx.networkType);

    // 連署したトランザクションをネットワークにアナウンス
    txRepo.announce(signedTx).toPromise();

 

■ゲーム時のコインベット
ゲームを行う際にゲームコインモザイクをプレイヤー側から徴収しています。
この時にプレイヤー側で署名を要するトランザクションではなく、ベット数に応じたリボーカブル(回収)トランザクションをゲーム側が発行することで
プレイヤー側の署名を必要とせずにゲームコインモザイクをプレイヤー側からゲーム側に転移させています。
上記によりベットの度に署名を行うという高頻度署名の煩わしさを取り払いました。
    const repositoryFactory = new symbol.RepositoryFactoryHttp(nodeUrl);
    const networkRepository = repositoryFactory.createNetworkRepository();
    const transactionHttp = repositoryFactory.createTransactionRepository();

    // ゲーム側秘密鍵からアカウントを復元
    const privateKey = getTenXymMasterAccount(type)
    const account = symbol.Account.createFromPrivateKey(
        privateKey,
        networkType,
    );

    // リボーカブルトランザクション作成
    let medianFee = null;
    let medianFee = (await networkRepository.getTransactionFees().toPromise()).medianFeeMultiplier;
    const tx = symbol.TransferTransaction.create(
        symbol.Deadline.create(EPOCH_ADJUSTMENT),
        symbol.Address.createFromRawAddress(playerAddress),
        [new symbol.Mosaic(new symbol.MosaicId(gameCoinMosaicId), symbol.UInt64.fromUint(amount))],
        symbol.PlainMessage.create("WIN REWARD"),
        networkType,
    ).setMaxFee(medianFee)

    // 署名
    const signedTransaction = account.sign(tx, GENERATION_HASH);
    // ネットワークにアナウンス
    transactionHttp.announce(signedTransaction).subscribe(
        (x) => console.log(x),
        (err) => console.error(err),
    );


■特定ブロック高到達によるイベント発生

速習に記載は無いかと思いますが気に入ってる機能なので紹介します。
以下を定期実行することで特定ブロック高到達時にイベントを解禁することができました。
ゲームのアップデートを予め仕込んでおいてSymbolブロックチェーンのブロック高が
特定ブロック高に到達した場合にアップデート(今回の場合はイベント)解禁フラグをONにすることで時限式にアップデートを適用しました。
12. 監視 を使うことでも実現できるかと思いますが、RPGツクールの制限かうまく実装できずこの形をとりました。
実世界の日時を参照すれば良いって?ブロック高参照の方がブロックチェーンゲームっぽくで良いじゃないですか!!
const repositoryFactory = new symbol.RepositoryFactoryHttp(nodeUrl);
const chainHttp = repositoryFactory.createChainRepository();
// ブロック高を取得してイベント解禁フラグのON/OFFを判断
chainHttp.getChainInfo().subscribe((info) =>
    blockHeightChecker(info.height.compact())
    , (err) => console.error(err));

// ブロック高が特定ブロックに達したときにイベント解禁フラグをON
const blockHeightChecker(blockHeight){
    if(blockHeight >= 1533846){
        // イベント解禁フラグをON
        $gameSwitches.setValue(49, true);
    }
}


なるほど、と思える内容はあったでしょうか。もしあったのであれば幸いです。

速習Symbol無くしては今回の実装、特に連署用の秘密鍵をクライアント側に置かずに連署が可能な交換の仕組みの肝となっているオフライン署名を組み込むことは出来ませんでした。
また、署名用Chrome拡張機能のSSS Extensionも無くしては実現できない内容でした。
速習Symbol著者XEMBookさん、SSS Extension開発者いなたつさん、素晴らしいものを世に出していただきありがとうございます。

ここまでお読みいただきありがとうございました。

Previous and Next articles
Writer
Comment
Login required to post comment
No Comment
https://symbol-sakura-16.next-web-technology.com:3001,https://symbol.harvest-monitor.com:3001,https://hideyoshi-node.net:3001,https://harvest-01.symbol.farm:3001,https://criptian-xym-node.net:3001,https://35665.xym.stir-hosyu.com:3001,https://yuna.keshet.finance:3001,https://cryptocat-xym-node.com:3001,https://misaki-xym.com:3001,https://ik1-305-12844.vs.sakura.ne.jp:3001,https://17107.xym.stir-hosyu.com:3001,https://23639.xym.stir-hosyu.com:3001,https://sym-main-01.opening-line.jp:3001,https://sym-main-02.opening-line.jp:3001,https://sym-main-03.opening-line.jp:3001,https://sym-main-04.opening-line.jp:3001,https://sym-main-05.opening-line.jp:3001,https://sym-main-06.opening-line.jp:3001,https://sym-main-07.opening-line.jp:3001,https://sym-main-08.opening-line.jp:3001,https://sym-main-09.opening-line.jp:3001,https://sym-main-10.opening-line.jp:3001,https://symbol-node-01.kokichi.tokyo:3001,https://50038.xym.stir-hosyu.com:3001,https://27423.xym.stir-hosyu.com:3001,https://angel.vistiel-arch.jp:3001,https://xym.stakeme.tokyo:3001,https://00-symbol-node.yagiyoshi.com:3001,
6BED913FA20223F8,051FAEC15105C808,73019335A785A3AE,5289A9B0DBB7EB25,6B245EAF1302E444,2C4A4893229DD0A9,63509D495CAD7B80,481D74291A71FD1F,009388A38C91A8B2,4E94920841641B77,027C6AD49DE2C9F9,29FCA5D3092205EC,56085AD9FCE150AC,7D47EE5423795E68,1FCD7C6B47C7AC5C,34F165131675B97B,2277A3D3C939E3BD,1BEE7E51C52B9E4C,7DE0FA227D5C284A,69C715D2F4E80338,5CF9D768B02E51EA,5B96E6496754C18E,35584939456EE19C,430AB6F115D709C5,202FC8C6762F1286,1DA6F5986C56CAB4,77F0C0918BDDCA75,05D935AB3DBBBDA7,3AB05B7B3373067F,383EEBD409479BDD,282E5F730EEF0F3C,108AFF21E54B77EB,0C4AAE8E82E054FD,18964001EF89D687,15BDE2DDF36FE763,01E2769C92FFB187,427C7F0474B2170B,3F2EEE17968715D5,6C0AFBA20EEBF08B,22975F2097F16D15,2A30D04FF35E8641,03ACEC4F51A71804,5235C4249928922B,2BFEB24F70B91041,741320E728843965,7C4E97850A863208,0586FF7E5ED1C680,606432C50BDE4EB5,15A6AD5E5ED6AC0E,59932634EDBF2A21,5E4F05ED77C64434,71C79278A325FF67,616EC15DE7D641A0,25C2DAB4FE687CBE,3D78B91922741293,5DC4B8DCDC4D3098,2693037B1881D5FA,178FE008F5DCE797,174A93DA9B651FBD,128B2CD071838D3E,1F5630200C5A6025,43449FF761B3DD67,4B20F46C289CA548,35DBA0A70A96615F,78CE44A425CD4514,37179FDE046FE7F2,1739E47C99BAF114,0F3B4A0F2E58703B,3C41561BE3DF7461,2AF0FBFEBF29231F,74CD75720BA4F553,1A5229DC90802C6C,0D2888CD0CBB78EE,2558F02FE439C78F,5DDD07C51EC896A7,437B094116B91BF3,6C9E7CFE33220C8F,38E1C92F412FCA96,2FB263845F59AF82,28C3C38B84138804,659821D4511185A7,7C6095DA82D3E981,70ADA95932385F9F,0BE6C51AD316BF2B,78E6ED1B6C05FD98,39534A8DFD9C0F1E,1BAF1FE1AAF90CD0,6782B8D9162D0C12,5EF22919F6EA15F7,04A3F3F5C088626A,7484B044AC8EF734,305B0ECE763A1E81,08738DB598D0BA37,6F4018163AC51F88,46650CE0C72739A8,0208FCB9186781E5,4E685399C9F8C991,77DB7D348B042A18,578ED7B270B43E64,0B34212023AA5139,479CEDAEE6FAC642,4266458B86437B4B,1F7022D092066B94,0CA28E24CB575AA6,4DCC3C025CDC0BB8,1BD05266C7410EE1,45C46BC710C187AF,7CC2C672543CA102,555BA3719D30D3B9,75C4079CCCCF3E82,66C98D947EAC56FB,1C041AB88A8D222B,79C13E9FE7DA49AB,54D872B5DB378F52,5C97B2414157B039,31E7A64CFF778586,1DFD11EEDA4662C4,18506C180280D8AB,02E95DB8811B3F7A,2D289599AF071703,17EA80052AF590B0,5239AE78AA9011CC,6B6511925501765B,2730974F2D11E9FA,08AB4E53E95E417E,1D0DD379C8C4BA49,26EE7E07E1255FAB,015FABE3F602FA91,366D7D6540335156,2D1E8E73B9F09391,043694AD272BCEBA,064335A213099492,3C40AE05D9758591,71C4BCA24A2FA6C1,20170ABD32244B78,74EBE4C9FD9C3BC8,713583F712D574FA,7C05BBBD486CF544,634D5A328A659C41,2DA0EC8CA32B7CE6,4D0ECC9F6F70D67D,7426EE72B3DFD620,3C3E58B64A8CD43C,5007CF79DF192FB2,1531F39F1B0AA8C3,5D1E928E7902404F,74599424DE012A19,52F96AC0043B02D8,0C694031CFC22C84,03D6EE58174D16DD,054B071E97323D54,2B58942A0A9D3908,7FE10DF86F32776F,1A08F9B88AE4CCF9,03CB7A99E6F6F65A,2F0BAAAD886013B2,7D8FF6C570E909C3,237ACBD2073D7C22,29498519D19B27CF,03E85F9F15A92D27,62B4F899B6DADDFF,6649A6F82740775E,1FA7CAD8017C3045,5FFB7126DD87CC5B,07039874D46F8B44,3391DD8105AD0C82,56C97E544EA9C149,6103DBFFA022249B,41E9771D99FF96A5,65CF7BE26FC7BF56,789063B9FDA7D02B,3EE026A17E4D1895,44B13D3F4EEB8AC9,248AAD9D9E2C1984,0CEDE2DEDDB4832F,5DFE51E2C6617CA4,4FAE3B092339A328,03B7E30D264E586B,3B5B8D2389EEB8C9,0058930348E8DA39,43C44AE173D50661,16E0B9B56763A2A6,43B05E6CCD87A765,33276E3A53106C11,24F35BCEA31E9AFB,49320DB27BA10BF5,01B69528B052EFA3,21C3025E1B2B9D3C,613FFCD9027A73D9,79A5BC34AE8B6314,35BFF98EA19690D4,156CDDD2D6791814,6E4E5A6C8F1ECA31,21BCF4CD26C307FF,15F6E07EAC6D5186,71A24162BA611651,0EDC8542160D8DAA,771BAE5451E881A8,1E47CA21337876B5,7688DC299D77C545,1166391DB32354A4,6A0AF7328EBBE113,38E48122C0FC8BCF,484874B2C669D6E1,4EF142A5D00BDA63,0508049897379E24,50B28C0858AE222C,75F2674CEE45E919,2F0DC3D306469BF9,5A86884742F89C7F,78F0F31776134860,1576EB985E541EA9,17D9C6FCB4FAB2A0,6B2E9EAF2632AEC8,007050CE9F59E77C,5D88028B3A9E633A,554191B46C16E4C4,5A0E1268E65E99F2,41A67647778AEA4D,7EADDBC2119EADA0,3D4235EB6FC9247D,5D25658948613563,5629D816678CF8FC,48A147A7F34C0131,46DF1D828736AFE1,748D83A906C897E4,0708BF363BA9595A,58384D5F0D793C09,531A0934DE43FBAC,1C4EEB7AD6B44AF4,383B57EBD5272C12,41F8077951D07ADF,0F52B4D356FDA824,1FE0805A112A03DF,38E8FFDFCD6510FF,0F65746CB1D7AD53,1E9E3010A1412DDB,7401A59C5DBD66A4,5FDC6383C702EC40,3B97B8CA49739205,5A799437D541B2DA,187E2C0B8F401ED9,0ACA1FDDEB9F6A07,4F2DC8BA863044C6,7D2383791D91CD04,54ACEAF1E8632DF1,7B9DC57CC154B2E0,0B7766A119D5E4B5,49D82E72E99EB836,509A58B6FDFFC197,0C1058BB20787615,581B528745FE0F3B,28D1C21EDE70206A,23A72A2876482029,22EB02FCBC661527,5A8F12439B09B33E,5FF7741F1AE008DE,220DE9C58B8E0E71,5D9D5C6BFE968E2C,3F0B0C29CFD04713,11B52399E03C7E5C,14ABF8C934D15151,7290BAEF5C9CF307,6A44EB5C0F8ED639,255AC0628631B47B,7ADA0B238BB2E29D,05E545728E183EFA,73908A9FF9A309BE,7542C49F2737C4DE,6BE5318AED3E68DB,581F8B63FDF26797,6936D8BB20F4668A,410ECE9587841E7F,149C29AC78DA6465,2181451A49AB930A,7930B6BDAED90925,311AD92AC357CFD7,14340B796150DF92,4D4C35DA96550436,1B5CF0365D451EED,7691E0E6C687B9C0,76D453D709EEE2ED,22AE5C6526EBDE2B,21EF55EAB088DE3E,3FAD71C1671AF556,7EBEF5D03311CF41,367C8B5BD73BC538,182E30BC4D588B3B,66DE982975F20E6D,66224C8828E08DD9,5477E77F11971CE0,2F8D4B55DAB027A2,5CF59ED990B042A0,520C69A467DE143D,53AF2CD5F4182C4B,24DA833B682CFF33,18B9A7F824DE39D0,795E7258AE19BDBA,1D03222C163B5644,1228B33FAF1BA5EC,1A13850AE27D3AD8,2879D87D53FD0746,4193B73A91FED91D,5074A9675B1AF1FC,71C8C78201C17DB6,44C0564B7D636F5B,21D0B4A593F6BDC5,2423B10B716C71BC,2410CC81562813B6,1650FEF9F8851DFF,53AE2554AE824670,79E55067BF754193,4A40CD86619B4180,6D14053CD3B145B5,3D9C134BF6D881E0,3D2C324DEDDB77CC,54A0F165772FABB1,1850571F18C8E94D,0C13ED6B28CBFD6D,50C4B7E3E3BCD78C,7ADBDCD78954D7AC,17D8FB32611FC5D9,347753AD2E6D1EC0,222E3DC735FA327D,2FDAD31E8BF7C5DD,632CA54C0FE884EC,6CD5DACF2F42CD65,1DEF8FEE60F2255D,0109802FE4BC173A,3A341CC2FDB1A49D,1A5FC2F48C0047F0,