Thinking Skeever

Skyrim/The Witcher 3 Modについてのあれこれ。FoModの作り方、Mod導入時のトラブル事例などのニッチな話を書いていきます。a.k.a. BowmoreLover@nexusmods

FOModの作り方(C#スクリプトチュートリアル01)

これまでxmlを使ってFOModを作ってきましたが、今回はC# Scriptという言語を使ってFOModを作ってみます。

C# Scriptの概要

  • Nexus Mod Manager、Mod Organizerどちらでも使えます。
  • 画面の作成・表示・インストール動作すべてを自力で行う必要があります。
  • 事前にコンパイルする必要はありません。FOModフォルダにscirpt.csを配置するとインストール時にコンパイルされます。
  • Microsoft Visual Studio C#と互換性があります。
  • FOMod用のフレームワーク(枠組み)の元で動作しますので、Visual Studio C#で作成したものをそのまま実行することはできません。部分的に処理をコピペして持ってくるのはOKです。
  • .NET Frameworkのクラス群は基本的にどれでも使えるようです。.NET Frameworkの正確なバージョンは不明ですが、おそらく.NET Framework 4ではないかと踏んでいます。
  • FOModの実行環境はサンドボックス化(隔離)されていてファイルI/Oを直接行うことはできません。C# Scriptが提供する専用メソッドを使い、限られたファイルにのみアクセスできます。
  • 画面を視覚的に作成するエディタは用意されていません。試行錯誤で位置を調整するか、Visual Studioで作成したものをコピペするなどの工夫が必要です。

準備するもの

 私は2010 Expressを使っています。
 なくても作成できますが、Visual Studio側で動作確認をしてからコピペすることで楽ができます。

今回のお題

2つの体型を持つフォロワーModのFOModを作成します。

  • ページ切り替えは無し。
  • 体型の選択(UNP/CBBE)ができる。
  • 選択肢にマウスを乗せると説明文と画像が表示される。

Modファイル FOModC#Tutor01.zip の構成

FOModC#Tutor01\
    00 Core Files\
        meshes\
            core-mesh.txt
        textures\
            core-texture.txt
    10 CBBE\
        cbbe-esp.txt
        meshes\
            cbbe-mesh.txt
        textures\
            cbbe-texture.txt
    10 UNP\
        unp-esp.txt
        meshes\
            unp-mesh.txt
        textures\
            unp-texture.txt
    FOMod\
        info.xml
        script.cs
        Images\
            CBBE.jpg
            Intro.jpg
            UNP.jpg

info.xmlの作成

<?xml version="1.0" encoding="UTF-16"?>
<fomod>
    <Name>FOMod C# Tutorial 01</Name>
    <Author>ThinkingSkeever</Author>
    <Version>0.0</Version>
    <Website>http://thinkingskeever.hatenablog.com/</Website>
    <Description>
    <p>This is FOMod C# script tutorial.</p>
    </Description>
</fomod>

script.csの作成

UTF-8またはUTF-16(BOM付)で保存します。

// FOMod C# script tutorial 01 by ThinkingSkeever
using System;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;
using fomm.Scripting;

class Script : BaseScript
{
    private bool bInstalled = false;

    private Form frmMain;
    private Label lblDesc;
    private GroupBox grpBodyTypes;
    private RadioButton rbUnp;
    private RadioButton rbCbbe;
    private PictureBox picPreview;
    private Button btnExit;
    private Button btnInstall;

    private Image imgIntro;
    private Image imgUnp;
    private Image imgCbbe;

    private enum BodyTypes
    {
        Unp,
        Cbbe
    };
    private BodyTypes bodyType = BodyTypes.Unp;

    const string strModLongName = "FOMod C# Tutor 01";
    const string strModShortName = "FOModC#Tutor01";
    const string strInstall = "インストール";
    const string strBodyTypes = "体型を選択します";
    const string strExit = "終了";
    const string strUnp = "UNP";
    const string strCbbe = "CBBE";

    const string strDescIntro = "ダウンロードしてくれてありがとう。\nこれはFOMod C# scriptのチュートリアルです。";
    const string strDescUnp = "UNP体型です。\nどちらかといえばスリムな体型です。";
    const string strDescCbbe = "CBBE体型です。\nむちむちして肉感的な体型です。";

    //
    // *** Event handlers ***
    //

    public bool OnActivate()
    {
        InitializeForm();
        frmMain.ShowDialog();
        return bInstalled;
    }

    private void btnExit_Click(object sender, EventArgs e)
    {
        frmMain.Close();
    }

    private void btnInstall_Click(object sender, EventArgs e)
    {
        InstallFiles();
        bInstalled = true;
        frmMain.Close();
    }

    private void rbUnp_CheckedChanged(object sender, EventArgs e)
    {
        bodyType = BodyTypes.Unp;
    }

    private void rbUnp_MouseEnter(object sender, EventArgs e)
    {
        lblDesc.Text = strDescUnp;
        picPreview.Image = imgUnp;
    }

    private void rbUnp_MouseLeave(object sender, EventArgs e)
    {
        lblDesc.Text = strDescIntro;
        picPreview.Image = imgIntro;
    }

    private void rbCbbe_CheckedChanged(object sender, EventArgs e)
    {
        bodyType = BodyTypes.Cbbe;
    }

    private void rbCbbe_MouseEnter(object sender, EventArgs e)
    {
        lblDesc.Text = strDescCbbe;
        picPreview.Image = imgCbbe;
    }

    private void rbCbbe_MouseLeave(object sender, EventArgs e)
    {
        lblDesc.Text = strDescIntro;
        picPreview.Image = imgIntro;
    }

    //
    // *** File installer ***
    //

    private void InstallFiles()
    {
        CopyDataFolder("00 Core Files/");
        if (bodyType == BodyTypes.Unp)
            CopyDataFolder("10 UNP/");
        else
            CopyDataFolder("10 CBBE/");
    }

    private void CopyDataFolder(string targetFolder)
    {
        string folder = targetFolder;
        
        if (!folder.EndsWith("/"))
        {
            folder += "/";
        }
        
        foreach(string file in GetFomodFileList())
        {
            if (file.StartsWith(folder, StringComparison.OrdinalIgnoreCase))
            {
                string dest = file.Substring(folder.Length);
                CopyDataFile(file, dest);
            }
        }
    }

    //
    // *** Initializers ***
    //
    private Image LoadImage(string filename)
    {
        Image img;
        byte[] data = GetFileFromFomod(filename);
        using (MemoryStream ms = new MemoryStream(data))
        {
            img = Image.FromStream(ms);
        }
        return img;
    }

    private void InitializeForm()
    {
        int tabIndex = 0;

        // Load resouces
        imgIntro = LoadImage("FOMod/Images/intro.jpg");
        imgUnp = LoadImage("FOMod/Images/UNP.jpg");
        imgCbbe = LoadImage("FOMod/Images/CBBE.jpg");

        // Main form
        frmMain = CreateCustomForm();
        frmMain.SuspendLayout();

        frmMain.ClientSize = new Size(520, 300);
        frmMain.FormBorderStyle = FormBorderStyle.FixedSingle;
        frmMain.MaximizeBox = false;
        frmMain.MinimizeBox = false;
        frmMain.Name = strModShortName + "Installer";
        frmMain.Text = strModLongName + " Installer";

        // Controls
        lblDesc = new Label();
        lblDesc.Name = "lblDesc";
        lblDesc.Location = new Point(10, 10);
        lblDesc.Size = new Size(250, 90);
        lblDesc.TabIndex = tabIndex++;
        lblDesc.TabStop = false;
        lblDesc.UseMnemonic = false;
        lblDesc.Text = strDescIntro;
        frmMain.Controls.Add(lblDesc);

        grpBodyTypes = new GroupBox();
        grpBodyTypes.Name = "grpBodyTypes";
        grpBodyTypes.Location = new Point(10, 110);
        grpBodyTypes.Size = new Size(250, 50);
        grpBodyTypes.TabIndex = tabIndex++;
        grpBodyTypes.TabStop = false;
        grpBodyTypes.Text = strBodyTypes;
        frmMain.Controls.Add(grpBodyTypes);

        rbUnp = new RadioButton();
        rbUnp.Name = "rbUnp";
        rbUnp.AutoSize = true;
        rbUnp.Location = new Point(10, 20);
        rbUnp.TabIndex = 0;
        rbUnp.TabStop = true;
        rbUnp.UseMnemonic = false;
        rbUnp.Text = strUnp;
        rbUnp.CheckedChanged += new EventHandler(rbUnp_CheckedChanged);
        rbUnp.MouseEnter += new EventHandler(rbUnp_MouseEnter);
        rbUnp.MouseLeave += new EventHandler(rbUnp_MouseLeave);
        grpBodyTypes.Controls.Add(rbUnp);

        rbCbbe = new RadioButton();
        rbCbbe.Name = "rbCbbe";
        rbCbbe.AutoSize = true;
        rbCbbe.Location = new Point(100, 20);
        rbCbbe.TabIndex = 1;
        rbCbbe.TabStop = false;
        rbCbbe.UseMnemonic = false;
        rbCbbe.Text = strCbbe;
        rbCbbe.CheckedChanged += new EventHandler(rbCbbe_CheckedChanged);
        rbCbbe.MouseEnter += new EventHandler(rbCbbe_MouseEnter);
        rbCbbe.MouseLeave += new EventHandler(rbCbbe_MouseLeave);
        grpBodyTypes.Controls.Add(rbCbbe);

        picPreview = new PictureBox();
        picPreview.Name = "picPreview";
        picPreview.Location = new Point(270, 0);
        picPreview.Size = new Size(250, 300);
        picPreview.TabIndex = tabIndex++;
        picPreview.TabStop = false;
        picPreview.Image = imgIntro;
        picPreview.SizeMode = PictureBoxSizeMode.StretchImage;
        frmMain.Controls.Add(picPreview);

        btnExit = new Button();
        btnExit.Name = "btnExit";
        btnExit.Location = new Point(10, 270);
        btnExit.Size = new Size(100, 24);
        btnExit.TabIndex = tabIndex++;
        btnExit.TabStop = true;
        btnExit.Text = strExit;
        btnExit.Click += new EventHandler(btnExit_Click);
        frmMain.Controls.Add(btnExit);

        btnInstall = new Button();
        btnInstall.Name = "btnInstall";
        btnInstall.Location = new Point(160, 270);
        btnInstall.Size = new Size(100, 24);
        btnInstall.TabIndex = tabIndex++;
        btnInstall.TabStop = true;
        btnInstall.Text = strInstall;
        btnInstall.Click += new EventHandler(btnInstall_Click);
        frmMain.Controls.Add(btnInstall);

        // Redraw
        frmMain.ResumeLayout(false);
        frmMain.PerformLayout();
    }
}

インストール方法

  • FOModC#Tutor01フォルダごとZIPファイルなどに圧縮してMod管理ツールでインストールします。

動作確認のポイント

  • 体型の選択肢にマウスカーソルを乗せると、説明文と画像が切り替わることを確認します。
  • インストールオプションを変えて、対応するダミーファイルがインストールされることを確認します。

f:id:thinkingskeever:20150501170055j:plain

解説

C#言語、.NET Frameworkの機能については説明しません。http://dobon.net/vb/dotnet/などの解説サイトを参照してください。

  • using fomm.Scripting;

 C# Scriptを使うための宣言です。

  • class Script : BaseScript

 BaseScriptはC# Scriptが提供するインストーラ用クラスのうち、一番基本的なものです。
 このクラスの派生クラスとして SkyrimBaseScript, FalloutNewVegasBaseScript といったゲーム特化のクラスが用意されています。

  • public bool OnActivate()

 インストーラが起動されたときに呼び出されるイベントメソッドです。
 このメソッド内でインストーラの初期化、表示、インストールの実行を行います。
 インストールが成功した場合はtrueを、インストールが失敗した/キャンセルした場合はfalseを返却します。

  • private void InstallFiles()

 ここでインストールオプションに応じたファイルをコピーしています。

  • private void CopyDataFolder(string targetFolder)

 指定したフォルダ以下のファイルをまとめてコピーするメソッドです。
 BaseScriptクラスではファイル単体をコピーするCopyDataFileメソッドしか提供していません。
 このため、GetFomodFileListメソッドを使ってFOMod内のファイル一覧を取得し、フォルダ名と前方一致させながら該当するファイルをコピーしています。

  • private Image LoadImage(string filename)

 FOMod内のイメージファイルを読み込むメソッドです。
 C# Scriptでは直接的なファイルI/Oが許可されていません。そこで、FOMod内のファイル内容を読み取るGetFileFromFomodメソッドをつかってファイル内容を読み込み、MemoryStreamを経由してImageに変換しています。

  • private void InitializeForm()

 インストーラの画面を作成しています。おそらくここが一番大変です。
 Visual Studio C#でフォームを作成してコピペしてくるのが楽だと思われます。


以上です。

Copyright (C) 2015-2020 ThinkingSkeever, All Rights Reserved.
ブログの記事内に記載されているメーカー名、製品名称等は、日本及びその他の国における各企業の商標または登録商標です。
リンクはご自由に。記事の転載はご遠慮ください。記事を引用する場合はトラックバックするか元のURLを明記してください。