mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Merge remote-tracking branch 'origin/master' into feat/optimization
# Conflicts: # tools/run-creative-e2e.sh
This commit is contained in:
commit
22e987070a
106 changed files with 20031 additions and 3930 deletions
|
|
@ -411,6 +411,37 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
return ReadNextNbt(cache, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an ItemStackTemplate (26.1+) from a cache of bytes.
|
||||
/// Unlike ItemStack, this uses item-first encoding: item_id, count, DataComponentPatch.
|
||||
/// ItemStackTemplate is always non-empty (no count=0 sentinel).
|
||||
/// </summary>
|
||||
public Item ReadNextItemStackTemplate(Queue<byte> cache, ItemPalette itemPalette)
|
||||
{
|
||||
var itemId = ReadNextVarInt(cache);
|
||||
var itemCount = ReadNextVarInt(cache);
|
||||
var item = new Item(itemPalette.FromId(itemId), itemCount, null);
|
||||
|
||||
var numberOfComponentsToAdd = ReadNextVarInt(cache);
|
||||
var numberofComponentsToRemove = ReadNextVarInt(cache);
|
||||
var structuredComponentHandler = new StructuredComponentsHandler(protocolversion, this, itemPalette);
|
||||
var strcturedComponentsToAdd = new List<StructuredComponent>(numberOfComponentsToAdd);
|
||||
|
||||
for (var i = 0; i < numberOfComponentsToAdd; i++)
|
||||
{
|
||||
var componentTypeId = ReadNextVarInt(cache);
|
||||
strcturedComponentsToAdd.Add(structuredComponentHandler.Parse(componentTypeId, cache));
|
||||
}
|
||||
|
||||
for (var i = 0; i < numberofComponentsToRemove; i++)
|
||||
ReadNextVarInt(cache);
|
||||
|
||||
if (strcturedComponentsToAdd.Count > 0)
|
||||
item.Components = strcturedComponentsToAdd;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a single item slot from a cache of bytes and remove it from the cache
|
||||
/// </summary>
|
||||
|
|
@ -664,8 +695,10 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
}
|
||||
}
|
||||
|
||||
return new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch,
|
||||
var entity = new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch,
|
||||
data);
|
||||
entity.UUID = entityUUID;
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1021,20 +1054,44 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
}
|
||||
}
|
||||
|
||||
private static bool HasLpVec3Continuation(int firstByte) => (firstByte & 4) == 4;
|
||||
|
||||
private static double UnpackLpVec3(long packedAxis)
|
||||
{
|
||||
return Math.Min((double)(packedAxis & 32767L), 32766.0) * 2.0 / 32766.0 - 1.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+).
|
||||
/// Variable-length encoding: first byte 0 = zero vector; otherwise
|
||||
/// 2 bytes + 4 bytes (6 total), plus an optional VarInt continuation.
|
||||
/// Read and decode an LpVec3 (low-precision vec3) from the cache (1.21.9+).
|
||||
/// Returned vector is expressed in blocks per tick.
|
||||
/// </summary>
|
||||
public void ReadNextLpVec3(Queue<byte> cache)
|
||||
public (double X, double Y, double Z) ReadNextLpVec3Values(Queue<byte> cache)
|
||||
{
|
||||
int first = ReadNextByte(cache);
|
||||
if (first == 0)
|
||||
return;
|
||||
ReadNextByte(cache); // second byte
|
||||
ReadData(4, cache); // uint32
|
||||
if ((first & 4) == 4) // continuation bit set
|
||||
ReadNextVarInt(cache);
|
||||
return (0.0, 0.0, 0.0);
|
||||
|
||||
int second = ReadNextByte(cache);
|
||||
uint high = (uint)ReadNextInt(cache);
|
||||
long packed = ((long)high << 16) | (long)(second << 8) | (uint)first;
|
||||
|
||||
long scale = first & 3;
|
||||
if (HasLpVec3Continuation(first))
|
||||
scale |= ((long)ReadNextVarInt(cache) & 0xFFFFFFFFL) << 2;
|
||||
|
||||
return (
|
||||
UnpackLpVec3(packed >> 3) * scale,
|
||||
UnpackLpVec3(packed >> 18) * scale,
|
||||
UnpackLpVec3(packed >> 33) * scale
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+) and discard it.
|
||||
/// </summary>
|
||||
public void ReadNextLpVec3(Queue<byte> cache)
|
||||
{
|
||||
ReadNextLpVec3Values(cache);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1715,16 +1772,15 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
public byte[] GetLocation(Location location)
|
||||
{
|
||||
byte[] locationBytes;
|
||||
ulong x = (ulong)(int)Math.Floor(location.X) & 0x3FFFFFF;
|
||||
ulong y = (ulong)(int)Math.Floor(location.Y) & 0xFFF;
|
||||
ulong z = (ulong)(int)Math.Floor(location.Z) & 0x3FFFFFF;
|
||||
if (protocolversion >= Protocol18Handler.MC_1_14_Version)
|
||||
{
|
||||
locationBytes = BitConverter.GetBytes(((((ulong)location.X) & 0x3FFFFFF) << 38) |
|
||||
((((ulong)location.Z) & 0x3FFFFFF) << 12) |
|
||||
(((ulong)location.Y) & 0xFFF));
|
||||
locationBytes = BitConverter.GetBytes((x << 38) | (z << 12) | y);
|
||||
}
|
||||
else
|
||||
locationBytes = BitConverter.GetBytes(((((ulong)location.X) & 0x3FFFFFF) << 38) |
|
||||
((((ulong)location.Y) & 0xFFF) << 26) |
|
||||
(((ulong)location.Z) & 0x3FFFFFF));
|
||||
locationBytes = BitConverter.GetBytes((x << 38) | (y << 26) | z);
|
||||
|
||||
Array.Reverse(locationBytes); //Endianness
|
||||
return locationBytes;
|
||||
|
|
|
|||
|
|
@ -811,6 +811,11 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
return false; //Currently not implemented
|
||||
}
|
||||
|
||||
public bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll)
|
||||
{
|
||||
return false; //MC 1.8-1.12.1 recipe book not supported
|
||||
}
|
||||
|
||||
public bool SendCloseWindow(int windowId)
|
||||
{
|
||||
return false; //Currently not implemented
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -190,6 +190,15 @@ namespace MinecraftClient.Protocol
|
|||
|
||||
bool ClickContainerButton(int windowId, int buttonId);
|
||||
|
||||
/// <summary>
|
||||
/// Send a place recipe packet to the server for the active recipe book container.
|
||||
/// </summary>
|
||||
/// <param name="windowId">Id of the window being clicked</param>
|
||||
/// <param name="recipeId">Recipe identifier to craft</param>
|
||||
/// <param name="makeAll">True to craft as many items as possible</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll);
|
||||
|
||||
/// <summary>
|
||||
/// Plays animation
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -295,6 +295,16 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="onGround">TRUE if on ground</param>
|
||||
void OnEntityTeleport(int entityID, Double x, Double y, Double z, bool onGround);
|
||||
|
||||
/// <summary>
|
||||
/// Called when an entity velocity update packet is received.
|
||||
/// Velocity values are in blocks per tick.
|
||||
/// </summary>
|
||||
/// <param name="entityID">Entity ID</param>
|
||||
/// <param name="velocityX">Velocity X</param>
|
||||
/// <param name="velocityY">Velocity Y</param>
|
||||
/// <param name="velocityZ">Velocity Z</param>
|
||||
void OnEntityVelocity(int entityID, double velocityX, double velocityY, double velocityZ);
|
||||
|
||||
/// <summary>
|
||||
/// Called when additional properties have been received for an entity
|
||||
/// </summary>
|
||||
|
|
@ -371,6 +381,17 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="affectedBlocks">Amount of affected blocks</param>
|
||||
void OnExplosion(Location location, float strength, int affectedBlocks);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a sound packet is received.
|
||||
/// </summary>
|
||||
/// <param name="soundName">Sound key if available, otherwise null</param>
|
||||
/// <param name="location">Sound location for world sounds, or null if unavailable</param>
|
||||
/// <param name="category">Sound category id</param>
|
||||
/// <param name="volume">Sound volume</param>
|
||||
/// <param name="pitch">Sound pitch</param>
|
||||
/// <param name="entityID">Source entity id for entity-sound packets, if any</param>
|
||||
void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, int? entityID);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a player's game mode has changed
|
||||
/// </summary>
|
||||
|
|
@ -434,6 +455,19 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="factorCodec">factorCodec</param>
|
||||
void OnEntityEffect(int entityid, Effects effect, int amplifier, int duration, byte flags, bool hasFactorData, Dictionary<String, object>? factorCodec);
|
||||
|
||||
/// <summary>
|
||||
/// Called when an entity has an effect removed
|
||||
/// </summary>
|
||||
/// <param name="entityid">Entity ID</param>
|
||||
/// <param name="effect">Effect that was removed</param>
|
||||
void OnRemoveEntityEffect(int entityid, Effects effect);
|
||||
|
||||
/// <summary>
|
||||
/// Get the player's active effects
|
||||
/// </summary>
|
||||
/// <returns>Dictionary of active effects</returns>
|
||||
Dictionary<Effects, EffectData> GetPlayerEffects();
|
||||
|
||||
/// <summary>
|
||||
/// Called when Soreboard Objective
|
||||
/// </summary>
|
||||
|
|
@ -455,6 +489,23 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="numberFormat">Number format: 0 - blank, 1 - styled, 2 - fixed</param>
|
||||
void OnUpdateScore(string entityName, int action, string objectiveName, string objectiveDisplayName, int objectiveValue, int numberFormat);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Teams packet is received from the server.
|
||||
/// </summary>
|
||||
/// <param name="teamName">Internal team name (up to 16 chars)</param>
|
||||
/// <param name="method">0=create, 1=remove, 2=update, 3=add players, 4=remove players</param>
|
||||
/// <param name="displayName">Display name (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="friendlyFlags">Bit 0=allowFriendlyFire, bit 1=seeFriendlyInvisibles. Present when method is 0 or 2.</param>
|
||||
/// <param name="nameTagVisibility">Nametag visibility rule string. Present when method is 0 or 2.</param>
|
||||
/// <param name="collisionRule">Collision rule string. Present when method is 0 or 2.</param>
|
||||
/// <param name="color">ChatFormatting color value (-1=none). Present when method is 0 or 2.</param>
|
||||
/// <param name="prefix">Member name prefix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="suffix">Member name suffix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="players">Player/entity names. Present when method is 0, 3, or 4.</param>
|
||||
void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags,
|
||||
string nameTagVisibility, string collisionRule, int color,
|
||||
string prefix, string suffix, List<string> players);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the client received the Tab Header and Footer
|
||||
/// </summary>
|
||||
|
|
@ -504,6 +555,33 @@ namespace MinecraftClient.Protocol
|
|||
|
||||
public void SetCanSendMessage(bool canSendMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Called when recipe book recipes are added or replaced.
|
||||
/// </summary>
|
||||
/// <param name="recipes">Recipe entries to add</param>
|
||||
/// <param name="replace">True to replace the currently tracked recipe book entries</param>
|
||||
public void OnRecipeBookAdd(RecipeBookRecipeEntry[] recipes, bool replace);
|
||||
|
||||
/// <summary>
|
||||
/// Called when recipe book recipes are removed.
|
||||
/// </summary>
|
||||
/// <param name="recipeIds">Recipe identifiers to remove</param>
|
||||
public void OnRecipeBookRemove(string[] recipeIds);
|
||||
|
||||
/// <summary>
|
||||
/// Called when achievement/advancement data is received from the server.
|
||||
/// </summary>
|
||||
/// <param name="added">Achievements that were added or updated</param>
|
||||
/// <param name="removedIds">IDs of achievements that were removed</param>
|
||||
/// <param name="reset">True if all existing state should be cleared before applying</param>
|
||||
public void OnAchievementsUpdate(IReadOnlyList<Achievement> added, IReadOnlyList<string> removedIds, bool reset);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server selects an advancement tab.
|
||||
/// </summary>
|
||||
/// <param name="tabId">The tab identifier, or null if no tab is selected</param>
|
||||
public void OnSelectAdvancementTab(string? tabId);
|
||||
|
||||
/// <summary>
|
||||
/// Send a click container button packet to the server.
|
||||
/// Used for Enchanting table, Lectern, stone cutter and loom
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using DnsClient;
|
||||
using MinecraftClient.Protocol.Handlers;
|
||||
using MinecraftClient.Protocol.Handlers.Forge;
|
||||
|
|
@ -388,6 +389,61 @@ namespace MinecraftClient.Protocol
|
|||
}
|
||||
}
|
||||
|
||||
private static readonly Regex VersionTokenRegex = new(@"\d+\.\d+(?:\.\d+)?", RegexOptions.Compiled);
|
||||
|
||||
private static readonly int[] SupportedProtocols18 =
|
||||
[
|
||||
4, 5, 47, 107, 108, 109, 110, 210, 315, 316, 335, 338, 340, 393, 401, 404,
|
||||
477, 480, 485, 490, 498, 573, 575, 578, 735, 736, 751, 753, 754, 755, 756,
|
||||
757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771,
|
||||
772, 773, 774, 775
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// For multi-version servers (e.g. "Requires MC 1.8 / 1.21"), try to find the
|
||||
/// highest protocol version that both the server and MCC support.
|
||||
/// Returns true if the protocol was upgraded, with the new value in
|
||||
/// <paramref name="protocolVersion"/>.
|
||||
/// </summary>
|
||||
public static bool TryUpgradeProtocolVersion(string versionName, ref int protocolVersion)
|
||||
{
|
||||
if (string.IsNullOrEmpty(versionName))
|
||||
return false;
|
||||
|
||||
var matches = VersionTokenRegex.Matches(versionName);
|
||||
if (matches.Count < 2)
|
||||
return false;
|
||||
|
||||
int bestProtocol = protocolVersion;
|
||||
string bestVersion = "";
|
||||
|
||||
foreach (Match m in matches)
|
||||
{
|
||||
int proto = MCVer2ProtocolVersion(m.Value);
|
||||
if (proto <= 0)
|
||||
continue;
|
||||
if (Array.IndexOf(SupportedProtocols18, proto) < 0)
|
||||
continue;
|
||||
if (proto > bestProtocol)
|
||||
{
|
||||
bestProtocol = proto;
|
||||
bestVersion = m.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestProtocol > protocolVersion && bestVersion.Length > 0)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(
|
||||
Translations.mcc_server_info_version_upgrade,
|
||||
ProtocolVersion2MCVer(protocolVersion), protocolVersion,
|
||||
"§a" + bestVersion + "§8", bestProtocol));
|
||||
protocolVersion = bestProtocol;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a network protocol version number to human-readable Minecraft version number
|
||||
/// </summary>
|
||||
|
|
|
|||
119
MinecraftClient/Protocol/ServerStatusDisplay.cs
Normal file
119
MinecraftClient/Protocol/ServerStatusDisplay.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using MinecraftClient.Protocol.Message;
|
||||
using MinecraftClient.Scripting;
|
||||
|
||||
namespace MinecraftClient.Protocol
|
||||
{
|
||||
internal static class ServerStatusDisplay
|
||||
{
|
||||
private const int MaxSamplePlayers = 10;
|
||||
|
||||
internal static void Show(ServerStatusInfo info)
|
||||
{
|
||||
if (ConsoleIO.Backend is Tui.TuiConsoleBackend tuiBackend)
|
||||
ShowTui(info, tuiBackend);
|
||||
else
|
||||
ShowClassic(info);
|
||||
}
|
||||
|
||||
private static void ShowClassic(ServerStatusInfo info)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine();
|
||||
sb.Append("§8§m");
|
||||
sb.Append(new string('-', 50));
|
||||
sb.AppendLine("§r");
|
||||
|
||||
if (!string.IsNullOrEmpty(info.MotdRaw))
|
||||
{
|
||||
try
|
||||
{
|
||||
sb.AppendLine(ChatParser.ParseText(info.MotdRaw));
|
||||
}
|
||||
catch
|
||||
{
|
||||
sb.AppendLine(info.MotdRaw);
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("§f");
|
||||
sb.Append(Translations.mcc_server_info_label_server);
|
||||
sb.Append(" §b");
|
||||
sb.Append(info.Host);
|
||||
sb.Append("§7:§b");
|
||||
sb.AppendLine(info.Port.ToString());
|
||||
|
||||
sb.Append("§f");
|
||||
sb.Append(Translations.mcc_server_info_label_version);
|
||||
sb.Append(" §b");
|
||||
sb.Append(ChatBot.GetVerbatim(info.VersionName));
|
||||
sb.Append(" §7(");
|
||||
sb.Append(string.Format(Translations.mcc_server_info_label_protocol, "§e" + info.ProtocolVersion + "§7"));
|
||||
sb.AppendLine(")");
|
||||
|
||||
if (info.ResolvedProtocol != 0)
|
||||
{
|
||||
string resolvedMcVer = ProtocolHandler.ProtocolVersion2MCVer(info.ResolvedProtocol);
|
||||
sb.Append("§f");
|
||||
sb.Append(Translations.mcc_server_info_label_connecting_as);
|
||||
sb.Append(" §a");
|
||||
sb.Append(resolvedMcVer);
|
||||
sb.Append(" §7(");
|
||||
sb.Append(string.Format(Translations.mcc_server_info_label_protocol, "§a" + info.ResolvedProtocol + "§7"));
|
||||
sb.AppendLine(")");
|
||||
}
|
||||
|
||||
if (info.PingMs >= 0)
|
||||
{
|
||||
sb.Append("§f");
|
||||
sb.Append(Translations.mcc_server_info_label_ping);
|
||||
sb.Append(" §a");
|
||||
sb.AppendLine(string.Format(Translations.mcc_server_info_label_ping_ms, info.PingMs));
|
||||
}
|
||||
|
||||
sb.Append("§f");
|
||||
sb.Append(Translations.mcc_server_info_label_players);
|
||||
sb.Append(" §a");
|
||||
sb.Append(info.OnlinePlayers);
|
||||
sb.Append("§7/§c");
|
||||
sb.AppendLine(info.MaxPlayers.ToString());
|
||||
|
||||
if (info.SamplePlayers.Count > 0)
|
||||
{
|
||||
sb.Append("§f");
|
||||
sb.AppendLine(Translations.mcc_server_info_label_online);
|
||||
|
||||
int shown = Math.Min(info.SamplePlayers.Count, MaxSamplePlayers);
|
||||
for (int i = 0; i < shown; i++)
|
||||
sb.AppendLine($" §a{info.SamplePlayers[i].Name}");
|
||||
|
||||
if (info.SamplePlayers.Count > shown)
|
||||
sb.AppendLine($" §7{string.Format(Translations.mcc_server_info_sample_more, info.SamplePlayers.Count - shown)}");
|
||||
}
|
||||
|
||||
sb.Append("§8§m");
|
||||
sb.Append(new string('-', 50));
|
||||
sb.Append("§r");
|
||||
|
||||
ConsoleIO.WriteLineFormatted(sb.ToString(), acceptnewlines: true);
|
||||
}
|
||||
|
||||
private static void ShowTui(ServerStatusInfo info, Tui.TuiConsoleBackend backend)
|
||||
{
|
||||
var view = backend.GetView();
|
||||
if (view is null)
|
||||
{
|
||||
ShowClassic(info);
|
||||
return;
|
||||
}
|
||||
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var panel = Tui.ServerStatusPanelBuilder.Build(info);
|
||||
view.AppendControlToLog(panel);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
30
MinecraftClient/Protocol/ServerStatusInfo.cs
Normal file
30
MinecraftClient/Protocol/ServerStatusInfo.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds the structured result of a Minecraft server status (SLP) ping,
|
||||
/// including MOTD, player counts, sample player list, version, and favicon.
|
||||
/// </summary>
|
||||
public sealed class ServerStatusInfo
|
||||
{
|
||||
public string Host { get; init; } = string.Empty;
|
||||
public int Port { get; init; }
|
||||
public string VersionName { get; init; } = string.Empty;
|
||||
public int ProtocolVersion { get; init; }
|
||||
public int ResolvedProtocol { get; set; }
|
||||
public int OnlinePlayers { get; init; }
|
||||
public int MaxPlayers { get; init; }
|
||||
public List<SamplePlayer> SamplePlayers { get; init; } = [];
|
||||
public string MotdRaw { get; init; } = string.Empty;
|
||||
public string? FaviconBase64 { get; init; }
|
||||
public long PingMs { get; init; }
|
||||
|
||||
public sealed class SamplePlayer
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Id { get; init; } = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue