Alignment with C++
This commit is contained in:
parent
620b7e5273
commit
9d68f2043d
@ -25,7 +25,7 @@ namespace RoboidControl {
|
||||
};
|
||||
}
|
||||
|
||||
public override void Update(ulong currentTimeMs, bool recurse = true) {
|
||||
public override void Update(bool recurse = true) {
|
||||
|
||||
// The left wheel turns forward when nothing is touched on the right side
|
||||
// and turn backward when the roboid hits something on the right
|
||||
@ -40,7 +40,7 @@ namespace RoboidControl {
|
||||
|
||||
this.SetWheelVelocity(leftWheelSpeed, rightWheelSpeed);
|
||||
|
||||
base.Update(currentTimeMs, recurse);
|
||||
base.Update(recurse);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ namespace RoboidControl {
|
||||
};
|
||||
}
|
||||
|
||||
public override void Update(ulong currentTimeMs, bool recurse = true) {
|
||||
public override void Update(bool recurse = true) {
|
||||
|
||||
// The left wheel turns forward when nothing is touched on the right side
|
||||
// and turn backward when the roboid hits something on the right
|
||||
@ -48,7 +48,7 @@ namespace RoboidControl {
|
||||
|
||||
this.SetWheelAngularVelocity(leftWheelVelocity, rightWheelVelocity);
|
||||
|
||||
base.Update(currentTimeMs, recurse);
|
||||
base.Update(recurse);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,10 +15,13 @@ namespace RoboidControl {
|
||||
/// It is used as a basis for the local participant, but also as a reference to remote participants.
|
||||
public class Participant {
|
||||
|
||||
#region Init
|
||||
|
||||
/// <summary>
|
||||
/// Create a generic participant
|
||||
/// </summary>
|
||||
private Participant() {
|
||||
this.root = Thing.CreateRoot(this);
|
||||
this.root.name = "Root";
|
||||
this.Add(this.root);
|
||||
Thing.CreateRoot(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -26,47 +29,60 @@ namespace RoboidControl {
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">The IP address of the participant</param>
|
||||
/// <param name="port">The UDP port of the participant</param>
|
||||
/// <remarks>This does not belong here, it should move to ParticipantUDP or something like that in the future
|
||||
public Participant(string ipAddress, int port, Participant localParticipant = null) {
|
||||
this.ipAddress = ipAddress;
|
||||
this.port = port;
|
||||
|
||||
this.root = Thing.CreateRoot(this);
|
||||
this.root.name = "Root";
|
||||
Thing.CreateRoot(this);
|
||||
|
||||
if (localParticipant != null)
|
||||
this.udpClient = localParticipant.udpClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The local participant for this application
|
||||
/// </summary>
|
||||
public static Participant localParticipant = new();
|
||||
public static void ReplaceLocalParticipant(Participant participant) {
|
||||
Participant.localParticipant = participant;
|
||||
/// <summary>
|
||||
/// Replace the local participant
|
||||
/// </summary>
|
||||
/// <param name="newParticipant">The new local participant</param>
|
||||
public static void ReplaceLocalParticipant(Participant newParticipant) {
|
||||
Participant.localParticipant = newParticipant;
|
||||
}
|
||||
|
||||
public Thing root = null;
|
||||
#endregion Init
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// The Ip Address of a participant. When the participant is local, this contains 0.0.0.0
|
||||
/// </summary>
|
||||
/// <remarks>This does not belong here, it should move to ParticipantUDP or something like that in the future
|
||||
public string ipAddress = "0.0.0.0";
|
||||
/// <summary>
|
||||
/// The port number for UDP communication with the participant. This is 0 for isolated participants.
|
||||
/// </summary>
|
||||
/// <remarks>This does not belong here, it should move to ParticipantUDP or something like that in the future
|
||||
public int port = 0;
|
||||
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
/// <summary>
|
||||
/// A reference to the representation of the thing in Unity
|
||||
/// The udpClient for this participant
|
||||
/// </summary>
|
||||
[NonSerialized]
|
||||
public Unity.Participant component = null;
|
||||
#endif
|
||||
|
||||
/// <remarks>This does not belong here, it should move to ParticipantUDP or something like that in the future
|
||||
public UdpClient udpClient = null;
|
||||
|
||||
/// <summary>
|
||||
/// he network Id to identify the participant
|
||||
/// The network Id to identify the participant
|
||||
/// </summary>
|
||||
public byte networkId = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The root thing for this participant
|
||||
/// </summary>
|
||||
public Thing root = null;
|
||||
|
||||
/// <summary>
|
||||
/// The things managed by this participant
|
||||
/// </summary>
|
||||
@ -110,26 +126,47 @@ namespace RoboidControl {
|
||||
this.things.Remove(thing);
|
||||
}
|
||||
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
/// <summary>
|
||||
/// A reference to the representation of the thing in Unity
|
||||
/// </summary>
|
||||
[NonSerialized]
|
||||
public Unity.Participant component = null;
|
||||
#endif
|
||||
|
||||
#endregion properties
|
||||
|
||||
#region Update
|
||||
|
||||
/// <summary>
|
||||
/// Update all things for this participant
|
||||
/// </summary>
|
||||
/// <param name="currentTimeMS">The current time in milliseconds (optional)</param>
|
||||
public virtual void Update(ulong currentTimeMS = 0) {
|
||||
public virtual void Update() {
|
||||
int n = this.things.Count;
|
||||
for (int ix = 0; ix < n; ix++) {
|
||||
Thing thing = this.things[ix];
|
||||
if (thing != null)
|
||||
thing.Update(currentTimeMS, true);
|
||||
thing.Update(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event for a participant
|
||||
/// </summary>
|
||||
public class UpdateEvent {
|
||||
/// <summary>
|
||||
/// The type of event happened.
|
||||
/// </summary>
|
||||
/// This value is filled with the Ids of the communication messages.
|
||||
public int messageId; // see the communication messages
|
||||
/// <summary>
|
||||
/// The thing relevant fo the event
|
||||
/// </summary>
|
||||
public Thing thing;
|
||||
public Participant participant;
|
||||
}
|
||||
/// <summary>
|
||||
/// Queue containing events happened to this participant
|
||||
/// </summary>
|
||||
public ConcurrentQueue<UpdateEvent> updateQueue = new();
|
||||
|
||||
#endregion Update
|
||||
@ -155,6 +192,8 @@ namespace RoboidControl {
|
||||
|
||||
#endregion Send
|
||||
|
||||
#region Participant Registry
|
||||
|
||||
/// <summary>
|
||||
/// The collection of known participants.
|
||||
/// </summary>
|
||||
@ -213,6 +252,7 @@ namespace RoboidControl {
|
||||
Participant.participants.Add(participant);
|
||||
}
|
||||
|
||||
#endregion Participant Registery
|
||||
}
|
||||
|
||||
}
|
@ -150,9 +150,8 @@ namespace RoboidControl {
|
||||
protected ulong nextPublishMe = 0;
|
||||
protected ulong nextSendUpdate = 0;
|
||||
|
||||
public override void Update(ulong currentTimeMS = 0) {
|
||||
if (currentTimeMS == 0)
|
||||
currentTimeMS = Thing.GetTimeMs();
|
||||
public override void Update() {
|
||||
ulong currentTimeMS = Thing.GetTimeMs();
|
||||
|
||||
if (this.isIsolated == false) {
|
||||
if (this.publishIntervalMS > 0 && currentTimeMS > this.nextPublishMe) {
|
||||
@ -185,7 +184,7 @@ namespace RoboidControl {
|
||||
// Because when a thing creates a thing in the update,
|
||||
// that new thing is not sent out (because of hierarchyChanged)
|
||||
// before it is updated itself: it is immediatedly updated!
|
||||
thing.Update(currentTimeMS, false);
|
||||
thing.Update(false);
|
||||
if (!(this.isIsolated || this.networkId == 0)) {
|
||||
if (thing.terminate) {
|
||||
DestroyMsg destroyMsg = new(this.networkId, thing);
|
||||
@ -213,7 +212,7 @@ namespace RoboidControl {
|
||||
if (participant == null || participant == this)
|
||||
continue;
|
||||
|
||||
participant.Update(currentTimeMS);
|
||||
participant.Update();
|
||||
if (this.isIsolated)
|
||||
continue;
|
||||
|
||||
|
@ -72,7 +72,7 @@ namespace RoboidControl {
|
||||
if (thing == null)
|
||||
continue;
|
||||
|
||||
thing.Update(currentTimeMS, false);
|
||||
thing.Update(false);
|
||||
|
||||
if (this.isIsolated == false) {
|
||||
// Send to all other participants
|
||||
@ -104,8 +104,7 @@ namespace RoboidControl {
|
||||
// this.Send(sender, new NetworkIdMsg(sender.networkId));
|
||||
sender.Send(new NetworkIdMsg(sender.networkId));
|
||||
UpdateEvent e = new() {
|
||||
messageId = ParticipantMsg.Id,
|
||||
participant = sender
|
||||
messageId = ParticipantMsg.Id
|
||||
};
|
||||
this.updateQueue.Enqueue(e);
|
||||
}
|
||||
|
183
src/Thing.cs
183
src/Thing.cs
@ -38,73 +38,14 @@ namespace RoboidControl {
|
||||
|
||||
#region Init
|
||||
|
||||
private Thing(Participant owner) {
|
||||
this.type = Type.Root;
|
||||
|
||||
this.positionUpdated = true;
|
||||
this.orientationUpdated = true;
|
||||
this.hierarchyChanged = true;
|
||||
|
||||
this.owner = owner;
|
||||
this.parent = null;
|
||||
}
|
||||
|
||||
public static Thing CreateRoot(Participant owner) {
|
||||
return new Thing(owner);
|
||||
}
|
||||
|
||||
static Thing localRoot {
|
||||
get {
|
||||
Participant participant = Participant.localParticipant;
|
||||
return participant.root;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// <summary>
|
||||
/// Create a new thing without communication abilities
|
||||
/// Create a new Thing
|
||||
/// </summary>
|
||||
/// <param name="thingType">The type of thing (can use \ref RoboidControl::Thing::Type "Thing.Type")</param>
|
||||
/// <param name="invokeEvent">Invoke a OnNewThing event when the thing has been created</param>
|
||||
public Thing(byte thingType = Type.Undetermined, bool invokeEvent = true) : this(ParticipantUDP.Isolated(), thingType, 0, invokeEvent) {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new thing for a participant
|
||||
/// </summary>
|
||||
/// <param name="owner">The owning participant</param>
|
||||
/// <param name="thingType">The type of thing (can use \ref RoboidControl::Thing::Type "Thing.Type")</param>
|
||||
/// <param name="thingId">The ID of the thing, leave out or set to zero to generate an ID</param>
|
||||
/// <param name="invokeEvent">Invoke a OnNewThing event when the thing has been created</param>
|
||||
public Thing(Participant owner, byte thingType = Type.Undetermined, byte thingId = 0, bool invokeEvent = true) {
|
||||
this.owner = owner;
|
||||
this.id = thingId;
|
||||
this.type = thingType;
|
||||
if (this.owner != null)
|
||||
this.owner.Add(this);
|
||||
if (invokeEvent) {
|
||||
Participant.UpdateEvent e = new() {
|
||||
messageId = ThingMsg.id,
|
||||
thing = this
|
||||
};
|
||||
this.owner.updateQueue.Enqueue(e);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Create a new child thing
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent thing</param>
|
||||
/// <param name="thingType">The type of thing (can use \ref RoboidControl::Thing::Type "Thing.Type")</param>
|
||||
/// <param name="thingId">The ID of the thing, leave out or set to zero to generate an ID</param>
|
||||
/// <param name="invokeEvent">Invoke a OnNewThing event when the thing has been created</param>
|
||||
/// <note>The owner will be the same as the owner of the parent thing</note>
|
||||
/*
|
||||
public Thing(Thing parent, byte thingType = Type.Undetermined, byte thingId = 0, bool invokeEvent = true) : this(parent.owner, thingType, thingId, invokeEvent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
*/
|
||||
/// <param name="parent">(optional) The parent thing</param>
|
||||
/// <note>The owner will be the same as the owner of the parent thing, it will
|
||||
/// be Participant.LocalParticipant if the parent is not specified. A thing
|
||||
/// without a parent will be connected to the root thing.
|
||||
/// </note>
|
||||
public Thing(Thing parent = default) {
|
||||
this.type = Type.Undetermined;
|
||||
|
||||
@ -127,12 +68,41 @@ namespace RoboidControl {
|
||||
this.owner.updateQueue.Enqueue(e);
|
||||
}
|
||||
|
||||
// public static Thing CreateRemote(Participant owner, byte thingId) {
|
||||
// Thing remoteThing = new(owner.root) {
|
||||
// id = thingId
|
||||
// };
|
||||
// return remoteThing;
|
||||
// }
|
||||
/// <summary>
|
||||
/// Constructor to create a root thing
|
||||
/// </summary>
|
||||
/// <param name="owner">The participant who will own this root thing</param>
|
||||
/// <remarks>This function is private because CreateRoot() should be used instead</remarks>
|
||||
private Thing(Participant owner) {
|
||||
this.type = Type.Root;
|
||||
this.name = "Root";
|
||||
|
||||
this.positionUpdated = true;
|
||||
this.orientationUpdated = true;
|
||||
this.hierarchyChanged = true;
|
||||
|
||||
this.owner = owner;
|
||||
this.parent = null;
|
||||
this.owner.Add(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a root Thing for a participant
|
||||
/// </summary>
|
||||
/// <param name="owner">The participant who will own this root thing</param>
|
||||
public static void CreateRoot(Participant owner) {
|
||||
owner.root = new Thing(owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The root thing for the local participant
|
||||
/// </summary>
|
||||
public static Thing localRoot {
|
||||
get {
|
||||
Participant participant = Participant.localParticipant;
|
||||
return participant.root;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Function which can be used to create components in external engines.
|
||||
@ -147,14 +117,13 @@ namespace RoboidControl {
|
||||
|
||||
#endregion Init
|
||||
|
||||
/// <summary>
|
||||
/// Terminated things are no longer updated
|
||||
/// </summary>
|
||||
public bool terminate = false;
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// The participant owning this thing
|
||||
/// </summary>
|
||||
public Participant owner = null;
|
||||
/// <summary>
|
||||
/// The ID of this thing
|
||||
/// </summary>
|
||||
@ -166,6 +135,11 @@ namespace RoboidControl {
|
||||
/// This can be either a \ref RoboidControl::Thing::Type "Thing.Type" or a byte value for custom types.
|
||||
public byte type = Type.Undetermined;
|
||||
|
||||
/// <summary>
|
||||
/// The participant owning this thing
|
||||
/// </summary>
|
||||
public Participant owner = null;
|
||||
|
||||
private string _name = "";
|
||||
/// <summary>
|
||||
/// The name of the thing
|
||||
@ -186,6 +160,9 @@ namespace RoboidControl {
|
||||
/// <summary>
|
||||
/// An URL pointing to the location where a model of the thing can be found
|
||||
/// </summary>
|
||||
/// <remarks>Although the roboid implementation is not dependent on the model,
|
||||
/// the only official supported model formats are .png (sprite), .gltf and .glb
|
||||
/// </remarks>
|
||||
public string modelUrl {
|
||||
get => _modelUrl;
|
||||
set {
|
||||
@ -230,6 +207,12 @@ namespace RoboidControl {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indication whether this is a root thing
|
||||
/// </summary>
|
||||
public bool isRoot {
|
||||
get => this == localRoot || this.parent == null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The children of this thing
|
||||
@ -392,32 +375,11 @@ namespace RoboidControl {
|
||||
|
||||
#region Update
|
||||
|
||||
/// <summary>
|
||||
/// Get the current time in milliseconds
|
||||
/// </summary>
|
||||
/// <returns>The current time in milliseconds</returns>
|
||||
public static ulong GetTimeMs() {
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
return (ulong)(UnityEngine.Time.time * 1000);
|
||||
#else
|
||||
return TimeManager.GetCurrentTimeMilliseconds();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update de state of the thing
|
||||
/// </summary>
|
||||
/// <param name="recurse">When true, this will Update the descendants recursively</param>
|
||||
public void Update(bool recurse = false) {
|
||||
Update(GetTimeMs(), recurse);
|
||||
}
|
||||
// #endif
|
||||
/// <summary>
|
||||
/// Update this thing
|
||||
/// </summary>
|
||||
/// <param name="currentTime">he current clock time in milliseconds; if this is zero, the current time is retrieved automatically</param>
|
||||
/// <param name="recurse">When true, this will Update the descendants recursively</param>
|
||||
public virtual void Update(ulong currentTimeMs, bool recurse = false) {
|
||||
public virtual void Update(bool recurse = false) {
|
||||
this.positionUpdated = false;
|
||||
this.orientationUpdated = false;
|
||||
this.linearVelocityUpdated = false;
|
||||
@ -430,19 +392,42 @@ namespace RoboidControl {
|
||||
Thing child = this.children[childIx];
|
||||
if (child == null)
|
||||
continue;
|
||||
child.Update(currentTimeMs, recurse);
|
||||
child.Update(recurse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An event happened to this event
|
||||
/// </summary>
|
||||
/// <note>The messageId indicates which kind of event happened
|
||||
public class CoreEvent {
|
||||
public CoreEvent(int messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
public int messageId; // see the communication messages
|
||||
/// <summary>
|
||||
/// The type of event happened.
|
||||
/// </summary>
|
||||
/// This value is filled with the Ids of the communication messages.
|
||||
public int messageId;
|
||||
};
|
||||
/// <summary>
|
||||
/// Queue containing events happened to this thing
|
||||
/// </summary>
|
||||
public ConcurrentQueue<CoreEvent> updateQueue = new();
|
||||
|
||||
/// <summary>
|
||||
/// Get the current time in milliseconds
|
||||
/// </summary>
|
||||
/// <returns>The current time in milliseconds</returns>
|
||||
public static ulong GetTimeMs() {
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
return (ulong)(UnityEngine.Time.time * 1000);
|
||||
#else
|
||||
return TimeManager.GetCurrentTimeMilliseconds();
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion Update
|
||||
|
||||
/// <summary>
|
||||
|
@ -52,13 +52,14 @@ namespace RoboidControl {
|
||||
float pidD = 0.0F;
|
||||
//float pidI = 0.0F;
|
||||
|
||||
public override void Update(ulong currentTimeMs, bool recurse = false) {
|
||||
public override void Update(bool recurse = false) {
|
||||
float actualSpeed = this.encoder.angularSpeed;
|
||||
// Simplified rotation direction, shouldbe improved
|
||||
// This goes wrong when the target speed is inverted and the motor axcle is still turning in the old direction
|
||||
if (this.targetAngularSpeed < 0)
|
||||
actualSpeed = -actualSpeed;
|
||||
|
||||
ulong currentTimeMs = Thing.GetTimeMs();
|
||||
float deltaTime = (currentTimeMs - this.lastUpdateTime) / 1000.0f;
|
||||
|
||||
float error = this.targetAngularSpeed - actualSpeed;
|
||||
|
@ -86,7 +86,7 @@ namespace RoboidControl {
|
||||
}
|
||||
|
||||
/// @copydoc RoboidControl::Thing::Update(unsigned long)
|
||||
public override void Update(ulong currentMs, bool recursive = true) {
|
||||
public override void Update(bool recursive = true) {
|
||||
if (this.linearVelocityUpdated) {
|
||||
// this assumes forward velocity only....
|
||||
float linearVelocity = this.linearVelocity.distance;
|
||||
|
@ -52,13 +52,14 @@ namespace RoboidControl {
|
||||
float pidD = 0.0F;
|
||||
//float pidI = 0.0F;
|
||||
|
||||
public override void Update(ulong currentTimeMs, bool recurse = false) {
|
||||
public override void Update(bool recurse = false) {
|
||||
float actualSpeed = this.encoder.angularSpeed;
|
||||
// Simplified rotation direction, shouldbe improved
|
||||
// This goes wrong when the target speed is inverted and the motor axcle is still turning in the old direction
|
||||
if (this.targetAngularSpeed < 0)
|
||||
actualSpeed = -actualSpeed;
|
||||
|
||||
ulong currentTimeMs = Thing.GetTimeMs();
|
||||
float deltaTime = (currentTimeMs - this.lastUpdateTime) / 1000.0f;
|
||||
|
||||
float error = this.targetAngularSpeed - actualSpeed;
|
||||
|
@ -18,7 +18,7 @@ namespace RoboidControl.test {
|
||||
ulong milliseconds = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
ulong startTime = milliseconds;
|
||||
while (milliseconds < startTime + 7000) {
|
||||
participant.Update(milliseconds);
|
||||
participant.Update();
|
||||
|
||||
Thread.Sleep(100);
|
||||
milliseconds = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
@ -34,7 +34,7 @@ namespace RoboidControl.test {
|
||||
ulong milliseconds = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
ulong startTime = milliseconds;
|
||||
while (milliseconds < startTime + 7000) {
|
||||
siteServer.Update(milliseconds);
|
||||
siteServer.Update();
|
||||
|
||||
Thread.Sleep(100);
|
||||
milliseconds = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
@ -51,8 +51,8 @@ namespace RoboidControl.test {
|
||||
ulong milliseconds = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
ulong startTime = milliseconds;
|
||||
while (milliseconds < startTime + 1000) {
|
||||
siteServer.Update(milliseconds);
|
||||
participant.Update(milliseconds);
|
||||
siteServer.Update();
|
||||
participant.Update();
|
||||
|
||||
Thread.Sleep(100);
|
||||
milliseconds = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
@ -73,8 +73,8 @@ namespace RoboidControl.test {
|
||||
ulong milliseconds = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
ulong startTime = milliseconds;
|
||||
while (milliseconds < startTime + 7000) {
|
||||
siteServer.Update(milliseconds);
|
||||
participant.Update(milliseconds);
|
||||
siteServer.Update();
|
||||
participant.Update();
|
||||
|
||||
Thread.Sleep(100);
|
||||
milliseconds = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
Loading…
x
Reference in New Issue
Block a user