20201006のC#に関する記事は5件です。

ReactivePropertyの初期化が面倒なのでまとめて初期化してみる(Plain Object版)をソース ジェネレーターでしてみる

ReactiveProperty に関しては毎日エゴサしているので記事を書いてもらっているのを見つけると嬉しく思ってます。そんな中こんな記事を見つけました!

ReactivePropertyの初期化が面倒なのでまとめて初期化してみる(Plain Object版)

似たようなことを昔したこと思い出したのですが、来月出る予定の .NET 5 以降だとソース ジェネレーターのネタに丁度いいかな?と思ったのでやってみました。

ソースコードは GitHub に上げてます。

https://github.com/runceel/ReactiveBinderGenerator

ジェネレーターの部分はこんな感じになってます。とりあえずべったりと処理を書いてます。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using static ReactiveBinderGenerators.Consts;

namespace ReactiveBinderGenerators
{
    [Generator]
    public class ReactiveBinderGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            context.AddSource("BindFrom", SourceText.From(MarkerAttributeCode, Encoding.UTF8));

            if (context.SyntaxReceiver is not SyntaxReceiver r)
            {
                return;
            }

            var options = (context.Compilation as CSharpCompilation)?.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions;
            var compilation = context
                .Compilation
                .AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(MarkerAttributeCode, Encoding.UTF8), options));

            var bindFromAttribute = compilation.GetTypeByMetadataName(MarkerAttributeFullName) ?? throw new InvalidOperationException();
            var classes = r.CandidateClasses.Select(x =>
                {
                    var model = compilation.GetSemanticModel(x.SyntaxTree);
                    if (model.GetDeclaredSymbol(x) is INamedTypeSymbol typeSymbol)
                    {
                        return (syntax: x, symbol: typeSymbol);
                    }

                    throw new InvalidOperationException(@$"{model.GetDeclaredSymbol(x)}");
                })
                .Where(x => x.symbol.GetAttributes().Any(x => x.AttributeClass?.Equals(bindFromAttribute, SymbolEqualityComparer.Default) ?? false));

            var s = new StringBuilder();
            foreach (var c in classes)
            {
                processClass(s, c, bindFromAttribute, compilation);
            }

            context.AddSource("Binder", SourceText.From(s.ToString(), Encoding.UTF8));


            static void processClass(StringBuilder s,
                (ClassDeclarationSyntax syntax, ITypeSymbol symbol) clazz,
                INamedTypeSymbol bindFromAttribute,
                Compilation compilation)
            {
                var (syntax, symbol) = clazz;
                var generateFrom = symbol
                    .GetAttributes()
                    .Single(x => x.AttributeClass?.Equals(bindFromAttribute, SymbolEqualityComparer.Default) ?? false);
                var typeofExpression = generateFrom.ConstructorArguments.First();
                if (typeofExpression.Value is null)
                {
                    throw new InvalidOperationException();
                }

                var fromType = compilation.GetTypeByMetadataName(typeofExpression.Value.ToString()) ?? throw new InvalidOperationException();
                var properties = fromType.GetMembers()
                    .OfType<IPropertySymbol>()
                    .Where(x => !x.IsStatic && !x.IsIndexer && !x.IsReadOnly);

                s.AppendLine($@"
using Reactive.Bindings.Extensions;
namespace {symbol.ContainingNamespace.ToDisplayString()}
{{
    public partial class {symbol.Name} : System.IDisposable
    {{
        protected System.Reactive.Disposables.CompositeDisposable Disposables {{ get; }} = new System.Reactive.Disposables.CompositeDisposable();");

                foreach (var prop in properties)
                {
                    s.AppendLine($@"public {MakeReactivePropertyOf(prop.Type.ToDisplayString())} {prop.Name} {{ get; }}");
                }

                s.AppendLine($@"
        public {symbol.Name}({fromType.ToDisplayString()} model)
        {{");
                foreach (var prop in properties)
                {
                    s.AppendLine($@"this.{prop.Name} = Reactive.Bindings.ReactiveProperty.FromObject(model, x => x.{prop.Name})
                        .AddTo(Disposables);");
                }

                s.AppendLine($@"
        }}
        public void Dispose() => Disposables.Dispose();
    }}
}}");
            }
        }

        public void Initialize(GeneratorInitializationContext context)
        {
            context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
        }

        class SyntaxReceiver : ISyntaxReceiver
        {
            public List<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();
            public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
            {
                if (syntaxNode is ClassDeclarationSyntax c && c.AttributeLists.SelectMany(x => x.Attributes).Any())
                {
                    CandidateClasses.Add(c);
                }
            }
        }

    }

}

このジェネレーターを使うと以下のようなコードを書くと

public class Person
{
    public string Name { get; set; }
}

[BindFrom(typeof(Person))]
public partial class PersonViewModel
{

}

こんな感じのコードが生成されます。

using Reactive.Bindings.Extensions;
namespace ReactiveBinderGenerators.Tests
{
    public partial class PersonViewModel : System.IDisposable
    {
        private System.Reactive.Disposables.CompositeDisposable Disposables { get; } = new System.Reactive.Disposables.CompositeDisposable();
        public Reactive.Bindings.ReactiveProperty<string> Name { get; }

        public PersonViewModel(ReactiveBinderGenerators.Tests.Person model)
        {
            this.Name = Reactive.Bindings.ReactiveProperty.FromObject(model, x => x.Name)
                .AddTo(Disposables);

        }
        public void Dispose() => Disposables.Dispose();
    }
}

いい感じですね。
今後は、こんな感じのめんどくさいコードはソース ジェネレーターを使って生成できるのでいいですね!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

暗号化データをPythonとC#間でやりとりする

暗号化データをPythonとC#間でやりとりする

はじめに

公開鍵暗号、浪漫ですよね(個人の感想)。個人開発のソフトに組み込んだりとか、実用性は別にして愉快なことができそうです。

しかしながら、ざっくりググって見る限り、自分みたいなスクリプトキディが嬉々としてコピペしたくなるシンプル側に極振りした実装がひとところに纏まっているというのが見当たらなかったため、練習のために作ってみました。

5%位は実用性を考慮しても良いだろうと思ったので、PythonとC#の2つでシンプルに書いてみて、2者間で暗号データをやりとりしてみます。コレができれば、例えばクライアント側(C#)で暗号化したデータを、サーバ側(Python)で復号、とかができるので、ほんの少しですが実用に与することができます。

もっと実用に良い手段があるとか言わない。

想定する状況

  1. サーバ側(Python)でRSAのキーを作成
  2. クライアント側(C#)は作られた公開鍵を得ており、これでテキストファイルを暗号化する
  3. サーバ側に送られた(ことにする)暗号化ファイルを、秘密鍵で復号化する

環境

Python側

  • Windows 10
  • Anaconda3(4.8.2)
    • Python 3.7.6
    • pycrypto 2.6.1

Anacondaならだいたい全部入りで、暗号化モジュールも入ってるので楽です。

Python単体から始める場合、pycryptoをインストールします。1

cmd.exe
pip install -U pycrypto

C#側

  • Windows 10
  • Visual Studio 2019 community
    • .NET Core 3.1
    • コンソールアプリケーション

一部の関数が.NET Standard 2.1の適用のため、.NET Core 3.0以上(.NET Frameworkは5.0 RC1)が必要です。

実装

公開鍵・秘密鍵の作成

CreateKey.py
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5

key_length = 1024

def createKey():

    key = RSA.generate(key_length)

    private_key = key.exportKey().decode('utf-8')
    with open(file="private.pem", mode="w", encoding='utf-8') as file_out:
        file_out.write(private_key)

    public_key = key.publickey().exportKey().decode('utf-8')
    with open(file="public.pem", mode="w", encoding='utf-8') as file_out:
        file_out.write(public_key)

これでキーが生成されます。変数key_lengthは1024以上を指定しますが、別の似たモジュールのマニュアルによれば、2048以上を推奨、1024,2048,3072のいずれかにすべき、とのお話。2 今回は長くする必要が無いので1024。

生成されたキーは、秘密鍵と公開鍵の2つに分けて保存します。3

暗号化(C#)

Pythonで作成した公開鍵ファイルを読み込み、短文を暗号化してみます。

サンプルはコンソールアプリ(.NET Core3.1)で作成しています。

main.cs
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

class Encrypt_sample
{
    static void Main(string[] args)
    {
        const string message = @"This is test message!";

        const string key_begin = "-----BEGIN PUBLIC KEY-----\r\n";
        const string key_end = "\r\n-----END PUBLIC KEY-----";

        string public_key = File.ReadAllText(@"public.pem");
        public_key = public_key.Replace(key_begin, "").Replace(key_end, "");

        var publicKeyBytes = Convert.FromBase64String(public_key);

        using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
        {
            rsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _);

            byte[] encrypted = rsa.Encrypt(Encoding.UTF8.GetBytes(message), false);

            File.WriteAllText(@"encrypted.txt", Convert.ToBase64String(encrypted));
        }
    }
}

変数messageはこれから暗号化する文字列です。

Python側で作成した公開鍵ファイルpublic.pemを読み込んだ後、ヘッダ、フッタを除いてからbyteに変換、RSACryptoServiceProvider型の変数rsaにImportSubjectPublicKeyInfo()で読み込んでいます。4

あとは、バイト列に変換したmessageをEncrypt()で暗号化して、base64変換したテキストを保存します。変換しているのは、テキストファイルとして読めるようにするためです。

Encrypt()の第2引数をfalseにすると、PKCS#1 v1.5のパディングを利用します。

暗号化(Python)

想定状況では暗号化はC#側で行いますが、Python上でも暗号化/復号化ができることを確認出来るよう、関数を作っておきます。

Encrypt.py
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5

message = R"This is test message!"

def encrypt():
    with open(file="public.pem", mode='rb') as file_read:
        public_pem = file_read.read()

    public_key = RSA.importKey(public_pem)
    public_cipher = PKCS1_v1_5.new(key=public_key)

    encrypted = public_cipher.encrypt(message=message.encode())

    with open(file="encrypted.txt", mode='w', encoding='utf-8') as w:
        w.write(base64.b64encode(encrypted).decode('utf-8'))

実作業としては、

  1. 公開鍵ファイルをインポートする(public_key)
  2. public_keyからPKCS1_v1_5のフォーマット(?)に変換(?)(public_cipher)
  3. public_cipherを使ってmessageを暗号化

です。

復号化(Python)

最後は復号化です。

Decrypt.py
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5

def decrypt():
    with open(file="private.pem", mode='rb') as file_read:
        private_pem = file_read.read()

    private_key = RSA.importKey(private_pem)
    private_cipher = PKCS1_v1_5.new(key=private_key)

    with open(file="encrypted.txt", mode='r', encoding='utf-8') as r:
        encrypt_data = base64.b64decode(s=r.read())

    decrypt_data = private_cipher.decrypt(ct=encrypt_data, sentinel="")

    print(decrypt_data.decode('utf-8'))
  1. 秘密鍵ファイルをインポート(private_key)
  2. private_keyからPKCS1_v1_5のフォーマット(?)に変換(?)(private_cipher)
  3. private_cipherを使って、暗号化ファイルの中身を復号化

最後のprint()で表示される文字列が、C#で作成したときのmessageと同一なら、復号化成功です。

感想

思った以上に短いコードでなんとかなるもんだ、と言うのが率直な感想です。エラー処理とか実際のセキュリティ面とか実用上の暗号強度とか、細かな設定をすっ飛ばせば、比較的簡単に言語を跨いで暗号化/復号化ができるのが分かってニッコリです。

なお、コードは本当に適当なので、実用にしないでください。

加えた方がよろしいもの

  • エラー処理全般
  • 署名に関するあれこれ
  • より暗号強度の高い設定
  • このやり方で実装して大丈夫だと信じれる強い心

Q&A

  • Q: なんでC#側のキー作成と復号化が無いの?
    → 復号化はさておき(想定上のクライアント側に秘密鍵預けるの?)、キーの作成は共用できる形式への出力が微妙にめんどうくさい感じだったのでパス。良い手段があれば追記します。

  1. 同種の機能を持つモジュールが複数存在していて(かつ、関数名が微妙に異なったりして)検索時等に混乱の元になってるようです。ここではAnacondaに標準で入っているものを使います。 

  2. pycryptoのマニュアルが404になってるんですが、どこに正本があるんでしょう? 

  3. 実際には、秘密鍵ファイルの中には公開鍵のデータも入っているらしい(?) 

  4. ImportRSAPublicKey()という、いかにもな名前の関数があるが、こっちが正解。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReShaperでLiveTemplateを設定

前説

LivetTemplateのプロジェクトを編集していて、素のVisualStudioでは展開されるsnippetがReSharperを入れた環境でうまく動かないので、なんとかしたかった。

前提環境は、
- VisualStudio2019
- ReShaper2020.2
- Livet 3.2.3

ReShaper用のSnippet(LiveTemplate)を設定する

ReShaper側で新たに設定する。
まずメニューから、 拡張機能ReShaperToolsTemplatesExplorerを開く。

image.png

上部の殻ツールボタンから NewTemplateをクリック。

image.png

編集画面が後ろに表示されるので、元のテンプレートなどを見つつ適当に設定する。

Shortcut: lprop
Description: Create a Livet property
など

左側にsnippetのテンプレートを入力するタブが開いているはずなので、同様に入力。
おおむね互換(らしい)

private $type$ _$name$;

public $type$ $name$
{
get
{ return _$name$; }
set
{ $END$
if(_$name$ == value)
return; 
_$name$ = value; 
RaisePropertyChanged();
}
}

1つだけはまったのが、\$end\$
Microsoftのドキュメントによるとテンプレート展開後にカーソルを持ってくる場所らしい。

JetBrains IDEのテンプレートだと、大文字で \$END\$ なので、ここだけ修正する。

テンプレートを入力すると、$マークのついたパラメータの処遇を右側で聞かれるようになる。
今回型をあらわす \$type\$ がいるので、Choose Macroから↓を選択してみた。

image.png

とりあえず、上記設定などで lsprop[tab] の操作で、スニペット展開されるようになった。

雑に設定しているので、なにか足りないかもしれない。詳しくは公式ドキュメントをちゃんと読む。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】ガチャを実装してみました【PHP】

概要

ソシャゲのガチャの一部を実装してみました。
以下のようなシンプルな仕様です。
・どのガチャも単発と10連。
・ピックアップガチャに対応。
・結果はテキストで表示。

デモ画面

Gacha

クライアント側のリポジトリです。

バックエンド側のphpのコードです。
Gacha.php
<?php

// 同一レアリティ内のアイテムの抽選
function get_item($item_rarity,$gacha_group_id){

  // ガチャの重みの合計
  $item_weight_sum = 0;

  // ガチャの重みの配列
  $item_weight_array = array();

  // ガチャのIDの配列
  $item_id_array = array();

  // PDOインスタンスを生成 
  $item_pdo = get_PDO();

  // SELECT文を変数に格納
  $item_select_sql = "SELECT item_weight,item_id FROM item_config WHERE item_rarity ='". $item_rarity. "' AND gacha_group_id ='". $gacha_group_id. "'";


  // SQLステートメントを実行し、結果を変数に格納
  $item_select_stmt = $item_pdo->query($item_select_sql);

  // foreach文で配列の中身を一行ずつ出力
  // $rou['列名']で取り出せる
  foreach ($item_select_stmt as $item_row) {
    // データベースのフィールド名で出力
    $item_weight_sum += (int)$item_row['item_weight'];
    $item_weight_array[] = (int)$item_row['item_weight'];
    $item_id_array[] = (int)$item_row['item_id'];
  }

  // 乱数生成
  $random = rand(1,$item_weight_sum);
  $item_weight_total=0;

  // アイテムIDの抽選
  foreach($item_weight_array as $item_key => $item_value ){
    $item_weight_total += $item_value;
    if($random <= $item_weight_total){
      $item_result = $item_id_array[$item_key];
      break;
    }
  }

  // アイテムIDからレアリティとアイテム名を抽出
  $item_sql = "SELECT item_rarity,item_name FROM item WHERE item_id ='". $item_result. "'";
  $item_stmt = $item_pdo->query($item_sql);
  foreach($item_stmt as $item){
    echo 'レアリティ : ' . $item['item_rarity'] . ' ';
    echo 'アイテム名 : ' . $item['item_name'];
    echo "\n"; 

  }

}

// レアリティの抽選
function get_rarity($gacha_group_id){

  try{
    // ガチャの重みの合計
    $gacha_weight_sum = 0;
    // ガチャの重みの配列
    $gacha_weight_array = array();
    // ガチャのレアリティの配列
    $gacha_rarity_array = array();

    // ガチャグループを条件に抽出
    $dbh = get_PDO();
    $sql = "SELECT gacha_weight,gacha_rarity FROM rarity_config WHERE gacha_group_id ='". $gacha_group_id. "'";
    $stmt = $dbh->query($sql);

    // ガチャグループで条件を絞ってガチャの重みを合計する
    foreach ($stmt as $row) {
      // データベースのフィールド名で出力
      $sum += (int)$row['gacha_weight'];
      $gacha_weight_array[] = (int)$row['gacha_weight'];
      $gacha_rarity_array[] = (int)$row['gacha_rarity'];
    }

    // 乱数生成
    $random = rand(1,$sum);
    // ガチャの重みのトータル(順次加算)
    $gacha_weight_total=0;

    // 抽選ロジック(乱数を用いて、範囲内だった重みを探す)
    foreach($gacha_weight_array as $key => $value ){
      $gacha_weight_total += $value;
      if($random <= $gacha_weight_total){
        // 範囲内だった重み
         $rarity_result = $gacha_rarity_array[$key];
        break;
      }
    }

    // アイテムの抽選
    get_item($rarity_result,$gacha_group_id);



  // エラー(例外)が発生した時の処理を記述
  }catch (PDOException $e) {

    // エラーメッセージを表示させる
    echo 'データベースにアクセスできません!' . $e->getMessage();

    // 強制終了  
    exit;
  }
}

// PDOインスタンス生成
function get_PDO(){
  return new PDO(
    'mysql:host=mysql1.php.starfree.ne.jp;dbname=opnvtw_stdb;charset=utf8', 
    'opnvtw_lantitor',
    '1211stst',
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]
  );
}

// 最初に呼ばれる関数
function get_gacha(){

  // tryにPDOの処理を記述
  try {

    $gacha_id = $_REQUEST['gacha_id'];


    $pdo = get_PDO();
    $stmt = $pdo-> prepare("SELECT * FROM gacha WHERE gacha_id = ?");
    $stmt->bindValue(1,$gacha_id,PDO::PARAM_STR);
    $stmt->execute();


    // gachaテーブルのgacha_times分ガチャを回す。
    foreach ($stmt as $row) {
      for($i = 1; $i <= (int)$row['gacha_times']; $i++){
        // レアリティの抽選からして、その後にアイテムの抽選をする
        get_rarity($row['gacha_group_id']);
      }



    }
  // エラー(例外)が発生した時の処理を記述
  } catch (PDOException $e) {

    // エラーメッセージを表示させる
    echo 'データベースにアクセスできません!' . $e->getMessage();

    // 強制終了  
    exit;

  }

}

get_gacha();

?>

開発環境

MacOS Catalina 10.15.5
クライアントはC#+Unity 2019.2.12f1
バックエンドはPHP
レンタルサーバーStarSeverFreeのフリー PHP+MySQLプラン
今回はレンタルサーバーで実装してますが、ローカルで確認したいならXamppを使います。

バックエンド

抽選ロジック

抽選ロジックは、重み付きランダムアルゴリズムを用いています。
例の図は☆5が5%、☆4が10%、☆3が85%の確率です。
探索範囲はそれぞれ、☆5が1~5、☆4が6~15、☆3が16~100になります。
1~100の間のランダムで値を出し、出た値が60なら16~100の間なので、抽選結果は☆3になります。探索には累積和を使っています。
このロジックを用いて、レア度の抽選→同一レアリティ内のアイテムの抽選を行っています。

RandomRange

GachaLogic.php
  // ガチャの重みの配列
  $gacha_weight_array = array(5,10,85);
  // ガチャのレア度の配列
  $gacha_rarity_array = array(5,4,3);
  // 1~100の間で乱数を生成
  $random = rand(1,100);
  // ガチャの重みのトータル(順次加算)
  $gacha_weight_total=0;

  // 乱数の範囲を探す
  foreach($gacha_weight_array as $key => $value ){
    $gacha_weight_total += $value;
    // 範囲内の数値
    if($random <= $gacha_weight_total){
      // レア度の配列からレア度を取り出す 
      $result = $gacha_rarity_array[$key];
      break;
    }
  }

  echo $result;

マスタデータ・DB

2種類のガチャ(普通のガチャの単発と10連、ピックアップガチャの単発と10連)のデータです。
今回は、ピックアップガチャの10連が押された場合を例に全体の流れを説明したいと思います。

ガチャテーブルは、ガチャの詳細が格納さているテーブルです。
ピックアップガチャが押された場合、gacha_idのpickup_sort_10がサーバ側(PHP)に渡されます。
gacha_idはガチャを特定するためのもので、一意です。
gacha_timesが10は、10回ガチャを回すことを表しているので、抽選は10回行われます。
gacha_group_idは、ガチャの種類を特定することができます。
単発と10連では、回数やコストは違いますが、種類は同じです。
そのため、pickup-sort_1とpickup-sort_10のgacha_group_idは両方ともBです。

ガチャテーブル

gacha_id gacha_group_id gacha_name gacha_cost gacha_times
normal_1 A test_1 10 1
normal_10 A test_10 100 10
pickup_sort_1 B test_pickup_sort_1 10 1
pickup_sort_10 B test_pickup_sort_1 100 10

レアリティ設定テーブルは、gacha_group_idごとの重みを設定しているテーブルです。
ピックアップガチャのgacha_group_idであるBのgacha_weightを用いて、抽選します。
87%の確率で☆3、10%の確率で☆4、3%の確率で☆5が出る設定になっています。
gacha_weightの合計が1000であれば0.1%きざみ、100であれば1%きざみにすることができます。
ここの抽選で出たgacha_rarityを用いて、次にアイテムの抽選を行います。

レアリティ設定テーブル

gacha_group_id gacha_rarity gacha_weight
A 3 870
A 4 100
A 5 30
B 3 870
B 4 100
B 5 30

アイテム設定テーブルは、gacha_group_idとitem_rarityごとの重みを設定しているテーブルです。
gacha_group_idのBと、先ほど抽選したレアリティ(gacha_rarity)を用いて、抽選を行います。
テーブルにデータを挿入するのに手間がかかりますが、一つ一つ設定できるようにすることで、コードの変更をせずにピックアップを実装することができます。
例えば、gacha_group_idがBで、item_idが4001や5001は、同一レアリティ内でも、抽選が当たりやすくしています。

アイテム設定テーブル

gacha_group_id item_id item_rarity item_weight
A 3001 3 100
A 3002 3 100
A 3003 3 100
A 3004 3 100
A 3005 3 100
A 3006 3 100
A 3007 3 100
A 3008 3 100
A 3009 3 100
A 3010 3 100
A 4001 4 500
A 4002 4 125
A 4003 4 125
A 4004 4 125
A 4005 4 125
A 5001 5 333
A 5002 5 333
A 5003 5 334
B 3001 3 100
B 3002 3 100
B 3003 3 100
B 3004 3 100
B 3005 3 100
B 3006 3 100
B 3007 3 100
B 3008 3 100
B 3009 3 100
B 3010 3 100
B 4001 4 500
B 4002 4 125
B 4003 4 125
B 4004 4 125
B 4005 4 125
B 5001 5 500
B 5002 5 250
B 5003 5 250

アイテムテーブルは、アイテムの詳細が格納されているテーブルです。
抽選で出たitem_idを用いて、名前(item_name)やアイテムの種類(item_type)を表示することができます。

アイテムテーブル

item_id item_rarity item_name item_type
3001 3 バブルソート 1
3002 3 ヒープソート 1
3003 3 線形探索 2
3004 3 レイトレーシング 3
3005 3 ユークリッドの互除法 3
3006 3 バケットソート 1
3007 3 シェルソート 1
3008 3 挿入ソート 1
3009 3 選択ソート 1
3010 3 シェーカーソート 1
4001 4 マージソート 1
4002 4 二分探索 2
4003 4 ダイクストラ法 3
4004 4 ベルマンフォード法 3
4005 4 ワーシャルフロイド法 3
5001 5 クイックソート 1
5002 5 幅優先探索 2
5003 5 深さ優先探索 2

以上がバックエンド側の実装になっています。

クライアント

クライアント側はUnity,C#で書いてます。
UIアーキテクチャは簡易的なMV(R)Pで実装しています。
以前にMV(R)Pで実装した際のお話をQiitaに投稿しましたので、共有させて頂きます。
【Unity】アウトゲームでの設計でMV(R)Pパターンを採用しました【設計】
また、今回はバックエンドの実装や考えを中心に行ったので、クライアント側の実装について説明は、省かさせてもらいます。

最後に

実際ガチャを実装するには、もっと細かいテーブル設定やロジックが必要になると思います。
他にもいくつかQiitaで記事を投稿しているのでよければ見てください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[SharePoint]CSOMでログインにパスワード以外を使う

経緯

もうすぐ全ての.NETが.NET5(旧Core)に集約されることになりますが、この.NET5にはCSOMのパスワード認証で標準的に使われているSharePointOnlineCredentialsが使えなくなるそうです。

それ以前に、パスワードレス認証でログインしている人が困ってしまうのでそれに対応したいというのが動機です。

.NET5以前

.NET5以前の場合は、NuGetで「SharePointPnPCoreOnline」を入れたあと、

var authenticationManager = new OfficeDevPnP.Core.AuthenticationManager();
ClientContext context = authenticationManager.GetWebLoginClientContext("<Site URL>", null);

で出来る様です。(.NET5以前では試してはいません)

2020/10/4現在では.NET5環境ではうまく動作しませんでした。
ビルド時に.NET5に対応していない旨の警告が出るのが関係していると思います。

今回行なった方法

PnP Coreの認証ライブラリを使用した後、アクセストークンを取り出してCSOMのClientContextを初期化します。

ただ、そもそもPnPにはCSOMと同様なSharePointアクセスだけでなくMicrosoftGraphも扱えるので、CSOMで実装済みの資源をなるべく修正したくない場合を除けば、CSOMをあえて使う必要は無いです。

サンプル

サンプルは以下に置いておきます。
https://github.com/RYO-4947123/CSOM_Sample

※あらかじめAADにアプリ登録をしておく必要があります。
https://github.com/pnp/pnpcore/tree/dev/src/samples/Demo.Console

要点

PnP.Core.Authのヘルパーを駆使してログインを行なった後、アクセストークンを取り出します。

var accessToken =  await context.AuthenticationProvider.GetAccessTokenAsync(context.Uri);

その後、CSOMのClientContext作成時にリクエストヘッダにトークンを加える様に初期化します。

context.ExecutingWebRequest += (sender, e) =>
{
    // Insert the access token in the request
    e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;
};

あとは今まで通りClientContextを使ってSharePointにアクセスすることができます。

※注意点

コンソールアプリの場合、一度接続を切る(Host.Dispose())と再接続してもGetAccessTokenAsyncメソッドがデッドロックして帰ってこなくなります。

参考にしたサイト

.NET Standard に対応した CSOM で注意するべき点について
.NET Framework で CSOM ではなく、.NET Standard に対して CSOM を使用する
PnP Core SDK
独自のAzureADアプリケーションを構成したい

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む