2009年08月31日
lslのlistで連想配列もどき・・
listがあまりにも不便なので、便利物です。
hash_add(string 名称 , list 値のリスト)
戻りはありません。名称を識別名にして、値のリストを登録します。
list hash(string 名称)
名称を登録済みの内容から検索して、見つかった値のリストを返します。
list hash_remove(string 名称)
名称を登録済みの内容から検索して、見つかった値のリストを返した上で、登録内容から削除します。
popの代用品の様な動き。
hash_clear()
登録内容を全消しします。
内容的には、与えられた名前,listの長さ,値のリスト・・・が順番に並んでいて、名前を探して見つかったものを出してくるだけの簡単なものです。
本当はなんと言う名前なんでしょう。。無学が曝されます。。
hash_add(string 名称 , list 値のリスト)
戻りはありません。名称を識別名にして、値のリストを登録します。
list hash(string 名称)
名称を登録済みの内容から検索して、見つかった値のリストを返します。
list hash_remove(string 名称)
名称を登録済みの内容から検索して、見つかった値のリストを返した上で、登録内容から削除します。
popの代用品の様な動き。
hash_clear()
登録内容を全消しします。
内容的には、与えられた名前,listの長さ,値のリスト・・・が順番に並んでいて、名前を探して見つかったものを出してくるだけの簡単なものです。
本当はなんと言う名前なんでしょう。。無学が曝されます。。
//ハッシュ?の登録
list tmpHash=[];
hash_add(string mykey,list l)
{
mykey="LSL_TMP_HASH"+mykey;
integer i=llListFindList(tmpHash,[mykey]);
if (i<0)
{
tmpHash+=[mykey,llGetListLength(l)]+l;
return;
}
else
{
integer len=llList2Integer(tmpHash,i+1);
tmpHash=llListReplaceList(tmpHash,[llGetListLength(l)]+l,i+1,i+len+1);
}
}
//ハッシュ?の削除
//削除されたリストを返す
list hash_remove(string mykey)
{
mykey="LSL_TMP_HASH"+mykey;
integer i=llListFindList(tmpHash,[mykey]);
if (i<0)
{
return [];
}
else
{
integer len=llList2Integer(tmpHash,i+1);
list l=llList2List(tmpHash,i+2,i+len+1);
tmpHash=llListReplaceList(tmpHash,[],i,i+len+1);
return l;
}
}
//ハッシュ?の読み出し
list hash(string mykey)
{
mykey="LSL_TMP_HASH"+mykey;
integer i=llListFindList(tmpHash,[mykey]);
if (i<0) return [];
integer len=llList2Integer(tmpHash,i+1);
return llList2List(tmpHash,i+2,i+len+1);
}
//ハッシュ?の全消し
hash_clear()
{
tmpHash=[];
}
2009年08月20日
ポイントカード

サーバーと連動して顧客情報を保存し、設定済みのポイントをお客様に加算し、しゃべるスクリプトをつくりました。
顧客が購入の瞬間にポイント加算処理を行う通常のpay方式のベンダーを最初は考えていたのですが、既存で店舗がある方が使おうとする場合、既存のベンダーを全て入れ替えなければなず、さらに新設のベンダーが既存のベンダーの機能と同等か、それを上回る機能を備えていなければならない、という事になりますから、それなら商品の初rezなり初装備の瞬間に一度だけポイント加算処理をするようにして、商品側にスクリプトを入れておけば良いのではないだろうか、という事で、rezまたは装備の瞬間に外部サーバーに問い合わせに行く様になっています。(それに、そうしておけばbuyでも動きますし・・・)
実際、ベテランの方お二人ほどに質問してみましたが、一人は
「ベンダー変えても良いよ?」
でしたが、もう一人は
「ありえへん」
でした。。。(笑
ですので、(安定して動くなら)既存店舗の改造が不要で、また、既存の優れたベンダー・サービスと並行して運用できるサービスを追加する為の要素、のような位置づけになっています。
モニターつまり実験台的に仕様を注文してくださる方を募集しています。何しろ自分でショップを経営したことがありませんので、どうすれば便利か、どうすればより面白いか、アイディアがありません。。^^;
仕様
1:rezの瞬間に初rezかどうかをチェックし、初でない場合(既にデーター登録済みの場合)はスクリプトを消して実行を終了します。この際、顧客(現在オーナー)の権限がコピー不可である場合は何度でも購入される商品と看做して商品のUUIDが登録されているかもチェックします。
2:商品情報データーベースから付加すべきポイントを拾い、加算した後に登録情報データーベースに書き込みます。全てのプロセスが成功すると、スクリプト自身を消して実行を終了します。
3:実行中に、実行開始と実行完了時点に話すべき内容をリストで指定できます。
4:セリフ内に置換文字列を入れると情報と入れ替えてから話します。
5:エラー発生時のセリフを指定でき、指定が無い場合は沈黙してこっそり処理します。
6:サーバーとの通信に失敗の際、オーナーにIMでサーバー不具合を通知してきます。
商品ごとにスクリプトに設定してやらなければならないのはサーバー上に保存した商品名(商品コード?)で、これに不一致があると誰かがrezするたびにオーナーにIMが飛んできます。
スクリプトカテゴリで何もスクリプト入らない記事が連続するのも何ですので、内部にあるセリフをしゃべる部分を。。
文字列
あいうえお{CUSTOMER_NAME}かきくけこ{CUSTOMER_NAME}さしすせそ
があったとして、{CUSTOMER_NAME}を置換するということは、一旦文字列を{CUSTOMER_NAME}で分割した後に、そのlistを置換すべき新しい文字で連結してやればよい事になります。
関数の呼び出しのコストがどのくらいなのかは不明ですが、便利物なのでphpの名前で関数化しました。
2009/11/3 追記
explodeとimplodeはlslに相当する関数がありました。
// www.lsleditor.org by Alphons van der Heijden (SL: Alphons Jano)
//セリフ
list l=["こんにちは{CUSTOMER_NAME}さん",
"このようになります。"];
string detectedName;
//explode(string セパレーター,string 文字列)
//http://search.net-newbie.com/php/function.explode.html
list explode(string sep,string src)
{
list res=[];
integer p=0;
integer sepLen=llStringLength(sep);
integer end=llStringLength(src)-1;
while (src!="")
{
p=llSubStringIndex(src,sep);
if (p<0) {
res+=[src];
src="";
}
else
{
if (p==0)
{
res+=[""];
}
else
{
res+=[llGetSubString(src,0,p-1)];
}
src=llGetSubString(src,p+sepLen,end);
if (src=="") res+=[""];
}
}
return res;
}
//implode(string セパレーター,string 文字列)
//http://search.net-newbie.com/php/function.implode.html
string implode(string sep,list src)
{
string res="";
integer max=llGetListLength(src);
integer i=0;
while(i<max)
{
res=res+llList2String(src,i)+sep;
i++;
}
res=llGetSubString(res,0,llStringLength(res)-llStringLength(sep)-1);
return res;
}
//str_replace(stirng 検索対象,string 置換内容,string 元の文字列)
//http://search.net-newbie.com/php/function.str-replace.html
string str_replace(string s,string r,string sub)
{
list res=explode(s,sub);
return implode(r,res);
}
//ownerSayByList(list せりふのリスト)
//リストに入っている文字列を順番にllOwnerSay()でしゃべる。
ownerSayByList(list l)
{
integer iMax=llGetListLength(l);
integer i=0;
while(i<iMax)
{
string s=llList2String(l,i);
s=str_replace("{CUSTOMER_NAME}",detectedName,s);
llOwnerSay(llBase64ToString(llStringToBase64(s)));
i++;
}
}
default
{
state_entry()
{
}
touch_start(integer total_number)
{
detectedName=llDetectedName(0);
ownerSayByList(l);
}
}
2009年08月13日
情報管理システムプロトタイプ
http://xiao.slmame.com/e705867.html
で会話していた内容で、実際にサーバーに情報を溜め込むためのサーバー側が、とりあえず動き出しました。
MySQL4+PHP5にて。
稼動しているURLはブログにはかけませんが、どういう代物なのかを簡単に書いておくほうが会話がスムーズになる気がしますので、仕様といいますか。。。
使用しているPHPプログラムは私のオリジナルですので、探しても手に入りません。
名前はUnauna-Umiといいます。
//Unauna-Umi-Module
/*************************************************************************
Unauna-SECONDLIFE
セカンドライフ情報管理/サーバー側モジュール
Copyright(c)RBK 2009.
**************************************************************************/
write("userdatas") {
id=3;
userid="demo";
passwd="demo";
permission="normal";
visible=1;
default_table='customer_data';
default_shopping_table='customer_shopping_data';
}
とりあえずデモアカウントをひとつ発行しています。
顧客情報テーブルと、購入履歴情報テーブルをユーザー毎に指定できるようにしてあります。
permissionでいろいろ切り分けできるようにしておきました。
/*************************************************************************
テーブル
**************************************************************************/
createTable("customer_data"){
}
createColumn("customer_data"){
userid,text,default '';
passwd,text,default '';
visible,int,default 1;
uuid,text,default '';
point,numeric,default 0;
rez_date,text,default '';
rez_date_unix,int,default 0;
}
createIndex("customer_data"){
uuid="customer_data_uuid";
}
顧客情報テーブルです。
オーナーのIDとパスワード、顧客のUUID、ポイント残高、誕生日とそのUNIXタイムを作らせて見ました。
ユーザーUUIDをキーにする検索が多そうなのでインデックスを張っています。
createTable("customer_shopping_data"){
like="customer_data";
}
createColumn("customer_shopping_data"){
item_name,text,default '';
item_uuid,text,default '';
price,int;
}
createIndex("customer_shopping_data"){
item_name="customer_shopping_data_item_name";
item_uuid="customer_shopping_data_item_uuid";
}
お買い物履歴のテーブルです。
顧客の情報に加え、商品名とそのUUID、価格のカラムを追加してみています。
/*************************************************************************
関数
**************************************************************************/
function mkXML(datas){
var res='';
foreach(datas as $did=>$data){
switch (did){
case "tablename":
case "parenttable":
case "contentstype":
case "templatefile":
case "usermode":
case "userid":
case "passwd":
break;
default:
res &&&='<'&&&did&&&'>'&&&data&&&'</'&&&did&&&'>';
}
}
return res;
}
XMLテンプレートで使う関数mkXML()を記述しています。
&&&は文字列の連結です。
内部処理用のカラム以外のものを、カラムの名前をタグに変換して囲んで返します。
useridとpasswdカラムも見せると嫌な気配なので生成しないことにしています。
カラム名称をそのまま使うのが気持ち悪ければ少し改造ですね。
function checkOwnerUUID(){
flags.customerDataTable="";
flags.customerShoppingDataTable="";
if (!isset(submitted.owner_userid) || !isset(submitted.owner_passwd) || !submitted.owner_userid || !submitted.owner_passwd) {
flags.errorMessage='認証情報が不正です。引数が足りません
';
return false;
}
var test=readData('userdatas','*',"userid='"&&&submitted.owner_userid&&&"' AND passwd='"&&&submitted.owner_passwd&&&"'");
if (!test.count) {
flags.errorMessage='認証情報が不正です。認証ができませんでした
';
return false;
}
flags.customerDataTable=test.0.default_table;
flags.customerShoppingDataTable=test.0.default_shopping_table;
return true;
}
オーナーのユーザーIDとパスワードで使うテーブル名を変数flagsの所定のキー名に入れている関数です。
エラーがある場合変数flags.errorMessageにエラーが入ります。
XMLテンプレートで場合わけに使うために、boolの戻り値を返します。
後々複数ユーザーによって利用することを前提にする為ですね。
<?xml version="1.0" encoding="UTF-8"?>
<response>
<doit if="submitted.rezDate">
<settarget sorce="rezDate" targetcolumn="rez_date" checktype="match:/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/" print="誕生日" />
<settarget exec="var tmp=explode('-',submitted.rezDate);submitted.rezUnixTime=mktime(0,0,0,tmp[1],tmp[2],tmp[0]);" />
<settarget sorce="rezUnixTime" targetcolumn="rez_date_unix" checktype="int" print="誕生日(UNIX)" />
</doit>
<doit if="submitted.submitted">
<doit if="checkOwnerUUID()">
<settarget sorce="owner_userid" targetcolumn="userid" checktype="required" print="Ownerの認証ID" />
<settarget sorce="owner_passwd" targetcolumn="passwd" checktype="required" print="OwnerのPassword" />
<settarget sorce="uuid" targetcolumn="uuid" checktype="required" print="UUID" />
<settarget sorce="point" targetcolumn="point" checktype="numericif" print="Point" />
<doit if="submitted.submitted=='pay'">
<settarget sorce="itemName" targetcolumn="item_name" checktype="required" print="商品名称" />
<settarget sorce="itemUUID" targetcolumn="item_uuid" checktype="required" print="商品UUID" />
</doit>
<settarget exec="checkSubmitted();" />
<settarget if="!flags.errorMessage && submitted.submitted=='pay'" exec="flags.tmpTargetId=submitted.targetId;submitted.targetId='';writeSubmitted(flags.customerShoppingDataTable);submitted.targetId=flags.tmpTargetId;" />
<settarget if="!flags.errorMessage" exec="writeSubmitted(flags.customerDataTable);" />
</doit>
</doit>
<settarget only="category" sort="updated:desc" />
<doit if="checkOwnerUUID()">
<settarget if="submitted.type=='customer'" use="flags.customerDataTable" />
<settarget if="submitted.type=='shopping'" use="flags.customerShoppingDataTable" />
<settarget sorce="owner_userid" target="userid" type="=" />
<settarget sorce="owner_passwd" target="passwd" type="=" />
<settarget sorce="uuid" target="uuid" type="=" />
<settarget sorce="rezDate" target="rez_date" type="=" />
<settarget sorce="rezUnixTime" target="rez_date_unix" type="=" />
</doit>
<doit if="flags.errorMessage"><error>1</error></doit><doit if="!flags.errorMessage"><error>0</error></doit>
<message>{errorMessage}</message>
<count>{count}</count>
<body>
<doit if="db.count"><doit exec="flags.dataXML=mkXML(db)" output="datas" limit="none">{dataXML}</doit></doit>
</body>
</response>
ブログが流行し始める前から仕様を勝手に決めて作っていたもので、タグの入力補完機能を使って楽をしたかったので、命令が全部タグになっています。
<?xml version="1.0" encoding="UTF-8"?>
<response>
とりあえずルートタグで囲まないとパーサー通らないので、MTのトラックバックを真似てみました。
何でもいいです。
<settarget exec="flags.dataEditTarget='targetId'" />
書き込み要求を送信する際に、データーベース側の管理番号が入っているクエリの名前を指定しています。
このクエリ名の内容を基にしてデーターを追加するか、アップデートするかが決まります。
<doit if="submitted.rezDate">
クエリ引数にrezDateがあれば、中身を評価します。
<settarget sorce="rezDate" targetcolumn="rez_date" checktype="match:/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/" print="誕生日" />
正規表現で****-**-**の日付形式になっているかチェックするように指示し、書き込みする場合はカラムrez_dateに書き込むように指示しています。
<settarget exec="var tmp=explode('-',submitted.rezDate);submitted.rezUnixTime=mktime(0,0,0,tmp[1],tmp[2],tmp[0]);" />
誕生日日付をUNIXタイムスタンプに変換させています。
<settarget sorce="rezUnixTime" targetcolumn="rez_date_unix" checktype="int" print="誕生日(UNIX)" />
誕生日UNIXタイムスタンプの書き込み先をrez_date_unixに指定しています。
今のところ意味はありませんが、整数であるかもチェックするように指示しています。
</doit>
<doit if="submitted.submitted">
クエリ引数にsubmittedというものがあり、値が何かはいっていて0でなければ、中身を評価します。
とりあえず、これに何か値がある場合、動作としては書き込みさせることにしました。
<doit if="checkOwnerUUID()">
追加した関数checkOwnerUUID()を呼び出して書き込み先のテーブルをセットしようとしています。
戻りの値がfalseの場合はこの中は実行されません。
<settarget sorce="owner_userid" targetcolumn="userid" checktype="required" print="Ownerの認証ID" />
<settarget sorce="owner_passwd" targetcolumn="passwd" checktype="required" print="OwnerのPassword" />
<settarget sorce="uuid" targetcolumn="uuid" checktype="required" print="UUID" />
<settarget sorce="point" targetcolumn="point" checktype="numericif" print="Point" />
書き込み先とクエリの対応関係を指定し、情報チェックの方法も指定しています。
ポイントは「送信されてきたなら実数」であること、その他は必須入力の指定になっています。
<doit if="submitted.submitted=='pay'">
<settarget sorce="itemName" targetcolumn="item_name" checktype="required" print="商品名称" />
<settarget sorce="itemUUID" targetcolumn="item_uuid" checktype="required" print="商品UUID" />
</doit>
クエリ名submittedの値がpayだった場合は、購入したアイテムの名前とUUIDも書き込むよう、書き込み先の指定と必須入力の指定をしています。
<settarget exec="checkSubmitted();" />
実際に入力情報をチェックする関数checkSubmitted()を呼び出しています。
エラーがあった場合はflags.errorMessageにエラー記述が入ります。
<settarget if="!flags.errorMessage && submitted.submitted=='pay'" exec="flags.tmpTargetId=submitted.targetId;submitted.targetId='';writeSubmitted(flags.customerShoppingDataTable);submitted.targetId=flags.tmpTargetId;" />
購入情報を、エラーがなければかきに行かせています。
submitted.targetIdの値をいじっているのは、指定されている場合更新だとみなそうとするからです。
買い物履歴は常に追加ですから、submitted.targetIdの値をクリアしています。
<settarget if="!flags.errorMessage" exec="writeSubmitted(flags.customerDataTable);" />
エラーがないなら、checkOwnerUUID()で拾わせた顧客情報テーブルに書き込みに行かせています。
この際、クエリ名targetIdの値がセットされていて、当該管理番号の情報があれば、アップデート、targetIdの値がなければ新規追加になります。
とりあえず更新させておくとカラムupdatedの値がその時刻のものになるので、最終利用時刻を拾えます。
</doit>
</doit>
<settarget only="contents" sort="updated:desc" />
テーブル情報ではなく、実際のデーターだけを扱うようにonly属性で指示し、さらに並び順を更新時刻の逆順にしています。
<doit if="checkOwnerUUID()">
とりあえず後から見ても思い出せるように、二度手間ですがオーナーのIDとパスワードでテストします。
戻りの値をあらかじめ何かに保存しておけば不要な関数呼び出しですね。
<settarget if="submitted.type=='customer' || !submitted.type" use="flags.customerDataTable" />
<settarget if="submitted.type=='shopping'" use="flags.customerShoppingDataTable" />
引数typeの値で、検索対象にするテーブルの名称を変えています。
<settarget sorce="owner_userid" target="userid" type="=" />
<settarget sorce="owner_passwd" target="passwd" type="=" />
<settarget sorce="uuid" target="uuid" type="=" />
<settarget sorce="rezDate" target="rez_date" type="=" />
<settarget sorce="rezUnixTime" target="rez_date_unix" type="=" />
各々のsorce属性にある名前の送信情報で、検索を行うべきカラムの名称と、一致のタイプを指定しています。
全部、完全一致検索になっています。
このほかにmatch属性を指定して結合条件をORにすることも出来ますが、指定のない場合AND(絞込み)で連結されます。送信されてきていない設定内容は無視されます。
内部的には上の設定はこうなります。
WHERE (((userid='demo')) AND ((passwd='demo')) AND ・・・・・・・ AND ((rez_date_unix=11245341)))
</doit>
<doit if="flags.errorMessage"><error>1</error></doit><doit if="!flags.errorMessage"><error>0</error></doit>
エラーメッセージがあれば<error>1</error>、なければ<error>0</error>を採用します。
<message>{errorMessage}</message>
エラーメッセージをはかせています。
<count>{count}</count>
検索結果の数を書いています。
<body>
<doit if="db.count"><doit exec="flags.dataXML=mkXML(db)" output="datas" limit="none">{dataXML}</doit></doit>
</body>
検索結果があれば、関数mkXML()で作ったタグたちをあるだけループ処理で<body>の中に書かせています。
</response>
ルートタグを閉じて、おしまいです。
作動のフロー
プリムでおきた何らかのイベント時に顧客のUUID(イベントをしたアバターのUUID)を拾って、サーバー側に
http://example.com?type=customer&owner_userid=demo&owner_passwd=demo&uuid=***&limit=1
などのクエリで検索結果を要求します。
オーナーのuseridとパスワードを指定しないで検索結果を返すと個人情報漏れになってしまうため、プリムからの要求では、何をするにせよ、かならず引数に指定しておかなければなりません。また、機能拡張の際にも、そのようにXMLテンプレートを作ってやる必要があります。
戻りは
最後に、処理結果に加えて上の戻りの<id>タグの中の数字を引数targetIdとして指定し、引数submittedにpay、ないし、なにか文字を入れて、書き込み要求として送信します。
http://example.com?submitted=pay&owner_userid=demo&owner_passwd=demo&targetId=3&uuid=***&・・・・・・・・・&limit=1
みたいな感じ。
書き込みが正常に成功すれば、書き込まれたデーターが同じXML形式で戻ってくるはずです。
失敗していると情報が見つからないため、<count>タグの中が0になります。または、入力情報チェックに引っかかった項目のエラーが<message>タグの中に羅列されてきます。
まとめ
要するに、
1:設定モジュールのテーブルのカラムを作っているところに増減してフラグや情報の数はすぐ増やせる。
2:<settarget>タグのsorce属性指定とtargetclumn属性指定で書き込むべきクエリ名とカラム名の対応関係は任意に変えられる。
3:<settarget>タグのsorce属性指定とtarget属性指定でクエリ名と検索対象とすべきカラム名の対応関係は任意に変えられ、複数条件指定にも自由に対応できる。
4:あちこちにあるif属性で場合わけできるので作動内容も任意に増やせる。
という動作をテンプレをいじるだけでしますので、拡張に関するサーバー側の問題はあまり考えなくて良いかと思います。
欠点は、
1:汎用コアなので基本的に動作がまったり。
2:複数ユーザーに使わせる場合、ユーザーに許す変更の範囲に気をつけないとセキュリティホールが出来てしまう。
3:このままでは正規化とかを一切考えていないのでデーターは冗長。メモリが少ないと止まるかも。
4:簡易CMS用のコアなので、副問い合わせなどを使う場合は関数にして戻りをとる方式で検索させないと出来ない。
といったところです。。
で会話していた内容で、実際にサーバーに情報を溜め込むためのサーバー側が、とりあえず動き出しました。
MySQL4+PHP5にて。
稼動しているURLはブログにはかけませんが、どういう代物なのかを簡単に書いておくほうが会話がスムーズになる気がしますので、仕様といいますか。。。
使用しているPHPプログラムは私のオリジナルですので、探しても手に入りません。
名前はUnauna-Umiといいます。
モジュール(初期設定ファイルおよび追加関数)の中身
//Unauna-Umi-Module
/*************************************************************************
Unauna-SECONDLIFE
セカンドライフ情報管理/サーバー側モジュール
Copyright(c)RBK 2009.
**************************************************************************/
write("userdatas") {
id=3;
userid="demo";
passwd="demo";
permission="normal";
visible=1;
default_table='customer_data';
default_shopping_table='customer_shopping_data';
}
とりあえずデモアカウントをひとつ発行しています。
顧客情報テーブルと、購入履歴情報テーブルをユーザー毎に指定できるようにしてあります。
permissionでいろいろ切り分けできるようにしておきました。
/*************************************************************************
テーブル
**************************************************************************/
createTable("customer_data"){
}
createColumn("customer_data"){
userid,text,default '';
passwd,text,default '';
visible,int,default 1;
uuid,text,default '';
point,numeric,default 0;
rez_date,text,default '';
rez_date_unix,int,default 0;
}
createIndex("customer_data"){
uuid="customer_data_uuid";
}
顧客情報テーブルです。
オーナーのIDとパスワード、顧客のUUID、ポイント残高、誕生日とそのUNIXタイムを作らせて見ました。
ユーザーUUIDをキーにする検索が多そうなのでインデックスを張っています。
createTable("customer_shopping_data"){
like="customer_data";
}
createColumn("customer_shopping_data"){
item_name,text,default '';
item_uuid,text,default '';
price,int;
}
createIndex("customer_shopping_data"){
item_name="customer_shopping_data_item_name";
item_uuid="customer_shopping_data_item_uuid";
}
お買い物履歴のテーブルです。
顧客の情報に加え、商品名とそのUUID、価格のカラムを追加してみています。
/*************************************************************************
関数
**************************************************************************/
function mkXML(datas){
var res='';
foreach(datas as $did=>$data){
switch (did){
case "tablename":
case "parenttable":
case "contentstype":
case "templatefile":
case "usermode":
case "userid":
case "passwd":
break;
default:
res &&&='<'&&&did&&&'>'&&&data&&&'</'&&&did&&&'>';
}
}
return res;
}
XMLテンプレートで使う関数mkXML()を記述しています。
&&&は文字列の連結です。
内部処理用のカラム以外のものを、カラムの名前をタグに変換して囲んで返します。
useridとpasswdカラムも見せると嫌な気配なので生成しないことにしています。
カラム名称をそのまま使うのが気持ち悪ければ少し改造ですね。
function checkOwnerUUID(){
flags.customerDataTable="";
flags.customerShoppingDataTable="";
if (!isset(submitted.owner_userid) || !isset(submitted.owner_passwd) || !submitted.owner_userid || !submitted.owner_passwd) {
flags.errorMessage='認証情報が不正です。引数が足りません
';
return false;
}
var test=readData('userdatas','*',"userid='"&&&submitted.owner_userid&&&"' AND passwd='"&&&submitted.owner_passwd&&&"'");
if (!test.count) {
flags.errorMessage='認証情報が不正です。認証ができませんでした
';
return false;
}
flags.customerDataTable=test.0.default_table;
flags.customerShoppingDataTable=test.0.default_shopping_table;
return true;
}
オーナーのユーザーIDとパスワードで使うテーブル名を変数flagsの所定のキー名に入れている関数です。
エラーがある場合変数flags.errorMessageにエラーが入ります。
XMLテンプレートで場合わけに使うために、boolの戻り値を返します。
後々複数ユーザーによって利用することを前提にする為ですね。
XMLテンプレート
<?xml version="1.0" encoding="UTF-8"?>
<response>
<doit if="submitted.rezDate">
<settarget sorce="rezDate" targetcolumn="rez_date" checktype="match:/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/" print="誕生日" />
<settarget exec="var tmp=explode('-',submitted.rezDate);submitted.rezUnixTime=mktime(0,0,0,tmp[1],tmp[2],tmp[0]);" />
<settarget sorce="rezUnixTime" targetcolumn="rez_date_unix" checktype="int" print="誕生日(UNIX)" />
</doit>
<doit if="submitted.submitted">
<doit if="checkOwnerUUID()">
<settarget sorce="owner_userid" targetcolumn="userid" checktype="required" print="Ownerの認証ID" />
<settarget sorce="owner_passwd" targetcolumn="passwd" checktype="required" print="OwnerのPassword" />
<settarget sorce="uuid" targetcolumn="uuid" checktype="required" print="UUID" />
<settarget sorce="point" targetcolumn="point" checktype="numericif" print="Point" />
<doit if="submitted.submitted=='pay'">
<settarget sorce="itemName" targetcolumn="item_name" checktype="required" print="商品名称" />
<settarget sorce="itemUUID" targetcolumn="item_uuid" checktype="required" print="商品UUID" />
</doit>
<settarget exec="checkSubmitted();" />
<settarget if="!flags.errorMessage && submitted.submitted=='pay'" exec="flags.tmpTargetId=submitted.targetId;submitted.targetId='';writeSubmitted(flags.customerShoppingDataTable);submitted.targetId=flags.tmpTargetId;" />
<settarget if="!flags.errorMessage" exec="writeSubmitted(flags.customerDataTable);" />
</doit>
</doit>
<settarget only="category" sort="updated:desc" />
<doit if="checkOwnerUUID()">
<settarget if="submitted.type=='customer'" use="flags.customerDataTable" />
<settarget if="submitted.type=='shopping'" use="flags.customerShoppingDataTable" />
<settarget sorce="owner_userid" target="userid" type="=" />
<settarget sorce="owner_passwd" target="passwd" type="=" />
<settarget sorce="uuid" target="uuid" type="=" />
<settarget sorce="rezDate" target="rez_date" type="=" />
<settarget sorce="rezUnixTime" target="rez_date_unix" type="=" />
</doit>
<doit if="flags.errorMessage"><error>1</error></doit><doit if="!flags.errorMessage"><error>0</error></doit>
<message>{errorMessage}</message>
<count>{count}</count>
<body>
<doit if="db.count"><doit exec="flags.dataXML=mkXML(db)" output="datas" limit="none">{dataXML}</doit></doit>
</body>
</response>
作動内容
このコアは検索条件の類をDreamweaverなどのHTMLエディタ上でざくざく書いていくことが出来、実質の動作順序は(大半は)テンプレートで指示します。ブログが流行し始める前から仕様を勝手に決めて作っていたもので、タグの入力補完機能を使って楽をしたかったので、命令が全部タグになっています。
<?xml version="1.0" encoding="UTF-8"?>
<response>
とりあえずルートタグで囲まないとパーサー通らないので、MTのトラックバックを真似てみました。
何でもいいです。
<settarget exec="flags.dataEditTarget='targetId'" />
書き込み要求を送信する際に、データーベース側の管理番号が入っているクエリの名前を指定しています。
このクエリ名の内容を基にしてデーターを追加するか、アップデートするかが決まります。
<doit if="submitted.rezDate">
クエリ引数にrezDateがあれば、中身を評価します。
<settarget sorce="rezDate" targetcolumn="rez_date" checktype="match:/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/" print="誕生日" />
正規表現で****-**-**の日付形式になっているかチェックするように指示し、書き込みする場合はカラムrez_dateに書き込むように指示しています。
<settarget exec="var tmp=explode('-',submitted.rezDate);submitted.rezUnixTime=mktime(0,0,0,tmp[1],tmp[2],tmp[0]);" />
誕生日日付をUNIXタイムスタンプに変換させています。
<settarget sorce="rezUnixTime" targetcolumn="rez_date_unix" checktype="int" print="誕生日(UNIX)" />
誕生日UNIXタイムスタンプの書き込み先をrez_date_unixに指定しています。
今のところ意味はありませんが、整数であるかもチェックするように指示しています。
</doit>
<doit if="submitted.submitted">
クエリ引数にsubmittedというものがあり、値が何かはいっていて0でなければ、中身を評価します。
とりあえず、これに何か値がある場合、動作としては書き込みさせることにしました。
<doit if="checkOwnerUUID()">
追加した関数checkOwnerUUID()を呼び出して書き込み先のテーブルをセットしようとしています。
戻りの値がfalseの場合はこの中は実行されません。
<settarget sorce="owner_userid" targetcolumn="userid" checktype="required" print="Ownerの認証ID" />
<settarget sorce="owner_passwd" targetcolumn="passwd" checktype="required" print="OwnerのPassword" />
<settarget sorce="uuid" targetcolumn="uuid" checktype="required" print="UUID" />
<settarget sorce="point" targetcolumn="point" checktype="numericif" print="Point" />
書き込み先とクエリの対応関係を指定し、情報チェックの方法も指定しています。
ポイントは「送信されてきたなら実数」であること、その他は必須入力の指定になっています。
<doit if="submitted.submitted=='pay'">
<settarget sorce="itemName" targetcolumn="item_name" checktype="required" print="商品名称" />
<settarget sorce="itemUUID" targetcolumn="item_uuid" checktype="required" print="商品UUID" />
</doit>
クエリ名submittedの値がpayだった場合は、購入したアイテムの名前とUUIDも書き込むよう、書き込み先の指定と必須入力の指定をしています。
<settarget exec="checkSubmitted();" />
実際に入力情報をチェックする関数checkSubmitted()を呼び出しています。
エラーがあった場合はflags.errorMessageにエラー記述が入ります。
<settarget if="!flags.errorMessage && submitted.submitted=='pay'" exec="flags.tmpTargetId=submitted.targetId;submitted.targetId='';writeSubmitted(flags.customerShoppingDataTable);submitted.targetId=flags.tmpTargetId;" />
購入情報を、エラーがなければかきに行かせています。
submitted.targetIdの値をいじっているのは、指定されている場合更新だとみなそうとするからです。
買い物履歴は常に追加ですから、submitted.targetIdの値をクリアしています。
<settarget if="!flags.errorMessage" exec="writeSubmitted(flags.customerDataTable);" />
エラーがないなら、checkOwnerUUID()で拾わせた顧客情報テーブルに書き込みに行かせています。
この際、クエリ名targetIdの値がセットされていて、当該管理番号の情報があれば、アップデート、targetIdの値がなければ新規追加になります。
とりあえず更新させておくとカラムupdatedの値がその時刻のものになるので、最終利用時刻を拾えます。
</doit>
</doit>
<settarget only="contents" sort="updated:desc" />
テーブル情報ではなく、実際のデーターだけを扱うようにonly属性で指示し、さらに並び順を更新時刻の逆順にしています。
<doit if="checkOwnerUUID()">
とりあえず後から見ても思い出せるように、二度手間ですがオーナーのIDとパスワードでテストします。
戻りの値をあらかじめ何かに保存しておけば不要な関数呼び出しですね。
<settarget if="submitted.type=='customer' || !submitted.type" use="flags.customerDataTable" />
<settarget if="submitted.type=='shopping'" use="flags.customerShoppingDataTable" />
引数typeの値で、検索対象にするテーブルの名称を変えています。
<settarget sorce="owner_userid" target="userid" type="=" />
<settarget sorce="owner_passwd" target="passwd" type="=" />
<settarget sorce="uuid" target="uuid" type="=" />
<settarget sorce="rezDate" target="rez_date" type="=" />
<settarget sorce="rezUnixTime" target="rez_date_unix" type="=" />
各々のsorce属性にある名前の送信情報で、検索を行うべきカラムの名称と、一致のタイプを指定しています。
全部、完全一致検索になっています。
このほかにmatch属性を指定して結合条件をORにすることも出来ますが、指定のない場合AND(絞込み)で連結されます。送信されてきていない設定内容は無視されます。
内部的には上の設定はこうなります。
WHERE (((userid='demo')) AND ((passwd='demo')) AND ・・・・・・・ AND ((rez_date_unix=11245341)))
</doit>
<doit if="flags.errorMessage"><error>1</error></doit><doit if="!flags.errorMessage"><error>0</error></doit>
エラーメッセージがあれば<error>1</error>、なければ<error>0</error>を採用します。
<message>{errorMessage}</message>
エラーメッセージをはかせています。
<count>{count}</count>
検索結果の数を書いています。
<body>
<doit if="db.count"><doit exec="flags.dataXML=mkXML(db)" output="datas" limit="none">{dataXML}</doit></doit>
</body>
検索結果があれば、関数mkXML()で作ったタグたちをあるだけループ処理で<body>の中に書かせています。
</response>
ルートタグを閉じて、おしまいです。
作動のフロー
プリムでおきた何らかのイベント時に顧客のUUID(イベントをしたアバターのUUID)を拾って、サーバー側に
http://example.com?type=customer&owner_userid=demo&owner_passwd=demo&uuid=***&limit=1
などのクエリで検索結果を要求します。
オーナーのuseridとパスワードを指定しないで検索結果を返すと個人情報漏れになってしまうため、プリムからの要求では、何をするにせよ、かならず引数に指定しておかなければなりません。また、機能拡張の際にも、そのようにXMLテンプレートを作ってやる必要があります。
戻りは
<?xml version="1.0" encoding="UTF-8"?>のようなXML文書になるので、プリム側ではこれらの情報を元に買い物など処理を行います。
<response>
<error>0</error>
<message></message>
<count>1</count>
<body>
<datas>
<id>3</id>
<date>1250134497</date>
<updated>1250139960</updated>
<visible>1</visible>
<uuid>****-**-******</uuid>
<is_receive>1</is_receive>
<rez_date>1970-01-01</rez_date>
<rez_date_unix>0</rez_date_unix>
<point>0</point>
</datas>
</body>
</response>
最後に、処理結果に加えて上の戻りの<id>タグの中の数字を引数targetIdとして指定し、引数submittedにpay、ないし、なにか文字を入れて、書き込み要求として送信します。
http://example.com?submitted=pay&owner_userid=demo&owner_passwd=demo&targetId=3&uuid=***&・・・・・・・・・&limit=1
みたいな感じ。
書き込みが正常に成功すれば、書き込まれたデーターが同じXML形式で戻ってくるはずです。
失敗していると情報が見つからないため、<count>タグの中が0になります。または、入力情報チェックに引っかかった項目のエラーが<message>タグの中に羅列されてきます。
まとめ
要するに、
1:設定モジュールのテーブルのカラムを作っているところに増減してフラグや情報の数はすぐ増やせる。
2:<settarget>タグのsorce属性指定とtargetclumn属性指定で書き込むべきクエリ名とカラム名の対応関係は任意に変えられる。
3:<settarget>タグのsorce属性指定とtarget属性指定でクエリ名と検索対象とすべきカラム名の対応関係は任意に変えられ、複数条件指定にも自由に対応できる。
4:あちこちにあるif属性で場合わけできるので作動内容も任意に増やせる。
という動作をテンプレをいじるだけでしますので、拡張に関するサーバー側の問題はあまり考えなくて良いかと思います。
欠点は、
1:汎用コアなので基本的に動作がまったり。
2:複数ユーザーに使わせる場合、ユーザーに許す変更の範囲に気をつけないとセキュリティホールが出来てしまう。
3:このままでは正規化とかを一切考えていないのでデーターは冗長。メモリが少ないと止まるかも。
4:簡易CMS用のコアなので、副問い合わせなどを使う場合は関数にして戻りをとる方式で検索させないと出来ない。
といったところです。。
2009年08月11日
スカート製造装置/プロトタイプ

複数の部品対応、各部品についていろいろ微調整できる楕円形配置スクリプトです。
意地になって調整可能な変数を増やしましたので、多分簡単に使えるジェネレーターではなくなっていると思われます。まだまだ増やすかもしれません。
ゆえに今のところは知り合い限定となり、スクリプト内容は非公開とさせて頂きます。
ただのチュートリアルです。
使用方法 0:初期化
初期化は手動です。(笑
初期状態ではRBKが最後に作動実験をしていた状態で手渡されるはずですので、中にあるはずの次の3物体を自分のイベントリにコピーします。
ほかはゴミです。捨ててください。
1:main スクリプトファイル
2:centralDogma 同じ名前の物体を装備するかrezして開いた、その中のスクリプト
3:settings ノートカード
これらのうち、settings以外の二つにコピー権限が無い場合はIMしてください。動きません。
settingsの内容を以下のものに差し替えてください。
最初の状態
使用方法 1:著作者表示変更
スクリプト「centralDogma」を、作成する物体の中心になる何らかのオブジェクトに入れてください。
初期状態では赤い玉ですが、何でもかまいません。このスクリプトが入っている物体のクリエイター表示が、クリエイターの名前になります。
適当に名前をつけてください。
編集画面を開いて、スクリプト「main」の最上部にある初期設定を編集してください。赤字の部分に、上で用意した中心物の名前を設定します。
//スカート製造装置・装置オブジェクト
//************************************
//動作設定項目
//1:本体(中心に出現させるコア)の名前
string centralDogma="centralDogma";
//2:コアを出現させる位置の調整
float offsetX=0.0;
float offsetY=0.0;
float offsetZ=0.0;
//3:デフォルトで許すrezの上限数
integer MAX_REZ_LIMIT=64;
//4:デフォルトの設定ノートカードの名称
string settingFileName="settings";
//************************************
使用方法 2:作動試験
準備が終わったらスクリプト「main」、「centralDogma」が入っている物体、ノートカード「settings」を何らかの物体に入れてください。
次に何らかの物体「test」を用意して、同じく入れてください。
タッチしてしばらく待ち、半径1メートルの円状に物体が出れば成功です。
放置しておくとリンクし、そのうち消えます。
下の画像ではキューブ「test」を適当につくって放り込み、テストしています。

使用方法 3:チャットコマンド
半角スラッシュで始まるコマンドで、二種類の設定が出来ます。
1:出現させるプリムの最大数
/sk limit 数
で設定します。初期状態では64個になっています。
設定ノートカードで指定の数がこれより多くても、設定ノートカードの指定が無視される安全弁です。
2:設定ノートカードの名前
/sk file ファイル名
で設定します。初期状態では「settings」になっています。
複合する円を使用した作成物を作ったり、複数の設定を内部においておく場合、ノートカードを適当に名づけて入れておいて、このコマンドで切り替えてください。
使用方法 4:設定ノートの内容
規則その1
行頭が「//」で始まる行はすべて無視されます。
行頭しか効果がありません。
規則その2
行頭が「/*」~「*/」で囲まれている範囲はすべて無視されます。
複数行に対応していますが、厳密なチェックはしておりませんので、
/*
なんだか
/*
なんとか
*/
ここは無視されません
*/
という具合になります。
規則その3
設定ファイルの内容をコピペして複数繰り返すと、おのおのの指定に従ってループ製造を何度も繰り返そうとします。
ただし読み込みに時間がかかるのであまり価値はありませんでした。^^;
以下、数がありますが基本的に説明がついているとおりの動作をします。
ややこしそうなものを抜粋して画像を取ってみました。
rez_type:実験モード
行頭を//にして無視させていない場合、作成される物体を自動リンク後に臨時オブジェクトにしようとします。
rezした物体の総数が64個を超えると、自動臨時オブジェクト化はうまく動作しません。手動で消してください。
また、実験モードを無視させている場合や、自動リンクが行われない数をrezさせた場合、今のところ、コアの中に本体スクリプトや部品、設定ファイルなどが残ります。実害はありませんが気になれば手動で捨ててください。
#プリム名称,プリム名称,・・・
行頭が#で始まる行は、出現させる物体の名称の指定です。
半角カンマ「,」で区切って複数指定すると、一つrezする毎に入れ替わりながらrezされていきます。
例)紅白のスカート
赤い部品「red」と白い部品「white」をマネキンの中にいれ、以下に設定してタッチします。
#white,red

Ra:数
Rb:数
楕円のX軸とY軸の半径です。
直径ではなく半径を指定します。脳がところてんになっている私はよく直径を指定して期待通りにならずイラついています。
大よそネックレスサイズまでは作動していましたが、ブレスレットサイズで作動するかどうかは未検証です。
AngleZ:数
部品の回転に一律に傾きを指定します。
実はDefaultRotYを指定するのと動作が同じです。^^;
deltaXZ:数
配置を傾けます。
例)20度を指定。真正面(X軸)から見た図。

deltaYZ:数
配置を傾けます。
例)20度を指定。真正面(X軸)から見た図。

useAccelX:数
accelXA:数
accelXB:数
useSinX:0か1か-1
配置曲面の指定です。
二次式z=useAccelX*(x-accelA)(x-accelB)にしたがって頂点を指定した曲面上に物体を配置できます。
指定する場合、x座標が(-Ra,Ra)を(-1,1)に読み替えて指定します。
つまり、補正を0にしたい場所の割合を、accelAとaccelBに指定します。
useSinXが1になっている場合、0度180度付近で変形による回転が減衰します。

例)
useAccelX:1.0
accelXA:-1.0
accelXB:1.0
useSinX:1.0
z=(x+1)(x-1) の意味です。
x座標が-RaとRaの場所で補正が0になるため、ちょうどY軸上が頂点になる放物線を描きます。
頂点位置が中心から1メートルになり、-RaとRaで(つまりX軸上で)元の高さになります。

useAccelX:1.0
accelXA:-1.0
accelXB:0.0
useSinX:1.0
z=x(x+1) の意味です。
x座標が-Raと0の場所で補正が0になるため、頂点が後ろにある放物線に見えるようになります。
頂点位置が高さ1メートル補正になり、前側はマイナス側に補正されています。

useAccelX:1.0
accelXA:-1.0
accelXB:0.0
useSinX:1.0
を別アングルから。
useSinXが1の場合、このように天板の回転がつながって見えます。

useSinXを0にすると、こうなります。

useSinXを-1にすると、こうなります。

useAccelT:数
accelTA:数
accelTB:数
useSinT:0か1
二次曲線に従って配置される角度を補正します。
accelX系と異なるのは、0度から180度を0~1に読み替えることです。
accelX系と同じ法物線を指定して配置間隔を微調整したり、もともとの楕円の扁平率が大きい場合に微調整して、当幅な配置にすることが出来ます。
例)上がX軸正方向です。
useAccelT:1.0
accelTA:-1.0
accelTB:1.0
useSinT:1.0

useAccelT:5.0
accelTA:-1.0
accelTB:0.0
useSinT:1.0
補正の大きさuseAccelTが怪しいと結果も怪しいことになります。
この場合は補正が大きすぎます。
一応暴走はしないはずですが・・^^; 、本来紅白にならぶはずの物体がずれています。

rotationDeltaZ:数
指定の角度で軸周りに回転します。
要するに、チェーンなどです。。
例)30度で回転

/**********************************************************************
設定ファイルの初期状態
***********************************************************************/
/**********************************************************************/
//動作モード
//rez_type:実験モードとすると、作成されるスカートは臨時オブジェクトになります。
//本作成時にはコメント化してください。
rez_type:実験モード
/**********************************************************************/
/**********************************************************************/
//対象プリム名称
//半角カンマで区切って複数指定すると、一つrezするたびに順に入れ替わっていきます。
#test
/**********************************************************************/
//パーツの初期回転(実数・度)
//パーツデザイン画面の値をそのまま入れられます。
//指定すると初期回転として使用されます。
//パーツの回転計算はすべてを0指定の場合を基準にしておこなわれ、
//指定の初期回転をした後に、計算結果の回転が付加されます。
DefaultRotX:0.0
DefaultRotY:90.0
DefaultRotZ:0.0
//いくつのプリムで分割するか(正の整数)
//ひとつの設定ファイルで生成される部品プリムの合計が64を超えると自動リンク処理は実行されません。
Partitions:12
//配置開始の角度(実数・度)
//設定すると配置開始位置が0方向からずれます。
offsetTheta:0.0
//配置終了の角度(実数・度)
//設定すると配置開始終了位置が0方向からずれます。
limitTheta:360.0
//楕円のX軸方向の半径(正の実数・メートル)
Ra:1.0
//楕円のY軸方向の半径(正の実数・メートル)
Rb:1.0
//ひらひらの縦方向すぼまりの傾き(正の実数・度)。
//プラス指定で上がすぼまるテーパー状に配置されます。
//スカート用
AngleZ:0
//スカートを出現させる装置中心位置からのX位置調整(実数・メートル)
//0でマネキンの位置。
PositionX:0.0
//スカートを出現させる装置中心位置からのY位置調整(実数・メートル)
//0でマネキンの位置。
PositionY:0
//スカートを出現させる装置中心位置からの高さ調整(実数・メートル)
//値を大きくすると下に下がります。
PositionZ:0.0
//X軸とスカート上端面の傾き(実数・度)
//指定が0以外の場合、前方向から見て傾斜して見えます。
deltaXZ:0
//Y軸とスカート上端面の傾き(実数・度)
//指定が0以外の場合、横方向から見て傾斜して見えます。
deltaYZ:0
//X軸による生成位置の加速
//Z位置の調整 dZ=(x-a)*(x-b)
//の二次式に従って出現位置が法物面になります。
//xの値は-a~a(x軸の直径)を-1~1に置き換えた値をとります。
//ネックレスの自動生成などに。
//useAccelXは効果の大きさで、0の場合は計算されません。
//useAccelX値を大きくしすぎると変異が大きすぎて自動リンクが作動しなくなります。
//小さ目の値から試してください。
//useSinXに1を指定した場合、サインカーブによって傾き計算に減速補正がかかり、プリム天井面が滑らかに並んで見えます。
//useSinXに0を指定した場合、計算補正は行われません。指定のままの2次曲線に従います。
//useSinXに-1を指定した場合、加速結果による回転自体を行いません。
useAccelX:0.0
accelXA:-1.0
accelXB:1.0
useSinX:1.0
//角度位置による回転角の加速
//XY位置の、楕円中心から見た回転角の調整 dθ=(θ-a)*(θ-b)
//の二次式に従って出現方向が調整されます。
//θの値は0~180度を0~1に置き換えた値をとります。
//Partitionsで指定した分割角度を基準にして調整されます。
//ネックレスの自動生成などに。
//useAccelTは効果の大きさで、0の場合は計算されません。
//useSinTに1を指定した場合、サインカーブによって計算に減速補正がかかり、開始位置と180度位置付近で角度補正の値が小さくなります。
//useSinTに0を指定した場合、計算補正は行われません。指定のままの2次曲線に従います。
useAccelT:0.0
accelTA:-1.0
accelTB:1.0
useSinT:1.0
//一回REZ毎のオブジェクトの横軸周り回転(実数・度)
//指定すると軸を回転しながら生成されます。
//チェーンの自動生成などに。
rotationDeltaZ:0.0;
2009年08月09日
PIの比較で90度と270度を切り分けるには・・
ループ処理で角度を加算している部分で、
単純に
if(Theta==PI*0.5) {} else if (Theta==PI*1.5) { }
では浮動小数点の比較でうまくいかない状態でした。
そこで、
if ((float)((string)(Theta*RAD_TO_DEG))==90.0) { }
の様に、いったん文字列にした後にfloatに戻して比較すると期待通りに動作しました。
また、PHPのround()関数の様な物を用意しておいて、任意ケタでいったん丸めてから比較してやると動きました。
型変換とどちらが良いのでしょうか。。。ちょっと不明。
もっとも、90度と270度を切り分けるなんて、めったに出てきませんが・・^^;
単純に
if(Theta==PI*0.5) {} else if (Theta==PI*1.5) { }
では浮動小数点の比較でうまくいかない状態でした。
そこで、
if ((float)((string)(Theta*RAD_TO_DEG))==90.0) { }
の様に、いったん文字列にした後にfloatに戻して比較すると期待通りに動作しました。
また、PHPのround()関数の様な物を用意しておいて、任意ケタでいったん丸めてから比較してやると動きました。
型変換とどちらが良いのでしょうか。。。ちょっと不明。
もっとも、90度と270度を切り分けるなんて、めったに出てきませんが・・^^;
// www.lsleditor.org by Alphons van der Heijden (SL: Alphons Jano)
float round(float a,integer p)
{
float t=llPow(10.0,p);
return llRound(a*t)/t;
}
test(float theta)
{
if (ceil(theta,6)==ceil(0.5*PI,6)) {
llSay(0, "0.5");
}else if (ceil(theta,6)==ceil(1.5*PI,6))
{
llSay(0, "1.5");
}
}
default
{
state_entry()
{
integer i=64;
float dtheta=2.0*PI/i;
float theta=0;
while(i--)
{
theta=dtheta*i;
test(theta);
}
}
}
2009年08月08日
LSLコード色づけ~これはすごいですね
Hidenori Glushenko様作成によるスクリプト整形CGI
これは便利です。(;_;/~
色づけはもちろん、Wikiへのリンクも自動的に行ってくれます。
詳細はScripters Cafe・「ブログ用LSLコード色づけ」で解説しておられます。
これは便利です。(;_;/~
色づけはもちろん、Wikiへのリンクも自動的に行ってくれます。
詳細はScripters Cafe・「ブログ用LSLコード色づけ」で解説しておられます。
2009年08月08日
スカート製造装置(物体の回転その3)

楕円の方程式は
x*x/a*a-y*y/b*b=1
y=tan(θ)*x
を代入して変形すると、
(1/a/a+tan(θ)*tan(θ)/b/b)x*x-1=0
となり、あとは二次方程式の根の公式なり、手計算変形なりで部品をrezすべきy座標はみつかります。
それに対空砲を組み合わせてやれば、スカートは自動で製造出来るということになります。
多少アレンジして設定ファイルを読み込むようにしました。
本体が入っているオブジェクトの中にsettingsがあり、本体のインベントリにスカートの部品をいれると準備が完了します。あとはタッチで。
本体
//90度を何分割するか
integer maxNumOfPartitions=10;
//楕円のX軸方向の半径(メートル)
float a=0.25;
//楕円のY軸方向の半径(メートル)
float b=0.2;
//ひらひらの縦方向の傾き(度)。プラス指定で上がすぼまる
float ThetaZ=10;
//スカートを出現させる位置の装置中心からのX軸距離
float offsetX=0.0;
//スカートを出現させる位置の装置中心からのY軸距離
float offsetY=0.0;
//スカートを出現させる装置中心位置からの高さ調整
float offsetZ=0.5;
//X平面から離れていく大きさ
float dXZ=0;
//X平面から離れていく大きさ
float dYZ=0;
float x1=0.0;
float y1=0.0;
float dTheta=PI;
float Theta=0;
string partsName="";
key settingsHandle;
integer settingsLine;
list objectNames_Inventory=[];
integer isReading=FALSE;
integer isMaking=FALSE;
init()
{
Theta=0;
//設定読み込み
readConf();
//向き直る
llSetRot(ZERO_ROTATION);
}
readConf()
{
//設定のノートカード読み取り
isReading=TRUE;
settingsLine=0;
settingsHandle=llGetNotecardLine("settings",settingsLine);
}
remveObject()
{
//インベントリー内部掃除
integer num=llGetInventoryNumber(INVENTORY_OBJECT);
integer i=0;
if (num!=INVENTORY_NONE)
{
for (i=0;i<num;i++)
{
llRemoveInventory(llGetInventoryName(INVENTORY_OBJECT,0));
}
}
}
setPoint()
{
if (llFabs(Theta)==PI/2)
{
y1=b;
}
else
{
y1=llSqrt((1-x1*x1/a/a)*b*b);
}
}
rezOb(float x,float y)
{
vector targetVec=<x,y,0>;
vector norm=llVecNorm(targetVec);
rotation newRot=llRotBetween(<1,0,0>,<norm.x,norm.y,0>);
rotation newRot2=llEuler2Rot(<0,PI+ThetaZ*DEG_TO_RAD*-1.0,0>);
if (partsName!="" && llGetInventoryKey(partsName)!=NULL_KEY)
{
llRezObject(partsName,llGetRootPosition()+<x+offsetX,y+offsetY,offsetZ*-1-x*llSin(dXZ*DEG_TO_RAD)-y*llSin(dYZ*DEG_TO_RAD)>,ZERO_VECTOR,newRot2*newRot,1);
}
}
rezObject()
{
setPoint();
rezOb(x1,y1);
rezOb(x1*-1,y1);
rezOb(x1,y1*-1);
rezOb(x1*-1,y1*-1);
}
default
{
state_entry()
{
init();
remveObject();
}
on_rez(integer start_param)
{
init();
remveObject();
}
touch_start(integer total_number)
{
if (isMaking)
{
llSay(0,"Ooops! Now,I'm busy.Please wait a moment");
return;
}
llSay(0,"Start create a new skirt");
if (partsName=="" || llGetInventoryKey(partsName)==NULL_KEY)
{
llOwnerSay("ERROR:I can't find prim "+partsName+" in my inventory.");
return;
}
isMaking=TRUE;
init();
llSetTimerEvent(0.1);
}
timer()
{
if (isReading)
{
return;
}
llSetTimerEvent(0.0);
integer i=0;
float CA=0;
float CB=0;
float CC=-1;
rezOb(a,0);
rezOb(a*-1,0);
rezOb(0,b);
rezOb(0,b*-1);
for (i=1;i<maxNumOfPartitions;i++)
{
Theta=dTheta*i;
CA=1/a/a+llTan(Theta)*llTan(Theta)/b/b;
x1=(llSqrt(CB*CB-4*CA*CC)-CB)/(2*CA);
rezObject();
}
llSleep(2.0);
llSay(0,"Finished");
isMaking=FALSE;
}
changed(integer change)
{
integer num=llGetListLength(objectNames_Inventory);
integer num2=llGetInventoryNumber(INVENTORY_OBJECT);
integer i=0;
string objName="";
integer j=-1;
list tmp=[];
if (change & CHANGED_INVENTORY)
{
if (num2>0)
{
for (i=0;i<num2;i++)
{
objName=llGetInventoryName(INVENTORY_OBJECT,i);
tmp+=objName;
j=llListFindList(objectNames_Inventory,[objName]);
if (j==-1)
{
partsName=objName;
llOwnerSay("Found new item:"+partsName);
}
}
}
else
{
partsName="";
}
objectNames_Inventory=tmp;
}
}
dataserver(key requested, string data)
{
integer p=-1;
string m1="";
string com="";
if (requested==settingsHandle)
{
if (data != EOF) {
p=llSubStringIndex(data,"//");
if (p==-1)
{
//データー行
p=llSubStringIndex(data,":");
m1=llGetSubString(data,p+1,llStringLength(data)-1);
com=llGetSubString(data,0,p-1);
if (com=="Partitions") maxNumOfPartitions=(integer)m1;
if (com=="Ra") a=(float)m1;
if (com=="Rb") b=(float)m1;
if (com=="AngleZ") ThetaZ=(float)m1;
if (com=="PositionX") offsetX=(float)m1;
if (com=="PositionY") offsetY=(float)m1;
if (com=="PositionZ") offsetZ=(float)m1;
if (com=="deltaXZ") dXZ=(float)m1;
if (com=="deltaYZ") dYZ=(float)m1;
}
settingsLine++;
settingsHandle=llGetNotecardLine("settings",settingsLine);
}
else
{
//dθ
dTheta=PI/2/maxNumOfPartitions;
//初期位置
x1=a;
isReading=FALSE;
}
}
}
}
設定ファイル(ノートカード:settings)
//90度を何分割するか(正の整数)
Partitions:2
//楕円のX軸方向の半径(正の実数・メートル)
Ra:0.25
//楕円のY軸方向の半径(正の実数・メートル)
Rb:0.2
//ひらひらの縦方向すぼまりの傾き(正の実数・度)。
//プラス指定で上がすぼまるテーパー状に配置されます。
AngleZ:20
//スカートを出現させる装置中心位置からのX位置調整(実数・メートル)
//0でマネキンの位置。
PositionX:0
//スカートを出現させる装置中心位置からのY位置調整(実数・メートル)
//0でマネキンの位置。
PositionY:0
//スカートを出現させる装置中心位置からの高さ調整(実数・メートル)
//値を大きくすると下に下がります。
//パーツのZ軸サイズの半分で大体マネキンの腰のあたりになります。
PositionZ:0.25
//X軸とスカート上端面の傾き(実数・度)
//指定が0以外の場合、前方向から見て傾斜して見えます。
deltaXZ:0
//Y軸とスカート上端面の傾き(実数・度)
//指定が0以外の場合、横方向から見て傾斜して見えます。
deltaYZ:10
2009年08月02日
パーティクル設定項目について
パーティクルの設定項目で特にわかりにくかったものを、忘れないためにメモ。
PSYS_SRC_PATTERN_ANGLE
PSYS_SRC_ANGLE_BEGINとPSYS_SRC_ANGLE_ENDで指定された平坦な扇形(V字?)にパーティクルを噴霧。
この際、放出平面はYZ平面上となり、PSYS_SRC_ANGLE_BEGIN、PSYS_SRC_ANGLE_ENDはいずれもZ軸が0度となる角度(ラジアン)。

PSYS_SRC_PATTERN_ANGLE_CONE
PSYS_SRC_ANGLE_BEGINとPSYS_SRC_ANGLE_ENDで指定されたコーン(円錐?)にパーティクルを噴霧。
PSYS_SRC_ANGLE_BEGIN、PSYS_SRC_ANGLE_ENDはいずれもZ軸が0度となる角度(ラジアン)。
PSYS_SRC_BURST_RADIUS
噴霧が開始される位置の中心からの距離(半径・メートル)。
ただし、オブジェクトと共に移動するマスク(PSYS_PART_FOLLOW_SRC_MASK)が指定されている場合は無視されてしまうので注意。
PSYS_PART_FOLLOW_VELOCITY_MASK
指定するとパーティクルの上端が常に進行方向を向くようになります。
なので、上向きの矢印などをテクスチャに指定しておくと、弓矢が飛んでいく様な見え方。
PSYS_SRC_PATTERN_ANGLE
PSYS_SRC_ANGLE_BEGINとPSYS_SRC_ANGLE_ENDで指定された平坦な扇形(V字?)にパーティクルを噴霧。
この際、放出平面はYZ平面上となり、PSYS_SRC_ANGLE_BEGIN、PSYS_SRC_ANGLE_ENDはいずれもZ軸が0度となる角度(ラジアン)。

PSYS_SRC_PATTERN_ANGLE_CONE
PSYS_SRC_ANGLE_BEGINとPSYS_SRC_ANGLE_ENDで指定されたコーン(円錐?)にパーティクルを噴霧。
PSYS_SRC_ANGLE_BEGIN、PSYS_SRC_ANGLE_ENDはいずれもZ軸が0度となる角度(ラジアン)。
PSYS_SRC_BURST_RADIUS
噴霧が開始される位置の中心からの距離(半径・メートル)。
ただし、オブジェクトと共に移動するマスク(PSYS_PART_FOLLOW_SRC_MASK)が指定されている場合は無視されてしまうので注意。
PSYS_PART_FOLLOW_VELOCITY_MASK
指定するとパーティクルの上端が常に進行方向を向くようになります。
なので、上向きの矢印などをテクスチャに指定しておくと、弓矢が飛んでいく様な見え方。
2009年08月02日
物体の回転その2(対空砲)

XY平面上でのX軸(単位ベクトル<1,0,0>)からの回転(水平回転)
rotation newRot=llRotBetween(<1,0,0>,<norm.x,norm.y,0>);
垂直回転
rotation newRot2=llEuler2Rot(<0,llAcos(norm.z)-PI/2.0,0>);
回転実行(順番が大事)
llSetRot(newRot2*newRot);
だいぶシンプルになりました。が、相変わらず思考はvector。。。^^;
ホントにいいのかな、これで。。(笑
対空砲の動きをする箱の中身
default
{
state_entry()
{
llSensorRepeat("","",AGENT|ACTIVE,30,PI,0.1);
}
sensor(integer detectedNum)
{
if (llDetectedKey(0)==llGetOwner() && llDetectedKey(0)!=NULL_KEY)
{
vector targetVector=llDetectedPos(0)-llGetPos();
vector norm=llVecNorm(targetVector);
rotation newRot=llRotBetween(<1,0,0>,<norm.x,norm.y,0>);
rotation newRot2=llEuler2Rot(<0,llAcos(norm.z)-PI/2.0,0>);
llSetRot(newRot2*newRot);
}
}
no_sensor()
{
}
}