|
| 1 | +/* |
| 2 | + * Copyright (c) 2014-2025 Wurst-Imperium and contributors. |
| 3 | + * |
| 4 | + * This source code is subject to the terms of the GNU General Public |
| 5 | + * License, version 3. If a copy of the GPL was not distributed with this |
| 6 | + * file, You can obtain one at: https://www.gnu.org/licenses/gpl-3.0.txt |
| 7 | + */ |
| 8 | +package net.wurstclient.commands; |
| 9 | + |
| 10 | +import java.util.Arrays; |
| 11 | +import java.util.LinkedHashMap; |
| 12 | +import java.util.Locale; |
| 13 | +import java.util.Map; |
| 14 | +import java.util.Set; |
| 15 | +import java.util.UUID; |
| 16 | +import net.minecraft.core.BlockPos; |
| 17 | +import net.wurstclient.command.CmdError; |
| 18 | +import net.wurstclient.command.CmdException; |
| 19 | +import net.wurstclient.command.CmdSyntaxError; |
| 20 | +import net.wurstclient.command.Command; |
| 21 | +import net.wurstclient.util.ChatUtils; |
| 22 | +import net.wurstclient.waypoints.Waypoint; |
| 23 | +import net.wurstclient.waypoints.WaypointDimension; |
| 24 | + |
| 25 | +public final class WaypointCmd extends Command |
| 26 | +{ |
| 27 | + private static final Set<String> VALID_ICONS = |
| 28 | + Set.of("square", "circle", "triangle", "star", "diamond", "skull", |
| 29 | + "heart", "check", "x", "arrow_down", "sun", "snowflake"); |
| 30 | + |
| 31 | + public WaypointCmd() |
| 32 | + { |
| 33 | + super("waypoint", "Create waypoints via chat.", |
| 34 | + ".waypoint add <name> [x=<int>] [y=<int>] [z=<int>] [dim=<overworld|nether|end>]" |
| 35 | + + " [color=<#RRGGBB|#AARRGGBB>] [icon=<" |
| 36 | + + String.join("|", VALID_ICONS) |
| 37 | + + ">] [visible=<true|false>] [lines=<true|false>]" |
| 38 | + + " [opposite=<true|false>] [beacon=<off|solid|esp>]" |
| 39 | + + " [action=<disabled|hide|delete>] [actiondist=<int>]" |
| 40 | + + " [maxvisible=<int>] [scale=<decimal>]"); |
| 41 | + } |
| 42 | + |
| 43 | + @Override |
| 44 | + public void call(String[] args) throws CmdException |
| 45 | + { |
| 46 | + if(args.length == 0) |
| 47 | + throw new CmdSyntaxError("Missing subcommand."); |
| 48 | + |
| 49 | + String sub = args[0].toLowerCase(Locale.ROOT); |
| 50 | + switch(sub) |
| 51 | + { |
| 52 | + case "add": |
| 53 | + handleAdd(Arrays.copyOfRange(args, 1, args.length)); |
| 54 | + break; |
| 55 | + |
| 56 | + default: |
| 57 | + throw new CmdSyntaxError("Unknown subcommand: " + sub); |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + private void handleAdd(String[] args) throws CmdException |
| 62 | + { |
| 63 | + if(args.length == 0) |
| 64 | + throw new CmdSyntaxError("Missing waypoint name."); |
| 65 | + |
| 66 | + Map<String, String> options = new LinkedHashMap<>(); |
| 67 | + String name = null; |
| 68 | + for(String arg : args) |
| 69 | + { |
| 70 | + int eq = arg.indexOf('='); |
| 71 | + if(eq < 0) |
| 72 | + { |
| 73 | + if(name != null) |
| 74 | + throw new CmdError("Unexpected argument \"" + arg + "\"."); |
| 75 | + name = arg; |
| 76 | + continue; |
| 77 | + } |
| 78 | + |
| 79 | + String key = arg.substring(0, eq).toLowerCase(Locale.ROOT); |
| 80 | + String value = arg.substring(eq + 1); |
| 81 | + options.put(key, value); |
| 82 | + } |
| 83 | + |
| 84 | + if(options.containsKey("name")) |
| 85 | + name = options.remove("name"); |
| 86 | + |
| 87 | + if(name == null || name.isEmpty()) |
| 88 | + throw new CmdError("Waypoint name is required."); |
| 89 | + name = name.replace('_', ' '); |
| 90 | + |
| 91 | + BlockPos playerPos = MC.player == null ? null |
| 92 | + : BlockPos.containing(MC.player.position()); |
| 93 | + Integer px = playerPos == null ? null : playerPos.getX(); |
| 94 | + Integer py = playerPos == null ? null : playerPos.getY(); |
| 95 | + Integer pz = playerPos == null ? null : playerPos.getZ(); |
| 96 | + |
| 97 | + int x = parseInt(options.remove("x"), "x", px); |
| 98 | + int y = parseInt(options.remove("y"), "y", py); |
| 99 | + int z = parseInt(options.remove("z"), "z", pz); |
| 100 | + |
| 101 | + WaypointDimension dim = parseDimension(options.remove("dim")); |
| 102 | + int color = parseColor(options.remove("color")); |
| 103 | + String icon = parseIcon(options.remove("icon")); |
| 104 | + boolean visible = |
| 105 | + parseBoolean(options.remove("visible"), true, "visible"); |
| 106 | + boolean lines = parseBoolean(options.remove("lines"), false, "lines"); |
| 107 | + boolean opposite = |
| 108 | + parseBoolean(options.remove("opposite"), false, "opposite"); |
| 109 | + Waypoint.BeaconMode beacon = parseBeaconMode(options.remove("beacon")); |
| 110 | + Waypoint.ActionWhenNear action = parseAction(options.remove("action")); |
| 111 | + int actionDistance = |
| 112 | + parsePositiveInt(options.remove("actiondist"), "actiondist", 8, 1); |
| 113 | + int maxVisible = parsePositiveInt(options.remove("maxvisible"), |
| 114 | + "maxvisible", 5000, 0); |
| 115 | + double scale = parseScale(options.remove("scale")); |
| 116 | + |
| 117 | + if(!options.isEmpty()) |
| 118 | + { |
| 119 | + String unknown = options.keySet().iterator().next(); |
| 120 | + throw new CmdError("Unknown option \"" + unknown + "\"."); |
| 121 | + } |
| 122 | + |
| 123 | + Waypoint waypoint = |
| 124 | + new Waypoint(UUID.randomUUID(), System.currentTimeMillis()); |
| 125 | + waypoint.setName(name); |
| 126 | + waypoint.setPos(new BlockPos(x, y, z)); |
| 127 | + waypoint.setDimension(dim); |
| 128 | + waypoint.setColor(color); |
| 129 | + waypoint.setIcon(icon); |
| 130 | + waypoint.setVisible(visible); |
| 131 | + waypoint.setLines(lines); |
| 132 | + waypoint.setOpposite(opposite); |
| 133 | + waypoint.setBeaconMode(beacon); |
| 134 | + waypoint.setActionWhenNear(action); |
| 135 | + waypoint.setActionWhenNearDistance(actionDistance); |
| 136 | + waypoint.setMaxVisible(maxVisible); |
| 137 | + waypoint.setScale(scale); |
| 138 | + |
| 139 | + WURST.getHax().waypointsHack.addWaypointFromCommand(waypoint); |
| 140 | + ChatUtils |
| 141 | + .message(String.format("Added waypoint \"%s\" at %d, %d, %d in %s.", |
| 142 | + name, x, y, z, dim.name())); |
| 143 | + } |
| 144 | + |
| 145 | + private int parseInt(String raw, String key, Integer fallback) |
| 146 | + throws CmdException |
| 147 | + { |
| 148 | + if(raw != null) |
| 149 | + return parseMandatoryInt(raw, key); |
| 150 | + if(fallback != null) |
| 151 | + return fallback; |
| 152 | + throw new CmdError("Missing value for " + key + "."); |
| 153 | + } |
| 154 | + |
| 155 | + private int parseMandatoryInt(String raw, String key) throws CmdException |
| 156 | + { |
| 157 | + try |
| 158 | + { |
| 159 | + return Integer.parseInt(raw); |
| 160 | + }catch(NumberFormatException e) |
| 161 | + { |
| 162 | + throw new CmdError("Invalid integer for " + key + ": " + raw); |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + private int parsePositiveInt(String raw, String key, int fallback, int min) |
| 167 | + throws CmdException |
| 168 | + { |
| 169 | + if(raw == null) |
| 170 | + return Math.max(min, fallback); |
| 171 | + int value = parseMandatoryInt(raw, key); |
| 172 | + if(value < min) |
| 173 | + throw new CmdError(key + " must be >= " + min + "."); |
| 174 | + return value; |
| 175 | + } |
| 176 | + |
| 177 | + private double parseScale(String raw) throws CmdException |
| 178 | + { |
| 179 | + if(raw == null) |
| 180 | + return 1.5; |
| 181 | + try |
| 182 | + { |
| 183 | + double value = Double.parseDouble(raw); |
| 184 | + return Math.max(0.1, Math.min(10.0, value)); |
| 185 | + }catch(NumberFormatException e) |
| 186 | + { |
| 187 | + throw new CmdError("Invalid scale: " + raw); |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + private boolean parseBoolean(String raw, boolean fallback, String key) |
| 192 | + throws CmdException |
| 193 | + { |
| 194 | + if(raw == null) |
| 195 | + return fallback; |
| 196 | + String value = raw.toLowerCase(Locale.ROOT); |
| 197 | + switch(value) |
| 198 | + { |
| 199 | + case "true": |
| 200 | + case "1": |
| 201 | + case "yes": |
| 202 | + case "on": |
| 203 | + return true; |
| 204 | + |
| 205 | + case "false": |
| 206 | + case "0": |
| 207 | + case "no": |
| 208 | + case "off": |
| 209 | + return false; |
| 210 | + |
| 211 | + default: |
| 212 | + throw new CmdError("Invalid boolean for " + key + ": " + raw); |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + private Waypoint.BeaconMode parseBeaconMode(String raw) throws CmdException |
| 217 | + { |
| 218 | + if(raw == null) |
| 219 | + return Waypoint.BeaconMode.OFF; |
| 220 | + String value = raw.toLowerCase(Locale.ROOT); |
| 221 | + return switch(value) |
| 222 | + { |
| 223 | + case "off", "none", "false", "disabled" -> Waypoint.BeaconMode.OFF; |
| 224 | + case "solid", "on" -> Waypoint.BeaconMode.SOLID; |
| 225 | + case "esp", "beam", "true" -> Waypoint.BeaconMode.ESP; |
| 226 | + default -> throw new CmdError("Unknown beacon mode: " + raw); |
| 227 | + }; |
| 228 | + } |
| 229 | + |
| 230 | + private Waypoint.ActionWhenNear parseAction(String raw) throws CmdException |
| 231 | + { |
| 232 | + if(raw == null || raw.isEmpty()) |
| 233 | + return Waypoint.ActionWhenNear.DISABLED; |
| 234 | + String v = raw.toUpperCase(Locale.ROOT); |
| 235 | + try |
| 236 | + { |
| 237 | + return Waypoint.ActionWhenNear.valueOf(v); |
| 238 | + }catch(IllegalArgumentException e) |
| 239 | + { |
| 240 | + throw new CmdError("Unknown action: " + raw); |
| 241 | + } |
| 242 | + } |
| 243 | + |
| 244 | + private String parseIcon(String raw) throws CmdException |
| 245 | + { |
| 246 | + if(raw == null || raw.isEmpty()) |
| 247 | + return "star"; |
| 248 | + String value = raw.toLowerCase(Locale.ROOT); |
| 249 | + if(!VALID_ICONS.contains(value)) |
| 250 | + throw new CmdError("Unknown icon: " + raw); |
| 251 | + return value; |
| 252 | + } |
| 253 | + |
| 254 | + private int parseColor(String raw) throws CmdException |
| 255 | + { |
| 256 | + if(raw == null || raw.isEmpty()) |
| 257 | + return 0xFFFFFFFF; |
| 258 | + String value = raw.trim(); |
| 259 | + if(value.startsWith("#")) |
| 260 | + value = value.substring(1); |
| 261 | + if(value.startsWith("0x") || value.startsWith("0X")) |
| 262 | + value = value.substring(2); |
| 263 | + if(value.length() != 6 && value.length() != 8) |
| 264 | + throw new CmdError("Color must be RRGGBB or AARRGGBB."); |
| 265 | + try |
| 266 | + { |
| 267 | + int color = (int)Long.parseLong(value, 16); |
| 268 | + if(value.length() == 6) |
| 269 | + color |= 0xFF000000; |
| 270 | + return color; |
| 271 | + }catch(NumberFormatException e) |
| 272 | + { |
| 273 | + throw new CmdError("Invalid color: " + raw); |
| 274 | + } |
| 275 | + } |
| 276 | + |
| 277 | + private WaypointDimension parseDimension(String raw) throws CmdException |
| 278 | + { |
| 279 | + if(raw == null || raw.isEmpty()) |
| 280 | + return currentDimension(); |
| 281 | + String v = raw.toLowerCase(Locale.ROOT); |
| 282 | + return switch(v) |
| 283 | + { |
| 284 | + case "overworld", "over", "ow" -> WaypointDimension.OVERWORLD; |
| 285 | + case "nether", "hell" -> WaypointDimension.NETHER; |
| 286 | + case "end", "the_end" -> WaypointDimension.END; |
| 287 | + default -> throw new CmdError("Unknown dimension: " + raw); |
| 288 | + }; |
| 289 | + } |
| 290 | + |
| 291 | + private WaypointDimension currentDimension() |
| 292 | + { |
| 293 | + if(MC.level == null) |
| 294 | + return WaypointDimension.OVERWORLD; |
| 295 | + String key = MC.level.dimension().location().getPath(); |
| 296 | + return switch(key) |
| 297 | + { |
| 298 | + case "the_nether" -> WaypointDimension.NETHER; |
| 299 | + case "the_end" -> WaypointDimension.END; |
| 300 | + default -> WaypointDimension.OVERWORLD; |
| 301 | + }; |
| 302 | + } |
| 303 | +} |
0 commit comments