mirror of
https://github.com/Sanae6/SmoOnlineServer.git
synced 2024-11-21 18:55:17 +00:00
Untested conversion to variable size packets
This commit is contained in:
parent
713e5f4cce
commit
8c5cd0ced2
16 changed files with 132 additions and 99 deletions
|
@ -1,6 +1,7 @@
|
|||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using Shared;
|
||||
using Shared.Packet;
|
||||
using Shared.Packet.Packets;
|
||||
|
@ -36,25 +37,26 @@ public class Client : IDisposable {
|
|||
public event PacketTransformerDel? PacketTransformer;
|
||||
|
||||
public async Task Send<T>(T packet, Client? sender = null) where T : struct, IPacket {
|
||||
IMemoryOwner<byte> memory = MemoryPool<byte>.Shared.RentZero(Constants.MaxPacketSize);
|
||||
IMemoryOwner<byte> memory = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + packet.Size);
|
||||
packet = (T) (PacketTransformer?.Invoke(sender, packet) ?? packet);
|
||||
PacketHeader header = new PacketHeader {
|
||||
Id = sender?.Id ?? Guid.Empty,
|
||||
Type = Constants.PacketMap[typeof(T)].Type
|
||||
Type = Constants.PacketMap[typeof(T)].Type,
|
||||
PacketSize = packet.Size
|
||||
};
|
||||
Server.FillPacket(header, packet, memory.Memory);
|
||||
await Send(memory.Memory[..Constants.MaxPacketSize], sender);
|
||||
await Send(memory.Memory[..], sender);
|
||||
memory.Dispose();
|
||||
}
|
||||
|
||||
public async Task Send(ReadOnlyMemory<byte> data, Client? sender) {
|
||||
if (!Connected) {
|
||||
Server.Logger.Info($"Didn't send {(PacketType) data.Span[16]} to {Id} because they weren't connected yet");
|
||||
Server.Logger.Info($"Didn't send {MemoryMarshal.Read<PacketType>(data.Span[16..])} to {Id} because they weren't connected yet");
|
||||
return;
|
||||
}
|
||||
|
||||
// Server.Logger.Info($"Sending {(PacketType) data.Span[16]} to {Id} from {other?.Id.ToString() ?? "server"}");
|
||||
await Socket!.SendAsync(data[..Constants.MaxPacketSize], SocketFlags.None);
|
||||
int packetSize = MemoryMarshal.Read<short>(data.Span[18..]);
|
||||
await Socket!.SendAsync(data[..(Constants.HeaderSize + packetSize)], SocketFlags.None);
|
||||
}
|
||||
|
||||
public static bool operator ==(Client? left, Client? right) {
|
||||
|
|
|
@ -78,11 +78,11 @@ public class Server {
|
|||
}
|
||||
|
||||
public async Task Broadcast<T>(T packet, Client sender) where T : struct, IPacket {
|
||||
IMemoryOwner<byte> memory = memoryPool.RentZero(Constants.MaxPacketSize);
|
||||
|
||||
IMemoryOwner<byte> memory = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + packet.Size);
|
||||
PacketHeader header = new PacketHeader {
|
||||
Id = sender?.Id ?? Guid.Empty,
|
||||
Type = Constants.PacketMap[typeof(T)].Type
|
||||
Type = Constants.PacketMap[typeof(T)].Type,
|
||||
PacketSize = packet.Size
|
||||
};
|
||||
FillPacket(header, packet, memory.Memory);
|
||||
await Broadcast(memory, sender);
|
||||
|
@ -118,11 +118,12 @@ public class Server {
|
|||
bool first = true;
|
||||
try {
|
||||
while (true) {
|
||||
memory = memoryPool.Rent(Constants.MaxPacketSize);
|
||||
{
|
||||
int readOffset = 0;
|
||||
while (readOffset < Constants.MaxPacketSize) {
|
||||
int size = await socket.ReceiveAsync(memory.Memory[readOffset..Constants.MaxPacketSize], SocketFlags.None);
|
||||
memory = memoryPool.Rent(Constants.HeaderSize);
|
||||
|
||||
async Task Read(Memory<byte> readMem, int readOffset = 0, int readSize = -1) {
|
||||
if (readSize == -1) readSize = Constants.HeaderSize;
|
||||
while (readOffset < readSize) {
|
||||
int size = await socket.ReceiveAsync(readMem[readOffset..readSize], SocketFlags.None);
|
||||
if (size == 0) {
|
||||
// treat it as a disconnect and exit
|
||||
Logger.Info($"Socket {socket.RemoteEndPoint} disconnected.");
|
||||
|
@ -132,11 +133,17 @@ public class Server {
|
|||
|
||||
readOffset += size;
|
||||
}
|
||||
|
||||
if (readOffset < Constants.MaxPacketSize) break;
|
||||
}
|
||||
|
||||
PacketHeader header = GetHeader(memory.Memory.Span[..Constants.MaxPacketSize]);
|
||||
await Read(memory.Memory[..Constants.HeaderSize]);
|
||||
PacketHeader header = GetHeader(memory.Memory.Span[..Constants.HeaderSize]);
|
||||
{
|
||||
IMemoryOwner<byte> memTemp = memory;
|
||||
memory = memoryPool.Rent(Constants.HeaderSize + header.Size);
|
||||
memTemp.Memory.CopyTo(memory.Memory);
|
||||
memTemp.Dispose();
|
||||
}
|
||||
await Read(memory.Memory[Constants.HeaderSize..(Constants.HeaderSize + header.Size)]);
|
||||
|
||||
// connection initialization
|
||||
if (first) {
|
||||
|
@ -144,7 +151,7 @@ public class Server {
|
|||
if (header.Type != PacketType.Connect) throw new Exception($"First packet was not init, instead it was {header.Type}");
|
||||
|
||||
ConnectPacket connect = new ConnectPacket();
|
||||
connect.Deserialize(memory.Memory.Span[Constants.HeaderSize..Constants.MaxPacketSize]);
|
||||
connect.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + header.PacketSize)]);
|
||||
lock (Clients) {
|
||||
client.Name = connect.ClientName;
|
||||
bool firstConn = false;
|
||||
|
@ -188,10 +195,11 @@ public class Server {
|
|||
|
||||
List<Client> otherConnectedPlayers = Clients.FindAll(c => c.Id != header.Id && c.Connected && c.Socket != null);
|
||||
await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => {
|
||||
IMemoryOwner<byte> tempBuffer = MemoryPool<byte>.Shared.RentZero(Constants.MaxPacketSize);
|
||||
IMemoryOwner<byte> tempBuffer = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + (other.CurrentCostume.HasValue ? Math.Max(connect.Size, other.CurrentCostume.Value.Size) : connect.Size));
|
||||
PacketHeader connectHeader = new PacketHeader {
|
||||
Id = other.Id,
|
||||
Type = PacketType.Connect
|
||||
Type = PacketType.Connect,
|
||||
PacketSize = connect.Size
|
||||
};
|
||||
MemoryMarshal.Write(tempBuffer.Memory.Span, ref connectHeader);
|
||||
ConnectPacket connectPacket = new ConnectPacket {
|
||||
|
@ -199,12 +207,13 @@ public class Server {
|
|||
ClientName = other.Name
|
||||
};
|
||||
connectPacket.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..]);
|
||||
await client.Send(tempBuffer.Memory, null);
|
||||
await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connect.Size)], null);
|
||||
if (other.CurrentCostume.HasValue) {
|
||||
connectHeader.Type = PacketType.Costume;
|
||||
connectHeader.PacketSize = other.CurrentCostume.Value.Size;
|
||||
MemoryMarshal.Write(tempBuffer.Memory.Span, ref connectHeader);
|
||||
other.CurrentCostume.Value.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..]);
|
||||
await client.Send(tempBuffer.Memory, null);
|
||||
other.CurrentCostume.Value.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + connectHeader.PacketSize)]);
|
||||
await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connectHeader.PacketSize)], null);
|
||||
}
|
||||
|
||||
tempBuffer.Dispose();
|
||||
|
@ -219,14 +228,14 @@ public class Server {
|
|||
CostumePacket costumePacket = new CostumePacket {
|
||||
BodyName = ""
|
||||
};
|
||||
costumePacket.Deserialize(memory.Memory.Span[Constants.HeaderSize..Constants.MaxPacketSize]);
|
||||
costumePacket.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + costumePacket.Size)]);
|
||||
client.CurrentCostume = costumePacket;
|
||||
}
|
||||
|
||||
try {
|
||||
// if (header.Type is not PacketType.Cap and not PacketType.Player) client.Logger.Warn($"lol {header.Type}");
|
||||
IPacket packet = (IPacket) Activator.CreateInstance(Constants.PacketIdMap[header.Type])!;
|
||||
packet.Deserialize(memory.Memory.Span[Constants.HeaderSize..Constants.MaxPacketSize]);
|
||||
packet.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + packet.Size)]);
|
||||
if (PacketHandler?.Invoke(client, packet) is false) {
|
||||
memory.Dispose();
|
||||
continue;
|
||||
|
|
|
@ -6,7 +6,6 @@ using Shared.Packet.Packets;
|
|||
namespace Shared;
|
||||
|
||||
public static class Constants {
|
||||
public const int MaxPacketSize = 256;
|
||||
public const int MaxClients = 8;
|
||||
public const int CostumeNameSize = 0x20;
|
||||
|
||||
|
@ -23,5 +22,4 @@ public static class Constants {
|
|||
.ToDictionary(type => type.GetCustomAttribute<PacketAttribute>()!.Type, type => type);
|
||||
|
||||
public static int HeaderSize { get; } = Marshal.SizeOf<PacketHeader>();
|
||||
public static int PacketDataSize { get; } = MaxPacketSize - HeaderSize;
|
||||
}
|
|
@ -8,6 +8,9 @@ public struct PacketHeader : IPacket {
|
|||
// public int Length;
|
||||
public Guid Id;
|
||||
public PacketType Type;
|
||||
public short PacketSize;
|
||||
|
||||
public short Size => 20;
|
||||
|
||||
public void Serialize(Span<byte> data) {
|
||||
// MemoryMarshal.Write(data, ref Length);
|
||||
|
@ -18,5 +21,6 @@ public struct PacketHeader : IPacket {
|
|||
public void Deserialize(Span<byte> data) {
|
||||
Id = MemoryMarshal.Read<Guid>(data);
|
||||
Type = MemoryMarshal.Read<PacketType>(data[16..]);
|
||||
Type = MemoryMarshal.Read<PacketType>(data[16..]);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
namespace Shared.Packet;
|
||||
|
||||
public enum PacketType {
|
||||
public enum PacketType : short {
|
||||
Unknown,
|
||||
Player,
|
||||
Cap,
|
||||
|
|
|
@ -11,6 +11,8 @@ public struct CapPacket : IPacket {
|
|||
public Quaternion Rotation;
|
||||
public string CapAnim;
|
||||
|
||||
public short Size => 0x4C;
|
||||
|
||||
public void Serialize(Span<byte> data) {
|
||||
MemoryMarshal.Write(data, ref Position);
|
||||
MemoryMarshal.Write(data[12..], ref Position);
|
||||
|
|
|
@ -10,6 +10,7 @@ public struct CapturePacket : IPacket {
|
|||
|
||||
public bool IsCaptured;
|
||||
|
||||
public short Size => Constants.CostumeNameSize + 1;
|
||||
public void Serialize(Span<byte> data) {
|
||||
Encoding.UTF8.GetBytes(ModelName).CopyTo(data[..Constants.CostumeNameSize]);
|
||||
MemoryMarshal.Write(data[Constants.CostumeNameSize..], ref IsCaptured);
|
||||
|
@ -19,4 +20,5 @@ public struct CapturePacket : IPacket {
|
|||
ModelName = Encoding.UTF8.GetString(data[..Constants.CostumeNameSize]).TrimNullTerm();
|
||||
IsCaptured = MemoryMarshal.Read<bool>(data[Constants.CostumeNameSize..]);
|
||||
}
|
||||
|
||||
}
|
|
@ -10,6 +10,8 @@ public struct ConnectPacket : IPacket {
|
|||
|
||||
public ConnectPacket() { }
|
||||
|
||||
public short Size => 4 + Constants.CostumeNameSize;
|
||||
|
||||
public void Serialize(Span<byte> data) {
|
||||
MemoryMarshal.Write(data, ref ConnectionType);
|
||||
Encoding.UTF8.GetBytes(ClientName).CopyTo(data[4..(4 + Constants.CostumeNameSize)]);
|
||||
|
|
|
@ -11,6 +11,8 @@ public struct CostumePacket : IPacket {
|
|||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.CostumeNameSize)]
|
||||
public string CapName;
|
||||
|
||||
public short Size => Constants.CostumeNameSize * 2;
|
||||
|
||||
public void Serialize(Span<byte> data) {
|
||||
Encoding.UTF8.GetBytes(BodyName).CopyTo(data[..Constants.CostumeNameSize]);
|
||||
Encoding.UTF8.GetBytes(CapName).CopyTo(data[Constants.CostumeNameSize..]);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
[Packet(PacketType.Disconnect)]
|
||||
public struct DisconnectPacket : IPacket {
|
||||
//empty packet
|
||||
public short Size => 0;
|
||||
public void Serialize(Span<byte> data) { }
|
||||
|
||||
public void Deserialize(Span<byte> data) { }
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
// Packet interface for type safety
|
||||
public interface IPacket {
|
||||
short Size { get; }
|
||||
void Serialize(Span<byte> data);
|
||||
void Deserialize(Span<byte> data);
|
||||
}
|
|
@ -35,6 +35,8 @@ public struct PlayerPacket : IPacket {
|
|||
|
||||
public PlayerPacket() { }
|
||||
|
||||
public short Size => 0xE0;
|
||||
|
||||
public void Serialize(Span<byte> data) {
|
||||
int offset = 0;
|
||||
MemoryMarshal.Write(data, ref Position);
|
||||
|
|
|
@ -6,6 +6,8 @@ namespace Shared.Packet.Packets;
|
|||
public struct ShinePacket : IPacket {
|
||||
public int ShineId;
|
||||
|
||||
public short Size => 4;
|
||||
|
||||
public void Serialize(Span<byte> data) {
|
||||
MemoryMarshal.Write(data, ref ShineId);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ namespace Shared.Packet.Packets;
|
|||
public struct TagPacket : IPacket {
|
||||
public bool IsIt;
|
||||
|
||||
public short Size => 1;
|
||||
|
||||
public void Serialize(Span<byte> data) {
|
||||
MemoryMarshal.Write(data, ref IsIt);
|
||||
}
|
||||
|
|
|
@ -3,15 +3,19 @@
|
|||
[Packet(PacketType.Unknown)] // empty like boss
|
||||
// [Packet(PacketType.Command)]
|
||||
public struct UnhandledPacket : IPacket {
|
||||
public byte[] Data = new byte[Constants.PacketDataSize];
|
||||
public byte[] Data;
|
||||
|
||||
public UnhandledPacket() { }
|
||||
public UnhandledPacket() {
|
||||
Data = null!;
|
||||
}
|
||||
public short Size => 0;
|
||||
|
||||
public void Serialize(Span<byte> data) {
|
||||
Data.CopyTo(data);
|
||||
}
|
||||
|
||||
public void Deserialize(Span<byte> data) {
|
||||
data.CopyTo(Data);
|
||||
Data = data.ToArray();
|
||||
}
|
||||
|
||||
}
|
|
@ -6,73 +6,73 @@ using Shared;
|
|||
using Shared.Packet;
|
||||
using Shared.Packet.Packets;
|
||||
|
||||
TcpClient client = new TcpClient(args[0], 1027);
|
||||
// TcpClient client = new TcpClient(args[0], 1027);
|
||||
Guid ownId = new Guid(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
// Guid ownId = Guid.NewGuid();
|
||||
Guid otherId = Guid.Parse("d5feae62-2e71-1000-88fd-597ea147ae88");
|
||||
Logger logger = new Logger("Client");
|
||||
NetworkStream stream = client.GetStream();
|
||||
|
||||
Vector3 basePoint = Vector3.Zero;
|
||||
|
||||
PacketType[] reboundPackets = {
|
||||
PacketType.Player,
|
||||
PacketType.Cap,
|
||||
PacketType.Capture,
|
||||
PacketType.Costume,
|
||||
PacketType.Tag,
|
||||
PacketType.Shine
|
||||
};
|
||||
|
||||
string lastCapture = "";
|
||||
|
||||
async Task S() {
|
||||
IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(Constants.MaxPacketSize);
|
||||
while (true) {
|
||||
await stream.ReadAsync(owner.Memory);
|
||||
PacketHeader header = MemoryMarshal.Read<PacketHeader>(owner.Memory.Span);
|
||||
PacketType type = header.Type;
|
||||
if (header.Id != otherId) continue;
|
||||
if (type is PacketType.Player) {
|
||||
// CapPacket cap = new CapPacket();
|
||||
PlayerPacket playerPacket = new PlayerPacket();
|
||||
playerPacket.Deserialize(owner.Memory.Span[Constants.HeaderSize..]);
|
||||
logger.Info(playerPacket.Hack);
|
||||
if (playerPacket.Hack != lastCapture) logger.Info($"Changed to hack: {lastCapture = playerPacket.Hack}");
|
||||
// cap.Position = playerPacket.Position + Vector3.UnitY * 500f;
|
||||
// cap.Rotation = Quaternion.CreateFromYawPitchRoll(0,0,0);
|
||||
// cap.CapAnim = "StayR";
|
||||
// playerPacket.Position = new Vector3(1000000f);
|
||||
// playerPacket.ThrowingCap = true;
|
||||
// header.Id = ownId;
|
||||
// MemoryMarshal.Write(owner.Memory.Span, ref header);
|
||||
// playerPacket.Serialize(owner.Memory.Span[Constants.HeaderSize..]);
|
||||
// await stream.WriteAsync(owner.Memory[..Constants.MaxPacketSize]);
|
||||
// header.Type = PacketType.Cap;
|
||||
// MemoryMarshal.Write(owner.Memory.Span, ref header);
|
||||
// cap.Serialize(owner.Memory.Span[Constants.HeaderSize..]);
|
||||
// await stream.WriteAsync(owner.Memory[..Constants.MaxPacketSize]);
|
||||
// continue;
|
||||
}
|
||||
|
||||
if (reboundPackets.All(x => x != type)) continue;
|
||||
header.Id = ownId;
|
||||
MemoryMarshal.Write(owner.Memory.Span, ref header);
|
||||
await stream.WriteAsync(owner.Memory[..Constants.MaxPacketSize]);
|
||||
}
|
||||
}
|
||||
|
||||
PacketHeader coolHeader = new PacketHeader {
|
||||
Type = PacketType.Connect,
|
||||
Id = ownId
|
||||
};
|
||||
IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.RentZero(Constants.MaxPacketSize);
|
||||
MemoryMarshal.Write(owner.Memory.Span[..], ref coolHeader);
|
||||
ConnectPacket connect = new ConnectPacket {
|
||||
ConnectionType = ConnectionTypes.Reconnecting,
|
||||
ClientName = "Test Sanae"
|
||||
};
|
||||
connect.Serialize(owner.Memory.Span[Constants.HeaderSize..Constants.MaxPacketSize]);
|
||||
await stream.WriteAsync(owner.Memory);
|
||||
logger.Info("Connected");
|
||||
await S();
|
||||
// Guid otherId = Guid.Parse("d5feae62-2e71-1000-88fd-597ea147ae88");
|
||||
// Logger logger = new Logger("Client");
|
||||
// NetworkStream stream = client.GetStream();
|
||||
//
|
||||
// Vector3 basePoint = Vector3.Zero;
|
||||
//
|
||||
// PacketType[] reboundPackets = {
|
||||
// PacketType.Player,
|
||||
// PacketType.Cap,
|
||||
// PacketType.Capture,
|
||||
// PacketType.Costume,
|
||||
// PacketType.Tag,
|
||||
// PacketType.Shine
|
||||
// };
|
||||
//
|
||||
// string lastCapture = "";
|
||||
//
|
||||
// async Task S() {
|
||||
// IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(Constants.MaxPacketSize);
|
||||
// while (true) {
|
||||
// await stream.ReadAsync(owner.Memory);
|
||||
// PacketHeader header = MemoryMarshal.Read<PacketHeader>(owner.Memory.Span);
|
||||
// PacketType type = header.Type;
|
||||
// if (header.Id != otherId) continue;
|
||||
// if (type is PacketType.Player) {
|
||||
// // CapPacket cap = new CapPacket();
|
||||
// PlayerPacket playerPacket = new PlayerPacket();
|
||||
// playerPacket.Deserialize(owner.Memory.Span[Constants.HeaderSize..]);
|
||||
// logger.Info(playerPacket.Hack);
|
||||
// if (playerPacket.Hack != lastCapture) logger.Info($"Changed to hack: {lastCapture = playerPacket.Hack}");
|
||||
// // cap.Position = playerPacket.Position + Vector3.UnitY * 500f;
|
||||
// // cap.Rotation = Quaternion.CreateFromYawPitchRoll(0,0,0);
|
||||
// // cap.CapAnim = "StayR";
|
||||
// // playerPacket.Position = new Vector3(1000000f);
|
||||
// // playerPacket.ThrowingCap = true;
|
||||
// // header.Id = ownId;
|
||||
// // MemoryMarshal.Write(owner.Memory.Span, ref header);
|
||||
// // playerPacket.Serialize(owner.Memory.Span[Constants.HeaderSize..]);
|
||||
// // await stream.WriteAsync(owner.Memory[..Constants.MaxPacketSize]);
|
||||
// // header.Type = PacketType.Cap;
|
||||
// // MemoryMarshal.Write(owner.Memory.Span, ref header);
|
||||
// // cap.Serialize(owner.Memory.Span[Constants.HeaderSize..]);
|
||||
// // await stream.WriteAsync(owner.Memory[..Constants.MaxPacketSize]);
|
||||
// // continue;
|
||||
// }
|
||||
//
|
||||
// if (reboundPackets.All(x => x != type)) continue;
|
||||
// header.Id = ownId;
|
||||
// MemoryMarshal.Write(owner.Memory.Span, ref header);
|
||||
// await stream.WriteAsync(owner.Memory[..Constants.MaxPacketSize]);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// PacketHeader coolHeader = new PacketHeader {
|
||||
// Type = PacketType.Connect,
|
||||
// Id = ownId
|
||||
// };
|
||||
// IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.RentZero(Constants.MaxPacketSize);
|
||||
// MemoryMarshal.Write(owner.Memory.Span[..], ref coolHeader);
|
||||
// ConnectPacket connect = new ConnectPacket {
|
||||
// ConnectionType = ConnectionTypes.Reconnecting,
|
||||
// ClientName = "Test Sanae"
|
||||
// };
|
||||
// connect.Serialize(owner.Memory.Span[Constants.HeaderSize..Constants.MaxPacketSize]);
|
||||
// await stream.WriteAsync(owner.Memory);
|
||||
// logger.Info("Connected");
|
||||
// await S();
|
Loading…
Reference in a new issue