9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

VCIAdvent Calendar 2022

Day 9

1種類のVCI(Virtual Cast Interactive)で疑似TCP通信をさせて複数の役割をさせる

Last updated at Posted at 2022-12-08

はじめに

このVCIのスクリプトはバーチャルキャスト上のルームで1つのアイテムを
複数出せる機能を利用し、1種類のアイテムを複数出して
同じアイテム内でネットワークを作り連携した動作を目的としています

題材

  • ゲームAPIを利用した扉VCI
    2022120323344415_お座敷_瑞姫 亞希乃.png

アイテムと通信をするのに必要な条件

  • 出現時にVCIアイテムIDの他にナンバーリストを作成する
  • 扉を開ける時に接続先の扉がある場合のみ扉をつなげて開ける
  • 接続先が被った場合はすでに接続されている扉を切断処理をし、
    接続先の扉の状態を接続元と同期させる
  • 扉を操作していない人にも接続された扉を共有する
  • 後から来た人や入室時に終了時の状態を保持する

同期に使用する機能

  • ExportState (アイテム内同期変数)
    vci.state で共有出来るデータ量は最大16KBまで
  • ExportMessage (VCI間の通信)
    vci.message で送信出来る文字量は UTF-8 換算で 4000 byte まで
  • Transform (Vector3)
    SubItemによる位置の自動同期 (プラットフォーム側の処理)

全体的な通信の動き

まず制作前に基本的なネットワーク通信仕様を考えます
最小構成として2個のVCI「VCI-001」と「VCI-002」で表してみましょう

下記図では中央にVCI本体の動作左右とvci.stateを表しています
赤枠:VCI-001の動作
青枠:VCI-002の動作
黒枠:ユーザーの操作
矢印:通信の方向と上に予定する関数名と送信情報
初期ネットワーク通信仕様.png

通信コストを削減と動作改善

vci.stateで非同期ドアの開閉を連続的に枚フレーム同期更新した際に
vci.stateが値が不定になりやすく値が喧嘩したり値が戻る事があるため
扉が動きが不自然になってしまいこの方式を辞めて同期用のSubItemフラグ用に
オブジェクトを用意しTransformの自動同期に変更を行う

下記図では中央にVCI本体の動作左右とSubItemを表しています
赤枠:VCI-001の動作
青枠:VCI-002の動作
黒枠:ユーザーの操作
矢印:通信の方向と上に予定する関数名と送信情報
現ネットワーク通信仕様.png

共通で使用するグローバル変数

この配列変数をメッセージ機能を使い送受信を行います

main.lua
--通信配列:{Time(New),ID,Name,Mode,Message}
local SysMode = {}
--通信配列:{Time(New),ID,Name,Mode,Message}
SysMode[2] = vci.assets.GetInstanceId()   --VCI_ID
SysMode[3] = Owner.GetName()
--初期化待ち用
local Reset_iniFlag = false

配列展開用の関数

受信後に文字列を配列に展開していきます

main.lua
----------------------------------------
--受信関数(らーめんs)
----------------------------------------
function Explode(explodeCode, str)
    local result = {}
    --区切りがない場合は、新たに配列を作成して返す
    if(string.find(str,explodeCode) == nil) then
        result[1] = str
        return result
    end

    local maxIndex = #str
    local index=1
    local resultID = 1

    while (index<=maxIndex) do
        local findIndex = string.find(str,explodeCode,index)
        if(findIndex~=nil) then
            result[resultID] = string.sub(str,index,findIndex-1)
            resultID = resultID + 1
            index = findIndex + 1
        else
            result[resultID] = string.sub(str,index)
            break
        end
    end

    return result
end

送信コード全体

まずは送信コードの全体を見てみます

main.lua
local NotIdFlag = {}
NotIdFlag[1] = true
NotIdFlag[2] = ""
----------------------------------------
--ゲット機能メッセージ機能
----------------------------------------
local Get = {}
    
function Get.ID()
--IDをリクエスト

    local NewTimeUser = os.date("%Y,%m,%d,%H,%M,%S")
    SysMode[1] = NewTimeUser
    SysMode[4] = "SyncRequest"
    SysMode[5] = vci.assets.GetInstanceId()
    SysMode[6] = "PortalDoor"
    vci.message.Emit("PortalDoor_Controller", SysMode[1] .. "™" .. SysMode[2] .. "™" .. SysMode[3] .. "™" .. SysMode[4] .. "™" .. SysMode[5] .. "™" .. SysMode[6])

end--END_IDをリクエスト

function Get.CheckID(IdNo)
--ID有るか確認

    if IdNo == 0 then
        return
    end

    if IdNo >= #DoorList +1 then
        return
    end
    
    print("Get.CheckID : " .. IdNo .. " to DoorList : " .. #DoorList)

    local NewTimeUser = os.date("%Y,%m,%d,%H,%M,%S")
    SysMode[1] = NewTimeUser
    SysMode[4] = "CheckID"
    SysMode[5] = vci.assets.GetInstanceId()
    SysMode[6] = "PortalDoor"
    SysMode[7] = DoorList[IdNo][1]
    vci.message.Emit("PortalDoor_Controller", SysMode[1] .. "™" .. SysMode[2] .. "™" .. SysMode[3] .. "™" .. SysMode[4] .. "™" .. SysMode[5] .. "™" .. SysMode[6] .. "™" .. SysMode[7])

end--ID有るか確認

function Get.DoorPos(IdNo)
--IDをリクエスト

    --生存Flag
    NotIdFlag[1] = true
    NotIdFlag[2] = IdNo
    vci.state.Set("NotIdFlag", NotIdFlag)

    local player_Rot = Portal_obj.GetRotation()
    local player_Pos = GetPortal.GetPosition()

    --時間取得
    local NewTimeUser = os.date("%Y,%m,%d,%H,%M,%S")
    --送信時間
    SysMode[1] = NewTimeUser
    --送信コード
    SysMode[4] = "GetPortalPos"
    --送信先のID
    SysMode[5] = IdNo
    --VCI識別ID
    SysMode[6] = "PortalDoor"
    --自分のID
    SysMode[7] = vci.assets.GetInstanceId()
    --自分のカメラセット用位置
    SysMode[8] = tostring(GetCam_Pos.GetPosition().x) .. "," .. tostring(GetCam_Pos.GetPosition().y) .. "," .. tostring(GetCam_Pos.GetPosition().z)
    --自分のカメラセット用回転
    SysMode[9] = tostring(GetCam_Pos.GetRotation().x) .. "," .. tostring(GetCam_Pos.GetRotation().y) .. "," .. tostring(GetCam_Pos.GetRotation().z .. "," .. tostring(GetCam_Pos.GetRotation().w))
    --自分のPortal出口位置
    SysMode[10] = tostring(player_Pos.x) .. "," .. tostring(player_Pos.y) .. "," .. tostring(player_Pos.z)
    --自分のPortal出口回転
    SysMode[11] = tostring(player_Rot.x) .. "," .. tostring(player_Rot.y) .. "," .. tostring((player_Rot.z)) .. "," .. tostring((player_Rot.w))
    --送信
    vci.message.Emit("PortalDoor_Controller", SysMode[1] .. "™" .. SysMode[2] .. "™" .. SysMode[3] .. "™" .. SysMode[4] .. "™" .. SysMode[5] .. "™" .. SysMode[6] .. "™" .. SysMode[7] .. "™" .. SysMode[8] .. "™" .. SysMode[9] .. "™" .. SysMode[10] .. "™" .. SysMode[11])

end--END_IDをリクエスト

送信するコードは後から送信用として関数がの役割が分かりやすいように
構造体にしておきます

上記の送信関数と役割を表にしてみます

関数 役割 引数 送信
Get.ID() ドアの全数確認(存在する扉全てにSyncRequestに送信) 無し 無し  
Get.CheckID(IdNo) IDをリクエスト(同期先のCheckIDに送信)      同期元のID PortalDoor」要求  
Get.DoorPos(IdNo) 同期元の座標を送信
同期先座標を要求
相手のGetPortalPosに送信
同期先のID GetPortalPos」要求

ここでは、必要な情報を収集し情報を要求しています

受信コード全体

まずは要求されたときの処理全体を見てみましょう

main.lua
----------------------------------------
--リクエスト受信メッセージ機能
----------------------------------------
---{Time(New),ID,Name,Mode,VCI_ID,Message}

function PortalDoor_Controller(sender, name, message)
--受信処理
    
    local RxMessage = Explode("™", message)
    if Debug == true then
    --Debugモード
        print(SysMode[2])
        print(RxMessage[1])
        print(RxMessage[2])
        print(RxMessage[3])
        print(RxMessage[4])
        print(RxMessage[5])
    end--END_Debugモード

    if RxMessage[6] == "PortalDoor" then

        if RxMessage[4] =="SyncRequest" then
        --同期リクエスト

                --同期リクエスト
                print("Send Sync")
                --時間取得
                local NewTimeUser = os.date("%Y,%m,%d,%H,%M,%S")
                SysMode[1] = NewTimeUser
                SysMode[4] = "SyncID"
                SysMode[5] = vci.assets.GetInstanceId()
                SysMode[6] = "PortalDoor"
                SysMode[7] = MyDoorList[1][2]
                vci.message.Emit("PortalDoor_Controller", SysMode[1] .. "™" .. SysMode[2] .. "™" .. SysMode[3] .. "™" .. SysMode[4] .. "™" .. SysMode[5] .. "™" .. SysMode[6] .. "™" .. SysMode[7])

        elseif RxMessage[4] =="SyncID" then

            if RxMessage[5] ~= vci.assets.GetInstanceId() then
            --リスト追加

                local Index = 0

                for IndexCount = 1 , #DoorList do
                    if DoorList[IndexCount][1] == RxMessage[5] then
                        return
                    end
                    Index = Index + 1
                end

                table.insert(DoorList, {RxMessage[5],RxMessage[7]})

                table.sort(DoorList,
                function(a, b)
                  return (a[2] < b[2]) --小さい順に並べ替える
                end
                )
                print("ID list")
                for IndexCount = 1 , #DoorList do
                    print(DoorList[IndexCount][1])
                    if DoorList[IndexCount][1] == vci.assets.GetInstanceId() then
                        GetMyNumber.SetPosition(Vector3.__new(0, IndexCount, 0))

                        if GetID == vci.assets.GetInstanceId() then
                            GetlistNumberCount.SetPosition(Vector3.__new(0, IndexCount, 0))
                        end

                    end
                end

                if GetKnob.IsMine then
                    print("Set SyncID")
                    vci.state.Set("DoorList", DoorList)
                end

            end

        elseif RxMessage[4] =="ReSync" then
        --再同期
            if RxMessage[5] == vci.assets.GetInstanceId() then

                print("Set ReSync")

                if GetKnob.IsMine then
                --自分のVCIかチェック
                    SYS_ReSync()
                else
                    SyncState()
                end --自分のVCIかチェック

            end
        elseif RxMessage[4] =="CheckID" then

            if RxMessage[7] == vci.assets.GetInstanceId() then
            --自分のIDが来たか確認する
                print("Emit CheckID")
                --時間取得
                local NewTimeUser = os.date("%Y,%m,%d,%H,%M,%S")
                SysMode[1] = NewTimeUser
                SysMode[4] = "GetCheckID"
                SysMode[5] = RxMessage[5]--送信元ID
                SysMode[6] = "PortalDoor"
                SysMode[7] = RxMessage[7]
                
                vci.message.Emit("PortalDoor_Controller", SysMode[1] .. "™" .. SysMode[2] .. "™" .. SysMode[3] .. "™" .. SysMode[4] .. "™" .. SysMode[5] .. "™" .. SysMode[6] .. "™" .. SysMode[7])
            
            end--END_自分のIDが来たか確認する

        elseif RxMessage[4] =="GetCheckID" then

            if RxMessage[5] == vci.assets.GetInstanceId() then
            --通信開始元か確認

                print("Get to CheckID")
                --IDセット
                GetID = RxMessage[7]
                Get.DoorPos(GetID)

            end--通信開始元か確認

        elseif RxMessage[4] =="DelID" then
        --バッティング処理
            
            if GetKnob.IsMine then

                if RxMessage[7] == nil then
                    return
                end   

                print("Del Request : " .. RxMessage[5])
                print("DelID : " .. RxMessage[7])
                if RxMessage[7] == vci.assets.GetInstanceId() then
                --連携番号表示
                    for IndexCount = 1 , #DoorList do
                        if DoorList[IndexCount][1] == RxMessage[5] then
                            GetlistNumberCount.SetPosition(Vector3.__new(0, IndexCount, 0))
                            ReSync()
                            return
                        end
                    end
                end--END_連携番号表示

                if RxMessage[5] ~= vci.assets.GetInstanceId() then

                    if GetID == RxMessage[7] then
                    --連携先がバッティングしていた
                        print("Conflict ID : " .. RxMessage[7])
                        GetID = vci.assets.GetInstanceId()
                        Get.DoorPos(GetID)
                        deg = 5
                        DegValue.SetPosition(Vector3.__new(0, deg, 0))
                        ReSync()
                    end--END_連携先がバッティングしていた
                end

            end

        elseif RxMessage[4] =="NotID" then

            local My = 0
            local You = 0

            if nil ~= vci.state.Get("DoorList") then 
                DoorList = vci.state.Get("DoorList")
                table.sort(DoorList,
                function(a, b)
                  return (a[2] < b[2]) --小さい順に並べ替える
                end
                )
            end

            for IndexCount = 1 , #DoorList do
                if DoorList[IndexCount][1] == RxMessage[5] then
                    My = IndexCount
                end
                if DoorList[IndexCount][1] == RxMessage[7] then
                    You = IndexCount
                end
            end
            print("NotRequest : " .. You .. " => " .. My)

            print("NotID : " .. RxMessage[4])

            local DecCount = #DoorList

            for IndexCount = 1 , #DoorList - 1 do

                print("decCount")
                print(DecCount)

                if DoorList[DecCount][1] ~= nil then
                    if DoorList[DecCount][1] == RxMessage[7] then
                        table.remove(DoorList, DecCount)
                        break 
                    end
                end

                DecCount = DecCount -1

            end

            table.sort(DoorList,
            function(a, b)
              return (a[2] < b[2]) --小さい順に並べ替える
            end
            )

            for IndexCount = 1 , #DoorList do
                if DoorList[IndexCount][1] == vci.assets.GetInstanceId() then
                    GetMyNumber.SetPosition(Vector3.__new(0, IndexCount, 0))
                    print(DoorList[IndexCount][1])
                end
            end

            if GetKnob.IsMine then

                if RxMessage[3] == SysMode[3] then
                --自分のVCIかチェック
                    print("Set SyncID")
                    vci.state.Set("DoorList", DoorList)
                end --自分のVCIかチェック

            end

        elseif RxMessage[4] =="GetPortalDeg" then
        --ドア開閉同期

            if GetKnob.IsMine then

                if RxMessage[7] == vci.assets.GetInstanceId() then

                    --IDセット
                    DegValue.SetPosition(Vector3.__new(0, tonumber(RxMessage[8]), 0))
                    deg = tonumber(RxMessage[8])
                    
                    local Rxbool = nil
                    if RxMessage[9] == "true" then
                        Rxbool = 1
                    else
                        Rxbool = 0
                    end

                    GetControlFlag.SetPosition(Vector3.__new(0, Rxbool, 0))

                end
            end

        elseif RxMessage[4] =="SetUngrab" then
        --ドアアングラブ処理

            if GetKnob.IsMine then
                --IDセット
                if RxMessage[8] == vci.assets.GetInstanceId() then

                    local Rxbool = nil
                    if RxMessage[9] == "true" then
                        Rxbool = 1
                    else
                        Rxbool = 0
                    end

                    GetControlFlag.SetPosition(Vector3.__new(0, Rxbool, 0))

                end
            end

        elseif RxMessage[4] =="GetPortalPos" then  
        --Portal情報受信送信

            if RxMessage[5] == vci.assets.GetInstanceId() then

                GetID = RxMessage[7]

                local My = 0
                local You = 0
                for IndexCount = 1 , #DoorList do
                    if DoorList[IndexCount][1] == RxMessage[5] then
                        My = IndexCount
                    end
                    if DoorList[IndexCount][1] == RxMessage[7] then
                        You = IndexCount
                    end
                end
                print("Control target : " .. You .. " => " .. My)

                --print(vci.assets.GetInstanceId())
                --print(GetID)
                local GetCamPos_Pos = Explode(",", RxMessage[8])
                local GetCamPos_Rot = Explode(",", RxMessage[9])
                local GetPlayer_Pos = Explode(",", RxMessage[10])
                local GetPlayer_Rot = Explode(",", RxMessage[11])

                Cam_Pos.SetPosition(Vector3.__new(tonumber(GetCamPos_Pos[1]), tonumber(GetCamPos_Pos[2]), tonumber(GetCamPos_Pos[3])))
                Cam_Pos.SetRotation(Quaternion.__new(tonumber(GetCamPos_Rot[1]), tonumber(GetCamPos_Rot[2]), tonumber(GetCamPos_Rot[3]), tonumber(GetCamPos_Rot[4])))
                SetPlayer_Pos = Vector3.__new(tonumber(GetPlayer_Pos[1]), tonumber(GetPlayer_Pos[2]), tonumber(GetPlayer_Pos[3]))
                SetPlayer_Rot = Quaternion.__new(Quaternion.__new(tonumber(GetPlayer_Rot[1]), tonumber(GetPlayer_Rot[2]), tonumber(GetPlayer_Rot[3]), tonumber(GetPlayer_Rot[4])))


                local player_Rot = GetPortal.GetRotation()-- * OffsetPortalRota
                local player_Pos = GetPortal.GetPosition()-- + (player_Rot * Vector3.__new(0, 2.2, 0))

                --時間取得
                local NewTimeUser = os.date("%Y,%m,%d,%H,%M,%S")
                SysMode[1] = NewTimeUser
                SysMode[4] = "SetPortalPos"
                --送信元のID
                SysMode[5] = RxMessage[7]
                SysMode[6] = "PortalDoor"
                --自分のID
                SysMode[7] = vci.assets.GetInstanceId()
                --自分のカメラセット用位置
                SysMode[8] = tostring(GetCam_Pos.GetPosition().x) .. "," .. tostring(GetCam_Pos.GetPosition().y) .. "," .. tostring(GetCam_Pos.GetPosition().z)
                --自分のカメラセット用回転
                SysMode[9] = tostring(GetCam_Pos.GetRotation().x) .. "," .. tostring(GetCam_Pos.GetRotation().y) .. "," .. tostring(GetCam_Pos.GetRotation().z .. "," .. tostring(GetCam_Pos.GetRotation().w))
                --自分のPortal出口位置
                SysMode[10] = tostring(player_Pos.x) .. "," .. tostring(player_Pos.y) .. "," .. tostring(player_Pos.z)
                --自分のPortal出口回転
                SysMode[11] = tostring(player_Rot.x) .. "," .. tostring(player_Rot.y) .. "," .. tostring((player_Rot.z)) .. "," .. tostring((player_Rot.w))
                --送信
                vci.message.Emit("PortalDoor_Controller", SysMode[1] .. "™" .. SysMode[2] .. "™" .. SysMode[3] .. "™" .. SysMode[4] .. "™" .. SysMode[5] .. "™" .. SysMode[6] .. "™" .. SysMode[7] .. "™" .. SysMode[8] .. "™" .. SysMode[9] .. "™" .. SysMode[10] .. "™" .. SysMode[11])

            end
            
        elseif RxMessage[4] =="SetPortalPos" then  
        --Portal情報をセット

            if RxMessage[5] == vci.assets.GetInstanceId() then

                local My = 0
                local You = 0
                for IndexCount = 1 , #DoorList do
                    --相手が明けたらここで止まった
                    if DoorList[IndexCount][1] == RxMessage[5] then
                        My = IndexCount
                    end
                    if DoorList[IndexCount][1] == RxMessage[7] then
                        You = IndexCount
                    end
                end

                print("Set Pos My id: " .. My .. ", Get id=> " .. You)

                GetID = RxMessage[7]
                --print(vci.assets.GetInstanceId())
                --print(GetID)
                local GetCamPos_Pos = Explode(",", RxMessage[8])
                local GetCamPos_Rot = Explode(",", RxMessage[9])
                local GetPlayer_Pos = Explode(",", RxMessage[10])
                local GetPlayer_Rot = Explode(",", RxMessage[11])

                Cam_Pos.SetPosition(Vector3.__new(tonumber(GetCamPos_Pos[1]), tonumber(GetCamPos_Pos[2]), tonumber(GetCamPos_Pos[3])))
                Cam_Pos.SetRotation(Quaternion.__new(tonumber(GetCamPos_Rot[1]), tonumber(GetCamPos_Rot[2]), tonumber(GetCamPos_Rot[3]), tonumber(GetCamPos_Rot[4])))
                SetPlayer_Pos = Vector3.__new(tonumber(GetPlayer_Pos[1]), tonumber(GetPlayer_Pos[2]), tonumber(GetPlayer_Pos[3]))
                SetPlayer_Rot = Quaternion.__new(Quaternion.__new(tonumber(GetPlayer_Rot[1]), tonumber(GetPlayer_Rot[2]), tonumber(GetPlayer_Rot[3]), tonumber(GetPlayer_Rot[4])))

            end

        end--END_同期リクエスト
    end

    if RxMessage[3] == SysMode[3] then
        --自分のVCIかチェック

    end --自分のVCIかチェック

end --END_受信処理
vci.message.On("PortalDoor_Controller", PortalDoor_Controller)


上記の受信の役割を表にしてみます

コマンド 内容
PortalDoor VCIコマンド受付
SyncRequest 同期リクエスト
SyncID 要求の返答で返ってきたIDをリストに追加
ReSync vci.state再同期
CheckID 送信元の「GetCheckID」に返事する
DelID 同期先に新規同期が来ていれば同期を切断する
NotID 同期のリクエストが返って来なかったらリストから削除する
GetPortalDeg 同期元のドア開閉と同期をとる
SetUngrab ドアアングラブ処理
GetPortalPos 同期元の座標を受け取り同期元に自分の座標を送信を行う
SetPortalPos 同期先の座標を保存する

以外とvci.messageの受信コマンドが多くなります

実際のデバックウィンドの動き

下記画像は1つ目「VCI-001」を出した時にリストは自分のIDしか存在しません
2022120322321200_お座敷_瑞姫 亞希乃.png

下記画像は2つ目「VCI-002」を出した時に「ID list」という行の下にVCIのIDがお互いに取得できています
2022120322330439_お座敷_瑞姫 亞希乃.png

次は扉のIDを選択し掴んだ時に同期先のIDをチェック後、
リストIDでホスト「VCI-001」でどのVCI「VCI-002」の操作を行うのかを最後にログを出してます
2022120322331264_お座敷_瑞姫 亞希乃.png

次は接続されている状態から「VCI-002」を操作し「VCI-001」の操作を行った時のログです
一度「VCI-001」に他のVCIが接続していた場合のために「VCI-002」以外の切断リクエストを送信し
VCI-002」がホストになって「VCI-001」を操作します
2022120322331859_お座敷_瑞姫 亞希乃.png

以上がVCIがお互いにTCPのように対話しながら処理を行っていく内容になります
通信としては成立出来ました!

現在の課題

  • 初期化時に同期が上手く行く時と行かない時がある
  • 同じ人が複数出して出した人が操作し第三者が同期は確認されたが、
    最初に相手に触られると動作しない時がある
  • ステートにドアのリストIDをvci.stateからSubItemuTransform
    座標にを入れて自動同期に変更をしたらIDの共有が不安定で共有が
    出来ていない時がある
  • 第三者が出した場合の日時ベースでリスト化しているのに
    IDがずれて同期が上手くいっていない

まとめ

  • 1種類のVCIで役割を分けて操作元をホストとすることで、
    指定した対話通信は実現できた

  • 1種類のVCIだと1種類のVCIの特性上自分のメッセージを受け取り、
    誤動作の可能性があるため、どのVCIで誰が送信したのかが鍵になってくる

  • 別のVCI同士ではないので、通信の流れを書き出して整理する必要が
    出てきた

  • TCP通信としては上手く言っているが、データ状態の共有と
    いう場面で課題が出たため全体公開ができない状態が続いている

  • プレリリースまでは言ったが同期相手が同じVCIを出して協力する人が
    居ないと困難になってきた

  • PCを3台使い一人でデバックしていても操作する人が一人だと限界がある

  • デバックする際の人出が必要な場合頼める人が居なかったりすると辛い

結論

VCIなんもわからん

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?