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

C#・自分用のメモ書き

C#の標準入力を受け取り、その文字列を配列に格納する方法を探すのに時間がかかっているので備忘録として記す 標準入力で文字列を受け取った後にToCharArray()メソッドを用いてchar型の1次元配列に変換してる foreach文にて1要素ずつ表示しているだけ。 てか、Javaだとfor文でforeach書くからC#にforeach文あるの感動 因みに、区切り文字があれば、Split('区切り文字')を用いてstring[]配列に格納もできる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ListViewの全項目を(ちょっとだけ)高速に選択する

参考にした記事 ListBoxの全項目を高速に選択する - Qiita テストコード private void listView1_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.A && e.Control) { foreach (ListViewItem item in listView1.Items) { item.Selected = true; } } if (e.KeyCode == Keys.Q && e.Control) { if (0 < listView1.Items.Count) { listView1.Items[0].Selected = true; SendKeys.SendWait("{HOME}+{END}"); } } } 元のコードはSendKeys.SendWait("{HOME}+{END}");のみだったが、ListViewで途中を選択した状態から始めると最初のほうが選択されなかったので、最初の1個だけSelectedで選択するようにした。 体感的には、SendKeyのほうがわずかに早い。といっても、数百くらいなら体感差は無く、1万くらいになると差が出てくる感じ。古いPCなので、最近のPCならもっと差は縮まるかも。 アイテムが大量にある場合、foreach Selectedでは、キーを押してからGUIが変わるのに少し待ち時間があるが、SendKeyの場合は、キーを押したら瞬時に最初の1個が選択されて、それから少し遅れてすべてが選択されるので、視覚的に操作が開始されたことがわかりやすいSendKeyのほうが使い勝手は良さそう。 foreach Selectedでは、すべてを選択した際に、スクロール位置は変化しない。SendKeyの場合は、最初に一番上までスクロールし、続いて一番下までスクロールする。このあたりの見た目の動きは好みで分かれそう。 // DLLを叩くと素早く一気に選択できるらしいんだけど、コード量が一気に増えるのと、今のところは数百から最大でも2万弱までの見込みなので、今回は未確認
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LINE bot作成 C#+AWS(Lambda)

概要 自分でLINE botを作成した際、C#+AWS(Lambda)環境で実装してみたのですが、 この技術スタックでの情報が全く見つかりませんでした。 (自分の探し方が悪かっただけかもしれませんが…) そこで、今後C#+AWS(Lambda)で作成したい!という方のために手順をまとめておきます。 本記事では、LINEから送信された文字をオウム返しするbotが作成できます。 このベースさえ作ってしまえば、LINE公式ドキュメントを参考に色々いじって楽しめると思います! 内容としては、LINE Developers登録からLambda関数のプログラミング、AWSへのデプロイから最終的に実際にLINEで確認するところまでやってしまいます。 ※AWS Lambdaへのデプロイは、Windowsの場合、Visual Studioを使用すればGUIポチポチで完了するので簡単ですが、 Visual Studio for Macの場合はそうはいかないので、 今回はどの環境でも対応できるようCUIからデプロイする操作を記載しております。 事前準備編(LINE Developers登録) LINE Developers登録及びチャンネル作成 LINE botを作成するにあたり、「LINE Developers」に登録する必要があります。 手順は簡単で、申請待ち時間も無いので数分で登録可能です。 下記サイトを参考に、アカウントの作成及びチャンネルの作成まで行います。 ※1アカウントで「チャンネル」を複数作成することが可能で、「チャンネル」ごとに別のbotを作成できます。 チャンネルアクセストークンの発行 LINE botを作成するにあたり、最低限「チャンネルアクセストークン」は発行する必要があります。 チャンネルの作成まで完了したら、Line Develpersにログインし、作成したチャンネルのページにアクセスします。 「Messagin API設定」タブをクリックします。 ページ下部までスクロースし、「チャネルアクセストークン」の発行ボタンをクリックします。 発行されたキーは後ほど使用するのでどこかにメモをしておいて下さい。 ※後ほど再度このページにアクセスして発行済のキーを確認することは可能です。 プログラミング編 無事アクセストークンの発行が完了したら、続いてプログラミングを行っていきます。 C#でのLambda関数プログラミングは、Windowsの場合はVisual Studioを使用すればあらかじめテンプレートが用意されているので簡単ですが、 Visual Studio for Macには用意されていないため、gitにテンプレートを用意しておきました。 ※AWSへデプロイするための設定ファイル等も同梱してあります。 ローカルにプロジェクトの準備が完了したら、「Function.cs」ファイルを開きます。 この中の「FunctionHandler()」メソッドがLambda実行時に呼び出されるメソッドです。 つまり、LINEのMessagingAPIからWebhookで呼び出されることになります。 今回はサンプルでbot利用ユーザが入力された文章をそのままオウム返しするようにしてみます。 ●LINEからLambdaへのリクエスト内容をモデリング WebHookRequestModel.cs using System.Collections.Generic; using Newtonsoft.Json; namespace LambdaSample.Models { public class WebHookRequestModel { [JsonProperty("events")] public List<EventModel> EventModels { get; set; } } } EventModel.cs using Newtonsoft.Json; namespace LambdaSample.Models { public class EventModel { [JsonProperty("message")] public MessageModel Message { get; set; } [JsonProperty("replyToken")] public string ReplyToken { get; set; } } } MessageModel.cs using Newtonsoft.Json; namespace LambdaSample.Models { public class MessageModel { [JsonProperty("type")] public string Type { get; set; } [JsonProperty("text")] public string Text { get; set; } } } ●LambdaからLINEへのリプライモデル ReplyRequestModel.cs using System.Collections.Generic; using Newtonsoft.Json; namespace LambdaSample.Models { public class ReplyRequestModel { [JsonProperty("replyToken")] public string ReplyToken { get; set; } [JsonProperty("messages")] public List<MessageModel> Messages { get; set; } /// <summary> /// true: ユーザに通知されない(デフォルト) /// false: ユーザに通知される /// </summary> [JsonProperty("notificationDisabled")] public bool NotificationDisabled { get; set; } } } ●メインの処理 Function.cs using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; using BasicExtension; using LambdaSample.Models; [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] namespace LambdaSample { public class Function { public Function() { _httpClient = new HttpClient(); _httpClient.DefaultRequestHeaders.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8")); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("CHANNEL_ACCESS_TOKEN")); } public APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest input, ILambdaContext context) { WebHookRequestModel webHookRequestModel = input.Body.ToObject<WebHookRequestModel>(); string replyToken = webHookRequestModel.EventModels[0].ReplyToken; MessageModel message = webHookRequestModel.EventModels[0].Message; ReplyToLine(replyToken, message.Text); return CreateLambdaApiResponse(); } private void ReplyToLine(string replyToken, string inputText) { string stringRequest = CreateReplyRequestModel(replyToken, inputText).ToJson(); StringContent stringContent = new StringContent(stringRequest, Encoding.UTF8, "application/json"); _httpClient.PostAsync("https://api.line.me/v2/bot/message/reply", stringContent).Wait(); } private ReplyRequestModel CreateReplyRequestModel(string replyToken, string inputText) { ReplyRequestModel replyRequestModel = new ReplyRequestModel(); replyRequestModel.ReplyToken = replyToken; replyRequestModel.Messages = new List<MessageModel>() { new MessageModel() { Type = "text", Text = inputText } }; replyRequestModel.NotificationDisabled = false; return replyRequestModel; } private APIGatewayProxyResponse CreateLambdaApiResponse() { return new APIGatewayProxyResponse { StatusCode = 200, Body = null, IsBase64Encoded = false, Headers = new Dictionary<string, string>() { { "Content-Type", "application/json" } } }; } private readonly HttpClient _httpClient; } } AWSへデプロイ こちらの記事をご参考下さい。 https://qiita.com/ryohei0109_develop/items/ff05cfad2cb276af1169 今回の例ではjsonファイルはこのようになります。 ※「function-role」だけはどうしても書き換えて下さい! aws-lambda-tools-defaults.json { "Information": [ "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", "dotnet lambda help", "All the command line options for the Lambda command can be specified in this file." ], "configuration": "Release", "framework": "netcoreapp3.1", "profile": "default", // AWS CLI プロフィール "region": "ap-northeast-1", // AWSリージョン指定 "function-runtime": "dotnetcore3.1", // Lambbda実行ランタイム "function-memory-size": 512, // Lambdaメモリ(MB) "function-timeout": 30, // Lambdaタイムアウト時間(seconds) "function-handler": "LambdaSample::LambdaSample.Function::FunctionHandler", // Lambda実行時にコールするメソッドを指定 "function-name": "LambdaSample", // Lambda名称 "function-role": "arn:aws:iam::123456789012:role/xxxxxRole" // AWSロール名 } Lambda環境変数にチャンネルアクセストークンを設定 チャンネルアクセストークンを環境変数に設定しておくと、今後チャンネルアクセストークンの変更があった際、 プログラムを変更することなくGUIから操作するだけで対応可能になります。 AWSマネジメントコンソールにログイン->Lambda->先程デプロイした関数を選択->「設定」タブをクリック。 「編集」ボタンをクリック。 キーに「CHANNEL_ACCESS_TOKEN」を指定し、 値に先程Line Develpersで発行した「チャネルアクセストークン」を入力し、「保存」ボタンをクリック。 これで環境変数の設定は完了です! ここで設定した環境変数は、C#側からは以下のような形で参照することができます。 string value = Environment.GetEnvironmentVariable("CHANNEL_ACCESS_TOKEN") LambdaをAPI化 Lambda関数を作成しただけでは、まだLINEからのWebhookを受けることができません。 そこで「API Gateway」を行い、先程作成したLambda関数をAPI化します。 AWSのApiGatewayにアクセスし、「APIを作成」ボタンをクリックします。 少し下にスクロールし、「REST API」の「構築」ボタンをクリックします。 任意の「API名」を入力し、「APIの作成」ボタンをクリックします。 APIの大枠が作成できたら、続いてリソースを作成していきます。 「アクション」から「リソースの作成」をクリックします。 任意のリソース名を入力し、「API Gateway CROSを有効にする」にチェックを入れ、「リソースの作成」ボタンをクリックします。 作成したリソースにPOSTメソッドを追加していきます。 「アクション」から「メソッドの作成」をクリックします。 プルダウンから「POST」を選択し、 チェックアイコンをクリックすると、POSTメソッドが作成できます。 「Lambdaプロキシ統合の使用」にチェックし、 先程作成したLambda関数名を入力します。(※サジェストしてくれます) 完了したら「保存」ボタンをクリックします。 「Lambda 関数に権限を追加する」というダイアログボックスが表示されるので、 「OK」ボタンをクリックするとPOSTメソッドの作成が完了します。 POSTメソッドの作成が完了したので、続いてデプロイを行います。 「アクション」から「APIのデプロイ」をクリックします。 「デプロイされるステージ」に「新しいステージ」を選択し、 「ステージ名」に任意の名前を入力し、「デプロイ」ボタンをクリックします。 これでAPI化の準備は完了です! 「POST」を選択し、「URLの呼び出し」に表示されているURLがAPIのエンドポイントになるので、メモしておきます。 自動応答メッセージをOFF デフォルトの設定では、ユーザがLINEで文章を送ると自動的にメッセージが返信される設定になっています。 今回は作成したbotから返信を行うので、デフォルトの自動応答メッセージ機能をOFFにする必要があります。 LINE Official Account Managerにログインします。 作成したチャンネルをクリックします。 画面右上の「設定」をクリックします。 レフトナビの「応答設定」をクリックします。 ※ちなみにこの画面でアカウント名やプロフィール画像を変更できます。 ・応答モード: 「Bot」    「チャットモード」にすると、Webhookが利用できません。 ・あいさつメッセージ: 「オン」or「オフ」    友達登録時の初回自動送信メッセージのON/OFF切り替えになります。    こちらは好みで良いと思います。 ・応答メッセージ: 「オフ」    今回は自分で作成したプログラムから応答するので、オフにしておきます。 ・Webhook: 「オン」    オンでないとLINE botが作成できないですね! これでこの画面での設定は完了です。 あともう少しです!がんばりましょう! Webhook登録 先程発行したAPIのエンドポイントを、LINE DevelopersにてWebhook URLとして登録します。 LINE Developersにアクセスし、 作成したチャンネルのページにアクセスし、 「Messaging API設定」タブを選択します。 ・ページ下部までスクロールし、Webhookの利用をON ・Webhook設定の「編集」をクリックし、先程発行したAPIのURLを登録します。 Webhook URLの設定が完了したら、「検証」ボタンをクリックし、疎通確認を行います。 正常に疎通できている場合は、「成功」と表示されます。 LINEアプリで最終確認 最後は先程の画面に表示されているQRコードを読み取って友達登録を行い、 LINEアプリからも正常に動作するか確認してみましょう! 以上です!お疲れ様でした! 蛇足 参考までに今回私が作成したconpassのイベント検索ができるLINE botも紹介させて下さい。 git: https://github.com/ryohei0109-develop/SearchConnpassLineBot
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

tModLoader ちょこっとリファレンス

C#の勉強を一旦放棄してtModLoaderでMod製作をしている私は(悪い意味での)怠惰 なにそれ? tModLoaderとはTerrariaというゲームのMod製作ツール もともと非公式だったが1.4リリースに合わせて公式も協力して 現在Steamで入手可能 ただしバージョンは1.3.5.3(2021/6/12現在) がちがちのリファレンスじゃなくて ちょこっとしたメソッドやフィールドとかの 役に立ちそうなのを紹介 日本語のtModLoaderリファレンスが無さすぎるので共有 随時 役に立ったものとかを追記していく感じ (そもそも私はあまりQiitaに現れないので難しいかもしれない) 日本人tModder増えてほしいので頑張ります (最終追記日: 2021/06/12) tModLoader ちょこっとリファレンス まず大前提としてExample Mod 入れろ。 (入れてじゃなくて入れろ) Mod Browserに検索をかけると出てくる。 GitHubでもいいぞ。 だいたいの常套手段とか、どう書けばいいのかは書かれている。 ここではExample Modでも補えないものとか Example Modに書かれていたり、tModLoader wikiに書かれていても、 便利だと思ったし、まぁ日本語じゃないので。 みたいなものを書いていく予定です ちなみに知ってると思うが「バニラ」とはModを入れていない本家Terrariaのコンテンツを指す。 Terrariaじゃなくても「バニラ」という単語はModじゃない本家ゲームのコンテンツを指す。 英語でVanilla あとtModLoaderのデコンパイルしたコードあると便利。 ソースコードを公開するのは禁止だが、個人でILSpyなどによるデコンパイルは許可されている。 バニラアイテムの挙動変更など、場合によってはそのコードを見たり、マネしないとうまくいかないことも稀にある。 そもそもIL編集やるなら必須。 Mod Mod.Load() IL編集は大体ここに書いて。 Mod.PostSetupContent() これは全てのModがロードし終わった後に呼び出されるため、 NPCLoader.NPCCountなど○○Loader.○○CountでModで追加された物を含めたアイテム数を取得できる。 もちろん取得できるのはアイテム数に限らない。 ModItem ModItem.SetStaticDefaults() bool ItemID.Sets.ItemNoGravity[item.type] trueにするとアイテムを重力に逆らわなくできる。 Soul of lightとかソウル系のアイテムやStardust Fragmentとかのフラグメント系アイテムみたいな感じ。 ModItem.SetDefaults() item.value = Item.sellPrice([PlatinumCoin = 0], [GoldCoin = 0], [SilverCoin = 0], [CopperCoin = 0]); アイテムの価格は買う時と売る時によって値段が違う。(たしか5倍だっけかな) 分かりづらいのでItem.sellPrice()を使えば売る時の値段で価格指定ができる。 何よりわかりやすい。 ModItem.ModifyTooltips(List<TooltipLine> tooltips) Tooltip.SetDeafults("")で色付きのツールチップ([c:ffff00:]って書くやつ)がなんか違和感だったり、 (ほかのツールチップは少し色が変化する(明暗)のだが、[c:ff0000:]を使うとそのままの色になってしまう) 任意の位置にツールチップを入れたいけど、なぜか最後に表示されてしまう と言うときに使うと便利。 むしろTooltip.SetDefaults("")なんか使わないでこれ使え 不自然でない色付きのツールチップを追加する方法 public override void ModifyTooltips(List<TooltipLine> tooltips) { TooltipLine line = new TooltipLine(mod, "(ツールチップ名)", "(ツールチップの文章)") { overrideColor = new Color(255, 255, 0) }; tooltips.Add(line); } アイテム名の下に任意のツールチップを表示させる方法 public override void ModifyTooltips(List<TooltipLine> tooltips) { int index = tooltips.FindIndex(tooltipLine => tooltipLine.Name.Equals("ItemName")); if (index == -1) { return; } tooltips.Insert(index++, new TooltipLine(mod, "(ツールチップ名)", "(文章)")); } ModItem.PostDrawInInventory(SpriteBatch spriteBatch, Vector2 position, Rectangle frame, Color drawColor, Color itemColor, Vector2 origin, float scale) 回復アイテムを飲んだ時のバッテン印、あれを表示する方法がここにある。 大したものではないので、ここで共有するとしよう。 public override void PostDrawInInventory(SpriteBatch spriteBatch, Vector2 position, Rectangle frame, Color drawColor, Color itemColor, Vector2 origin, float scale) { Vector2 position3 = position + frame.Size() * scale / 2; position3 -= Main.cdTexture.Size() * Main.inventoryScale / 2f; spriteBatch.Draw(Main.cdTexture, position3, null, Color.White, 0f, default(Vector2), scale, SpriteEffects.None, 0f); } 自作発言とか普通に好きではないが、実際バニラのコードを元に、tModLoader用に書ているだけなので 使いますみたいな許可とか、クレジットに書く義務はない。(あったら嬉しいけどそこまでのものではない。) クレジットに書かなくていい代わりに、なぜこのようなコードなのか理解して使ってほしい。 趣味でプログラミングをやっている者だが、それは多分みんなにとって大切だと思うものなはずだから。 さぁ、ILSpy使ってTerraria.ModLoader.ModItem.PostDrawInInventoryからAnalyzeの旅だ! ModNPC bool ModNPC.PreNPCLoot() ここでfalseを呼び出すとModNPC.NPCLoot()が呼ばれなくなる。 ボスはデフォルトでLesser Healing Potionを落とすため、それが嫌ならfalseを返すべし。 ただし、*** has been defeated!も表示されない上、ハートやスターも自分で落とす処理を書かなければならなくなる。 ModProjectile projectile.aiStyle = 1; これに関してだが、これを使うと数千行のコードを参照することになる。 実はtModLoader wikiに5行程度で済むように書くことをお勧めしている(?) projectile.aiStyle = -1;にして AI()に下のリンクにあるコードを書くといい。 https://github.com/tModLoader/tModLoader/wiki/Basic-Projectile#aistyle-1 他のaiStyleに関しても同等のことが言えたりする。 NPCのaiStyleも。 ModPlayer bool ModPlayer.PreItemCheck() バニラアイテムの挙動を変えたい時、場合によってはこれが必要になる。 ただし、とても面倒なうえ、そこまでやる必要があるか考えること。 なるべくだったら違う手段を探した方がよい。 IL編集とか Player.ItemCheck()をtModLoader用に書き換え、不要な部分(if (item.type == x)など)を削る必要がある。 GlobalNPC GlobalNPC.EditSpawnPool(IDictionary<int, float> pool, NPCSpawnInfo spawnInfo) Example Modにこの説明がなかったので 使い方はModNPC.SpawnChance(NPCSpawnInfo spawnInfo)とほぼ同じ。 if文やSpawnConditonを使いつつ、 pool[NPCID.~ または ModContent.NPCType<T>()] = SpawnCondition.~ * 0.2f; みたいな感じで使う。 pool[npc.type] = float ModWorld ModWorld.ModifyWorldGenTasks(List<GenPass> tasks, ref float totalWeight) もう書くネタが多すぎますね。 常套手段 まず常套手段の書き方を置いておく public override void ModifyWorldGenTasks(List<GenPass> tasks, ref float totalWeight) { int index = tasks.FindIndex(genPass => genPass.Name.Equals("shinies"); // 地中に鉱石を埋めるならshiniesでよいが、場合によっては違うのを入れたりする。 if (index != -1) { tasks.insert(index++, new PassLegacy("(なんの処理か簡単に説明する)", delegate (GenerationProgress progress { progress.Messarge = "(実際に表示されるプログレス名)"; //(処理) })); } } 処理に関わるメソッドはWorldGenの項目やMainの項目を参照。 カスタムワールド生成 tasksに関してだが、List<GenPass>の意味がなんとなく分かるなら、もうカスタムでワールド生成コードを書くことができることに気付けるだろう。 私は最近、スーパーフラットのようなワールド生成がかけるか試したところ、成功したので共有する。 実際、バニラのコードを見れば分かるのだが、 Reset, Terrain, Spawn Point, Final Cleanupは最低限必要と見えるだろう 私は怠惰なのでReset, Terrain, Tile Cleanup, Final Cleanupを残しておいた。 スポーンポイントの設定はそこまで複雑ではない。 さて、ぱっと見上手くいったように見えるのだが、もしかしたら何かが足りない可能性は十二分にある。 これから書くコードは推奨しないうえ、自己責任でお願いします。 あと極力使わないでくれ。似たようなプロセスを得て、正しく実行できるプログラムならいいけど。 正直使うならクレジットに書いてほしい気持ちがある。 さらに言うと、ここに載せるのはエラー処理などは書いていない。 そのままコピペすればいずれ破滅するコードだ。 1 まず、必要最低限(だと思われる)のものをあらかじめ変数で引っこ抜いておく。 public override void ModifyWorldGenTasks(List<GenPass> tasks, ref float totalWeight) { int index = tasks.FindIndex(genpass => genpass.Name.Equals("Reset")); GenPass reset = tasks[index]; index = tasks.FindIndex(genpass => genpass.Name.Equals("Terrain")); GenPass terrain = tasks[index]; index = tasks.FindIndex(genpass => genpass.Name.Equals("Tile Cleanup")); GenPass tileCleanup = tasks[index]; index = tasks.FindIndex(genpass => genpass.Name.Equals("Final Cleanup")); GenPass final = tasks[index]; //続きはここから } 2 元のワールド生成を削除 tasks.Clear(); 3 ResetとTerrainを追加 tasks.Add(reset); tasks.Add(terrain); 4 スーパーフラット tasks.Add(new PassLegacy("SuperFlat", delegate (GenerationProgress progress) { progress.Messarge = "Super Flat"; for (int i = 0; i < Main.maxTilesX; i++) { for (int j = (int)Main.worldSurface - 100; j < Main.maxTilesY; j++) { WorldGen.PlaceTile(i, j, TileID.Dirt); //層ごとにタイル変えたいって? 自分で考えて... ここに書くのめんどくさいのよ... } } for (int i = 0; i < Main.maxTilesX; i++) { int j = (int)Main.worldSurface - 100; WorldGen.SpreadGrass(i, j); // 申し訳なさそうな程度の草生やすプロセス } })); 5 スポーン地点 tasks.Add(new PassLegacy("SetSpwanPointAlt", delegate (GenerationProgress progress) { progress.Messarge = "Set Spawn Point"; Main.spawnTileX = Main.maxTilesX / 2; Main.spawnTileY = (int)Main.worldSurface - 100 - 3; //多分-3じゃなくて-2程度がいいかも })); 6 Cleanup系統追加 tasks.Add(tileCleanup); tasks.Add(final); ↓実際に生成した様子 (ここに載せたコードとは異なるが、やったことはほぼ一緒) https://youtu.be/GeytP4mLqqE ちなみにプロセスバーが進んでいない原因は不明。 もしかしたらtotalWeight使うのだろうけど分からないので 誰か教えてくれ() GlobalTile WorldGen WorldGen.Convert(int i, int j, int conversionType) i, jはブロック座標 conversionTypeは 1で邪悪化 2で神聖化 4で真紅化 Main.item, Main.projectile, Main.npcなど Mainは多すぎるので項目を分ける。 ここではMain.itemなどについて説明。 Main.itemや、Main.npc、Main.projectileはSetDefaultsしたものを入れている配列ではない。 Main.itemは地面に落ちているアイテムの配列。 Main.projectileはフレーム内に存在する発射体の配列 Main.npcはフレーム内に存在するNPCの配列である。 あくまでゲーム内の情報を表すので バニラアイテムの挙動を変えるなどは、Global系を継承しよう。 Vanilla Item Info Recipe ピッケル = 木材x4 + 何らかの素材x12 1.4では8個にコストが下がった Value インゴット(Bar) と 鉱石(Ore)の価格は クラフト前と後では変わらない 鉱石1個当たりの価格 * インゴットにするに必要な数 = その鉱石のインゴット(1個)の価格 Rarity 役に立つサイト集 ↓ tModLoader Github 公式 wiki https://github.com/tModLoader/tModLoader/wiki ↓ Terraria Official Wiki ItemID https://terraria.fandom.com/wiki/Item_IDs ↓ Terraria Officaial Wiki TileID https://terraria.fandom.com/wiki/Tile_IDs ↓ Terraria Officaial Wiki ProjectileID https://terraria.fandom.com/wiki/Projectile_IDs ↓ Terraria Officaial Wiki NPCID https://terraria.fandom.com/wiki/NPC_IDs ↓ Terraria Officaial Wiki BuffID https://terraria.fandom.com/wiki/Buff_IDs ↓ Terraria AmmoID (tModLoader 公式wikiより) https://github.com/tModLoader/tModLoader/wiki/Vanilla-Ammo-IDs
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

タッチパッドのタッチ位置をRaw Input APIで.NETから捕捉する

目的 Windows上でタッチパッドのタッチ位置を捕捉する機能は、.NETでは標準で提供されていませんが、これをWin32のRaw Input APIで.NETから捕捉できるようにします。内容的には、TouchpadGestures AdvancedのC++のコードに基本的にならって、C#のP/Invokeで実行するようにしたものです。 背景 タッチパッド(タッチパネルにあらず)からの入力は、.NETではマウスの動きに変換された後に扱うようになっていて、タッチパッドの入力を直接扱えるようにはなっていません。それで実用的に困ることはないですし、タッチ操作用のデバイスとしてはタッチスクリーンの方が優れているので、タッチパッドにこだわる必要もないです。 ただ、入力デバイスの選択肢としてあって困ることはなく、その一方で先行例が見つからず手が出なかったのですが1、@kamektxさんのTouchpadGestures Advancedを見かけてC++での方法は分かったので、C#でも書けるかなと。 ノートPCのタッチパッドでタブを切り替えるソフトウェアを作りました ~TouchpadGestures Advanced~ その途中で@mfakaneさんのRawInput.Sharpを知り、既にライブラリ化されていたわけですが、いずれにせよこの二つを非常に参考にさせていただきました。 コード タッチパッドからの入力を捕捉するには、WM_INPUTメッセージが送られてくるように登録が必要ですが、ここは難しくないので省きます。問題はWM_INPUTメッセージを受けて、このデータからタッチ位置などの情報をどう取得するかですが、基本的にTouchpadGestures AdvancedのHidManagerの手順にならっています。 大まかな流れは、以下のようになります。 WM_INPUTのlParamからRAWINPUT構造体を取得 RAWINPUT構造体中のRAWINPUTHEADER構造体から、どの種類のデータがあるかを取得 これを使ってRAWINPUT構造体中のRAWHID構造体から、実際のデータの値を取得 始めに、タッチ位置などのコンタクトの値を格納するためのTouchpadContact構造体を作成。ContactIdはタッチ中に各コンタクトに継続的に振られる番号で、これでどのコンタクトかを判別します。XとYはタッチパッドの左上角を原点とした座標。 TouchpadContact.cs public struct TouchpadContact { public int ContactId { get; } public int X { get; } public int Y { get; } public TouchpadContact(int contactId, int x, int y) => (this.ContactId, this.X, this.Y) = (contactId, x, y); } 次に、TouchpadContactを生成するためのTouchpadContactCreatorクラスを作成。これはTouchpadContact中の値は一遍に取得できず、順番に一時保存してから生成する必要があるためです。 TouchpadContactCreator.cs internal class TouchpadContactCreator { public int? ContactId { get; set; } public int? X { get; set; } public int? Y { get; set; } public bool TryCreate(out TouchpadContact contact) { if (ContactId.HasValue && X.HasValue && Y.HasValue) { contact = new TouchpadContact(ContactId.Value, X.Value, Y.Value); return true; } contact = default; return false; } public void Clear() { ContactId = null; X = null; Y = null; } } 最後に、lParamからTouchpadContactを配列で取得するParseInputメソッド。 TouchpadHelper.cs internal static class TouchpadHelper { public static TouchpadContact[] ParseInput(IntPtr lParam) { // Get RAWINPUT. uint rawInputSize = 0; uint rawInputHeaderSize = (uint)Marshal.SizeOf<RAWINPUTHEADER>(); if (GetRawInputData( lParam, RID_INPUT, IntPtr.Zero, ref rawInputSize, rawInputHeaderSize) != 0) { return null; } RAWINPUT rawInput; byte[] rawHidRawData; IntPtr rawInputPointer = IntPtr.Zero; try { rawInputPointer = Marshal.AllocHGlobal((int)rawInputSize); if (GetRawInputData( lParam, RID_INPUT, rawInputPointer, ref rawInputSize, rawInputHeaderSize) != rawInputSize) { return null; } rawInput = Marshal.PtrToStructure<RAWINPUT>(rawInputPointer); var rawInputData = new byte[rawInputSize]; Marshal.Copy(rawInputPointer, rawInputData, 0, rawInputData.Length); rawHidRawData = new byte[rawInput.Hid.dwSizeHid * rawInput.Hid.dwCount]; int rawInputOffset = (int)rawInputSize - rawHidRawData.Length; Buffer.BlockCopy(rawInputData, rawInputOffset, rawHidRawData, 0, rawHidRawData.Length); } finally { Marshal.FreeHGlobal(rawInputPointer); } // Parse RAWINPUT. IntPtr rawHidRawDataPointer = Marshal.AllocHGlobal(rawHidRawData.Length); Marshal.Copy(rawHidRawData, 0, rawHidRawDataPointer, rawHidRawData.Length); IntPtr preparsedDataPointer = IntPtr.Zero; try { uint preparsedDataSize = 0; if (GetRawInputDeviceInfo( rawInput.Header.hDevice, RIDI_PREPARSEDDATA, IntPtr.Zero, ref preparsedDataSize) != 0) { return null; } preparsedDataPointer = Marshal.AllocHGlobal((int)preparsedDataSize); if (GetRawInputDeviceInfo( rawInput.Header.hDevice, RIDI_PREPARSEDDATA, preparsedDataPointer, ref preparsedDataSize) != preparsedDataSize) { return null; } if (HidP_GetCaps( preparsedDataPointer, out HIDP_CAPS caps) != HIDP_STATUS_SUCCESS) { return null; } ushort valueCapsLength = caps.NumberInputValueCaps; var valueCaps = new HIDP_VALUE_CAPS[valueCapsLength]; if (HidP_GetValueCaps( HIDP_REPORT_TYPE.HidP_Input, valueCaps, ref valueCapsLength, preparsedDataPointer) != HIDP_STATUS_SUCCESS) { return null; } uint scanTime = 0; uint contactCount = 0; TouchpadContactCreator creator = new(); List<TouchpadContact> contacts = new(); foreach (var valueCap in valueCaps.OrderBy(x => x.LinkCollection)) { if (HidP_GetUsageValue( HIDP_REPORT_TYPE.HidP_Input, valueCap.UsagePage, valueCap.LinkCollection, valueCap.Usage, out uint value, preparsedDataPointer, rawHidRawDataPointer, (uint)rawHidRawData.Length) != HIDP_STATUS_SUCCESS) { continue; } // Usage Page and ID in Windows Precision Touchpad input reports // https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-precision-touchpad-required-hid-top-level-collections#windows-precision-touchpad-input-reports switch (valueCap.LinkCollection) { case 0: switch (valueCap.UsagePage, valueCap.Usage) { case (0x0D, 0x56): // Scan Time scanTime = value; break; case (0x0D, 0x54): // Contact Count contactCount = value; break; } break; default: switch (valueCap.UsagePage, valueCap.Usage) { case (0x0D, 0x51): // Contact ID creator.ContactId = (int)value; break; case (0x01, 0x30): // X creator.X = (int)value; break; case (0x01, 0x31): // Y creator.Y = (int)value; break; } break; } if (creator.TryCreate(out TouchpadContact contact)) { contacts.Add(contact); if (contacts.Count >= contactCount) break; creator.Clear(); } } return contacts.ToArray(); } finally { Marshal.FreeHGlobal(rawHidRawDataPointer); Marshal.FreeHGlobal(preparsedDataPointer); } } [DllImport("User32.dll", SetLastError = true)] private static extern uint GetRawInputData( IntPtr hRawInput, // lParam in WM_INPUT uint uiCommand, // RID_HEADER IntPtr pData, ref uint pcbSize, uint cbSizeHeader); private const uint RID_INPUT = 0x10000003; [StructLayout(LayoutKind.Sequential)] private struct RAWINPUT { public RAWINPUTHEADER Header; public RAWHID Hid; } [StructLayout(LayoutKind.Sequential)] private struct RAWINPUTHEADER { public uint dwType; // RIM_TYPEMOUSE or RIM_TYPEKEYBOARD or RIM_TYPEHID public uint dwSize; public IntPtr hDevice; public IntPtr wParam; // wParam in WM_INPUT } private const uint RIM_TYPEMOUSE = 0; private const uint RIM_TYPEKEYBOARD = 1; private const uint RIM_TYPEHID = 2; [StructLayout(LayoutKind.Sequential)] private struct RAWHID { public uint dwSizeHid; public uint dwCount; public IntPtr bRawData; // This is not for use. } [DllImport("User32.dll", SetLastError = true)] private static extern uint GetRawInputDeviceInfo( IntPtr hDevice, // hDevice by RAWINPUTHEADER uint uiCommand, // RIDI_PREPARSEDDATA IntPtr pData, ref uint pcbSize); private const uint RIDI_PREPARSEDDATA = 0x20000005; [DllImport("Hid.dll", SetLastError = true)] private static extern uint HidP_GetCaps( IntPtr PreparsedData, out HIDP_CAPS Capabilities); private const uint HIDP_STATUS_SUCCESS = 0x00110000; [StructLayout(LayoutKind.Sequential)] private struct HIDP_CAPS { public ushort Usage; public ushort UsagePage; public ushort InputReportByteLength; public ushort OutputReportByteLength; public ushort FeatureReportByteLength; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)] public ushort[] Reserved; public ushort NumberLinkCollectionNodes; public ushort NumberInputButtonCaps; public ushort NumberInputValueCaps; public ushort NumberInputDataIndices; public ushort NumberOutputButtonCaps; public ushort NumberOutputValueCaps; public ushort NumberOutputDataIndices; public ushort NumberFeatureButtonCaps; public ushort NumberFeatureValueCaps; public ushort NumberFeatureDataIndices; } [DllImport("Hid.dll", CharSet = CharSet.Auto)] private static extern uint HidP_GetValueCaps( HIDP_REPORT_TYPE ReportType, [Out] HIDP_VALUE_CAPS[] ValueCaps, ref ushort ValueCapsLength, IntPtr PreparsedData); private enum HIDP_REPORT_TYPE { HidP_Input, HidP_Output, HidP_Feature } [StructLayout(LayoutKind.Sequential)] private struct HIDP_VALUE_CAPS { public ushort UsagePage; public byte ReportID; [MarshalAs(UnmanagedType.U1)] public bool IsAlias; public ushort BitField; public ushort LinkCollection; public ushort LinkUsage; public ushort LinkUsagePage; [MarshalAs(UnmanagedType.U1)] public bool IsRange; [MarshalAs(UnmanagedType.U1)] public bool IsStringRange; [MarshalAs(UnmanagedType.U1)] public bool IsDesignatorRange; [MarshalAs(UnmanagedType.U1)] public bool IsAbsolute; [MarshalAs(UnmanagedType.U1)] public bool HasNull; public byte Reserved; public ushort BitSize; public ushort ReportCount; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] public ushort[] Reserved2; public uint UnitsExp; public uint Units; public int LogicalMin; public int LogicalMax; public int PhysicalMin; public int PhysicalMax; // Range public ushort UsageMin; public ushort UsageMax; public ushort StringMin; public ushort StringMax; public ushort DesignatorMin; public ushort DesignatorMax; public ushort DataIndexMin; public ushort DataIndexMax; // NotRange public ushort Usage => UsageMin; // ushort Reserved1; public ushort StringIndex => StringMin; // ushort Reserved2; public ushort DesignatorIndex => DesignatorMin; // ushort Reserved3; public ushort DataIndex => DataIndexMin; // ushort Reserved4; } [DllImport("Hid.dll", CharSet = CharSet.Auto)] private static extern uint HidP_GetUsageValue( HIDP_REPORT_TYPE ReportType, ushort UsagePage, ushort LinkCollection, ushort Usage, out uint UsageValue, IntPtr PreparsedData, IntPtr Report, uint ReportLength); } 基本的には粛々とP/Invokeを書けばいいのですが、少し難しいのはRAWHID構造体で、Win32の定義は以下のようになっています。 RAWHID typedef struct tagRAWHID { DWORD dwSizeHid; DWORD dwCount; BYTE bRawData[1]; } RAWHID この1番目のdwSizeHidは各HID inputのデータの長さ、2番目のdwCountはHID inputの数で、3番目のbRawDataが実際のデータですが、byte配列でありながら長さは1になっています。RemarksによればdwSizeHidとdwCountの積がbRawDataの長さになるということですが、Surface Pro 4で実際に実行してみると、dwSizeHidが30、dwCountが1だったので、この積は30となり、計算が合いません。 ではどういうことかというと、これはWin32では時々ある、構造体の後ろにbyte配列が続いている形式で、bRawDataはこの配列の先頭byteを指しています。つまり、Surface Pro 4の例ではRAWINPUT構造体の長さは62だったので、RAWINPUTHEADERの長さが24(16 + 8)で、dwSizeHidとdwCountの長さがそれぞれ4なので、差し引き62 - 24 - 4 - 4 = 30がbRawDataの本当の長さということになります。これは、dwSizeHidとdwCountの積とも符合します。ちなみに、30でコンタクト5つ分のデータがありました。 もしこの長さが30で固定であれば、[MarshalAs(UnmanagedType.ByValArray, SizeConst = 30)]を付ければいいわけですが、固定ではないのでしょうから、これはなし。 したがって、RAWINPUT構造体を取得するときに、先にこのサイズを取得した後、IntPtrにメモリを確保し、このIntPtrにGetRawInputDataのデータを格納し、このIntPtrからRAWINPUT構造体に変換してbRawData以外の値を取得し、さらに同じIntPtrから直接byte配列を取得して、この後半のbRawDataに当たる部分を取り出すということをやっています。2 上記のコードのレポジトリはRawInput.Touchpadです。 テスト テスト環境は以下のとおりです。 Windows 10 21H1 Surface Pro 4(+タイプカバーのタッチパッド) .NET 5.0のWPF タッチパッドに指を同時に5本当てたところ。5つのコンタクトで各指のタッチ位置が示されています。 タッチ中は非常に短い間隔でWM_INPUTメッセージが送られてきますが、指が浮くとその指のコンタクトは途切れてしまうので、動きを自然にトレースするには同じIDのコンタクトを追う必要があります。ちなみに、Surface Pro 4の例ではタッチパッドの左上角が原点の0,0で、右下角が1956,997だったので、物理的なサイズ(101 × 53mm)に対比して結構細かいです。 以上のとおり、もし必要が出てくれば使えるのではないかと思います。 Synapticsのタッチパッド用のものは以前から存在しますが、こちらは汎用的なRaw Input APIを使うので、高精度タッチパッド(Precision Touchpad)であれば動くはず、というのも利点です。 ↩ RawInput.Sharpではunsafeにしてポインターでスマートに処理しています。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む