added ability to control flock from scripts.
parent
1c410fdb81
commit
e584d81e0c
|
@ -44,7 +44,6 @@ namespace Flocking
|
|||
private Random m_rndnums = new Random (Environment.TickCount);
|
||||
|
||||
private BoidBehaviour m_behaviour;
|
||||
private FlowField m_flowField;
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
@ -59,31 +58,31 @@ namespace Flocking
|
|||
/// <param name='mf'>
|
||||
/// Mf. max force / acceleration this boid can extert
|
||||
/// </param>
|
||||
public Boid (string id, Vector3 size, BoidBehaviour behaviour, FlowField flowField)
|
||||
public Boid (string id, Vector3 size, BoidBehaviour behaviour)
|
||||
{
|
||||
m_id = id;
|
||||
m_acc = Vector3.Zero;
|
||||
m_vel = new Vector3 (m_rndnums.Next (-1, 1), m_rndnums.Next (-1, 1), m_rndnums.Next (-1, 1));
|
||||
m_size = size;
|
||||
m_behaviour = behaviour;
|
||||
m_flowField = flowField;
|
||||
}
|
||||
|
||||
public Vector3 Location {
|
||||
get { return m_loc;}
|
||||
get { return m_loc; }
|
||||
set { m_loc = value; }
|
||||
}
|
||||
|
||||
public Vector3 Velocity {
|
||||
get { return m_vel;}
|
||||
get { return m_vel; }
|
||||
set { m_vel = value; }
|
||||
}
|
||||
|
||||
public Vector3 Size {
|
||||
get { return m_size;}
|
||||
get { return m_size; }
|
||||
}
|
||||
|
||||
public String Id {
|
||||
get {return m_id;}
|
||||
get { return m_id; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -92,15 +91,14 @@ namespace Flocking
|
|||
/// <param name='boids'>
|
||||
/// Boids. all the other chaps in the scene
|
||||
/// </param>
|
||||
public void MoveInSceneRelativeToFlock (List<Boid> neighbours)
|
||||
public void MoveInSceneRelativeToFlock (List<Boid> neighbours, FlowField field)
|
||||
{
|
||||
//List<Boid> neighbours = m_model.GetNeighbours(this);
|
||||
// we would like to stay with our mates
|
||||
Flock (neighbours);
|
||||
|
||||
// our first priority is to not hurt ourselves
|
||||
// however, our first priority is to not hurt ourselves
|
||||
// so adjust where we would like to go to avoid hitting things
|
||||
AvoidObstacles ();
|
||||
AvoidObstacles (field);
|
||||
|
||||
// then we want to avoid any threats
|
||||
// this not implemented yet
|
||||
|
@ -127,7 +125,7 @@ namespace Flocking
|
|||
Vector3 sep = Separate (neighbours); // Separation
|
||||
Vector3 ali = Align (neighbours); // Alignment
|
||||
Vector3 coh = Cohesion (neighbours); // Cohesion
|
||||
Vector3 ori = Orientation();
|
||||
Vector3 ori = Orientation(); // its tricky to fly directly up or down
|
||||
|
||||
// Arbitrarily weight these forces
|
||||
sep *= m_behaviour.separationWeighting;
|
||||
|
@ -213,10 +211,23 @@ namespace Flocking
|
|||
/// <summary>
|
||||
/// navigate away from whatever it is we are too close to
|
||||
/// </summary>
|
||||
void AvoidObstacles ()
|
||||
void AvoidObstacles (FlowField field)
|
||||
{
|
||||
//look tolerance metres ahead
|
||||
m_acc += m_flowField.AdjustVelocity( this, m_behaviour.tolerance );
|
||||
//m_acc += field.AdjustVelocity( this, m_behaviour.lookaheadDistance );
|
||||
Vector3 normVel = Vector3.Normalize (m_vel);
|
||||
Vector3 inFront = m_loc + normVel * m_behaviour.LookaheadDistance;
|
||||
|
||||
Vector3 adjustedDestintation = field.FieldStrength (m_loc, m_size, inFront);
|
||||
Vector3 newVel = Vector3.Normalize (adjustedDestintation - m_loc) * Vector3.Mag (m_vel);
|
||||
|
||||
float mOrigVel = Vector3.Mag(m_vel);
|
||||
float mNewVel = Vector3.Mag(newVel);
|
||||
if( mNewVel != 0f && mNewVel > mOrigVel ) {
|
||||
newVel *= mOrigVel / mNewVel;
|
||||
}
|
||||
m_vel = newVel;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -243,6 +254,9 @@ namespace Flocking
|
|||
tooCloseNeighbours.ForEach( delegate(Boid neighbour) {
|
||||
// Calculate vector pointing away from neighbor
|
||||
Vector3 diff = m_loc - neighbour.Location;
|
||||
if( diff == Vector3.Zero ) {
|
||||
diff = new Vector3( (float)m_rndnums.NextDouble(), (float)m_rndnums.NextDouble(), (float)m_rndnums.NextDouble());
|
||||
}
|
||||
steer += Utils.GetNormalizedVector(diff) / (float)(Utils.GetDistanceTo (m_loc, neighbour.Location));
|
||||
});
|
||||
|
||||
|
|
|
@ -1,3 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) Contributors, https://github.com/jonc/osboids
|
||||
* See CONTRIBUTORS.TXT for a full list of copyright holders.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the OpenSimulator Project nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using log4net;
|
||||
using OpenMetaverse;
|
||||
using OpenSim.Framework;
|
||||
using OpenSim.Region.Framework.Scenes;
|
||||
using OpenSim.Framework.Console;
|
||||
using OpenSim.Region.Framework.Interfaces;
|
||||
|
||||
namespace Flocking
|
||||
{
|
||||
public delegate void BoidCmdDelegate (string module,string[] args);
|
||||
|
||||
public class BoidCmdDefn
|
||||
{
|
||||
public string Help = "";
|
||||
public string Args = "";
|
||||
public int NumParams = 0;
|
||||
string m_name;
|
||||
|
||||
public BoidCmdDefn (string name, string args, string help)
|
||||
{
|
||||
Help = help;
|
||||
Args = args;
|
||||
m_name = name;
|
||||
|
||||
if (args.Trim ().Length > 0) {
|
||||
NumParams = args.Split (",".ToCharArray ()).Length;
|
||||
} else {
|
||||
NumParams = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetSyntax ()
|
||||
{
|
||||
return m_name + " " + Args + " (" + Help + ")";
|
||||
}
|
||||
}
|
||||
|
||||
public class ChatCommandParser
|
||||
{
|
||||
private static readonly ILog m_log = LogManager.GetLogger (System.Reflection.MethodBase.GetCurrentMethod ().DeclaringType);
|
||||
private string m_name;
|
||||
private Scene m_scene;
|
||||
private int m_chatChannel;
|
||||
private Dictionary<string, BoidCmdDelegate> m_commandMap = new Dictionary<string, BoidCmdDelegate> ();
|
||||
private Dictionary<string, BoidCmdDefn> m_syntaxMap = new Dictionary<string, BoidCmdDefn> ();
|
||||
|
||||
public ChatCommandParser (IRegionModuleBase module, Scene scene, int channel)
|
||||
{
|
||||
m_name = module.Name;
|
||||
m_scene = scene;
|
||||
m_chatChannel = channel;
|
||||
}
|
||||
|
||||
public void AddCommand (string cmd, string args, string help, CommandDelegate fn)
|
||||
{
|
||||
m_commandMap.Add (cmd, new BoidCmdDelegate (fn));
|
||||
m_syntaxMap.Add (cmd, new BoidCmdDefn (cmd, args, help));
|
||||
}
|
||||
|
||||
public void SimChatSent (Object x, OSChatMessage msg)
|
||||
{
|
||||
if (m_scene.ConsoleScene () != m_scene || msg.Channel != m_chatChannel)
|
||||
return; // not for us
|
||||
|
||||
// try and parse a valid cmd from this msg
|
||||
string cmd = msg.Message.ToLower ();
|
||||
|
||||
//stick ui in the args so we know to respond in world
|
||||
//bit of a hack - but lets us use CommandDelegate inWorld
|
||||
string[] args = (cmd + " <ui>").Split (" ".ToCharArray ());
|
||||
|
||||
BoidCmdDefn defn = null;
|
||||
if (m_syntaxMap.TryGetValue (args [0], out defn)) {
|
||||
if (CorrectSignature (args, defn)) {
|
||||
|
||||
// we got the signature of the command right
|
||||
BoidCmdDelegate del = null;
|
||||
if (m_commandMap.TryGetValue (args [0], out del)) {
|
||||
del (m_name, args);
|
||||
} else {
|
||||
// we don't understand this command
|
||||
// shouldn't happen
|
||||
m_log.ErrorFormat ("Unable to invoke command {0}", args [0]);
|
||||
RespondToMessage (msg, "Unable to invoke command " + args [0]);
|
||||
}
|
||||
|
||||
} else {
|
||||
// we recognise the command, but we got the call wrong
|
||||
RespondToMessage (msg, "wrong syntax: " + defn.GetSyntax ());
|
||||
}
|
||||
} else {
|
||||
// this is not a command we recognise
|
||||
RespondToMessage (msg, args [0] + " is not a valid command for osboids");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private bool CorrectSignature (string[] args, BoidCmdDefn defn)
|
||||
{
|
||||
// args contain cmd name at 0 and <ui> tagged in last pos
|
||||
return args.Length - 2 == defn.NumParams;
|
||||
}
|
||||
|
||||
public void RespondToMessage (OSChatMessage msg, string message)
|
||||
{
|
||||
m_log.Debug ("sending response -> " + message);
|
||||
IClientAPI sender = msg.Sender;
|
||||
sender.SendChatMessage (message, (byte)ChatTypeEnum.Say, msg.Position, "osboids", msg.SenderUUID, (byte)ChatSourceType.Agent, (byte)ChatAudibleLevel.Fully);
|
||||
}
|
||||
|
||||
public void SendMessage (ScenePresence recipient, string message)
|
||||
{
|
||||
IClientAPI ownerAPI = recipient.ControllingClient;
|
||||
ownerAPI.SendChatMessage (message,
|
||||
(byte)ChatTypeEnum.Say,
|
||||
recipient.AbsolutePosition,
|
||||
"osboids",
|
||||
recipient.UUID,
|
||||
(byte)ChatSourceType.Agent,
|
||||
(byte)ChatAudibleLevel.Fully
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
/*
|
||||
* Copyright (c) Contributors, https://github.com/jonc/osboids
|
||||
* See CONTRIBUTORS.TXT for a full list of copyright holders.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the OpenSimulator Project nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using log4net;
|
||||
using OpenMetaverse;
|
||||
using OpenSim.Framework;
|
||||
using OpenSim.Region.Framework.Scenes;
|
||||
using OpenSim.Framework.Console;
|
||||
using OpenSim.Region.Framework.Interfaces;
|
||||
|
||||
namespace Flocking
|
||||
{
|
||||
public delegate void BoidCmdDelegate (string module,string[] args);
|
||||
|
||||
public class BoidCmdDefn
|
||||
{
|
||||
public string Help = "";
|
||||
public string Args = "";
|
||||
public int NumParams = 0;
|
||||
string m_name;
|
||||
|
||||
public BoidCmdDefn (string name, string args, string help)
|
||||
{
|
||||
Help = help;
|
||||
Args = args;
|
||||
m_name = name;
|
||||
|
||||
if (args.Trim ().Length > 0) {
|
||||
NumParams = args.Split (",".ToCharArray ()).Length;
|
||||
} else {
|
||||
NumParams = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetSyntax ()
|
||||
{
|
||||
return m_name + " " + Args + " (" + Help + ")";
|
||||
}
|
||||
}
|
||||
|
||||
public class FlockingCommandParser
|
||||
{
|
||||
private static readonly ILog m_log = LogManager.GetLogger (System.Reflection.MethodBase.GetCurrentMethod ().DeclaringType);
|
||||
private IRegionModuleBase m_module;
|
||||
private Scene m_scene;
|
||||
private int m_chatChannel;
|
||||
private UUID m_owner;
|
||||
private Dictionary<string, BoidCmdDelegate> m_commandMap = new Dictionary<string, BoidCmdDelegate> ();
|
||||
private Dictionary<string, BoidCmdDefn> m_syntaxMap = new Dictionary<string, BoidCmdDefn> ();
|
||||
|
||||
|
||||
public FlockingCommandParser (IRegionModuleBase module, Scene scene, int channel)
|
||||
{
|
||||
m_module = module;
|
||||
m_scene = scene;
|
||||
m_chatChannel = channel;
|
||||
|
||||
// who do we respond to in send messages
|
||||
m_owner = scene.RegionInfo.EstateSettings.EstateOwner;
|
||||
|
||||
// register our event handlers
|
||||
m_scene.EventManager.OnChatFromClient += ProcessChatCommand; //listen for commands sent from the client
|
||||
|
||||
IScriptModuleComms commsMod = scene.RequestModuleInterface<IScriptModuleComms>();
|
||||
commsMod.OnScriptCommand += ProcessScriptCommand;
|
||||
}
|
||||
|
||||
public void Deregister ()
|
||||
{
|
||||
m_scene.EventManager.OnChatFromClient -= ProcessChatCommand;
|
||||
IScriptModuleComms commsMod = m_scene.RequestModuleInterface<IScriptModuleComms>();
|
||||
commsMod.OnScriptCommand -= ProcessScriptCommand;
|
||||
}
|
||||
|
||||
public void AddCommand (string cmd, string args, string help, CommandDelegate fn)
|
||||
{
|
||||
string argStr = "";
|
||||
if (args.Trim ().Length > 0) {
|
||||
argStr = " <" + args + "> ";
|
||||
}
|
||||
m_commandMap.Add (cmd, new BoidCmdDelegate (fn));
|
||||
m_syntaxMap.Add (cmd, new BoidCmdDefn (cmd, args, help));
|
||||
// register this command with the console
|
||||
m_scene.AddCommand (m_module, "flock-" + cmd, "flock-" + cmd + argStr, help, fn);
|
||||
}
|
||||
|
||||
|
||||
#region handlers
|
||||
|
||||
public void ProcessScriptCommand (UUID scriptId, string reqId, string module, string input, string key)
|
||||
{
|
||||
if (m_module.Name != module) {
|
||||
return;
|
||||
}
|
||||
|
||||
string[] tokens = (input+"|<script>").Split ( new char[] { '|' }, StringSplitOptions.None);
|
||||
|
||||
string command = tokens [0];
|
||||
m_log.Debug("Input was " + input + ", command is " + command);
|
||||
BoidCmdDefn defn = null;
|
||||
if (m_syntaxMap.TryGetValue (command, out defn)) {
|
||||
if (CorrectSignature (tokens, defn)) {
|
||||
|
||||
// we got the signature of the command right
|
||||
BoidCmdDelegate del = null;
|
||||
if (m_commandMap.TryGetValue (command, out del)) {
|
||||
m_log.Info("command ok - executing");
|
||||
del (module, tokens);
|
||||
} else {
|
||||
// we don't understand this command
|
||||
// shouldn't happen
|
||||
m_log.ErrorFormat ("Unable to invoke command {0}", command);
|
||||
}
|
||||
} else {
|
||||
m_log.Error(" signature wrong for " + command);
|
||||
}
|
||||
} else {
|
||||
m_log.Error("no command for " + command);
|
||||
}
|
||||
}
|
||||
|
||||
void RespondToScript (UUID scriptId, int msgNum, string response)
|
||||
{
|
||||
IScriptModuleComms commsMod = m_scene.RequestModuleInterface<IScriptModuleComms>();
|
||||
if( commsMod != null ) {
|
||||
commsMod.DispatchReply (scriptId, msgNum, response, "");
|
||||
} else {
|
||||
Console.WriteLine("No script comms");
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessChatCommand (Object x, OSChatMessage msg)
|
||||
{
|
||||
if (m_scene.ConsoleScene () != m_scene || msg.Channel != m_chatChannel)
|
||||
return; // not for us
|
||||
|
||||
// try and parse a valid cmd from this msg
|
||||
string cmd = msg.Message.ToLower ();
|
||||
|
||||
//stick ui in the args so we know to respond in world
|
||||
//bit of a hack - but lets us use CommandDelegate inWorld
|
||||
string[] args = (cmd + " <ui>").Split (" ".ToCharArray ());
|
||||
|
||||
BoidCmdDefn defn = null;
|
||||
if (m_syntaxMap.TryGetValue (args [0], out defn)) {
|
||||
if (CorrectSignature (args, defn)) {
|
||||
|
||||
// we got the signature of the command right
|
||||
BoidCmdDelegate del = null;
|
||||
if (m_commandMap.TryGetValue (args [0], out del)) {
|
||||
del (m_module.Name, args);
|
||||
} else {
|
||||
// we don't understand this command
|
||||
// shouldn't happen
|
||||
m_log.ErrorFormat ("Unable to invoke command {0}", args [0]);
|
||||
RespondToChat (msg, "Unable to invoke command " + args [0]);
|
||||
}
|
||||
|
||||
} else {
|
||||
// we recognise the command, but we got the call wrong
|
||||
RespondToChat (msg, "wrong syntax: " + defn.GetSyntax ());
|
||||
}
|
||||
} else {
|
||||
// this is not a command we recognise
|
||||
RespondToChat (msg, args [0] + " is not a valid command for osboids");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void RespondToChat (OSChatMessage msg, string message)
|
||||
{
|
||||
m_log.Debug ("sending response -> " + message);
|
||||
IClientAPI sender = msg.Sender;
|
||||
sender.SendChatMessage (message, (byte)ChatTypeEnum.Say, msg.Position, "osboids", msg.SenderUUID, (byte)ChatSourceType.Agent, (byte)ChatAudibleLevel.Fully);
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
private bool CorrectSignature (string[] args, BoidCmdDefn defn)
|
||||
{
|
||||
// args contain cmd name at 0 and <ui> tagged in last pos
|
||||
return args.Length - 2 == defn.NumParams;
|
||||
}
|
||||
|
||||
public void ShowResponse (string response, string [] cmd)
|
||||
{
|
||||
bool inWorld = IsInWorldCmd (cmd);
|
||||
if (inWorld) {
|
||||
ScenePresence owner = m_scene.GetScenePresence(m_owner);
|
||||
SendMessage(owner, response);
|
||||
} else {
|
||||
MainConsole.Instance.Output (response);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsInWorldCmd (string [] args)
|
||||
{
|
||||
bool retVal = false;
|
||||
|
||||
if (args.Length > 0 && args [args.Length - 1].Equals ("<ui>")) {
|
||||
retVal = true;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
||||
public void SendMessage (ScenePresence recipient, string message)
|
||||
{
|
||||
IClientAPI ownerAPI = recipient.ControllingClient;
|
||||
ownerAPI.SendChatMessage (message,
|
||||
(byte)ChatTypeEnum.Say,
|
||||
recipient.AbsolutePosition,
|
||||
"osboids",
|
||||
recipient.UUID,
|
||||
(byte)ChatSourceType.Agent,
|
||||
(byte)ChatAudibleLevel.Fully
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ namespace Flocking
|
|||
private Random m_rnd = new Random(Environment.TickCount);
|
||||
private int m_flockSize;
|
||||
private Vector3 m_boidSize;
|
||||
private Vector3 m_startPos;
|
||||
|
||||
public int Size {
|
||||
get {return m_flockSize;}
|
||||
|
@ -51,21 +52,17 @@ namespace Flocking
|
|||
}
|
||||
}
|
||||
|
||||
public FlockingModel( BoidBehaviour behaviour ) {
|
||||
public FlockingModel( BoidBehaviour behaviour, Vector3 startPos ) {
|
||||
m_behaviour = behaviour;
|
||||
m_startPos = startPos;
|
||||
}
|
||||
|
||||
void AddBoid (string name)
|
||||
{
|
||||
Boid boid = new Boid (name, m_boidSize, m_behaviour, m_flowField);
|
||||
Boid boid = new Boid (name, m_boidSize, m_behaviour);
|
||||
|
||||
// find an initial random location for this Boid
|
||||
// somewhere not within an obstacle
|
||||
int xInit = m_rnd.Next(Util.SCENE_SIZE);
|
||||
int yInit = m_rnd.Next(Util.SCENE_SIZE);
|
||||
int zInit = m_rnd.Next(Util.SCENE_SIZE);
|
||||
Vector3 location = new Vector3 (Convert.ToSingle(xInit), Convert.ToSingle(yInit), Convert.ToSingle(zInit));
|
||||
boid.Location = location + m_flowField.AdjustVelocity(boid, 5f);
|
||||
boid.Location = m_startPos;
|
||||
boid.Velocity = Vector3.UnitX;
|
||||
m_flock.Add (boid);
|
||||
}
|
||||
|
||||
|
@ -77,6 +74,7 @@ namespace Flocking
|
|||
m_boidSize = boidSize;
|
||||
for (int i = 0; i < m_flockSize; i++) {
|
||||
AddBoid ("boid"+i );
|
||||
UpdateFlockPos();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,7 +89,7 @@ namespace Flocking
|
|||
public List<Boid> UpdateFlockPos ()
|
||||
{
|
||||
m_flock.ForEach( delegate(Boid boid) {
|
||||
boid.MoveInSceneRelativeToFlock(GetNeighbours(boid));
|
||||
boid.MoveInSceneRelativeToFlock(GetNeighbours(boid), m_flowField);
|
||||
} );
|
||||
|
||||
return m_flock;
|
||||
|
|
|
@ -36,6 +36,8 @@ using OpenSim.Region.Framework.Interfaces;
|
|||
using OpenSim.Region.Framework.Scenes;
|
||||
using OpenSim.Framework;
|
||||
using OpenSim.Framework.Console;
|
||||
using OpenSim.Services.Interfaces;
|
||||
|
||||
|
||||
namespace Flocking
|
||||
{
|
||||
|
@ -49,12 +51,12 @@ namespace Flocking
|
|||
private FlockingModel m_model;
|
||||
private FlockingView m_view;
|
||||
private bool m_enabled = false;
|
||||
private bool m_ready = false;
|
||||
private bool m_active = false;
|
||||
private uint m_frame = 0;
|
||||
private int m_frameUpdateRate = 1;
|
||||
private int m_chatChannel = 118;
|
||||
private string m_boidPrim;
|
||||
private ChatCommandParser m_chatCommandParser;
|
||||
private string m_boidPrim = "boid-prim";
|
||||
private FlockingCommandParser m_commandParser;
|
||||
private BoidBehaviour m_behaviour;
|
||||
private int m_flockSize = 100;
|
||||
|
||||
|
@ -95,50 +97,34 @@ namespace Flocking
|
|||
//m_log.Info ("ADDING FLOCKING");
|
||||
m_scene = scene;
|
||||
if (m_enabled) {
|
||||
//register commands
|
||||
m_chatCommandParser = new ChatCommandParser(this, scene, m_chatChannel);
|
||||
RegisterCommands ();
|
||||
|
||||
//register handlers
|
||||
m_scene.EventManager.OnFrame += FlockUpdate;
|
||||
m_scene.EventManager.OnChatFromClient += m_chatCommandParser.SimChatSent; //listen for commands sent from the client
|
||||
|
||||
// init module
|
||||
m_model = new FlockingModel (m_behaviour);
|
||||
|
||||
m_view = new FlockingView (m_scene);
|
||||
m_view.BoidPrim = m_boidPrim;
|
||||
}
|
||||
}
|
||||
|
||||
public void RegionLoaded (Scene scene)
|
||||
{
|
||||
if (m_enabled) {
|
||||
|
||||
//build a proper flow field based on the scene
|
||||
FlowField field = new FlowField(scene, new Vector3(128f, 128f, 128f), 200, 200, 200);
|
||||
|
||||
//ask the view how big the boid is
|
||||
Vector3 scale = m_view.GetBoidSize();
|
||||
// m_log.Error( m_boidPrim + " = " + scale.ToString());
|
||||
|
||||
// Generate initial flock values
|
||||
m_model.Initialise (m_flockSize, scale, field);
|
||||
|
||||
// who is the owner for the flock in this region
|
||||
m_owner = m_scene.RegionInfo.EstateSettings.EstateOwner;
|
||||
m_owner = scene.RegionInfo.EstateSettings.EstateOwner;
|
||||
|
||||
//register command handler
|
||||
m_commandParser = new FlockingCommandParser(this, scene, m_chatChannel);
|
||||
RegisterCommands ();
|
||||
|
||||
// init view
|
||||
m_view = new FlockingView (scene);
|
||||
m_view.PostInitialize (m_owner);
|
||||
|
||||
// Mark Module Ready for duty
|
||||
m_ready = true;
|
||||
m_view.BoidPrim = m_boidPrim;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void RemoveRegion (Scene scene)
|
||||
{
|
||||
if (m_enabled) {
|
||||
m_scene.EventManager.OnFrame -= FlockUpdate;
|
||||
m_scene.EventManager.OnChatFromClient -= m_chatCommandParser.SimChatSent;
|
||||
m_commandParser.Deregister();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,7 +142,7 @@ namespace Flocking
|
|||
|
||||
public void FlockUpdate ()
|
||||
{
|
||||
if (((m_frame++ % m_frameUpdateRate) != 0) || !m_ready || !m_enabled) {
|
||||
if (((m_frame++ % m_frameUpdateRate) != 0) || !m_active || !m_enabled) {
|
||||
return;
|
||||
}
|
||||
// work out where everyone has moved to
|
||||
|
@ -169,27 +155,37 @@ namespace Flocking
|
|||
|
||||
#endregion
|
||||
|
||||
|
||||
private void BuildFlowField(Vector3 centre, int width, int depth, int height) {
|
||||
m_log.Info("building flow field");
|
||||
//build a flow field based on the scene
|
||||
FlowField field = new FlowField(m_scene, centre, width, depth, height);
|
||||
m_log.Info("built");
|
||||
//ask the view how big the boid prim is
|
||||
Vector3 scale = m_view.GetBoidSize();
|
||||
|
||||
Vector3 startPos = m_scene.GetSceneObjectPart(m_view.BoidPrim).ParentGroup.AbsolutePosition;
|
||||
// init model
|
||||
m_log.Info("creating model");
|
||||
m_model = new FlockingModel (m_behaviour, startPos );
|
||||
// Generate initial flock values
|
||||
m_model.Initialise (m_flockSize, scale, field);
|
||||
m_log.Info("done");
|
||||
|
||||
}
|
||||
|
||||
#region Command Handling
|
||||
|
||||
private void AddCommand (string cmd, string args, string help, CommandDelegate fn)
|
||||
{
|
||||
string argStr = "";
|
||||
if (args.Trim ().Length > 0) {
|
||||
argStr = " <" + args + "> ";
|
||||
}
|
||||
m_scene.AddCommand (this, "flock-" + cmd, "flock-" + cmd + argStr, help, fn);
|
||||
m_chatCommandParser.AddCommand(cmd, args, help, fn);
|
||||
}
|
||||
|
||||
private void RegisterCommands ()
|
||||
{
|
||||
AddCommand ("stop", "", "Stop all Flocking", HandleStopCmd);
|
||||
AddCommand ("start", "", "Start Flocking", HandleStartCmd);
|
||||
AddCommand ("size", "num", "Adjust the size of the flock ", HandleSetSizeCmd);
|
||||
AddCommand ("stats", "", "show flocking stats", HandleShowStatsCmd);
|
||||
AddCommand ("prim", "name", "set the prim used for each boid to that passed in", HandleSetPrimCmd);
|
||||
AddCommand ("framerate", "num", "[debugging] only update boids every <num> frames", HandleSetFrameRateCmd);
|
||||
AddCommand ("set", "name, value", "change the flock dynamics", HandleSetParameterCmd);
|
||||
m_commandParser.AddCommand ("stop", "", "Stop all Flocking", HandleStopCmd);
|
||||
m_commandParser.AddCommand ("start", "", "Start Flocking", HandleStartCmd);
|
||||
m_commandParser.AddCommand ("size", "num", "Adjust the size of the flock ", HandleSetSizeCmd);
|
||||
m_commandParser.AddCommand ("stats", "", "show flocking stats", HandleShowStatsCmd);
|
||||
m_commandParser.AddCommand ("prim", "name", "set the prim used for each boid to that passed in", HandleSetPrimCmd);
|
||||
m_commandParser.AddCommand ("framerate", "num", "[debugging] only update boids every <num> frames", HandleSetFrameRateCmd);
|
||||
m_commandParser.AddCommand ("set", "name, value", "change the flock behaviour properties", HandleSetParameterCmd);
|
||||
}
|
||||
|
||||
private bool ShouldHandleCmd ()
|
||||
|
@ -197,26 +193,6 @@ namespace Flocking
|
|||
return m_scene.ConsoleScene () == m_scene;
|
||||
}
|
||||
|
||||
private bool IsInWorldCmd (ref string [] args)
|
||||
{
|
||||
bool retVal = false;
|
||||
|
||||
if (args.Length > 0 && args [args.Length - 1].Equals ("<ui>")) {
|
||||
retVal = true;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private void ShowResponse (string response, bool inWorld)
|
||||
{
|
||||
if (inWorld) {
|
||||
ScenePresence owner = m_scene.GetScenePresence(m_owner);
|
||||
m_chatCommandParser.SendMessage(owner, response);
|
||||
} else {
|
||||
MainConsole.Instance.Output (response);
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleSetParameterCmd(string module, string[] args)
|
||||
{
|
||||
if (ShouldHandleCmd ()) {
|
||||
|
@ -226,9 +202,8 @@ namespace Flocking
|
|||
if( m_behaviour.IsValidParameter( name ) ) {
|
||||
m_behaviour.SetParameter(name, newVal);
|
||||
} else {
|
||||
bool inWorld = IsInWorldCmd( ref args);
|
||||
ShowResponse( name + "is not a valid flock parameter", inWorld );
|
||||
ShowResponse( "valid parameters are: " + m_behaviour.GetList(), inWorld);
|
||||
m_commandParser.ShowResponse( name + "is not a valid flock parameter", args );
|
||||
m_commandParser.ShowResponse( "valid parameters are: " + m_behaviour.GetList(), args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -237,7 +212,7 @@ namespace Flocking
|
|||
{
|
||||
if (ShouldHandleCmd ()) {
|
||||
m_log.Info ("stop the flocking capability");
|
||||
m_enabled = false;
|
||||
m_active = false;
|
||||
m_view.Clear ();
|
||||
}
|
||||
}
|
||||
|
@ -254,7 +229,8 @@ namespace Flocking
|
|||
{
|
||||
if (ShouldHandleCmd ()) {
|
||||
m_log.Info ("start the flocking capability");
|
||||
m_enabled = true;
|
||||
BuildFlowField(new Vector3(128f, 128f, 128f), 200, 200, 200);
|
||||
m_active = true;
|
||||
FlockUpdate ();
|
||||
}
|
||||
}
|
||||
|
@ -273,9 +249,8 @@ namespace Flocking
|
|||
public void HandleShowStatsCmd (string module, string[] args)
|
||||
{
|
||||
if (ShouldHandleCmd ()) {
|
||||
bool inWorld = IsInWorldCmd (ref args);
|
||||
string str = m_model.ToString();
|
||||
ShowResponse (str, inWorld);
|
||||
m_commandParser.ShowResponse (str, args);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,8 +54,9 @@ namespace Flocking
|
|||
m_owner = owner;
|
||||
}
|
||||
|
||||
public String BoidPrim {
|
||||
set{ m_boidPrim = value;}
|
||||
public string BoidPrim {
|
||||
get{ return m_boidPrim; }
|
||||
set{ m_boidPrim = value; }
|
||||
}
|
||||
|
||||
public Vector3 GetBoidSize ()
|
||||
|
|
|
@ -1,3 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) Contributors, https://github.com/jonc/osboids
|
||||
* See CONTRIBUTORS.TXT for a full list of copyright holders.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the OpenSimulator Project nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
using System;
|
||||
using OpenMetaverse;
|
||||
using OpenSim.Region.Framework.Scenes;
|
||||
|
@ -16,7 +42,7 @@ namespace Flocking
|
|||
private float m_endZ;
|
||||
private UUID TERRAIN = UUID.Random ();
|
||||
private UUID EDGE = UUID.Random ();
|
||||
private UUID[,,] m_field = new UUID[256, 256, 256]; // field of the object at this position
|
||||
private UUID[,,] m_field = new UUID[256, 256, 256]; // field of objects at this position
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Flocking.FlowField"/> class.
|
||||
|
@ -93,95 +119,119 @@ namespace Flocking
|
|||
float fmaxX, fminX, fmaxY, fminY, fmaxZ, fminZ;
|
||||
int maxX, minX, maxY, minY, maxZ, minZ;
|
||||
sog.GetAxisAlignedBoundingBoxRaw (out fminX, out fmaxX, out fminY, out fmaxY, out fminZ, out fmaxZ);
|
||||
Vector3 pos = sog.AbsolutePosition;
|
||||
|
||||
minX = Convert.ToInt32 (fminX);
|
||||
maxX = Convert.ToInt32 (fmaxX);
|
||||
minY = Convert.ToInt32 (fminY);
|
||||
maxY = Convert.ToInt32 (fmaxX);
|
||||
minZ = Convert.ToInt32 (fminZ);
|
||||
maxZ = Convert.ToInt32 (fmaxZ);
|
||||
minX = Convert.ToInt32 (fminX + pos.X);
|
||||
maxX = Convert.ToInt32 (fmaxX + pos.X);
|
||||
minY = Convert.ToInt32 (fminY + pos.Y);
|
||||
maxY = Convert.ToInt32 (fmaxX + pos.Y);
|
||||
minZ = Convert.ToInt32 (fminZ + pos.Z);
|
||||
maxZ = Convert.ToInt32 (fmaxZ + pos.Z);
|
||||
|
||||
for (int x = minX; x < maxX; x++) {
|
||||
for (int y = minY; y < maxY; y++) {
|
||||
for (int z = minZ; z < maxZ; z++) {
|
||||
m_field [x, y, z] = sog.UUID;
|
||||
if( inBounds(x,y,z) ) {
|
||||
m_field [x, y, z] = sog.UUID;
|
||||
} else {
|
||||
Console.WriteLine(sog.Name + " OOB at " + sog.AbsolutePosition + " -> " + x + " " + y + " " + z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool inBounds (int x, int y, int z)
|
||||
{
|
||||
return x >= 0 && x < 256 && y >= 0 && y < 256 && z >= 0;
|
||||
}
|
||||
|
||||
#if false
|
||||
public Vector3 AdjustVelocity (Boid boid, float lookAheadDist)
|
||||
{
|
||||
Vector3 normVel = Vector3.Normalize (boid.Velocity);
|
||||
Vector3 loc = boid.Location;
|
||||
Vector3 inFront = loc + normVel * lookAheadDist;
|
||||
|
||||
Vector3 adjustedDestintation = inFront + FieldStrength (loc, boid.Size, inFront);
|
||||
Vector3 adjustedDestintation = FieldStrength (loc, boid.Size, inFront);
|
||||
Vector3 newVel = Vector3.Normalize (adjustedDestintation - loc) * Vector3.Mag (boid.Velocity);
|
||||
|
||||
float mOrigVel = Vector3.Mag(boid.Velocity);
|
||||
float mNewVel = Vector3.Mag(newVel);
|
||||
if( mNewVel != 0f && mNewVel > mOrigVel ) {
|
||||
newVel *= mOrigVel / mNewVel;
|
||||
}
|
||||
return newVel;
|
||||
}
|
||||
#endif
|
||||
|
||||
public Vector3 FieldStrength (Vector3 current, Vector3 size, Vector3 inFront)
|
||||
public Vector3 FieldStrength (Vector3 currentPos, Vector3 size, Vector3 targetPos)
|
||||
{
|
||||
Vector3 retVal = Vector3.Zero;
|
||||
float length = size.X/2;
|
||||
float width = size.Y/2;
|
||||
float height = size.Z/2;
|
||||
|
||||
//keep us in bounds
|
||||
if (inFront.X > m_endX)
|
||||
retVal.X -= inFront.X - m_endX - length;
|
||||
if (inFront.Y > m_endY)
|
||||
retVal.Y -= inFront.Y - m_endY - width;
|
||||
if (inFront.Z > m_endZ)
|
||||
retVal.Z -= inFront.Z - m_endZ - height;
|
||||
if (inFront.X < m_startX)
|
||||
retVal.X += m_startX - inFront.X + length;
|
||||
if (inFront.Y < m_startY)
|
||||
retVal.Y += m_startY - inFront.Y + width;
|
||||
if (inFront.Z < m_startZ)
|
||||
retVal.Z += m_startZ - inFront.Z + height;
|
||||
targetPos.X = Math.Min( targetPos.X, m_endX - length );
|
||||
targetPos.X = Math.Max(targetPos.X, m_startX + length);
|
||||
targetPos.Y = Math.Min( targetPos.Y, m_endY - width );
|
||||
targetPos.Y = Math.Max(targetPos.Y, m_startY + width);
|
||||
targetPos.Z = Math.Min( targetPos.Z, m_endZ - height );
|
||||
targetPos.Z = Math.Max(targetPos.Z, m_startZ + height);
|
||||
|
||||
int count = 0;
|
||||
|
||||
//now get the field strength at the inbounds position
|
||||
UUID collider = LookUp (inFront + retVal);
|
||||
while (collider != UUID.Zero) {
|
||||
UUID collider = LookUp (targetPos);
|
||||
while (collider != UUID.Zero && count < 100) {
|
||||
count++;
|
||||
if (collider == TERRAIN) {
|
||||
// ground height at current and dest averaged
|
||||
float h1 = m_scene.GetGroundHeight (current.X, current.Y);
|
||||
float h2 = m_scene.GetGroundHeight (inFront.X, inFront.Y);
|
||||
// ground height at currentPos and dest averaged
|
||||
float h1 = m_scene.GetGroundHeight (currentPos.X, currentPos.Y);
|
||||
float h2 = m_scene.GetGroundHeight (targetPos.X, targetPos.Y);
|
||||
float h = (h1 + h2) / 2;
|
||||
retVal.Z += h;
|
||||
targetPos.Z = h + height;
|
||||
} else if (collider == EDGE) {
|
||||
// we ain't ever going to hit these
|
||||
//keep us in bounds
|
||||
targetPos.X = Math.Min( targetPos.X, m_endX - length );
|
||||
targetPos.X = Math.Max(targetPos.X, m_startX + length);
|
||||
targetPos.Y = Math.Min( targetPos.Y, m_endY - width );
|
||||
targetPos.Y = Math.Max(targetPos.Y, m_startY + width);
|
||||
targetPos.Z = Math.Min( targetPos.Z, m_endZ - height );
|
||||
targetPos.Z = Math.Max(targetPos.Z, m_startZ + height);
|
||||
} else {
|
||||
//we have hit a SOG
|
||||
SceneObjectGroup sog = m_scene.GetSceneObjectPart (collider).ParentGroup;
|
||||
SceneObjectGroup sog = m_scene.GetSceneObjectPart(collider).ParentGroup;
|
||||
if (sog == null) {
|
||||
Console.WriteLine (collider);
|
||||
} else {
|
||||
float sogMinX, sogMinY, sogMinZ, sogMaxX, sogMaxY, sogMaxZ;
|
||||
sog.GetAxisAlignedBoundingBoxRaw (out sogMinX, out sogMaxX, out sogMinY, out sogMaxY, out sogMinZ, out sogMaxZ);
|
||||
Vector3 pos = sog.AbsolutePosition;
|
||||
//keep us out of the sog
|
||||
if (inFront.X > sogMinX)
|
||||
retVal.X -= inFront.X - sogMinX - length;
|
||||
if (inFront.Y > sogMinY)
|
||||
retVal.Y -= inFront.Y - sogMinY - width;
|
||||
if (inFront.Z > sogMinZ)
|
||||
retVal.Z -= inFront.Z - sogMinZ - height;
|
||||
if (inFront.X < sogMaxX)
|
||||
retVal.X += sogMaxX - inFront.X + length;
|
||||
if (inFront.Y < sogMaxY)
|
||||
retVal.Y += sogMaxY - inFront.Y + width;
|
||||
if (inFront.Z < sogMaxZ)
|
||||
retVal.Z += sogMaxZ - inFront.Z + height;
|
||||
// adjust up/down first if necessary
|
||||
// then turn left or right
|
||||
if (targetPos.Z > sogMinZ + pos.Z)
|
||||
targetPos.Z = (sogMinZ + pos.Z) - height;
|
||||
if (targetPos.Z < sogMaxZ + pos.Z)
|
||||
targetPos.Z = (sogMaxZ + pos.Z) + height;
|
||||
if (targetPos.X > sogMinX + pos.X)
|
||||
targetPos.X = (sogMinX + pos.X) - length;
|
||||
if (targetPos.Y > sogMinY + pos.Y)
|
||||
targetPos.Y = (sogMinY + pos.Y) - width;
|
||||
if (targetPos.X < sogMaxX + pos.X)
|
||||
targetPos.X = (sogMaxX + pos.X) + length;
|
||||
if (targetPos.Y < sogMaxY + pos.Y)
|
||||
targetPos.Y = (sogMaxY + pos.Y) + width;
|
||||
}
|
||||
}
|
||||
collider = LookUp (inFront + retVal);
|
||||
//inFront += retVal;
|
||||
|
||||
// we what is at the new target position
|
||||
collider = LookUp (targetPos);
|
||||
}
|
||||
|
||||
return retVal;
|
||||
return targetPos;
|
||||
}
|
||||
|
||||
public UUID LookUp (Vector3 loc)
|
||||
|
|
Loading…
Reference in New Issue