Quantcast
Channel: 気ままなタンス*プログラミングなどのノートブック
Viewing all 242 articles
Browse latest View live

【intra-mart】Electron+Vue.jsで、IM-LogicDesignerのzipからユーザ定義情報(js, sql等)を読み込み、ソースが確認できる補助アプリ(LogicViewer for IM-LogicDesigner)を公開しました。

$
0
0

こんばんは。

今回はintra-mart IM-LogicDesignerの補助アプリを作りました。

TL;DR

  • IM-LogicDesignerのユーザ定義のソースコード(JavaScript、SQL等)を確認する時に、1つ1つ定義情報をポチポチ開くのが辛かったのでアプリ作った
  • 影響調査とかで今後使えるので、自分の作業改善に繋がって嬉しい

概要

intra-martのIM-LogicDesignerのエクスポートデータ(im_logicdesigner-data.zip)から
JavaScriptやSQL、FreeMakerテンプレートといったユーザ定義情報を読み込み
ツリービューでソースコードの確認ができるビューアです。

ユーザ定義の数が膨大になってくると、調査時など一つ一つのWeb画面から
各定義を開いて確認するのが辛いので、Electron + Vue.jsの勉強がてら作成してみました。

作成したアプリのリポジトリ

github.com

Before(アプリ作成前)

  • [1] こんな感じで一覧から1つの定義を選択

    f:id:rinne_grid2_1:20191121000326p:plain

  • [2] 選択した定義のソースコードの内容を確認

    f:id:rinne_grid2_1:20191121000533p:plain

  • 以下[1],[2]を繰り返し (ユーザ定義の数が2, 3個だったらまだ大丈夫だけど、10個、20個とかになってくるとしんどい。。)

After(アプリ利用時)

IM-LogicDesignerのエクスポートデータさえ読み込めば、エディタ的な画面で参照可能になりました。

アプリ画面

f:id:rinne_grid2_1:20191121002717p:plain

操作イメージ

LogicViewer操作イメージ

アプリのダウンロード・インストール

リリースページよりlogic-viewer-for-im-logicdesigner.Setup.x.x.x-win.exeを見つけてダウンロードします

アプリ操作方法

  1. アプリの左上のファイルアイコンをクリックします
  2. IM-LogicDesignerからエクスポートしたzipファイル(im_logicdesigner-data.zip)を選択します
  3. 【!注意!】エクスポートデータにユーザ定義が含まれている必要があります
  4. zip内容を元に、アプリの左側にJavaScript、SQL等のソースコードが表示されます

Tips

  • アプリ右上のアイコンクリップボードボタンで表示中のソースコードをクリップボードにコピーできます。
  • アプリ右上のアイコン保存ボタンで表示中のソースコードを保存できます。

感想など

  • 普段、「コード書きたいけど作るモノがない」症候群(適当)がよく発症するのですが、今回は作るモノがあったので、短期集中でいけたような気がします
  • ただし、自分自身のVue.jsのコーディングスキルが未熟なこともあり、もっとキレイに書けそうなのに・・・という部分が見受けられました。

今後の機能追加など

  • 入出力パラメータの表示に対応できていないので、パラメータ表示エリアを追加する
  • 上記に関係する部分ですが、REST定義を表示する際に、現状jsonを表示している状態であるため、もっとユーザフレンドリーな見せ方ができるようにする
  • FormaDesignerのアクション定義も、LogicDesignerと同じように、数が増えると辛い問題があるので、これにも対応したい

--- Article Removed ---

$
0
0
***
***
*** RSSing Note: Article removed by member request. ***
***

【聖剣伝説3-ToM_mod】uassetとuexpファイルのバイナリ変更方法まとめ

$
0
0

2020年5月上旬から現在まで、聖剣伝説3ToMのmod作成に夢中になっています。

今回、いわゆるシステム系のmodを作成する時の手順について、備忘録として本記事に残しておこうと思います。

目次

本記事で扱う内容

  • mod作成に必要となるuasset/uexpのアドレス変更位置

本記事で扱わない内容

必要なツール

nameテーブル要素の文字数変更

uassetファイルのnameテーブル要素の文字数を変更した場合に変更すべきデータアドレス一覧

Noアドレス範囲
[1-1] 0x0018-0x001B(※1)
[1-2] (※1)で示された値を持つシリアル値(ファイルの下の方)
[1-3] 0x003D-0x0040
[1-4] 0x0045-0x0048
[1-5]0x0049-0x004C
[1-6]0x00A5-0x00A8
[1-7]0x00A9-0x00AC
[1-8]0x00BD-0x00C0
[1-9]nameテーブル要素の文字数

【例】 パーティにデュランがいなくても、「自由都市マイヤ」にデュランを出現させる

アプローチ: フラグの変更で対応する

対象ファイル:
Content\Game00\Data\Csv\NPCPram\NPCParamMaia.uasset

変更概要:

SAVE_DURAN_JOINというフラグ(nameテーブル要素)をev_2_raf_204_ENDに置換。
これによって、パーティにデュランがいなくても「ev_2_raf_204_END」というフラグがtrueであれば表示されるようになる

・SAVE_DURAN_JOIN : 15バイト  
・ev_2_raf_204_END : 16バイト  
    ->1バイト増えるので、[1-1]-[1-9]の値を1増やしてあげる必要がある

[1-1]
0x0018-0x001B: 49 4F 00 00(10進数で20297)なので、4A 4F 00 00(20298)を設定

[1-2]
20297を示す値を検索すると、0x004E4A-0x004E4Dが見つかる(複数見つかる場合もあるが、変更すべきはファイルの下の方の値)
-> 0x004E4A-0x004E4D:4A 4F 00 00(20298)を設定

[1-3]
0x003D-0x0040: 25 4E 00 00(10進数で20005)なので、 1を足した値 26 4E 00 00(20006) を設定

[1-4]
0x0045-0x0048: 71 2D 00 00(10進数で11633)なので、 1を足した値 72 2D 00 00(11634) を設定

[1-5]
0x0049-0x004C: 8D 4E 00 00(10進数で20109)なので、 1を足した値 8E 4E 00 00(20110) を設定

[1-6]
0x0049-0x004C: 91 4E 00 00(10進数で20113)なので、 1を足した値 92 4E 00 00(20114) を設定

[1-7]
0x00A9-0x00AC: DA 9A 01 00(10進数で105178)なので、1を足した値 DB 9A 01 00(105179)を設定

[1-8]
0x00BD-0x00C0: 95 4E 00 00(10進数で20117)なので、 1を足した値 96 4E 00 00(20118) を設定

[1-9]
nameテーブル要素の文字列 SAVE_DURAN_JOINをev_2_raf_204_ENDに置換

  • 0x2AB2-0x2AC0のSAVE_DURAN_JOINが置換&追加され、0x2AB2-0x2AC1にev_2_raf_204_ENDが入る
    • 0x2AB2の直前の4バイトを見てみると、10 00 00 00(10進数で16)になっている・・・これは、SAVE_DURAN_JOINという文字列自体の長さを示している。
    • 文字列の場合、最後は00(NUL)で終わるという仕様。したがって、SAVE_DURAN_JOINという15バイトと00(NUL)の1バイトで、16バイトというデータになっている
  • 今回、ev_2_raf_204_ENDという16バイトと00(NUL)の1バイトに変更したため、17バイトに変更する必要がある
    • 0x2AAE-0x2AB1: 10 00 00 00(10進数で16)なので、1を足した値 11 00 00 00 を設定

nameテーブルへの要素の追加

uassetファイルのnameテーブルの要素の追加方法と、その場合に変更すべきアドレス

Noアドレス範囲
[2-1] 0x0018-0x001B(※2)
[2-2] (※2)で示された値を持つシリアル値(ファイルの下の方)
[2-3] 0x003D-0x0040
[2-4] 0x0045-0x0048
[2-5] 0x0049-0x004C
[2-6] 0x00A5-0x00A8
[2-7] 0x00A9-0x00AC
[2-8] 0x00BD-0x00C0
[2-9] 0x0029-0x002C:nameテーブルの要素数

nameテーブルの要素のバイト列は以下のようになっている

バイト範囲項目の示す意味例(※3)
最初の4バイト文字数16 00 00 00(リトルエンディアンで22)
それ以降の00(NUL)まで文字列全体6D 73 67 5F 62 61 74 5F 73 6B 69 6C 6C 5F 6E 61 6D 65 30 30 31 00(msg_bat_skill_name001)
00(NUL)の次の2バイトnon_case_preserving_hash5A 81
その次の2バイトcase_preserving_hash7C 99

(※3) Content\Game00\Data\Csv\SkillData\SkillDataEditTableDEF.uassetファイルのmsg_bat_skill_name001の場合

f:id:rinne_grid2_1:20200724144048p:plain

したがって、uassetのnameテーブルの要素を追加する場合、 文字数を示す(4バイト) + 文字列本体 + NUL(1バイト) + 末尾のハッシュ(2バイト+2バイト)をセットで追記する必要がある。

【例】 ラベル表示内容の置き換え

変更概要:

Content\Game00\Data\Csv\SkillData\SkillDataEditTableDEF.uasset内のmsg_bat_skill_name001というName要素を
Content\Game00\Data\Csv\SkillData\SkillDataEditTableATK.uassetに追加し、
SkillDataEditTableATK.uasset側のmsg_bat_skill_name004をmsg_bat_skill_name001に差し替える

SkillDataEditTableDEF.uasset
・0x1F5E-0x1F7Bをコピー(計:30バイト 文字サイズ(4バイト) + 文字列全体(21バイト) + 文字終端(1バイト) + 末尾のハッシュ(2バイト+2バイト)

f:id:rinne_grid2_1:20200724144048p:plain

SkillDataEditTableATK.uasset
・0x36DDにコピーしたバイト列を追記

f:id:rinne_grid2_1:20200724150129p:plain

・[2-1]-[2-8]に30を加算
・[2-9]に1を加算・・・407→408(nameテーブルに要素を追加したため)
・msg_bat_skill_name004のインデックス:271
 →SkillDataEditTableATK.uexpをバイナリエディタで開き、271の値を検索し、該当部分を408に置き換え

f:id:rinne_grid2_1:20200724150554p:plain

これで、追加したnameテーブル要素を利用することができる。

[補足:nameテーブルの要素一覧の取得方法]
msg_bat_skill_name004という文字のインデックスが271であるということは、
1 - uasset to JSON--dumpnames.batを実行することによって取得できる、namesファイルで特定することができる。

namesファイルは、uasset内で利用されているname要素の一覧を表している。
なお、nameテーブルのインデックスは0から始まるため注意が必要。

例:
仮にSkillDataEditTableATK.uasset(結合済み)に対して、uasset to JSON--dumpnames.batを実行した場合 267行目:msg_bat_linkability_name043 →しかし、このname要素をuexp側で指定する場合は、行数の267ではなく、266(行数-1)を指定する必要がある

uexpファイルのバイト列フォーマットについて

uexpファイルは、(DataTableにおいては)nameテーブルのインデックスの組み合わせとその値が指定されているようです。

IntPropertyの場合

Noバイト範囲項目の示す意味
[3-1]8バイト整数のNameプロパティ(IntProperty)
[3-2]8バイト整数値

・仮に、Nameテーブルのインデックスが以下の値を示している場合
 ・IntPropertyのNameインデックス:86(0x56)

【整数値5を表すバイト列】
[3-1] 56 00 00 00 00 00 00 00
[3-2] 05 00 00 00 00 00 00 00

BoolPropertyの場合

Noバイト範囲項目の示す意味
[4-1]16バイト真偽値のNameプロパティ(BoolProperty)
[4-2]1バイト真偽値

・仮に、Nameテーブルのインデックスが以下の値を示している場合
 ・BoolPropertyのNameインデックス:5(0x05)

【true】(真)
[4-1] 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[4-2] 01

【false】(偽)
[4-1] 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[4-2] 00

ArrayPropertyの場合

Noバイト範囲項目の示す意味
[5-1]8バイト配列のNameプロパティ(ArrayProperty)
[5-2]8バイト配列全体のバイト数
[5-3]9バイト配列内の変数型のNameプロパティ
[5-4]4バイト配列の要素数
[5-5]以降のバイト配列要素数*4のバイト列が続く*1

・仮に、Nameテーブルのインデックスが以下の値を示している場合
 ・ArrayPropertyのNameインデックス:4(0x04)
 ・IntPropertyのNameインデックス:114(0x72)

【配列要素が存在しない時のバイト列】
[5-1] 04 00 00 00 00 00 00 00
[5-2] 04 00 00 00 00 00 00 00
[5-3] 72 00 00 00 00 00 00 00 00
[5-4] 00 00 00 00

【配列要素が存在する時のバイト列】(要素10(0x0A)が存在する場合)
[5-1] 04 00 00 00 00 00 00 00
[5-2] 08 00 00 00 00 00 00 00
[5-3] 72 00 00 00 00 00 00 00 00
[5-4] 01 00 00 00
[5-5] 0A 00 00 00

StructPropertyの場合

Content\Game00\Data\Csv\ShopData\ShopItemDataCsv.uexpの場合

Noバイト範囲項目の示す意味
[6-1]8バイト配列要素の種類(ItemListなど)
[6-2]8バイト配列のNameプロパティ(ArrayProperty)
[6-3]8バイト配列全体のバイト数
[6-4]9バイト構造体のNameプロパティ(StructProperty)
[6-5]4バイト配列の要素数
[6-6]8バイト配列要素の種類(ItemListなど)
[6-7]8バイト構造体のNameプロパティ(StructProperty)
[6-8]8バイト構造体のバイト数(※4)
[6-9]8バイト構造体の種類(StG00ShopCommonItemDataStructなど)
[6-10]17バイト*2
[6-11]これ以降データ本体

(※4) [6-3]から[6-5~6-10]までのバイト数を引いた値

つまり、StructPropertyの要素[6-11]を追加した場合、
[6-3],[6-8]のバイト数と[6-5]の配列要素数を増やす必要がある。

・仮に、Nameテーブルのインデックスが以下の値を示している場合

インデックスName要素16進数
4ArrayProperty0x04
79ItemList_20_7AB86DFD4DFFC48B0EB0749BF57435290x4F
117StG00ShopCommonItemDataStruct0x75
119StructProperty0x77

[6-1] 4F 00 00 00 00 00 00 00 (ItemList_20_7AB86DFD4DFFC48B0EB0749BF5743529)

[6-2] 04 00 00 00 00 00 00 00 (ArrayProperty)

[6-3] 65 08 00 00 00 00 00 00 (配列全体で2149バイト)

[6-4] 77 00 00 00 00 00 00 00 00 (StructProperty)

[6-5] 08 00 00 00 (配列要素数8)

[6-6] 4F 00 00 00 00 00 00 00
(ItemList_20_7AB86DFD4DFFC48B0EB0749BF5743529)

[6-7] 77 00 00 00 00 00 00 00 (StructProperty)

[6-8] 30 08 00 00 00 00 00 00
(構造体全体のバイト数:2149-53=2096)

[6-9] 75 00 00 00 00 00 00 00
(StG00ShopCommonItemDataStruct)

[6-10] EA DA 43 AF 77 64 4B 46 89 FA 06 33 0B AF E2 25 00

[6-11] (~0x0BF7のアドレスまで)

バイナリ編集で作ったシステム系のmod

上記の情報は、以下のmod作成時に調べました。 ゲームmod作成は今までやったことがなく、Unreal Engine4の知識もないため、見当違いのことを書いていたらすみません。

*1:これに当てはまらないケースもあるかもしれない

*2:たぶんDataStructの識別子

【雑記】[自己分析]なにがやりたいか聞かれて言葉に詰まった話

$
0
0

なにがやりたいか

懇親会で上司や先輩と飲んでいた時、質問されました。

「よく家でコーディングやってるみたいだけど、やりたいことは何?」

その質問を受けて、少々困惑してしまいました。 なぜならば、自分自身では何がやりたいのか、明確にできているつもりだったのですが、 客観的に見ると、明確に意識できていないことに気づいてしまったのです。

根底にある考え

基本的にはプラグインの開発やフリーゲームの作成を行い、 「人の役に立つことをしたい、人を楽しんでもらいたい」という想いの元に動いています。

仕事でも他の人から「これ作って」「これやって」と言われたら、 その人のことを優先的に考えて行動しています。*1

(どうやったら使いやすいか常に考えたり、はやく使わせてあげたいと常々考えている気がします)

しかし、人の役に立つことをしたいというのは、 先輩からしたら、曖昧な考えであったようでした。 今振り返ると確かにその指摘も妥当だと感じました。

指摘を受けて振り返る

なんとなくではあったのですが、 自分から主体的にモノを考え、何かに取り組むことができておらず、悩み続けていました。

何かを作ろうとしても、 「既存のサービスがあるから作っても無駄」「1から作るにも、数式が入ってくると、膨大な時間がかかりそう」 こんな理由で反射的・無意識的に断念していることがわかりました。

新しいプログラミング言語を学ぶにしても、そこからどんなものを生み出すか見当がつかない状況といえば良いのでしょうか。 (仕事で新言語での開発等に携わっていれば、ノウハウ蓄積の観点で違ったものになったのかもしれません)

本当に先輩が聞きたかったことは何か

会話にズレがあったようにも感じます。

先輩としては、BtoBか、BtoCかという、金銭の絡む話の中で「やりたいこと」が聞きたかった様子でした。

しかし、現在の自分としては報酬等、かかわりのない部分で動いています(が、やりたいことは明確に言えません)*2

今後自分はなにをしていくべきか

未完成の状態にあるものを挙げてみることにします

  • ツクールMVゲーム
  • ツクールVX aceゲーム
  • プラグイン(RecollectionMode)の機能追加

最近はツクールMVも影響で、JavaScriptを学ぶことが多くなってきました。 (ツクールMVJavaScript、Node.jsやES2015)

クライアント側で完結するものだけではなくWebアプリも作りたいと考えています(主にDjango。だけどネタがないのです・・・)

少し整理すると、「やりたいことは盛りだくさんな状況」であるが、やりたいことが多すぎて、結局何にも取り組めていない状況と言えそうです。

質問に即答できなかったことを後悔しつつ、自己分析を実施するきっかけとなったので、結果オーライですね。

*1:もちろん自分の仕事が忙しいときは少々ずれることはあります

*2:というよりも、今は言えないだけで作りたいものがでてきたら当然それが最優先になるので、一時的なものだとも思うのです

【RPGツクールMZ】ゲームに回想モードを追加するプラグイン(RecollectionModeMZ)を公開しました

$
0
0

RPGツクールMZ向けに、アドベンチャーゲーム等でよく見られる「シーン回想」や「CG閲覧」といった いわゆる「回想モード」機能を追加するプラグインを作りました。

もともとツクールMZの発売直後(2020年8月)に公開したかったのですが、仕事が忙しくなかなか着手することができず、だいぶ延びてしまいました。

イメージ

f:id:rinne_grid2_1:20201129140204p:plain

f:id:rinne_grid2_1:20201129140240p:plain

f:id:rinne_grid2_1:20201129140301p:plain

デモ

(※音が鳴るのでご注意ください)

http://www.rinsymbol.sakura.ne.jp/tkool/mz/RecollectionModeMZ/

利用について

  • MITライセンスの元で公開されています
  • 非商用/商用ゲーム問わずにご利用いただけます。
  • 年齢制限のあるコンテンツでのご利用も可能です。
  • 利用報告は必要ありませんが、ゲーム公開した際に、皆様のゲーム情報やURL等をコメントやツイートで教えていただけると、とても嬉しいです。

サンプルプロジェクトのファイルの利用について

  • 対象ファイル
    • never_watch_picture.png
    • blank_memories.ogg
  • 上記ファイルに関しても、非商用/商用ゲーム問わずご利用いただけます
  • 年齢制限のあるコンテンツでの利用も可能です
  • 利用報告は必要ありません

ソースコード&サンプルプロジェクト

https://github.com/rinne-grid/tkoolmz_plugin_RecollectionModeMZ

その他

ツクールMV版の回想モードプラグインと比べ、不足している機能があるため、 時間ができたらアップデートを実施していきます。

【intra-mart】[IM-FormaDesigner] デプロイ無しでセレクトボックスの制限値を変更する

$
0
0

こんにちは。

今回は、intra-martに関する記事です。

TL;DR

  • product_80_selectbox(2017Summer以降のバージョンで利用可能)
  • warファイルを差し替えなくとも、IM-FormaDesigner上だけのコーディングで、セレクトボックスの制限値の変更が可能な暫定対応
  • システム停止調整業務が面倒な場合に利用すると良いかも

目次

概要

intra-martのIM-FormaDesignerのセレクトボックスを利用する際、 システムの初期設定では30項目までしか表示されません。 31項目以降は切り捨てられて非表示になってしまいます。 f:id:rinne_grid2_1:20201225210548p:plain

これは、WEB-INF/conf/forma-config.xmlのselectbox-item-limitで設定されている値であり、 対象値を変更することで対応ができるようです。 しかしこれは同時に、設定ファイルを書き換えるといったサーバー上ファイルの変更やwarファイルの差し替えが必要になるということを表します。

こんな場合、きっと下記のような問題にぶち当たり、即時反映ができないかもしれません。

  • 本番環境への直接のファイル適用はNGである(ほとんどそうだと思われます)
  • intra-martテナントの利用規模が大きく、システム停止及び移送調整に時間がかかる、あるいは定例停止日まで対応できない
  • でもユーザは急いで対応してほしいと言う

解決策

IM-FormaDesigner上のカスタムスクリプトで対応する。

FormaDesigner ->アクション設定 ->初期表示イベントのカスタムスクリプトとして、下記のソースコードを指定します

f:id:rinne_grid2_1:20201225211503p:plain

ポイント

  • セレクトボックスのフィールド識別IDをRNGD_CONST.FIELD_ID_SELECTBOX_ITEM変数に指定します
    • サンプルではselectbox1
  • セレクトボックスに表示したい件数をRNGD_CONST.APPLY_LIST_LIMIT_COUNT変数に指定します
    • サンプルでは10000
( function( $ ){var RNGD_CONST = {};
  RNGD_CONST.FIELD_ID_SELECTBOX_ITEM = "selectbox1"; // ここにセレクトボックスのフィールド識別IDを指定します
  RNGD_CONST.APPLY_LIST_LIMIT_COUNT = 10000; // ここにセレクトボックスに表示したい件数を指定します// PC版かどうかを判断するfunction isPc() {return forma.funcs.getDisplayClientType() === "pc";
  }var responseType = $("#imfr_response_type").val();
  switch(responseType) {case"REGISTRATION":
    case"EDIT":
      if (isPc()) {var listId = $("#" + RNGD_CONST.FIELD_ID_SELECTBOX_ITEM).find("select")[0].id;

        if (!window.formaItems) {window.formaItems = {};
        }if (!window.formaItems.product_80_selectbox) {window.formaItems.product_80_selectbox = {};
        }// エラー表示if (!window.formaItems.product_80_selectbox.AcceptError) {window.formaItems.product_80_selectbox.AcceptError = function (event) {if ($('span#' + event.inputId).parent().find('span.forma-icon-exclamation-red').size() == 0) {
              $('span#' + event.inputId).find('input:text').addClass('imfr_item_input_error_check').css('outline', 'none')
                .parent().append('<span class="forma-icon-exclamation-red"></span>');
            }}}// エラー表示クリアif (!window.formaItems.product_80_selectbox.ClearError) {window.formaItems.product_80_selectbox.ClearError = function (event) {
            $('span#' + event.inputId).find('input:text').removeClass('imfr_item_input_error_check').parent().find('span.forma-icon-exclamation-red').remove();
          }}

        $(document).ready(function () {var fieldSize = '500';
          var item_id = $("#" + RNGD_CONST.FIELD_ID_SELECTBOX_ITEM).parent().attr("id");
          var input_id = RNGD_CONST.FIELD_ID_SELECTBOX_ITEM;

          $('#' + listId).children('option').each(function (index) {
            $(this).text($(this).text().split('').join(String.fromCharCode(8203)));
          });
          reflectStyle();

          if (!window.formaItems.product_80_selectbox.eventSettingInfo) {window.formaItems.product_80_selectbox.eventSettingInfo = {};
          }if (!window.formaItems.product_80_selectbox.getItemData) {window.formaItems.product_80_selectbox.getItemData = {};
          }if (!window.formaItems.product_80_selectbox.setItemData) {window.formaItems.product_80_selectbox.setItemData = {};
          }window.formaItems.product_80_selectbox.eventSettingInfo[item_id] = function (eventType) {var selectTag = $('[name="' + RNGD_CONST.FIELD_ID_SELECTBOX_ITEM + '"]');
            var selectorObj = selectTag;
            return selectorObj.selector;
          };

          //外部連携データ収集window.formaItems.product_80_selectbox.getItemData[input_id] = function () {var selector = $('#' + listId);
            var castsData = forma.cooperation.castsData('0', selector.val());
            return castsData;
          };

          // 外部連携データ反映window.formaItems.product_80_selectbox.setItemData[input_id] = function (selectboxInputs) {var fieldSize = '500';
            var tabindex = '1';
            var limit = RNGD_CONST.APPLY_LIST_LIMIT_COUNT;
            var displayLimit = '-1';
            var input_id = RNGD_CONST.FIELD_ID_SELECTBOX_ITEM;

            if ((selectboxInputs.master !== undefined) &&
              (selectboxInputs.master[input_id] !== undefined) &&
              (selectboxInputs.master[input_id].length !== 0) &&
              (selectboxInputs.master[input_id][0]['value_' + input_id] !== undefined)) {// 取得データを元にタグを生成var inputId = RNGD_CONST.FIELD_ID_SELECTBOX_ITEM;
              var thisList = $('#' + inputId);
              var selectedKey = $('[name="escape_' + input_id + '"]').val();
              var strHtml = [];
              var checkValueArray = [];
              var check = false;
              var option = {};
              var select = {};
              var selectTag = $('[name="' + RNGD_CONST.FIELD_ID_SELECTBOX_ITEM + '"]');

              // 初期表示時selectタグを生成if (selectboxInputs && !selectTag.attr('id')) {
                selectTag.attr('id', listId);
              }// option要素をクリア
              selectTag.children('option').remove();
              // 入力値のクリアvar input_ = $('#' + listId).next('input');
              selectTag.val('');
              input_.val('');

              if (selectboxInputs.master[input_id].length > 0) {// option要素の作成// 空白行
                option = new Option('', '');
                select = document.getElementById(listId);
                select.options[select.options.length] = option;


                selectedKey = (selectboxInputs.data[input_id] !== undefined) ? selectboxInputs.data[input_id] : selectedKey;
                var dataNumber = (displayLimit > 0 || selectboxInputs.master[input_id].length < limit) ? selectboxInputs.master[input_id].length : limit;
                for (var i = 0; i < dataNumber; i++) {
                  check = false;
                  // 選択値を反映var optionKey = (selectboxInputs.master[input_id][i]['key_' + input_id] === undefined) ? selectboxInputs.master[input_id][i]['value_' + input_id] : selectboxInputs.master[input_id][i]['key_' + input_id];
                  if (selectedKey === selectboxInputs.master[input_id][i]['value_' + input_id]) {
                    check = true;
                  }
                  option = new Option(String(optionKey).split('').join(String.fromCharCode(8203)), selectboxInputs.master[input_id][i]['value_' + input_id], '', check);
                  select = document.getElementById(listId);
                  select.options[select.options.length] = option;
                }/*                          //ie7,8NG                          selectTag.append( $( '<option>' ).val( args.data[i][sTargetIdKey] )                                        .text( args.escapedData[i][sTargetId] )                                        .attr( 'selected', check ) );                */}// 表示値セット
              input_.val(selectTag.children(':selected').text());
              selectTag.change();
            }else{if (selectboxInputs.data[input_id] !== undefined) {var options = document.getElementById(listId).getElementsByTagName('option');
                var displayValue;
                for (var i = 0; i < options.length; i++) {if (options[i].value == selectboxInputs.data[input_id]) {
                    displayValue = options[i].text;
                  }}
                $('#' + listId).val(selectboxInputs.data[input_id]).change();
                $('#' + listId).frSelectbox({'menuSize': -1 }).nextAll('input:first').val(displayValue);
              }}};

          // イベントアクション用関数if (!window.formaItems.product_80_selectbox.changeInputMode) {window.formaItems.product_80_selectbox.changeInputMode = function (controlSetting) {var editableFlg = (controlSetting.mode === 'valid') ? true : false;

              if (editableFlg) {
                $('select[name="' + controlSetting.inputId + '"]').nextAll('input,button').removeAttr('disabled readonly').not('input').css('cursor', '');
              }else{
                $('select[name="' + controlSetting.inputId + '"]').nextAll('input,button').attr({'disabled': 'disabled', 'readonly': true}).not('input').css('cursor', 'default');
              }}}function reflectStyle() {if (isFinite(fieldSize)) {
              $('[name="' + RNGD_CONST.FIELD_ID_SELECTBOX_ITEM + '"]').width(fieldSize + 'px');
            }// create時に空にしても自動でtitleに半角が入るのを制御
            $('#' + listId).frSelectbox({'menuSize': -1 }).nextAll('button:first').attr('title', '').end()
              .nextAll('input:first').attr('tabindex', '1');

            $('#' + listId).frSelectbox({'menuSize': -1 }).nextAll('input:first')
              .css('background-color', '')
              .css('color', '')
              .css('border-color', '')



              .css('font-family', '')
              .css('font-weight', 'normal')
              .css('font-style', 'normal')
              .css('text-decoration', 'none')
              .addClass(' imfr_input_shadows');
          }});
      }else{
        $("#" + RNGD_CONST.FIELD_ID_SELECTBOX_ITEM + "_properties").attr("data-imfr-limit", RNGD_CONST.APPLY_LIST_LIMIT_COUNT);
      }break;
    case"POSTSCRIPT":
    case"REFERENCE":
      if (isPc()) {var itemId = $("[name=" + RNGD_CONST.FIELD_ID_SELECTBOX_ITEM + "]").parent().parent().attr("id");
        var targetId = $("#" + itemId + "").find("input")[0].name;
        console.log(targetId);
        if (!window.formaItems) {window.formaItems = {};
        }

        $(document).ready(function () {var input_id = RNGD_CONST.FIELD_ID_SELECTBOX_ITEM;
          if (!window.formaItems.product_80_selectbox) {window.formaItems.product_80_selectbox = {};
          }if (!window.formaItems.product_80_selectbox.getItemData) {window.formaItems.product_80_selectbox.getItemData = {};
          }if (!window.formaItems.product_80_selectbox.setItemData) {window.formaItems.product_80_selectbox.setItemData = {};
          }// 外部連携データ収集window.formaItems.product_80_selectbox.getItemData[input_id] = function () {var selector = $('input[name="' + input_id + '"]');
            var castsData = forma.cooperation.castsData('0', selector.val());
            return castsData;
          };
          // 外部連携データ反映window.formaItems.product_80_selectbox.setItemData[input_id] = function (selectboxInputs) {var fieldSize = '500';
            var limit = RNGD_CONST.APPLY_LIST_LIMIT_COUNT;
            if ((selectboxInputs.master !== undefined) &&
              (selectboxInputs.master[input_id] !== undefined) &&
              (selectboxInputs.master[input_id].length !== 0) &&
              (selectboxInputs.master[input_id][0]['value_' + input_id] !== undefined)) {// 取得データを元にタグを生成var inputId = RNGD_CONST.FIELD_ID_SELECTBOX_ITEM;
              var thisList = $('#' + inputId);
              var selectedKey = $('[name="escape_' + input_id + '"]').val();
              var value = '';
              var optionLocale = $('input[name="' + input_id + '_locale"]').val();
              var masterData = [];
              selectedKey = (selectboxInputs.data[input_id] !== undefined) ? selectboxInputs.data[input_id] : selectedKey;
              var dataNumber = (selectboxInputs.master[input_id].length < limit) ? selectboxInputs.master[input_id].length : limit;
              for (var i = 0; i < dataNumber; i++) {// 選択値を反映if (selectedKey === selectboxInputs.master[input_id][i]['value_' + input_id]) {
                  value = (selectboxInputs.master[input_id][i]['key_' + input_id] === undefined) ? selectboxInputs.master[input_id][i]['value_' + input_id] : selectboxInputs.master[input_id][i]['key_' + input_id];
                }
                masterData[i] = {};
                masterData[i].display_names = {};
                masterData[i].display_names[optionLocale] = selectboxInputs.master[input_id][i]['key_' + input_id];
                masterData[i].send_value = selectboxInputs.master[input_id][i]['value_' + input_id];
              }
              $('input[name=' + targetId + ']').val(value);
              $('input[name=' + targetId + ']').attr("title", value);
              $('input[name="' + input_id + '_listData"]').val(ImJson.toJSONString(masterData));
            }else{if (selectboxInputs.data[input_id] !== undefined) {var locale = RNGD_CONST.FIELD_ID_SELECTBOX_ITEM + '_locale';
                var propertyListData = ImJson.parseJSON($('input[name="' + input_id + '_listData"]').val());
                var value = '';
                var hiddenValue = '';
                for (var i = 0; i < propertyListData.length; i++) {if (propertyListData[i].send_value == selectboxInputs.data[input_id]) {
                    value = propertyListData[i].display_names[$('[name=' + locale + ']').val()];
                    hiddenValue = propertyListData[i].send_value;
                    break;
                  }}
                $('input[name=' + targetId + ']').val(value);
                $('input[name=' + targetId + ']').attr("title", value);
                $('input[name="' + input_id + '"]').val(hiddenValue);
              }}};
        });

      }else{
        $("#" + RNGD_CONST.FIELD_ID_SELECTBOX_ITEM + "_properties").attr("data-imfr-limit", RNGD_CONST.APPLY_LIST_LIMIT_COUNT);
      }break;
  }} )( jQuery );

これで、FormaDesignerの画面を開いて見ると、セレクトボックスの制限値が変わり、31個以上の項目が表示されてることがわかります。

f:id:rinne_grid2_1:20201225211848p:plain

やっていること

どうやらproduct_80_selectboxは、forma-config.xmlのselectbox-item-limitの値を取得し、 セレクトボックス部品を生成・表示する際に動的にその値を埋め込みJavaScriptを生成しているようです。

そこで、埋め込まれたJavaScriptを抽出しselectbox-item-limitに該当する値を書き換えつつ、 それを初期処理タイミングで、再実行し、元々埋め込まれたセレクトボックスのアイテムの挙動の差し替えを行っているイメージです。

サンプル定義はこちら

IM-FormaDesigner定義

rngd_sample_rewrite_sbox_limit.zip

データソース定義

datasource_for_rngd_sample_rewrite_sbox_limit.zip

最後に注意点

本件はあくまでもサーバー上のforma-config.xml内容の設定変更が困難な状況であるという場合への暫定対策であり、パフォーマンス等を考慮すると妥当な手順とは言えません。 本当にこの内容で暫定対応を実施すべきかどうかを熟考した上で適用するべきと考えます。 この対応によって発生した障害等について、一切責任を負いません。

【intra-mart】[IM-FormaDesigner]一覧選択部品のページング数を増やす

$
0
0

概要

  • 一覧選択部品のページング数が4種類(15, 30, 45, 60)しかなくてデータが増えた時辛い

f:id:rinne_grid2_1:20200601194639p:plain

これはWEB-INF/conf/forma-config.xmlのpage_patternで設定されている値を書き換えれば良いですが、 下記の記事と同様、システム停止によるデプロイもしくはサーバー上にあるforma-config.xmlの置き換えが必要となります。

【intra-mart】[IM-FormaDesigner] デプロイ無しでセレクトボックスの制限値を変更する - 気ままなタンス*プログラミングなどのノートブック

また、intra-martテナントが大規模である場合に、共通的なforma-config.xmlの設定を変更するということ自体が 他のアプリケーションへの影響等でとても困難であるかもしれません。

対応

他のアプリケーションには影響を与えず、デプロイもせず、1つのFormaDesignerアプリ内で完結するという 課題を解決するためにこの方法を利用しました。

  • 一覧選択部品のソースコードを初期表示イベントで書き換える

f:id:rinne_grid2_1:20200601194748p:plain

/** * 一覧選択アイテムを利用する関数を提供します。 * @class 一覧選択アイテムを利用する関数を提供します。 * * @constructor * @version 1.0 * @since 8.0 * @author INTRAMART */// # Forma書き換え時の定数定義if(!window.CUSTOM_FORMA_CONST) {var CUSTOM_FORMA_CONST = window.CUSTOM_FORMA_CONST = {};
}
CUSTOM_FORMA_CONST.PAGE_SELECT_NUM_APPEND_LIST = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000];

if (!window.forma) {var forma = window.forma = {};
  }// # itemselectの定義else{var forma = window.forma;
  }if(!forma.hasOwnProperty("itemselect")) {
      forma.itemselect = {};
  }//   forma.itemselect = {};/**   * 一覧選択画面を表示するためのパラメータを設定します。   * @param {Object} param 一覧選択画面を表示するための構成情報   * @version 8.0.10   * @since 8.0   */
  forma.itemselect.addItemSelectPageParams = function (param) {
      forma.itemselect.itemSelectUrl              = param.url;
      forma.itemselect.itemSelectItemId           = param.itemId;
      forma.itemselect.itemSelectInputId          = param.inputId;
      forma.itemselect.itemSelectPageTitle        = param.pageTitle;
      forma.itemselect.itemSelectPageInSetting    = param.pageInSetting;
      forma.itemselect.itemSelectPageOutSetting   = param.pageOutSetting;
      forma.itemselect.itemSelectPageSelectType   = param.pageSelectType;
      forma.itemselect.itemSelectPageDatabaseList = param.pageDatabaseList;
  };
  
  /**   * 一覧選択画面を表示します。   * 本関数を事項する前にaddItemSelectPageParams関数で必ずパラメータを設定してください。   * @param {Object} param 一覧選択画面を表示するための情報   * @version 8.0.14   * @since 8.0   */
  forma.itemselect.openSelectItemPage = function (param) {if( typeofwindow.forma.itemselect.dialog !== 'undefined' ) return;
      if( (param.inputId !== "") && (param.inputId !== forma.itemselect.itemSelectInputId) ) return;
      var data = {
          im_selectitem_itemId       : forma.itemselect.itemSelectItemId,
          im_selectitem_inputId      : forma.itemselect.itemSelectInputId,
          im_selectitem_title        : forma.itemselect.itemSelectPageTitle,
          im_selectitem_inSetting    : forma.itemselect.itemSelectPageInSetting,
          im_selectitem_outSetting   : forma.itemselect.itemSelectPageOutSetting,
          im_selectitem_selectType   : forma.itemselect.itemSelectPageSelectType,
          im_selectitem_databaseList : forma.itemselect.itemSelectPageDatabaseList,
          im_selectitem_data         : ImJson.toJSONString(param.pageData),
          im_selectitem_option       : ImJson.toJSONString(param.pageOption)
      };
  
      var dialogHeight = data.im_selectitem_selectType === 'multi' ? 700 : 650;
      jQuery('<div class="imfr-dialog-page"/>').imuiPageDialog({
          title     : $('<div>').text(data.im_selectitem_title).html(),
          url       : forma.itemselect.itemSelectUrl,
          parameter : data,
          method    : 'post',
          width     : 875,
          height    : dialogHeight,
          modal     : true,
          zIndex    : 10000,
          toolbarRight : [{
              iconClass : "im-ui-icon-common-16-refresh",
              href      : "javascript:void(0);"}],
          open      : function (event, ui) {
              forma.itemselect.dialog = jQuery(this);  
           
          },
          close     : function (event, ui) {
              $(window).unbind('resize');
              jQuery(this).dialog('destroy');
              jQuery(event.target).remove();
              forma.itemselect.dialog = undefined;
              // タブインデックスを戻すvar iconSelector = !forma.util.isBlank( forma.itemselect.itemSelectIndex ) && forma.itemselect.itemSelectIndex > -1 ?
                  $( '[name="' + forma.itemselect.itemSelectInputId + '_index"]' ).eq( forma.itemselect.itemSelectIndex ) :
                  $( '[name="' + forma.itemselect.itemSelectInputId + '_index"]' );
              // Android端末の場合はフォーカスを戻さないif( navigator.userAgent.indexOf( 'Android' ) != -1 ) return;
              var index = !forma.util.isBlank( forma.itemselect.itemSelectIndex ) && forma.itemselect.itemSelectIndex > -1 ?
                          $( '[name="' + forma.itemselect.itemSelectInputId + '_display"]' ).eq( forma.itemselect.itemSelectIndex ).attr('tabindex')-0+1 :
                          $( '[name="' + forma.itemselect.itemSelectInputId + '_display"]' ).attr('tabindex')-0+1;
              iconSelector.attr('tabindex', index);
              iconSelector.focus();
              if( !( $( ':focus' ) === iconSelector ) ){window.focus();
                iconSelector.focus();
              }
              forma.itemselect.itemSelectIndex = '';
          },
          // # imuiPageDialogのAjaxイベント終了後にページング処理を挿入
          onAjaxComplete : function() {// # ページングの表示件数を拡張する
            CUSTOM_FORMA_CONST.PAGE_SELECT_NUM_APPEND_LIST.forEach(function(pgNum) {
                $(".imfr-dialog-page").find(".ui-pg-selbox")
                    .append($('<option>').html(pgNum).val(pgNum))
            });   
          }});
  }

最後に注意点

本件はあくまでもサーバー上のforma-config.xml内容の設定変更が困難な状況であるという場合への暫定対策であり、 パフォーマンス等を考慮すると妥当な手順とは言えません。 本当にこの内容で暫定対応を実施すべきかどうかを熟考した上で適用するべきと考えます。 この対応によって発生した障害等について、一切責任を負いません。

【intra-mart】[IM-FormaDesigner] 画面アイテムの値取得・設定用のラッパー関数を作って利用している話

$
0
0

こんにちは。

再びintra-martに関する記事です。 intra-martアドベントカレンダー2020の22日目です。(まだ誰も参加してなかったので、こっそり参加しました)

qiita.com

TL;DR

  • 画面アイテムの値の取得・設定をするためのコードがちょっと楽に書けるようになります
  • hidden, textbox, checkbox, radio, selectboxくらいにしか使ってないので他にバグあったら申し訳ないです

目次

IM-FormaDesignerとは

intra-martのローコード開発機能の一つである「IM-FormaDesigner」では、いわゆるIDEのように 画面部品をドラッグドロップで配置することで、簡単にCRUD画面を作成することができます。

簡易的な画面を作成する場合は、大変重宝します。 僕の場合、自社とユーザの都合で頻繁にデプロイができないため、IM-FormaDesignerを結構重要な場面で活用しています。

ちょっとした課題

各画面部品については、アクション定義におけるカスタムスクリプト(JavaScript)を利用して、 値の設定や取得を行うことができます。

www.intra-mart.jp

  • 文字列の入力
(function($){var result = formaItems.product_72_textbox.getItemData.%フィールド識別ID%();
    imuiAlert(result);
})(jQuery);
  • 文字列の反映
(function($){var args = {};
    args.data = {};
    args.data.%フィールド識別ID% = "入力値";
    formaItems.product_72_textbox.setItemData.%フィールド識別ID%(args);
})(jQuery);

設定方法はあるものの、普段コードを書くにしても、ちょっと長くて覚えづらい感じです。 短期間で役目を終え、保守がほとんど不要な画面であれば、このようなコードでもOKなのですが、 やはり中長期的に利用する画面だと、メンテナンスが辛くなってしまいます。

そこで、ラッパー関数を用意することにしました。

ラッパー関数

  • 使い方:以下のソースコードをアクション設定の初期処理にカスタムスクリプトとして定義します。
//-------------------------------------------------------------------------// Forma画面から値を取得するためのショートカット関数//-------------------------------------------------------------------------window.rngdGetValue = function (type, fieldId) {var fn = getRngdImFieldMap()[type];
    // スマホ版の場合if (forma.funcs.getDisplayClientType() === "pc") {return formaItems[fn].getItemData[fieldId]();
    }else{var args = {};
        args.input_id = fieldId;
        return formaItems[fn].getItemDataSp(args);
    }};
window.rngdSetValue = function (type, fieldId, value) {var fn = getRngdImFieldMap()[type];
    if (forma.funcs.getDisplayClientType() === "pc") {var args = {};
        args.data = {};
        args.data[fieldId] = value;
        formaItems[fn].setItemData[fieldId](args);
    }else{var args = {};
        args.input_id = fieldId;
        args.inputDataList = {};
        args.inputDataList[fieldId] = value;
        formaItems[fn].setItemDataSp(args)
    }};
window.rngdBindValue = function (type, fieldId, value) {var fn = getRngdImFieldMap()[type];
    if (forma.funcs.getDisplayClientType() === "pc") {
        formaItems[fn].setItemData[fieldId](value);
    }else{
        formaItems[fn].setItemDataSp(value)
    }}window.getRngdImFieldMap = function () {return{"textbox": "product_72_textbox",
        "textarea": "product_72_textarea",
        "number": "product_72_number",
        "calendar": "product_72_calendar",
        "terms": "product_72_terms",
        "itemSelect": "product_80_itemSelect",
        "table": "product_80_table",
        "checkbox": "product_80_checkbox",
        "radio": "product_80_radio",
        "selectbox": "product_80_selectbox",
        "listbox": "product_80_listbox",
        "gridtable": "product_80_gridtable",
        "userSelect": "product_72_userSelect",
        "departmentSelect": "product_72_departmentSelect",
        "departmentPostSelect": "product_72_departmentPostSelect",
        "affiliationSelect": "product_72_affiliationSelect",
        "hidden": "product_72_hidden"};
};
  • 他のカスタムスクリプトから利用します・・・下記のような形で、ちょっとだけシンプルに書くことができるようになります
// 部品の種類はgetRngdImFieldMap関数のキー値を参照
rngdSetValue("部品の種類", "フィールド識別ID", 設定したい値);
rngdGetValue("部品の種類", "フィールド識別ID");
// セレクトボックスやラジオは下記の関数を利用
rngdBindValue("部品の種類", "フィールド識別ID", 設定したいオブジェクト);
//-----------------------------------------------------------------------------// テキストボックスの利用例//-----------------------------------------------------------------------------// テキストボックスへの設定
rngdSetValue("textbox", "フィールド識別ID", "設定したい値");

// テキストボックスからの取得var text = rngdGetValue("textbox", "フィールド識別ID");

//-----------------------------------------------------------------------------// 隠しパラメータの利用例//-----------------------------------------------------------------------------// 隠しパラメータへの設定
rngdSetValue("hidden", "フィールド識別ID", "設定したい値");

// 隠しパラメータからの取得var hiddenValue= rngdGetValue("hidden", "フィールド識別ID");

//-----------------------------------------------------------------------------// セレクトボックスの利用例//-----------------------------------------------------------------------------// セレクトボックスへの設定 var selectBoxObj = {"data": {},
    "master": {"フィールド識別ID": [{"key_フィールド識別ID": "key_field1",
                "value_フィールド識別ID": "value_field1"},
            {"key_フィールド識別ID": "key_field2",
                "value_フィールド識別ID": "value_field2"},
        ]}}
rngdBindValue("hidden", "フィールド識別ID", selectBoxObj);

// セレクトボックスからの取得var selectBoxValue = rngdGetValue("selectbox", "フィールド識別ID");

注意事項

上記ラッパー関数ですが、実際に実務で利用してはいるものの、hidden, textbox, checkbox, radio, selectboxくらいでしか利用できていないので、その他の画面アイテムIDで利用する場合には何らかの問題が発生するかもしれません。 また、申し訳ありませんがこの対応を実施したことによって発生した障害等について、一切責任を負いません。


【聖剣伝説3-ToM_mod】uassetとuexpの関係、確認方法概要について

$
0
0

こんにちは。

2020年5月上旬から聖剣伝説3ToMのmod作成に夢中になり、色々と調査や検証を行ってきました。

以前とある方より「"すべての店に武器・防具を追加するmod"や"すべての店に秘薬やクラスチェンジアイテム追加するmod"ってどんなアプローチで作ったの?」という質問を頂きました。

英語での質問だったため、Google翻訳に頼りつつ、 uassetとuexpの関係や、自分自身のアプローチについてざっくりと書いて回答しました。 今回、回答した内容の日本語版をブログ記事として残しておきたいと思います。

目次

利用ツールについて

本記事では下記のツールを利用しています。
各種利用方法は割愛します。

はじめに

  • 通常の手順を試したができなかった
    • uasset+uexpの結合とJSON化
    • JSONの値変更と変更後のJSONのuasset化

まず、mod製作者の皆さんが使っているSerializerを用いて、uasset+uexpをJSONにしました。

しかしショップのファイルは、変更したJSONからbinに変換しても、内容が適用されず、通常の手順では不可能でした。 Serializerのみで編集完了するファイルは少ないようで、別のアプローチが必要になりました。

そこでnamesファイルとJSONファイルを元にuassetおよびuexpの16進数の並びを解析し推測しながら16進数を追加・変更することにしました。 (namesファイルはSerializerのuassetからJSONを出力するバッチ「1 - uasset to JSON--dumpnames.bat」を利用することで取得できます。)

加えて、別のSerializerであるJohnWickParseのソースコード(※1)からuassetのHEX情報を得ました。

※1 JohnWickParse/blob/master/src/assets.rsの435行目~470行目付近

上記の情報をもとに、推測と検証を行った結果、以下のことを理解することができました。

[1]uassetのnamesとuexpの関係
[2]namesキーの変更方法
[3]namesキーの追加方法
[4]uexpの配列と構造体のHEX列

uassetとuexpの関係

それではuassetとuexpの関係について具体的に確認していきましょう。
uexpは、namesのインデックスとその値(整数や浮動小数点数、names)で構成されているようです。 (例外があるかもしれませんが、DataTableとよばれるオブジェクトに関しては、この構成のようです。)

ShopItemDataCsv.uassetを用いた解析アプローチ例

ShopItemDataCsv.uasset (Content\Game00\Data\Csv\ShopData)を例にします。 これをSerializerでJSONにすると、下記のようなデータが得られます。 f:id:rinne_grid2_1:20201227110718p:plain

上記画像の1219行目に注目してみます。 "ItemId_24_320FB4CD4865E046DDF62B80E83DE9FD": "EItemType::ITEM_ID_CHOCO",と書かれているようです。

上記JSONの「キー:値」がバイナリエディタ上では、どのようになっているのか?
これを解析していきます。

1 - uasset to JSON--dumpnames.batで得られたShopItemDataCsvNames.namesではItemId_24_320FB4CD4865E046DDF62B80E83DE9FDは以下のとおりになっています。 f:id:rinne_grid2_1:20201227110752p:plain

79行目に注目してください。 この場合、ItemId_24_320FB4CD4865E046DDF62B80E83DE9FDは79行目にありますが、インデックスは0始まりなので行数から1を引く必要があります。

79-1=78、つまり78がItemId_24_320FB4CD4865E046DDF62B80E83DE9FDのインデックスということになります。

78を16進数(32bit)で表現すると、00 00 00 00 00 00 00 4Eです。

しかし、uexpの16進数はリトルエンディアンで表現されるため、 下記のとおりになります

4E 00 00 00 00 00 00 00

(リトルエンディアンについては、申し訳ありませんがここでは説明を割愛します。Web検索等ですぐに出てくるので、検索をおすすめします。)

試しに、HxDツールでShopItemDataCsv.uexpを開き、この16進数の値4E 00 00 00 00 00 00 00で検索してみると、160件のデータがマッチします。Serializerで出力したJSONで ItemId_24_320FB4CD4865E046DDF62B80E83DE9FDを検索すると同様に160件であり、個数が一致していることがわかります。

続いて、検索結果のうちの一つであるアドレス0x00000164の16進数に着目してみましょう。 f:id:rinne_grid2_1:20201227110819p:plain

4E 00 00 00 00 00 00 00 46 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 0F 00 00 00 00 00 00 00 00 1B 00 00 00 00 00 00 00

これらの16進数を上記で説明した方法でnamesと照合していきます。

16進数10進数namesキー(10進数に対応するインデックス)
4E 00 00 00 00 00 00 0078ItemId_24_320FB4CD4865E046DDF62B80E83DE9FD
08 00 00 00 00 00 00 008CameraId_26_4BE91328489B42F5B11C5E8894E1EF3Aなんだか関係なさそうなキー。
0F 00 00 00 00 00 00 00 0015EItemType
1B 00 00 00 00 00 00 0027EItemType::ITEM_ID_DROP

さて、 08 00 00 00 00 00 00 00をnamesとマッチングした結果、 CameraId_26_4BE91328489B42F5B11C5E8894E1EF3Aという関係なさそうなキーが出てきました。

このようにたまに突拍子もない値が登場することがあります。 これは、値の型を示す識別子の後に挿入されるケースが多いです。 (IntPropertyFloatPropertyなどの型識別子, EItemTypeEItemType::ITEM_ID_DROPなどのEnumProperty) この場合、namesキー値ではなく、値そのものであると考えた方が良いです。

しかしこれで、 “ItemId_24_320FB4CD4865E046DDF62B80E83DE9FD”: “EItemType::ITEM_ID_DROP”が示すバイト列が判明しました。

  • “ItemId_24_320FB4CD4865E046DDF62B80E83DE9FD”: “EItemType::ITEM_ID_DROP”が示すバイト列
    • 46 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 0F 00 00 00 00 00 00 00 00 1B 00 00 00 00 00 00 00

では、目的のEItemType::ITEM_ID_CHOCOを探すにはどうすれば良いでしょうか。

46 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 0F 00 00 00 00 00 00 00 00 1B 00 00 00 00 00 00 00のうち、1B 00 00 00 00 00 00 00の部分(10進数27)が、 namesのEItemType::ITEM_ID_DROPのインデックスと一致していました。

つまり、この部分をEItemType::ITEM_ID_CHOCOのインデックスである 14 00 00 00 00 00 00 00(10進数20)に置換すれば良いのです。

  • “ItemId_24_320FB4CD4865E046DDF62B80E83DE9FD”: “EItemType::ITEM_ID_CHOCO”が示すバイト列
    • 46 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 0F 00 00 00 00 00 00 00 00 14 00 00 00 00 00 00 00

この16進数で検索をしてみると、15件のデータが出てきました。 JSONでEItemType::ITEM_ID_CHOCOを検索してみると、同様に15件であり一致していることがわかります。 このようにして、各種データの解析や変更を行うことができます。

nameキーの変更方法、追加方法や、uexpの配列と構造体のHEX列については、 下記の記事をご参照ください。

【聖剣伝説3-ToM_mod】uassetとuexpファイルのバイナリ変更方法まとめ - 気ままなタンス*プログラミングなどのノートブック

【intra-mart】[IM-LogicDesigner] ユーザ定義「SQL」で忘れがちなコメント文(IF、ENDなど)をまとめる

$
0
0

intra-martのIM-LogicDesignerのユーザ定義「SQL」(以下SQL定義)に関する記事です。

IM-LogicDesignerとはビジネスロジックをローコードで作成することができる機能です。 エレメントと呼ばれる様々な処理の箱(変数代入、ループ、SQL定義、JavaScript定義)を組み合わせて処理を実現します。

IM-LogicDesigner www.intra-mart.jp

エレメントの一つであるSQL定義では、あらかじめ記述したSQL文を元にデータ操作を行うことができます。 SQL定義には、SQLコメント文/*param*/を指定することで条件による動的なSQL生成が可能になるのですが、 開発・保守時に使うときに忘れがちだったり、調べてもなかなか良さげな情報にたどり着けないことが多いので情報をまとめることにしました。

目次

環境

  • DB:Oracle Database
  • intra-martバージョン:intra-mart Accel Platform 2019 Summer (これ以外の環境だと、動かないこともあるかもしれません)

入力情報

f:id:rinne_grid2_1:20210306000657p:plain
インプット情報

項目名
param1string
param2string
param3string
param4string配列
param5integer
param6integer

対象テーブル

  • IMFR_T_IMW_MATTER
    • Formaアプリのアプリケーション種類「IM-Workflow」において、Forma画面で登録したデータ(INSERT_ID)の主キーや対象のワークフロー案件の主キー(システム案件ID)を管理するテーブル
項目名
INSERT_IDVARCHAR2(20)
APPLICATION_IDVARCHAR2(100)
FLOW_IDVARCHAR2(20)
CONTENTS_IDVARCHAR2(20)
SYSTEM_MATTER_IDVARCHAR2(20)
MATTER_NUMBERVARCHAR2(20)
MATTER_NAMEVARCHAR2(400)
APPLY_DATEDATE
AUTH_USER_CDVARCHAR2(100)
EXEC_USER_CDVARCHAR2(100)

1. パラメータ単純利用

SELECT
  *
FROM
  IMFR_T_IMW_MATTER
WHERE
  INSERT_ID = /*param1*/''OR INSERT_ID = /*param2*/''OR INSERT_ID = /*param3*/''

2. IN句に指定:配列パラメータ(param4)

SELECT
  *
FROM
  IMFR_T_IMW_MATTER
WHERE
  INSERT_ID IN/*param4*/('')

3. LIKE句に指定

  • param1: '8%'・・・INSERT_IDが8から始まるものを検索
  • インプットパラメータにパーセントを含める必要があります
    • strから始まる: 'str%'
    • strで終わる:'%str'
    • strを含む:'%str%'
SELECT
  *
FROM
  IMFR_T_IMW_MATTER
WHERE
  INSERT_ID LIKE/*param1*/'S%'

4. IFコメント

4-1. NULLでない場合に条件として利用

  • param2がnullでない場合にOR INSERT_ID = /*param2*/''をSQLとして利用
SELECT
  *
FROM
  IMFR_T_IMW_MATTER
WHERE
  INSERT_ID = /*param1*/''/*IF param2 != null */OR INSERT_ID = /*param2*/''/*END*/

4-2. パラメータと値の比較(以上、以下、より大きい、より小さい)

  • param5が1000以下ならばINSERT_ID = /*param1*/''を利用
  • param5が1000より大きければINSERT_ID = /*param2*/''を利用
SELECT
  *
FROM
  IMFR_T_IMW_MATTER
WHERE/*IF param5 <= 1000 */
  INSERT_ID = /*param1*/''/*END*//*IF param5 > 1000 */
  INSERT_ID = /*param2*/''/*END*/
  • param6が2000以上ならばINSERT_ID = /*param1*/''を利用
  • param5が2000より小さければINSERT_ID = /*param2*/''を利用
SELECT
  *
FROM
  IMFR_T_IMW_MATTER
WHERE/*IF param6 >= 2000 */
  INSERT_ID = /*param1*/''/*END*//*IF param6 < 2000 */
  INSERT_ID = /*param2*/''/*END*/

4-3. パラメータが特定の値に一致する場合にSQLとして利用

  • param3がEDITの場合にOR INSERT_ID = /*param2*/''を実行
SELECT
  *
FROM
  IMFR_T_IMW_MATTER
WHERE
  INSERT_ID = /*param1*/''/*IF param3 == "EDIT" */OR INSERT_ID = /*param2*/''/*END*/

4-4. 複数パラメータのOR条件に一致する場合にSQLとして利用

  • param3がEDITまたは REGISTRATIONの場合にOR INSERT_ID = /*param2*/''を実行
SELECT
  *
FROM
  IMFR_T_IMW_MATTER
WHERE
  INSERT_ID = /*param1*/''/*IF param3 == "EDIT" || param3 == "REGISTRATION" */OR INSERT_ID = /*param2*/''/*END*/

4-5. 複数パラメータのAND条件に一致する場合にSQLとして利用

  • param2がMATTERCOMPLETEまたは param3がPOSTSCRIPTの場合にOR AUTH_USER IN /*param4*/('')を実行
SELECT
  *
FROM
  IMFR_T_IMW_MATTER
WHERE
  INSERT_ID = /*param1*/''/*IF param2 == "MATTERCOMPLETE" && param3 == "POSTSCRIPT" */OR AUTH_USER_CD IN/*param4*/('')
/*END*/

4-6. WHERE句の不正なAND/ORの排除

  • param5が1000以下で、param6が2000以上の場合に、WHERE OR INSERT_ID = /*param2*/''となりSQLエラーとなる
  • これを排除し回避するために/*BEGIN*//*END*/で囲む
    • BEGINやENDについて、*の間に半角スペース等を入れると、排除してくれないので要注意。
SELECT
  *
FROM
  IMFR_T_IMW_MATTER
WHERE/*BEGIN*//*IF param5 <= 1000 */
    INSERT_ID = /*param1*/''/*END*//*IF param6 >= 2000 */OR INSERT_ID = /*param2*/''/*END*//*END*/

5. レアケース:配列パラメータの要素を利用

  • param4の0番目の要素を条件として利用する
SELECT
  *
FROM
  IMFR_T_IMW_MATTER
WHERE
  INSERT_ID = /*param4[0]*/''

6. レアケース:パラメータでクエリ実行対象テーブルを指定する(SQLリテラル指定1)

  • param2にIMFR_T_IMW_MATTERを設定
SELECT
  *
FROM/*$param2*/WHERE
  INSERT_ID = /*param1*/''
  • 返却値を定義する必要がありますが、「データ定義を取得する」でしか設定できません。したがって、このケースを利用する場合、あらかじめリテラルでテーブル名を記載したうえで、「データ定義を取得する」を実施した後にパラメータ化する必要があります

7. レアケース:パラメータでテーブルとカラムを指定する(SQLリテラル指定2)

  • param2にIMFR_T_IMW_MATTERを設定
  • param3にINSERT_IDを設定
SELECT/*$param3*/FROM/*$param2*/WHERE
  INSERT_ID = /*param1*/''

つまり、その他ORDER BY句などに指定する際も同様。

最後に

これで次回以降にSQL定義を作成するときには困らずに済みそうです。

これ以降、新規に利用したものがあったら追記します。

【intra-mart】Electron+Angularで、IM-FormaDesignerのzipファイルのカスタムスクリプトを読み込み編集できるアプリ(FCS-Editor)を公開しました。

$
0
0

こんにちは。

今回はintra-mart IM-FormaDesignerの補助エディタアプリを作りました。

TL;DR

  • IM-FormaDesignerのカスタムスクリプトを確認・修正する時に、1つ1つ設定をポチポチ開くのが辛かったのでアプリ作った
  • カスタムスクリプトを俯瞰的に編集できるので、自分の作業改善、作業効率化に繋がって嬉しい

目次

おさらい:IM-FormaDesignerとは

IM-FormaDesigner

intra-martのローコード開発機能の一つです。
いわゆるIDEのように 画面部品をドラッグドロップで配置することで、簡単にCRUD画面を作成することができます。 簡易的な画面を作成する場合は、大変重宝します。
僕の場合、自社とユーザの都合で頻繁にデプロイができないため、IM-FormaDesignerを結構重要な場面で活用しています。

Web画面上で入力部品やボタンの配置ができ、「カスタムスクリプト」として、jQueryを含むコードでカスタマイズできます。

f:id:rinne_grid2_1:20211007101304j:plain

概要:作成したアプリ

IM-FormaDesignerのエクスポートデータ(zipファイル)から
カスタムスクリプトのソースコードや説明、条件などを読み込み ツリービューでソースコードの確認や編集ができるエディタです。

IM-FormaDesignerで登録したカスタムスクリプト数が膨大になってくると、
調査時や改修時に一つ一つのWeb画面から定義を開いて 作業するのが辛いので、Electron + Angularの勉強がてら作成してみました。

作成したアプリのリポジトリ

github.com

Before(アプリ作成前)

  • こんな感じで鉛筆マークを一つ一つ開いて、確認しソースを書く必要がある
  • カスタムスクリプト設定画面は行数やシンタックスハイライトなし。
    • このままだと開発・保守しづらいので、現状は各カスタムスクリプトをファイルとして保存し、VSCode等で編集し、この画面に貼り付けていた

f:id:rinne_grid2_1:20211007093038j:plain

After(アプリ利用時)

IM-FormaDesignerのエクスポートデータさえ読み込めば、エディタ画面で編集可能になりました。
編集後は、もちろん新しいForma定義のzipファイルとして保存し、そのままintra-martにインポートすることができます。(カスタムスクリプト以外には影響を与えません)

アプリ画面

f:id:rinne_grid2_1:20211007093431p:plain

操作イメージ

FCS-Editor操作イメージ

アプリのダウンロード・インストール

リリースページよりダウンロードできます。

  • Windows
    • fcs-editor.Setup.x.x.x.exeを見つけてダウンロードします
  • macOS
    • fcs-editor-x.x.x.dmgを見つけてダウンロードします

アプリ操作方法

sample_appフォルダにサンプルのForma定義を配置してますので、試しに使ってみる場合はこちらをご利用ください。

  1. [Forma定義を開く]ボタンもしくは[ファイル]->[zipファイルを開く]メニュークリックします
  2. IM-FormaDesignerからエクスポートしたzipファイルを選択します
  3. zip内容を元に、アプリにツリーが表示され、各カスタムスクリプトの編集ができます
  4. ツリーをたどり、編集したいカスタムスクリプトを開き、編集を行います
  5. 変更後、[Ctrl+S]もしくは[ファイル]->[zipファイルにエクスポート]メニューをクリックすると、変更内容を反映したzipファイルとして保存することができます
  6. 保存したzipファイルをIM-FormaDesignerでインポートします

Tips

  • [ツール]->[カスタムスクリプトをファイルとしてエクスポート]メニューをクリックすると、zipファイルに含まれる全てのカスタムスクリプト(ボタンイベントやスクリプトアイテム含む)をファイルとして出力できます。
  • 実験的機能として、カスタムスクリプト全体の検索・置換機能がついています
    • 検索: [Ctrl+Shift+F]
    • 置換: [Ctrl+Shift+H]

感想など

ngrxを利用していたのですが、Electron上でReduxの拡張機能やAngularの拡張機能がうまく動かず、ステートやコンポーネントの状態確認に苦労しました。

最初はテストコードを頑張って書いたり、CSSにBEM記法を適用してみたりと、挑戦していたのですが…。

途中から、まずは動くものを完成させることを優先してしまい、案の定テストコードを書かなくなりました。

個人の趣味プロジェクトであるため、これでも問題にはならないのですが、やはりきちんとテストコードを書くことは、スキルとして必要である痛感します。
(ElectronやAngularのホットデプロイ機能があるとはいえ、Electron側のコードを修正すると、プロセス終了してやり直さないと変更が反映されない)
(GUI上でポチポチ動作確認する行為、とても時間がかかって途中、めげそうになった)

なお、今回angular-electronとmonaco-editorを利用したのですが、Electron上で動かす部分で色々ハマりました。 その他、ハマったことがいくつもあったので、書き出してみようと思います。

ハマりポイント

Angular Electron上でmonaco-editorを動かす

  • monaco-editorのimportScriptsでworkerMain.jsをインポートして、MonacoEnvironmentに設定する必要があった
    • (window as any).MonacoEnvironment = の部分
import{ Component, AfterViewInit, ViewChild, ElementRef }from"@angular/core";import * as monaco from"monaco-editor";@Component({
  selector: "app-main-edit",
  templateUrl: "./main-edit.component.html",
  styleUrls: ["./main-edit.component.scss"],})exportclass MainEditComponent implements AfterViewInit {
  ngAfterViewInit(){(windowasany).MonacoEnvironment ={
      getWorkerUrl: function(workerId, label){return`data:text/javascript;charset=utf-8,${encodeURIComponent(`self.MonacoEnvironment ={ baseUrl: '${window.location.origin}/'};
              importScripts('${window.location.origin}/vs/base/worker/workerMain.js');`)}`;},};this.initMonaco();}
  initMonaco(){const editor = monaco.editor.create(document.getElementById("editor"),{
      value: "",
      language: "javascript",// theme: "vs-dark",});}}
  • angular.jsonに設定追記が必要
    • projects.angular-electron.architect.build.assetsの配列に追記が必要
{"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "cli": {"analytics": false},
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {"angular-electron": {"root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "architect": {"build": {"builder": "@angular-builders/custom-webpack:browser",
          "options": {"outputPath": "dist",
            "index": "src/index.html",
            "main": "src/main.ts",
            "tsConfig": "src/tsconfig.app.json",
            "polyfills": "src/polyfills.ts",
            "assets": ["src/assets",
              {"glob": "**/*",
                "input": "node_modules/monaco-editor/min/vs",
                "output": "./vs"
              }

バイナリ作成後に、Cannot find module 'xxx'が発生

  • ipcRendererで定義した関数やクラスをipcMain側で利用していると発生する
  • もしくは、angular-electronにおいて、app/package.jsonに、モジュールを追記していないと発生する
    • 今回、unzipperやxml2jsをnpmでインストールしており、ルートのpackage.jsonには追記されていたが、app/package.json側にも必要だった

AngularからElectronへのipc通信方法

  • ElectronServiceをDIして利用する
    • electronService.ipcRenderer.onでMainプロセス(Electron側)からのイベントを処理
    • electronService.ipcRenderer.invokeでRendererプロセス(Angular側)からイベントを送信
import{ ElectronService }from'../../../core/services';import{ OnInit }from'@angular/core';@Component({
  selector: 'app-container',
  templateUrl: './container.component.html',
  styleUrls: ['./container.component.scss'],})exportclass ContainerComponent implements OnInit {constructor(private electronService: ElectronService){}
  ngOnInit(): void{this.electronService.ipcRenderer.on('ipc_channel_name1',(event, value)=>{});}

  anyMethod(){this.electronService.ipcRenderer
      .invoke('ipc_channel_name2', file.path)
      .then((data)=>{})
      .catch((error)=>{});}

ElectronからAngularへのipc通信方法

  • BrowserWindowのインスタンスメソッドwebContents.send利用する
  • browserWindow.webContents.send("ipc_channel_name1", value)

今後について

感想などあれば、気軽にこのブログのコメントやGithubにissueを上げていただければ嬉しいです。 すべてに返信できるかはわかりません。バグは許してください。

それでは、皆様も良いintra-martライフを!

【映像キャプチャ】HSV321を購入したので利用方法を備忘録としてまとめる

$
0
0

Nintendo Switchのゲーム練習動画を録画したくて、映像キャプチャ(HSV321)を購入しました。
映像が映っても、音が鳴らない問題を経験し、解消方法を理解したので、備忘録としてまとめます。

用意するもの

構成

上記のハード系の(1)~(4)が図でそれぞれ対応しています。

f:id:rinne_grid2_1:20220306232309p:plain
HSV321構成図

OBS Studio設定

  • [1] 画面下部の「ソース」の下の方にある「+」マークをクリックし、「映像キャプチャデバイス」を選択
  • [2] 新規作成をクリックし、「HSV321」を入力する(名前は何でも良いですが、本記事ではこの前提で記載します)
  • [3] デバイスの部分について、「MiraBox Video Capture」を見つけて選択
  • [4] 音声出力モードについて「音声のみキャプチャ」を選択
  • [5] カスタム音声デバイスを使用するにチェックを入れる
  • [6] 音声デバイスとして「デジタル オーディオ インターフェイス (4- MiraBox Video Capture)」を選択(4-の部分はそれぞれ異なる可能性があります)
  • [1]~[6]までの設定まとめ
    • f:id:rinne_grid2_1:20220306233140p:plain
  • [7] 画面下部の音声ミキサーの部分で右クリックし、「オーディオの詳細プロパティ」をクリック
    • f:id:rinne_grid2_1:20220306233352p:plain
  • [8] オーディオの詳細プロパティで「HSV321」の音声モニタリングの設定を「モニターのみ(出力はミュート)に変更。他のものは「モニターオフ」にする
    • f:id:rinne_grid2_1:20220306233537p:plain

これで、ゲーム映像と音が流れるようになり、録画したり、配信したりできるようになります。

実際にキャプチャしたゲーム動画

YOUTUBEにアップロードしてみました。 クリプト・オブ・ネクロダンサー Nintendo Switch版のデイリーチャレンジの動画です。 www.youtube.com

PS4版をプレイしたことがあって、なんとなくいけると思っていたのですが 動画の説明欄にも書いているとおり、新ボスや新アイテムが追加されていて ちょっと戸惑いました。 (最後はゾーン4-3Fでブレードマスターとオーズゴーレムにはめられてしまい死亡・・・)

今後は色んなゲームの練習・プレイ動画を、楽しみながら記録していければと思います。

【Django】Windows ServerでDjangoアプリをサービスとして動かす(Winsw+Waitress+Nginx)

$
0
0

TL;DR

  • どうしてもWindows ServerでDjangoアプリを動かす必要がある場合
    • gunicornの代わりにWaitressを利用する
      • WaitressはVirtualenv環境を参照しないためpip install時に要注意
    • WinSWのexeファイルとxmlファイルを組み合わせることで、Windowsサービスとして登録可能
      • WinSWとxmlファイルの名称に関して、拡張子以外を一致させることで、xmlの情報を元にWindowsサービスの作成が可能
  • nginxはWindowsバイナリで提供されているけど、ベータ版のため、高いパフォーマンス求められる場合はApacheを利用したほうが良い

目次

記事を書くに至った経緯

  • 詳細は書けないのですが、仕事で某製品のソースコードを解析し、イケてないコードを検出するためプロダクトを作りました
  • 構成としては以下のような感じで、某製品のexeファイルを用いてXMLファイルを出力する必要があり、Windows Serverでの環境構築が必須でした
    • 別途、某製品のXML出力処理を担う処理をラップして、REST APIとして提供するという手もあったのですが、サーバーインスタンスの追加が必要となり、ユーザーの負担費用が増えるため、コストとの兼ね合いで断念しました。
  • 記録として残す時間ができたのでアウトプットします。

f:id:rinne_grid2_1:20220313190558p:plain

  • ①某製品のソースコードファイル、あるいはアーカイブファイルをアップロード
  • ②Celery(タスクキューの仕組み)を利用して、Workerを作成し、某製品のexeファイルをコール
  • ③某製品のexeがXMLを出力する
  • ④xmlファイルの情報をDBに書き込み
  • ⑤後続処理でxmlファイルを解析し、検出したものを情報として記録

DjangoアプリをWindows Serverで動かす

前提

  • Djangoプロジェクト名:my_project
  • venvなどの仮想環境は利用しない(waitressが仮想環境に対応していないため)
  • waitress

Djangoプロジェクトフォルダに移動

$ cd any_folder\my_project

必要なモジュールのインストール

$ pip install waitress
$ pip install django
# その他必要なモジュール 

waitressの起動確認

# my_projectをポート8000で起動する
$ waitress-serve X:\any_folder\my_project --port=8000 my_project.wsgi:application

waitressの起動が確認できたらCtrl + Cでwaitressを終了する

WinSWの設定

  • Winsw-x.x.x-net461.exeをダウンロード github.com

  • WinSW-x.x.x-net461.exeをコピー作成し、WinSW-x.x.x-net461_my_project.exeにリネームする

  • 任意の作業ディレクトリにWinSW-x.x.x-net461_my_project.exeを配置する

    • 本例では、X:\services\waitressに配置
  • 上記ディレクトリにWinSW-x.x.x-net461_my_project.xmlファイルを作成し、以下の内容を書く

<service><!-- Windows ID 一意なものを指定すればOK --><id>my_project.django.waitress.service</id><!-- Windowsサービス名 --><name>My Project AP Server</name><!-- Windowsサービス説明 --><description>My Project APサーバー(waitress)用のサービスです</description><executable>waitress-serve</executable><!-- executableコマンド実行ディレクトリ --><workingdirectory>X:\any_folder\my_project</workingdirectory><!-- 実行時の引数 --><arguments>--port=8000 my_project.wsgi:application</arguments><logmode>rotate</logmode></service>
  • exe及びxml配置後のディレクトリ構成

    • X:\services\waitress
      • WinSW-x.x.x-net461_my_project.exe
      • WinSW-x.x.x-net461_my_project.xml
  • Windowsサービスとしてインストール

    • 上記作業ディレクトリに移動し、以下のコマンドを実行
    • WinSWファイル名.exe installを実行することで、Windowsサービスとして登録できる
$ Winsw-x.x.x-net461_my_project.exe install
  • もしアンインストールしたい場合は、uninstallコマンド実行すればOK
$ Winsw-x.x.x-net461_my_project.exe uninstall

Nginxも同様にWinswを利用する

  • WinSW-x.x.x-net461.exeをコピー作成し、Winsw-x.x.x-net461_nginx.exeにリネーム
  • xmlファイルを作成: WinSW-x.x.x-net461_nginx.xml
<service><id>nginx</id><name>nginx</name><description>nginx</description><logpath>X:\nginx\logs</logpath><logmode>roll</logmode><executable>X:\nginx\nginx.exe</executable><startargument></startargument><stopexecutable>X:\nginx\nginx.exe</stopexecutable><stopargument>-s</stopargument><stopargument>stop</stopargument></service>
  • WinSW-x.x.x-net461_nginx.exe installを実行し、サービス化

個人的にハマったポイント

  • Windowsサービス化した後に、個別のexeプログラムが動かない
    • サービス登録後に、サービスのプロパティ->ログオンでログインユーザを指定する必要がありました。デフォルトはSYSTEMになっているため、アクセスが制限される可能性があるようです。

f:id:rinne_grid2_1:20220313184850p:plain

  • モジュールをインストールしているに、unable to load celery application the module was not foundのようなエラーが発生する
    • WaitressがVirtualenv環境を参照しないようです(2022/01時点の情報)
    • グローバルのPythonに対してpip installを行う必要があります

その他

普段、pipenvを利用しているので、pipenv syncとかpipenv installで対応しようとして、だいぶハマった気がします。

以下のように、Winswの中で環境変数を定義しても読んでくれなかったので、思考停止したのを思い出しました。今となっては良い思い出です

<!-- --><env name="VIRTUAL_ENV"value="X:\.virtualenvs\my_project"></env><env name="PATH"value="%VIRTUAL_ENV%\Scripts;%PATH%"></env>

nginxはWindowsバイナリで提供されていますが、ベータ版のため、 高いパフォーマンス求められる場合はApacheなど、別のミドルウェアを利用したほうが良いらしいです。

nginx.org

【Node.js・Reactなど】Cannot read properties of nullやUNABLE_TO_GET_ISSUER_CERT_LOCALLYが発生した場合の対応方法

$
0
0
  • npm installでエラーが発生した場合の対応方法についてメモします。

エラー内容

・Cannot read properties of null (reading 'pickAlgorithm') 
・failed, reason: unable to get local issuer certificate 
・npm ERR! code UNABLE_TO_GET_ISSUER_CERT_LOCALLY 
・npm ERR! errno UNABLE_TO_GET_ISSUER_CERT_LOCALLY 

対応方法

以下のコマンドを実行し、設定を追加する

$ npm -g config set registry https://registry.npmjs.org/ 
$ npm -g config set strict-ssl false

もし、以下のようなメッセージが表示された場合

npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.

表示どおり、以下のコマンドで対応する

$ npm install --force

あるいは

$ npm install --legacy-peer-deps

【Windows Terminal】ターミナル起動時にプロキシ認証ダイアログが表示される問題の解消方法

$
0
0

概要

  • プロキシ認証下のパソコンでWindows Terminalを利用した際に、事象が発生したため対応方法についてメモします。

対応方法

  • [1] 資格情報マネージャーを開きます
  • [2] 「汎用資格情報」の追加を行い、プロキシサーバーの情報、ユーザーID、パスワードを登録します
  • [3] Windows Terminalを開くときに認証ダイアログが表示されなくなります。

資格情報マネージャー


【Windows11】最近開いた項目の表示制御を行う方法(ジャンプリスト)

$
0
0

概要

  • Windows11で、タスクバー上で起動中のアプリケーションを右クリックした際に「最近開いた項目」を表示させたい

ジャンプリスト

対応方法

  • 個人別設定 ->スタート ->最近開いた項目をスタート、ジャンプリスト、ファイルエクスプローラーに表示するを「オン」に変更する

【Kubernetes】Docker Desktop(Windows)でLoadBalancerのsessionAffinity: ClientIPを指定した場合にうまく通信できない問題への対応方法

$
0
0

概要

Docker Desktop(Windows) KubernetesのServicetype: LoadBalancerを指定し、
セッションを維持するためにsessionAffinity: ClientIPを指定すると対象のポートに通信できない問題が発生する。

  • 以下のようなメッセージが出て、とても悲しい
    • このページは動作していません
    • データが送信されませんでした。
    • ERR_EMPTY_RESPONSE

sessionAffinity: ClientIPを指定しない場合は通信できるが、レプリカ数を増やしたい場合に問題となるため、解消したい

  • sessionAffinity: ClientIPとは?・・・ステートフルなサービス利用時に必要。リクエスト先のPodに関して、ユーザーのクライアントIPを元に、セッションを考慮して振り分けてくれる
  • 例:APサーバー上でセッションを管理しているアプリがあり、APサーバーが2台あるとする*1
    • APサーバー1で処理したら、セッションが終わるまでAPサーバー1と通信する必要がある。理由としては、APサーバー1上にセッション情報があり、セッションIDを識別しているため。
    • ここで、sessionAffinityオプションがないと、途中でAPサーバー2(セッションが存在しないサーバー)に振り分けられてしまい、セッションタイムアウトが発生してしまう。
    • KubernetesのPodの1つが、APサーバー1つに対応するイメージ。
例: type: LoadBalancer, sessionAffinity: ClientIP
apiVersion: v1
kind: Service
metadata:name: ap
spec:selector:app: ap
  ports:- port:8888targetPort:8080type: LoadBalancer
  sessionAffinity: ClientIP

対応方法

  • 下記の記事のとおり、WSLカーネルをダウンロード&オプション付きでビルドし、利用するように設定すれば解消可能。*2

medium.com

  • 手順の概要

・[1] 以下のスクリプトを1行ずつ実行する
注意:25行目は、Windows側から実行する必要がある。

gist.github.com

・[2] 任意の場所にカーネルファイル(bzImage)を配置する
本記事では、C:\wsl\kernel\bzImageへの配置を前提とする

・[3] ユーザーのホームディレクトリ%USERPROFILE%.wslconfigファイルを作成する

・[4] .wslconfigに以下の内容を記載する

[wsl2]
kernel=C:\\wsl\\kernel\\bzImage

・[5] WSLを再起動する

> wsl --shutdown> wsl

これで心置きなく、LoadBalancerのsessionAffinityを指定できるようになる。

*1:APサーバー上ではなく、キーバリューストアなどの外部にセッション情報を持たせることができれば特に問題にはならない

*2:カーネルビルド用に、UbuntuのDockerイメージを取得し、カーネルビルドに必要なモジュールをインストール。WSLカーネルのソースコードを取得し、CONFIG_NETFILTER_XT_MATCH_RECENT設定を有効にした上で、ビルド実行。ビルドイメージをWindowsにコピーし、WSLで利用するように設定

【intra-mart】イントラマートの動作検証環境をKubernetes(Docker Desktop)上に構築する

$
0
0

こんにちは。

最近、Kubernetesの勉強をしています。
キーワードが多すぎて頭がパニックになりながらも、コツコツとやっている感じです。

ただ、やはり何事も実践してみないと身につかない・・・
ということで、Kubernetes(Docker Desktop)上でintra-martを動かしてみることにしました。

TL;DR

  • Kubernetes(Docker Desktop)上でintra-mart動いて嬉しかった
  • StatefulSetでレプリカ指定しているので、intra-martのPodが壊れたら、勝手に自動デプロイして、復活してくれるのでヨシ
  • StorageClass、PersistentVolumeなど、幅広く実践することができて良かった

リポジトリ

Githubにアップしました。

github.com

利用手順

[1] Docker Desktop のインストールとKubernetesの有効化

(複数Podで起動する場合) LoadBalancerのsessionAffinity問題への対応

以下の記事及び、そのリンク先を参考に、WSLカーネルのビルドを実施してください。
https://www.rinsymbol.net/entry/2022/09/04/024140

[2] Git のインストール

  • 下記の URL より、Git for Windows をダウンロードし、インストールします

https://gitforwindows.org/

[3] Docker プロジェクトのダウンロードと初期設定

  • 任意のフォルダで、以下のコマンドを実行し、docker プロジェクトをダウンロードします
> git clone https://github.com/rinne-grid/kubernetes-for-intra-mart im-k8s
>cd im-k8s
  • war ファイルの配置用フォルダを作成します
>mkdir .\ap\war

[4] Juggling で war ファイルを作成

プロジェクト名を imart にして、必要なモジュールを選択し、設定を行います。 今回の Docker 環境をそのまま利用するためには、下記ファイルの設定を変更する必要があります

  • storage-config.xml を設定する
  • resin-web.xml を設定する
  • 出力する war ファイル名を imart.war とする
storage-config.xml の設定

imart/config/storage-config.xml の 19 行目付近を以下のとおりに変更します

<root-path-name>/im-data/storage</root-path-name>
resin-web.xml の設定

imart/resin-web.xml 内容を下記のとおりにします

<web-app xmlns="http://caucho.com/ns/resin"xmlns:resin="urn:java:com.caucho.resin"><character-encoding>UTF-8</character-encoding><log-handler name=""class="jp.co.intra_mart.common.platform.log.handler.JDKLoggingOverIntramartLoggerHandler"/><logger name="debug.com.sun.portal"level="warning" /><!-- im_service(im_asynchronous) --><resource jndi-name="jca/work"type="jp.co.intra_mart.system.asynchronous.impl.executor.work.resin.ResinResourceAdapter" /><jsp><recycle-tags>false</recycle-tags></jsp><database jndi-name="jdbc/default"><driver><type>org.postgresql.Driver</type><url>jdbc:postgresql://db:5432/imart</url><user>imart</user><password>imart</password><init-param><param-name>preparedStatementCacheQueries</param-name><param-value>0</param-value></init-param></driver><max-connections>20</max-connections><prepared-statement-cache-size>8</prepared-statement-cache-size></database><session-config><reuse-session-id>false</reuse-session-id><session-timeout>30</session-timeout></session-config><mime-mapping extension=".json"mime-type="application/json"/></web-app>
war ファイルの出力

imart.war という名称で war ファイルを出力したら、 プロジェクトの im-k8s/ap/war フォルダの中に、war ファイルをコピーします

[5] intra-mart のサイトから Linux の resin-pro をダウンロード

intr-mart のサイトにアクセスし、プロダクトファイルダウンロードボタンを押下します。

https://www.intra-mart.jp/download/library/

ライセンスキーを入力すると、ダウンロード可能なファイル一覧が表示されます。

なお、intra-mart サイトにも書いているとおり、.tar.gz が Linux 用の resin-pro になります。

https://www.intra-mart.jp/download/product/iap/setup/iap_setup_guide/texts/install/linux/resin_linux.html

最新の Resinresin-pro-4.0.xx.tar.gzを入手します。

[6] 7zip をダウンロード、インストール

tar.gz 形式のファイルを展開するため、この記事では 7zip を利用します。

https://sevenzip.osdn.jp/

  1. resin-pro.4.0.xx.tar.gz を展開します
  2. resin-pro.4.0.xx.tar ファイルが作成されます
  3. resin-pro.4.0.xx.tar を展開します
  4. resin-pro.4.0.xx フォルダが作成されます
  5. resin-pro.4.0.xx フォルダの直下に、automake, bin といったフォルダが存在することを確認します

resin-proフォルダ

[7] Docker プロジェクトのフォルダに resin-pro をコピー

  • 上記の[6]の 5 のフォルダ「resin-pro.4.0.xx」の名称を resin-pro に変更します
  • resin-pro フォルダを im-k8s/ap フォルダにコピーします

[8] プロジェクトのフォルダ構成の確認

  • フォルダを確認し、以下の構成と同じになっていることを確認します
  • ポイント
    • im-k8s/ap/resin-pro フォルダがあり、直下に automake 等のファイルが存在する
    • im-k8s/ap/war フォルダがあり、imart.war ファイルが存在する
im-k8s
│  .env
│  .gitignore
│  docker-compose.yml
│  README.md
│
└─ap
│  │  Dockerfile
│  │
│  ├─resin-pro
│  │  ├─automake  など
│  │
│  └─war
│     ├─imart.war
│
└─k8s
    │  001_setup.yaml など

[9] 必要に応じて、設定ファイルを変更する

  • im-k8s/ap/resin-pro/conf/resin.properties の 82 行目付近 - jvm_args

-Xmx, -Xms の値が、初期状態だと 8192m(8GB)が設定されているため、自分の PC のメモリ状況に合わせて変更します

jvm_args : -Dfile.encoding=UTF-8 -Djava.io.tmpdir=tmp -Xmx1500m -Xms1500m -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=30 -XX:NewSize=512m -XX:MaxNewSize=512m -XX:+CMSClassUnloadingEnabled -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+HeapDumpOnOutOfMemoryError -Xloggc:log/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M
  • HTTP プロキシの設定 im-k8s/.env

社内ネットワーク等で、プロキシサーバーを経由する必要がある場合、.env の HTTP_PROXY、HTTPS_PROXY に値を設定します

HTTP_PROXY=http://user:password@server:port/
HTTPS_PROXY=http://user:password@server:port/

[10] Docker コンテナのビルド

  • プロジェクトフォルダに移動します
>cd any_folder\im-k8s
  • docker-compose を利用し、コンテナをビルドします
> docker-compose build --no-cache
  • rinne-grid/k8s-for-intra-mart:v1.0.0というDockerコンテナイメージが作成されます

[11] Docker DesktopのWSL上にリソースマウント用のディレクトリを作成する

> wsl -d docker-desktop 
>mkdir-p /mnt/host/wsl/docker-desktop-data/version-pack-data/community/k8s-pvs/pvc-imart-db
>mkdir-p /mnt/host/wsl/docker-desktop-data/version-pack-data/community/k8s-pvs/pv-imart-system-store
>mkdir-p /mnt/host/wsl/docker-desktop-data/version-pack-data/community/k8s-pvs/pv-imart-webapps

[12] Kubernetes クラスターにデプロイする

>cd k8s
> kubectl apply -f ./001_setup.yaml
> kubectl apply -f ./002_setup-db.yaml
> kubectl apply -f ./003_setup-ap.yaml
  • 1分~2分経過後に以下の画面にアクセスします(自動でデプロイが始まるため、画面表示までに時間がかかります)

http://localhost:8080/imart/system/login

tenant1

  • テナント ID は imart を指定します

tenant2

  • リソース参照名は一覧に表示されたものを選択します

tenant3

  • テナント登録を行い、しばらく待ちます

tenant4

  • テナント環境セットアップが適切に動作しているかどうかについては、Adminer からテーブル作成状況を参照することで確認できます
    • http://localhost:8889にアクセスします
    • Adminer が表示されるので、下記のとおり情報を入力します
情報名 入力情報
データベース情報 PostgreSQL
サーバ db
ユーザ名 imart
パスワード imart
データベース imart
  • テーブルの作成状況が確認できます(だいたい 500 テーブルくらいができたら、処理完了です)

adminer2

tenant5

  • データベースやストレージ情報は WSL(docker-desktop-data) に保存しているため、データは永続化されています

dashboard

[13] 複数PodでAPサーバーを起動する

  • 起動するまで、長くて10分程度かかるため、気長に待ってあげてください。
> kubectl delete -f ./003_setup-ap.yaml
> kubectl apply -f ./ap.yaml

> kubectl get statefulset
# READYが2/2になってから、だいたい5~10分程度# NAME            READY   AGE# intra-mart-ap   2/2     0s

(任意手順) 複数Pod(StatefulSet)で起動できていることの確認

> kubectl delete pod/intra-mart-ap-{ここにホスト名の番号が入ります}# 例:intra-mart-ap-2.intra-mart-ap.default.svc.cluster.local と表示されている場合> kubectl delete pod/intra-mart-ap-2

(その他) 停止手順

>cd k8s
> kubectl delete -f ./ap.yaml
> kubectl delete -f ./002_setup-db.yaml

(その他) 永続化したデータやストレージの削除手順

>cd k8s
> kubectl delete -f ./001_setup.yaml
> wsl -d docker-desktop
>rm-rf /mnt/host/wsl/docker-desktop-data/version-pack-data/community/k8s-pvs/pvc-imart-db
>rm-rf /mnt/host/wsl/docker-desktop-data/version-pack-data/community/k8s-pvs/pv-imart-system-store
>rm-rf /mnt/host/wsl/docker-desktop-data/version-pack-data/community/k8s-pvs/pv-imart-webapps

最後に

  • まだまだStorageClassやPersistentVolumeの利用に関する理解が浅い気がします。
  • Docker Desktopのボリューム前提ですが、StorageClassやPersistentVolume, PersistentVolumeClaimなどを指定し、データベースやストレージの永続化を行うことができて良かったです。
  • WSL(docker-desktop distro)のマウントパスの挙動がよくわからず、だいぶハマりました。
  • LoadBalancerのsessionAffinity問題とかも、だいぶ時間を溶かした気がします。
  • 本来はパスワードはSecretに持ち、設定ファイルはConfigMapに持つべきですが、今回は検証環境のためyamlに直指定しています
    • いつか時間ができたら対応します・・・
  • .envの情報はKubernetesからは参照できていません
    • いつか時間ができたら対応します・・・

【React】ESLintでTypeError: this.libOptions.parse is not functionが発生する場合の対応

$
0
0

概要

  • create-react-appにおいて作成したプロジェクトで以下の例外が発生する

TypeError: this.libOptions.parse is not function

  • 参考記事によれば、ESLint 8.23.0で導入された変更によって引き起こされているとのこと。

対応方法(解消されるまで待つ)

  • ESLintをダウングレードして利用する
  • npmのインストール時に、--save-exactオプションを指定し、バージョンを固定する
$ npm i eslint@8.22.0 --save-exact 
$ npm i eslint-config-react-app 
  • 上記対応をしても解消されない場合
    • package-lock.jsonファイル、node_modulesフォルダを削除
    • 再度インストールを試みる

参考記事

stackoverflow.com

【AWS】[S3]プロキシ通信環境下のEC2において、AWS SDK for Python(Boto3)でbotocore.exceptions.NoCredentialsError: Unable to locate credentialsが発生する場合の対応方法

$
0
0

概要

  • EC2マシン上で、環境変数HTTP_PROXYやHTTPS_PROXYを設定しており、IAMロールでS3へのアクセスを許可しているにも関わらずUnable to locate credentialsが発生する

botocore.exceptions.NoCredentialsError: Unable to locate credentials

想定環境

  • AWSにおいて、オンプレ環境を経由して、外部ネットワークに接続している

対応方法

  • 環境変数no_proxyに対して、169.254.169.254を追加する
$ export no_proxy=localhost,127.0.0.1,169.254.169.254 

docs.aws.amazon.com

体系的に学んでいないため、インスタンスメタデータサービス(?)のIPアドレスは考慮できていなかった。 一度、クラウドプラクティショナーの試験などで学習した方が良いと思った。

Viewing all 242 articles
Browse latest View live