ソラマメブログ

2007年05月29日

リモートロード

基本的な記事が続いているので、たまには中級者向けのスクリプトについても書いておこうと思います。
スクリプトのリモートロードについての記事です。

例えば・・・いろいろとスクリプトを駆使して面白い商品を作ったとします。
しかし、うっかり見落としていたバグやlslの仕様変更などで、欠陥商品になることも多々あります。
また、新たに機能を追加して、商品をバージョンアップするようなこともあるでしょう。
そのような時、すでに売れてしまった商品をどうしますか?

買ってくれた人全てを把握しておいて、新しい商品を送付するというのも、商売人としては誠意あるスタンスです。
ですが商品の数が増えてきたり、100個や200個が売れる人気商品ともなると、顧客をいちいち管理しておくことも難しくなってきます。
バグ対応やバージョンアップは何かと面倒な作業ですよね・・・。

そんなときに便利なのがスクリプトのリモートロードです。
スクリプトのリモートロード

実はlslには、オブジェクトに入っているスクリプトを書き換える関数が用意されています。
あんまり使っている人を見かけないのですが・・・これを使うとスクリプトのBugFixやバージョンアップを自動化できます。

簡単にその仕組みを図示してみます。

リモートロード

他のprim内のスクリプトを書き換えたり、新たなスクリプトを追加したりする仕組みのことをスクリプトのリモートロードと言います。
このリモートロードを行う際には、PINという特殊な識別番号を設定する必要があります。
というのも、何でもかんでもリモートロードを可能にしてしまうとセキュリティの面で大きな問題になってきますので、primに設定されているPINがわからなければロードできないようになっているのです。
PINというのは、リモートロードを行う際のパスワードみたいなものだと考えておくとわかりやすいかもしれません。

なお、PINはinteger型の数値です。
これを設定するには、llSetRemoteScriptAccessPin関数を使います。

  llSetRemoteScriptAccessPin(integer pin);

この関数によって指定されたPINはオブジェクトに対して設定されます。
オブジェクト内に複数のスクリプトがあったとしても、全て同じPINでリモートロードが可能です。
さらに言えば、llSetRemoteScriptAccessPin関数を実行したスクリプト自体が削除されたとしても、PINの設定自体は残ります。

つまり、スクリプトをあとから書き換えたい場合は、以下のようなスクリプト用意してオブジェクトに放り込んでおけば良いのです。

integer pin = 12345;

default {
  state_entry(){
    llSetRemoteScriptAccessPin(pin);
    llRemoveInventory(llGetScriptName());
  }
}

このスクリプトはオブジェクトにPINを設定した後、自分自身を削除します。
PINを秘密にするという観点からするとこの方法は効果的です。
もっとも、設定した本人がPINを忘れてしまったりするとリモートロードできなくなりますがw

【修正】商品に自己消滅型PIN設定スクリプトを組み込んだ場合、商品を販売した時点(手渡ししても同様)でオブジェクトのコピーが作られ、PINがクリアされてしまうため、正しくリモートロードできなくなります。自己消滅型のPIN設定は商品には向いていません。
さて次に・・・。
PINが設定されたオブジェクトに対し、スクリプトをロードするにはllRemoteLoadScriptPin関数です。

  llRemoteLoadScriptPin(key target, string name, integer pin, integer running, integer start_param);

引数が多いので順番に説明します。

key target

リモートロード先のターゲットprimのUUIDを指定します。
「どのオブジェクトに」ロードするのかを指定する引数です。

string name

リモートロードするスクリプトの名前を指定します。
「どのスクリプトを」ロードするのかを指定する引数です。
もちろんこのスクリプトはロード元のオブジェクトのコンテンツ内に無ければいけません。
また、スクリプトがNo Copyになってたりすると、「コピー」ではなく「移動」されてしまいますので注意して下さい。

ロード先のコンテンツ内に同一名称のスクリプトがあった場合は上書きされます。
同一名称のスクリプトが無ければ、コピーになります。

integer pin

PINを指定します。
先ほどのllSetRemoteScriptAccessPin関数で設定されたPINを使って下さい。

integer running

リモートロードしたスクリプトをすぐに動かしたいならTRUEです。
FALSEにすると、ロードされたスクリプトは停止状態になります。
停止状態のスクリプトをあとで動かすにはllSetScriptState関数を使います。

integer start_param

リモートロードしたスクリプトに渡すパラメータです。
任意の整数を指定できますが、特に渡したい情報がなければ0などの適当な整数でOKです。
このパラメータは、ロードされたスクリプトでllGetStartParameter関数を使って取得できます。

具体例を示しておきます。

  llRemoteLoadScriptPin("66864f3c-e095-d9c8-058d-d6575e6ed1b8",
    "main", 12345, TRUE, 2);

この例では、UUID"66864f3c-e095-d9c8-058d-d6575e6ed1b8"で特定されるオブジェクトに対して、"main"という名前のスクリプトをロードします。
PINは先ほど例示したスクリプトで設定した値、12345にしてみました。
ロードされたスクリプト"main"は即座に動き出します(defaultステートのstate_entryが発動します)。
"main"の中でllGetStartParameter()関数を使うと、2が取得されます。

なお、llRemoteLoadScriptPin関数でリモートロードを行う際は、ターゲットが同じSIM内になければなりません。

アップデート装置

さて、ではリモートロードの機能を使って、商品のアップデートを行う装置を作ってみましょう。
装置の外観はどんなものでも構いません。
お好みで自動販売機っぽいものや、スーパーコンピュータっぽいもの、あるいはただの球体でもいいです。

商品には以下のような2つのスクリプトを仕込みます。
PINは12345としておきますが、実際に使う場合には独自の数値に変更して下さい。

商品用スクリプトその1:PIN設定
integer pin = 12345;

default {
  state_entry(){
    llSetRemoteScriptAccessPin(pin);
  }
}


商品用スクリプトその2:アップデートコマンド処理
integer command_channel = 1;
integer remort_channel = -7;
integer handle;

default {
  state_entry(){
    handle = llListen(command_channel, "", llGetOwner(), "update");
  }

  listen(integer ch, string nm, key id, string msg){
    if (ch == command_channel && msg == "update"){
      llSay(remort_channel,"update");
    }
  }
}

一つ目のスクリプトは先ほども例示したPIN設定用のスクリプトです。
二つ目のほうは、ユーザーの「/1 update」という発言をlistenし、アップデートマシンに対してチャンネル-1で"update"と発言するだけのスクリプトです。
これがアップデートのきっかけとなります。

アップデート側のスクリプトは、オブジェクトの"update"という発言をlistenして動き出すようにします。
以下にアップデート側のスクリプトを示します。

リモートロードするスクリプトは、"main"と"sub"という名前のスクリプトにしてみました。
"main"と"sub"のスクリプトは、アップデート装置のコンテンツ内に入れておかなければなりません。

integer pin = 12345;
integer remort_channel = -7;
integer handle;

default {
  state_entry(){
    handle = llListen(remort_channel, "", NULL_KEY, "update");
  }

  listen(integer ch, string nm, key id, string msg){
    if (ch == remort_channel && msg == "update"){
      llSay(0, "Update process start. Please wait... > "
        + llKey2Name(llGetOwnerKey(id)));
      llRemoteLoadScriptPin(id,"main", pin, TRUE, 0);
      llRemoteLoadScriptPin(id,"sub", pin, TRUE, 0);
      llSay(0, "Update complete! > "
        + llKey2Name(llGetOwnerKey(id)));
    }
  }
}

こんな感じです。
llRemoteLoadScriptPin関数は、実行に3秒ほどかかります。
"main"と"sub"の二つをリモートロードしていますので、6秒ほどかかることになります。
その間、ユーザーがどこかに行ってしまわないよう、
「アップデート開始、ちょっと待ってね」
というメッセージを出すようにしました。
アップデート完了後には、「終わったよ」のメッセージです。

もしも商品の中に"main"と"sub"のスクリプトが存在しなければ、新たにコピーされることになります。
すでに"main"と"sub"がある場合は上書きです。

なお、注意すべき点は商品のパーミッションです。
アップデート装置のオーナーと商品のオーナーが異なる場合、商品がmod可でなければリモートロードは実行できません。
また、アタッチされた状態でも動作しませんので、商品を一度地面に置く必要があります。

アップデート装置と商品のオーナーが同じ場合はこの制限がありません。
アップデート装置そのものをフリー配布し、商品のオーナーにrezしてもらうと、mod不可の商品やアタッチメントでもリモートロードが可能になります。
この辺りは配布方法の工夫が必要です。

以上、最もシンプルなアップデート用のスクリプトでした。

データ保存

リモートロードを応用すると「データ保存」の仕組みも作れます。
RPGシステムなどで便利なセーブ&ロードが実現可能なわけです。

仕組みは以下の通り。

データ管理:
・データ管理用のスクリプトを用意し、保存すべきデータは全てこのスクリプト内に保持する

セーブ:
・セーブ用オブジェクトをrezし、データ管理用スクリプトをリモートロードする

ロード:
・セーブ用オブジェクトからデータ管理用スクリプトをリモートロードで戻す

※一度投稿した後、読み直したらものすごくわかりにくかったので図示してみました(^^;

リモートロード
リモートロード

まずデータ管理用スクリプトですが、単純なのは以下のような構造です。

【データ保管用スクリプト】(名称"data"で作成)
list data=[];

default {
  
  link_message(integer sender, integer num,string str, key id){
    if (str == "set"){
      integer i = llListFindList(data, [(string)id]);
      if ( i != -1){
         data = llListReplaceList(data, [(string)id, num], i, i+1);
      }else{
        data += [(string)id, num];
      }
    }else if (str == "get"){
      integer i = llListFindList(data, [(string)id]);
      if ( i != -1){
         llMessageLinked(sender, llList2Integer(data, i+1), "ret", id);
      }else{
         llMessageLinked(sender, num, "ret", id);
      }
    }
  }
  
}

データのやり取りはリンクメッセージを使って行います。
リンクメッセージで文字列"set"を受け取ったときは、キーidと整数値numをワンセットのデータとしてリスト型変数dataに保存します。
"get"を受け取ったときは、キーidで指定されたデータが保管されているかどうかを調べ、見つかれば対応するnumをリンクメッセージで送り返します。
・・・これ、整数値しか保管できませんけどもね(^^;

次のようなユーザー関数を用意しておくと楽かもしれません。

set_value(string keyword, integer value){
  llMessageLinked(LINK_THIS, value, "set", (key)keyword);
}

get_value(string keyword, integer default_value){
  llMessageLinked(LINK_THIS, default_value, "get", (key)keyword);
}

default {
  state_entry(){
    set_value("test", 128);
  }
  
  touch_start(integer detected){
    get_value("test", 0);
  }
  
  link_message(integer sender, integer num, string str, key id){
    if (str == "ret"){
      llSay(0, "Keyword:" + (string)id + "=" + (string)num);
    }
  }
}

この例ではまず最初に"test"というキーワードで値128を保管します。
タッチされると"test"というキーワードで保管されているデータを探し、見つかった値をsayします。
正しく128という整数値が保管されていればOKです。

次に、セーブとロードの仕組みです。
まずは保存用オブジェクトを用意します。
外観は何でもかまいません。CD型なり磁気テープ型なりメモリスティック型なりクリスタルなり、お好みで作ってください。
保存用オブジェクトの名前を"save data"とし、以下のスクリプトを組み込みます。

【保存用オブジェクトのスクリプト】
integer pin = 12345;
integer channel = -7;
integer handle;
string script_name = "data";

default {
  state_entry(){
    llSetRemoteScriptAccessPin(pin);
  }
  
  on_rez(integer i){
    if (i == 1){
      llSetObjectName(llGetObjectName() + "(" + llGetTimestamp() + ")");
    }
  }
  
  touch_start(integer detected){
    if (llDetectedKey(0) == llGetOwner()){
      handle = llListen(channel, "", NULL_KEY, "data load");
      llSay(channel, "data load");
    }
  }
  
  listen(integer ch, string nm, key id, string msg){
    if (msg == "data load"
        && id != llGetKey()
        && llGetOwnerKey(id) == llGetOwner()){
      llSay(0, "Data load process start. Please wait... ");
      llRemoteLoadScriptPin(id,script_name , pin, TRUE, 0);
      llSay(0, "Data load complete! ");
      llDie();
    }
  }
}

state_entryイベントとon_rezイベントはセーブ時に必要な機能を実装したものです。
touch_startイベントとlistenイベントはロード時に使われます。

まずstate_entryイベントではPINを設定し、データ保管用スクリプトを受け入れ可能にしています。
on_rezイベントではオブジェクト名の後ろに現在時刻を付与し、いつ保存されたデータなのかが判断できるようにしているだけです。

保存用オブジェクトがオーナーにタッチされると、チャンネル-7でlistenを開始し、"data load"と発言します。
これを本体側で受信し、本体もまた"data load"と発言するようにします。
すると保存用オブジェクトのlistenイベントが発動し、スクリプト"data"を本体へとリモートロードするようになっています。

ロード後は保存用オブジェクトは消滅します。
また、ゴミとして放置されることのないよう、保存用オブジェクトは一時的オブジェクトにしておくと良いかもしれません。

話が前後しますが、次に本体側のスクリプトです。
本体側では、主にセーブ時の処理を実装します。
本体のコンテンツには保存用オブジェクト"save data"と、先ほどのデータ保管用スクリプトを"data"という名前で入れておく必要があります。

【本体用スクリプト】
integer pin = 12345;
integer channel = -7;
integer handle;
string script_name = "data";
string dataobject_name = "save data";

default {
  state_entry(){
    llSetRemoteScriptAccessPin(pin);
    handle = llListen(channel, "", NULL_KEY, "");
  }
  
  touch_start(integer detected){
    if (llDetectedKey(0) == llGetOwner()){
      llRezObject(dataobject_name, llGetPos() + <1.0,0.0,0.0>,
        ZERO_VECTOR, ZERO_ROTATION, );
    }
  }
  
  object_rez(key id){
    llSleep(1.0);
    llSay(0, "Data save process start. Please wait... ");
    llRemoteLoadScriptPin(id,script_name , pin, TRUE, 0);
    llSay(0, "Data save complete! Take '" + dataobject_name + "'.");
  }
  
  listen(integer ch, string nm, key id, string msg){
    if (msg == "data load"
        && llGetOwnerKey(id) == llGetOwner()){
      llSay(channel, "data load");
    }
  }
}

この例ではタッチでセーブが実行されるようにしてありますが、実際に使うときにはチャットコマンド等に変更すべきでしょう。
タッチされると近くに保存用オブジェクト"save data"をrezします。
rezが完了するとobject_rezイベントが発生します。
1秒のウエイトが入れてあるのは保存用オブジェクト"save data"側でPINが正しく設定されるのを待つためです。
そしてrezした保存用オブジェクト"save data"に対して、データ保管用スクリプト"data"をリモートロードします。
これでデータが退避されたので、
「セーブ終わったよ!"save data"オブジェクトを拾っておいてね!」
とメッセージを出します。

一方ロード時には、保存用オブジェクト"save data"から"data load"という発言が来るので、本体側からもこれに答えて"data load"という発言を返すだけです。
あとは保存用オブジェクトのほうからデータ保管用スクリプト"data"がリモートロードされてきます。


同じカテゴリー(スクリプト小技)の記事
 モジュール化 (2007-09-04 12:15)
 高度なカメラ制御 (2007-07-19 12:15)
 Emailの送受信 (2007-05-23 15:18)
 鍵をかける (2007-05-10 16:24)
 パーティクル (2007-04-09 17:07)
 lslで日本語を使う (2007-04-06 14:21)
この記事へのトラックバック
複数のスクリプトの扱いについて、情報が散逸しているので自分用にまとめる。情報源は、http://wiki.secondlife.com/wiki/ やhttp://www.lslwiki.net/lslwiki/wakka.php やLSLGuide.pdf。(間違いがあったら教えて...
llRemoteLoadScriptPinで別スクリプト呼出【marchのBLOG】at 2007年06月03日 13:52
この記事へのコメント
Mizさん,どもおつかれさまです。
いやーまさか,この方法解説していただけるとは・・・これ某有名ペット店にアップローダが置いてあって,「どうやってるのかな」と当時謎だったんですよ。この関数を使用することまではわかったんですが,実際のやりかたはよくわからず実験も失敗続きでした。これは貴重なエントリですね,ありがとうございました。
Posted by のてす at 2007年05月29日 20:05
リモートロード、今一謎だったのですが、
この特集はすごいや。
しっかり読まさせていただきますね。
感謝 感激! とても貴重な資料になります。
ありがとうございます。
Posted by VtWin at 2007年05月30日 01:50
リモートロードは面白い機能なんですが、あんまり使われているのを見ないので記事にしてみました(^^
Posted by Miz at 2007年05月30日 12:59
アップデート装置面白いですね。
スクリプト以外のコンテンツはアップデート不可能なのでしょうか?。
Posted by もに at 2007年06月03日 11:17
貴重な記事ですね。私のページからもトラックバックしました。
llRemoteLoadScriptPinはどのイベント中に呼び出しても、defaultステートのstate_entryが発動するんですかねぇ。
Posted by march at 2007年06月03日 13:55
>もにさん

詳細に実験はしていませんが、llGiveInventory関数を使うとオブジェクトのコンテンツから他のオブジェクトのコンテンツにいろいろとものを渡すことができます。
ただ、同じ名前のアイテムが存在した場合は上書きされずに「item 1」「item 2」などのように名称の後ろに番号が増えていきます。
スクリプトからアイテムの存在チェックを行い、存在する場合はまず削除してからアイテムを受け取るようにすれば、一応アップデートにはなりますね。

>marchさん

リモートロードされたスクリプトは、スクリプトエディタで「保存」をしたときと同じ状況になりますので、defaultステートのstate_entryイベントから始まります。
ただし、スクリプトの起動パラメータ(整数値)を渡すことができますから、そのパラメータに応じて別のステートに遷移するなどの処理は実現可能です。
Posted by Miz at 2007年06月04日 12:15
いつも、大変参考にさせてい貰っています。
リモートロードのスクリプトを作ってみたのですが、うまくいかない点がありました。

>なお、注意すべき点は商品のパーミッションです。
>装置のオーナーと商品のオーナーが異なる場合、商品がmod可でなければリモートロードは実行できません。
>>また、アタッチされた状態でも動作しませんので、商品を一度地面に置く必要があります。
>>アップデート装置と商品のオーナーが同じ場合はこの制限がありません。

他の人に販売して第三者に渡ったときに、アップデート装置と商品のオーナーが同じ場合でもアップデートできませんでした。
なにか他に条件付けしなければ行けないのでしょうか?

良ければアドバイスしていただけないでしょうか?
Posted by もち at 2007年07月03日 22:57
>もちさん

記事見直していて気づいたのですが、PINを設定するスクリプトが自己消滅するのは商品の場合には問題がありました。

というのも、PINを設定した後、商品を販売したとき(もしくは手渡しても同じですが)にオブジェクトのコピーが作成され、PINはクリアされてしまうためです。

PIN設定スクリプトを自己消滅しないようにして動作確認してみて下さい。

integer pin = 12345;

default {
 state_entry(){
  llSetRemoteScriptAccessPin(pin);
 }
}

記事のほうも修正しておきます・・・。
Posted by Miz at 2007年07月04日 14:11
なるほどー。そういうことだったんですね。
自分では解明できなかったので助かりました^^
回答が早くて頭が下がります。
これからも新しい記事楽しみにしてます。
Posted by もち at 2007年07月05日 01:34
上記のやりかたで実装してみたのですが、Pinがセットされてもいきませんでした。
本体を次のオーナーがModifiy可能にすればいきます。Modifyにした場合はPinファイルを削除してもいきました。
Posted by もち at 2007年07月07日 03:23
>もちさん

むむ。。。オーナーが一緒でもMod権限がいるということですね。
今度確認してみます。
Posted by Miz at 2007年07月08日 21:07
llRemoteLoadScriptPin関数を使ってデータ保存できると言う事でこの記事を興味深く読ませて頂きました。
オブジェクト間の通信にリスナを使わずにこの方法でデータを飛ばしてやろうと思い色々試してみたのですが、どうしてもうまくいきません。
スクリプトを受け取った側で、送信前にセットしたはずのデータ内容を取り出そうとしてみても、中身が空っぽです。

>リモートロードされたスクリプトは、スクリプトエディタで「保存」をしたときと同じ状況になりますので、defaultステートのstate_entryイベントから始まります。

と仰っているとおり、送信側にてセットされた変数の内容まではllRemoteLoadScriptPin関数で送る事は出来ないのでは無いでしょうか?
上書きで書き戻した時も、スクリプト'data'の中身は初期化されてしまっているように思われます。
Posted by kundali at 2007年08月22日 23:40
>kundaliさん

そうですね、リモートロードはあくまでもスクリプトの「コード」をコピーするだけですので、変数の中身をそのまま送れるわけではありません・・・。

ということは、サンプルに載せてるコードは真っ赤な嘘ということに(><;
何を勘違いして書いたんだか、ちとこれはお馬鹿過ぎですね。

よくよく見直してみますので申し訳ありませんが少々お待ち下さい。
Posted by MizMiz at 2007年08月23日 18:36
いつもは意見させていただいてます
リモートロードいいですね
この記事みて おもったのが 昔みた
ファミコンディスクシステムであった 
おもちゃ屋での 書き換えシステムを おもいだします^^;
っと ここはわかるんですが 内容もわかるようになりたいとおもいます
(当分かかりそうですが)がんばります
Posted by 来美 at 2007年09月11日 20:12
 
<ご注意>
書き込まれた内容は公開され、ブログの持ち主だけが削除できます。