mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 17:47:32 +00:00
Better Filename Handling for Abilities, Dash Directional Improvements, Fixed Duplicate Ability Name Handling
This commit is contained in:
1 parent
71dbf77def
commit
3de8cd49b6
23 files changed
+752
-178
No files matched your search
@@ -11,6 +11,7 @@ import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import kamkeel.npcs.controllers.data.ability.data.IAbilityAction;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.IEffectAction;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ChainedAbility;
|
||||
import kamkeel.npcs.controllers.data.ability.data.entry.ChainedAbilityEntry;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.extender.IAbilityExtender;
|
||||
@@ -45,6 +46,7 @@ import noppes.npcs.api.handler.IAbilityHandler;
|
||||
import noppes.npcs.controllers.PlayerDataController;
|
||||
import noppes.npcs.controllers.data.PlayerData;
|
||||
import noppes.npcs.util.NBTJsonUtil;
|
||||
import kamkeel.npcs.util.FileNameHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
@@ -213,6 +215,7 @@ public class AbilityController implements IAbilityHandler {
|
||||
customAbilities.clear();
|
||||
customAbilitiesById.clear();
|
||||
int migrated = 0;
|
||||
Map<String, String> migratedAbilityNames = new HashMap<String, String>();
|
||||
|
||||
File dir = getDir();
|
||||
File[] files = dir.exists() ? dir.listFiles() : null;
|
||||
@@ -221,7 +224,7 @@ public class AbilityController implements IAbilityHandler {
|
||||
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
|
||||
try {
|
||||
String filename = file.getName();
|
||||
String key = filename.substring(0, filename.length() - 5);
|
||||
String fileKey = filename.substring(0, filename.length() - 5);
|
||||
|
||||
NBTTagCompound nbt = NBTJsonUtil.LoadFile(file);
|
||||
Ability ability = fromNBT(nbt);
|
||||
@@ -234,48 +237,55 @@ public class AbilityController implements IAbilityHandler {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Legacy migration: UUID-named files get renamed to name-based
|
||||
if (UUID_PATTERN.matcher(key).matches()) {
|
||||
String name = ability.getName();
|
||||
if (name == null || name.isEmpty()) {
|
||||
name = key; // Fallback to UUID if no name stored
|
||||
}
|
||||
String storedName = ability.getName();
|
||||
String preferredName;
|
||||
if (UUID_PATTERN.matcher(fileKey).matches()) {
|
||||
preferredName = (storedName != null && !storedName.isEmpty()) ? storedName : fileKey;
|
||||
} else {
|
||||
preferredName = fileKey;
|
||||
}
|
||||
String name = FileNameHelper.sanitizeName(preferredName, "Ability");
|
||||
name = makeUniqueNameForLoad(customAbilities, dir, file, name, fileKey);
|
||||
|
||||
// Ensure unique name
|
||||
while (hasCustomAbilityName(name) || nameFileExists(dir, name)) {
|
||||
name = name + "_";
|
||||
}
|
||||
ability.setName(name);
|
||||
|
||||
// Rename file from UUID.json to name.json
|
||||
File newFile = new File(dir, name + ".json");
|
||||
if (file.renameTo(newFile)) {
|
||||
LogWriter.info("Migrated ability file: " + key + ".json -> " + name + ".json");
|
||||
boolean dirty = false;
|
||||
if (!name.equals(fileKey)) {
|
||||
File renamedFile = new File(dir, name + ".json");
|
||||
if (file.renameTo(renamedFile)) {
|
||||
LogWriter.info("Migrated ability file: " + fileKey + ".json -> " + name + ".json");
|
||||
file = renamedFile;
|
||||
migrated++;
|
||||
dirty = true;
|
||||
} else {
|
||||
LogWriter.error("Failed to rename ability file: " + key + ".json -> " + name + ".json");
|
||||
LogWriter.error("Failed to rename ability file: " + fileKey + ".json -> " + name + ".json");
|
||||
}
|
||||
}
|
||||
|
||||
key = name;
|
||||
migrated++;
|
||||
ability.setName(name);
|
||||
if (storedName != null && !storedName.equals(name)) {
|
||||
dirty = true;
|
||||
if (!storedName.isEmpty()) {
|
||||
migratedAbilityNames.put(storedName, name);
|
||||
}
|
||||
}
|
||||
if (!fileKey.equals(name)) {
|
||||
migratedAbilityNames.put(fileKey, name);
|
||||
}
|
||||
|
||||
// Ensure ability has a UUID; generate one for legacy abilities
|
||||
String uuid = ability.getId();
|
||||
if (uuid == null || uuid.isEmpty() || uuid.equals(key)) {
|
||||
if (uuid == null || uuid.isEmpty() || uuid.equals(fileKey) || uuid.equals(name)) {
|
||||
// Legacy: id was the name or empty — generate a real UUID
|
||||
uuid = UUID.randomUUID().toString();
|
||||
ability.setId(uuid);
|
||||
// Re-save file with the new UUID
|
||||
try {
|
||||
NBTJsonUtil.SaveFile(file, ability.writeNBT());
|
||||
} catch (Exception ex) {
|
||||
LogWriter.error("Failed to save UUID for ability: " + key, ex);
|
||||
}
|
||||
migrated++;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
ability.setName(key);
|
||||
customAbilities.put(key, ability);
|
||||
if (dirty) {
|
||||
NBTJsonUtil.SaveFile(file, ability.writeNBT());
|
||||
}
|
||||
|
||||
customAbilities.put(name, ability);
|
||||
customAbilitiesById.put(uuid, ability);
|
||||
} catch (Exception e) {
|
||||
LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e);
|
||||
@@ -285,23 +295,68 @@ public class AbilityController implements IAbilityHandler {
|
||||
|
||||
customAbilityRevision++;
|
||||
if (migrated > 0) {
|
||||
LogWriter.info("Migrated " + migrated + " abilities (UUID-named files or missing UUIDs)");
|
||||
LogWriter.info("Migrated " + migrated + " abilities (filename normalization / UUID repair)");
|
||||
}
|
||||
LogWriter.info("Loaded " + customAbilities.size() + " custom abilities");
|
||||
|
||||
loadChainedAbilities();
|
||||
loadChainedAbilities(migratedAbilityNames);
|
||||
}
|
||||
|
||||
private boolean nameFileExists(File dir, String name) {
|
||||
return new File(dir, name + ".json").exists();
|
||||
private boolean nameFileExists(File dir, String name, File ignoredFile) {
|
||||
File existing = new File(dir, name + ".json");
|
||||
if (!existing.exists()) return false;
|
||||
return ignoredFile == null || !existing.getAbsolutePath().equals(ignoredFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
private String makeUniqueNameForLoad(Map<String, ?> loadedMap, File dir, File currentFile, String baseName, String selfKey) {
|
||||
String candidate = baseName;
|
||||
while ((loadedMap.containsKey(candidate) && !candidate.equals(selfKey))
|
||||
|| nameFileExists(dir, candidate, currentFile)) {
|
||||
candidate = candidate + "_";
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private String makeUniqueNameForMap(Map<String, ?> map, String baseName, String selfName) {
|
||||
String candidate = baseName;
|
||||
while (map.containsKey(candidate) && (selfName == null || !candidate.equals(selfName))) {
|
||||
candidate = candidate + "_";
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private boolean remapChainEntryReferences(ChainedAbility chain, Map<String, String> migratedAbilityNames) {
|
||||
if (chain == null || migratedAbilityNames == null || migratedAbilityNames.isEmpty()) return false;
|
||||
boolean changed = false;
|
||||
for (ChainedAbilityEntry entry : chain.getEntries()) {
|
||||
if (entry == null || !entry.isReference()) continue;
|
||||
String ref = entry.getAbilityReference();
|
||||
if (ref == null || ref.isEmpty()) continue;
|
||||
|
||||
String mapped = migratedAbilityNames.get(ref);
|
||||
if (mapped == null) {
|
||||
for (Map.Entry<String, String> e : migratedAbilityNames.entrySet()) {
|
||||
if (e.getKey().equalsIgnoreCase(ref)) {
|
||||
mapped = e.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mapped != null && !mapped.equals(ref)) {
|
||||
entry.setAbilityReference(mapped);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public boolean saveCustomAbility(Ability ability) {
|
||||
if (ability == null) return false;
|
||||
if (ability.isBuiltIn()) return false;
|
||||
|
||||
String name = ability.getName();
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
String name = FileNameHelper.sanitizeName(ability.getName(), "Ability");
|
||||
ability.setName(name);
|
||||
|
||||
String uuid = ability.getId();
|
||||
boolean isNew = (uuid == null || uuid.isEmpty());
|
||||
@@ -312,26 +367,17 @@ public class AbilityController implements IAbilityHandler {
|
||||
ability.setId(uuid);
|
||||
|
||||
// Ensure unique name
|
||||
while (hasCustomAbilityName(name)) {
|
||||
name = name + "_";
|
||||
}
|
||||
name = makeUniqueNameForMap(customAbilities, name, null);
|
||||
ability.setName(name);
|
||||
} else {
|
||||
// Existing ability — check for rename via UUID lookup
|
||||
Ability existing = customAbilitiesById.get(uuid);
|
||||
if (existing != null) {
|
||||
String oldName = existing.getName();
|
||||
if (!oldName.equals(name)) {
|
||||
// Name changed! Ensure new name is unique (excluding self)
|
||||
String testName = name;
|
||||
while (customAbilities.containsKey(testName) && !testName.equals(oldName)) {
|
||||
testName = testName + "_";
|
||||
}
|
||||
if (!testName.equals(name)) {
|
||||
name = testName;
|
||||
ability.setName(name);
|
||||
}
|
||||
name = makeUniqueNameForMap(customAbilities, name, oldName);
|
||||
ability.setName(name);
|
||||
|
||||
if (!oldName.equals(name)) {
|
||||
// Delete old file and remove old map entry
|
||||
File oldFile = new File(getDir(), oldName + ".json");
|
||||
if (oldFile.exists()) {
|
||||
@@ -339,6 +385,9 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
customAbilities.remove(oldName);
|
||||
}
|
||||
} else {
|
||||
name = makeUniqueNameForMap(customAbilities, name, null);
|
||||
ability.setName(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +423,15 @@ public class AbilityController implements IAbilityHandler {
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
|
||||
Ability removed = customAbilities.remove(name);
|
||||
if (removed == null) {
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
if (!sanitized.equals(name)) {
|
||||
removed = customAbilities.remove(sanitized);
|
||||
if (removed != null) {
|
||||
name = sanitized;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removed == null) return false;
|
||||
|
||||
// Also remove from UUID index
|
||||
@@ -412,7 +470,13 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
|
||||
public Ability getCustomAbility(String name) {
|
||||
return customAbilities.get(name);
|
||||
Ability ability = customAbilities.get(name);
|
||||
if (ability != null) return ability;
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
if (!sanitized.equals(name)) {
|
||||
return customAbilities.get(sanitized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Set<String> getCustomAbilityNames() {
|
||||
@@ -452,9 +516,10 @@ public class AbilityController implements IAbilityHandler {
|
||||
// CHAINED ABILITIES
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
private void loadChainedAbilities() {
|
||||
private void loadChainedAbilities(Map<String, String> migratedAbilityNames) {
|
||||
chainedAbilities.clear();
|
||||
chainedAbilitiesById.clear();
|
||||
int migrated = 0;
|
||||
|
||||
File dir = getChainedDir();
|
||||
File[] files = dir.exists() ? dir.listFiles() : null;
|
||||
@@ -463,19 +528,51 @@ public class AbilityController implements IAbilityHandler {
|
||||
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
|
||||
try {
|
||||
String filename = file.getName();
|
||||
String key = filename.substring(0, filename.length() - 5);
|
||||
String fileKey = filename.substring(0, filename.length() - 5);
|
||||
|
||||
NBTTagCompound nbt = NBTJsonUtil.LoadFile(file);
|
||||
ChainedAbility chain = new ChainedAbility();
|
||||
chain.readNBT(nbt);
|
||||
|
||||
chain.setName(key);
|
||||
chainedAbilities.put(key, chain);
|
||||
String storedName = chain.getName();
|
||||
String name = FileNameHelper.sanitizeName(fileKey, "Chain");
|
||||
name = makeUniqueNameForLoad(chainedAbilities, dir, file, name, fileKey);
|
||||
|
||||
boolean dirty = false;
|
||||
if (!name.equals(fileKey)) {
|
||||
File renamedFile = new File(dir, name + ".json");
|
||||
if (file.renameTo(renamedFile)) {
|
||||
LogWriter.info("Migrated chained ability file: " + fileKey + ".json -> " + name + ".json");
|
||||
file = renamedFile;
|
||||
dirty = true;
|
||||
} else {
|
||||
LogWriter.error("Failed to rename chained ability file: " + fileKey + ".json -> " + name + ".json");
|
||||
}
|
||||
}
|
||||
|
||||
chain.setName(name);
|
||||
if (storedName != null && !storedName.equals(name)) {
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (remapChainEntryReferences(chain, migratedAbilityNames)) {
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
String uuid = chain.getId();
|
||||
if (uuid != null && !uuid.isEmpty()) {
|
||||
chainedAbilitiesById.put(uuid, chain);
|
||||
if (uuid == null || uuid.isEmpty() || uuid.equals(fileKey) || uuid.equals(name)) {
|
||||
uuid = UUID.randomUUID().toString();
|
||||
chain.setId(uuid);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (dirty) {
|
||||
NBTJsonUtil.SaveFile(file, chain.writeNBT());
|
||||
migrated++;
|
||||
}
|
||||
|
||||
chainedAbilities.put(name, chain);
|
||||
chainedAbilitiesById.put(uuid, chain);
|
||||
} catch (Exception e) {
|
||||
LogWriter.error("Error loading chained ability: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
@@ -483,14 +580,17 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
|
||||
chainedAbilityRevision++;
|
||||
if (migrated > 0) {
|
||||
LogWriter.info("Migrated " + migrated + " chained abilities");
|
||||
}
|
||||
LogWriter.info("Loaded " + chainedAbilities.size() + " chained abilities");
|
||||
}
|
||||
|
||||
public boolean saveChainedAbility(ChainedAbility chain) {
|
||||
if (chain == null) return false;
|
||||
|
||||
String name = chain.getName();
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
String name = FileNameHelper.sanitizeName(chain.getName(), "Chain");
|
||||
chain.setName(name);
|
||||
|
||||
String uuid = chain.getId();
|
||||
boolean isNew = (uuid == null || uuid.isEmpty());
|
||||
@@ -499,30 +599,25 @@ public class AbilityController implements IAbilityHandler {
|
||||
uuid = UUID.randomUUID().toString();
|
||||
chain.setId(uuid);
|
||||
|
||||
while (hasChainedAbilityName(name)) {
|
||||
name = name + "_";
|
||||
}
|
||||
name = makeUniqueNameForMap(chainedAbilities, name, null);
|
||||
chain.setName(name);
|
||||
} else {
|
||||
ChainedAbility existing = chainedAbilitiesById.get(uuid);
|
||||
if (existing != null) {
|
||||
String oldName = existing.getName();
|
||||
if (!oldName.equals(name)) {
|
||||
String testName = name;
|
||||
while (chainedAbilities.containsKey(testName) && !testName.equals(oldName)) {
|
||||
testName = testName + "_";
|
||||
}
|
||||
if (!testName.equals(name)) {
|
||||
name = testName;
|
||||
chain.setName(name);
|
||||
}
|
||||
name = makeUniqueNameForMap(chainedAbilities, name, oldName);
|
||||
chain.setName(name);
|
||||
|
||||
if (!oldName.equals(name)) {
|
||||
File oldFile = new File(getChainedDir(), oldName + ".json");
|
||||
if (oldFile.exists()) {
|
||||
oldFile.delete();
|
||||
}
|
||||
chainedAbilities.remove(oldName);
|
||||
}
|
||||
} else {
|
||||
name = makeUniqueNameForMap(chainedAbilities, name, null);
|
||||
chain.setName(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,6 +653,15 @@ public class AbilityController implements IAbilityHandler {
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
|
||||
ChainedAbility removed = chainedAbilities.remove(name);
|
||||
if (removed == null) {
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
if (!sanitized.equals(name)) {
|
||||
removed = chainedAbilities.remove(sanitized);
|
||||
if (removed != null) {
|
||||
name = sanitized;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removed == null) return false;
|
||||
|
||||
String uuid = removed.getId();
|
||||
@@ -608,6 +712,12 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
}
|
||||
|
||||
String sanitizedKey = FileNameHelper.sanitizeTextInput(key);
|
||||
if (!sanitizedKey.equals(key)) {
|
||||
ChainedAbility sanitized = chainedAbilities.get(sanitizedKey);
|
||||
if (sanitized != null) return sanitized.deepCopy();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -618,12 +728,20 @@ public class AbilityController implements IAbilityHandler {
|
||||
for (String name : chainedAbilities.keySet()) {
|
||||
if (name.equalsIgnoreCase(key)) return true;
|
||||
}
|
||||
String sanitizedKey = FileNameHelper.sanitizeTextInput(key);
|
||||
if (!sanitizedKey.equals(key) && chainedAbilities.containsKey(sanitizedKey)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChainedAbility getChainedAbility(String name) {
|
||||
ChainedAbility chain = chainedAbilities.get(name);
|
||||
if (chain == null) {
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
if (!sanitized.equals(name)) {
|
||||
chain = chainedAbilities.get(sanitized);
|
||||
}
|
||||
}
|
||||
return chain != null ? chain.deepCopy() : null;
|
||||
}
|
||||
|
||||
@@ -646,7 +764,9 @@ public class AbilityController implements IAbilityHandler {
|
||||
|
||||
@Override
|
||||
public boolean hasChainedAbilityName(String name) {
|
||||
return chainedAbilities.containsKey(name);
|
||||
if (chainedAbilities.containsKey(name)) return true;
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
return !sanitized.equals(name) && chainedAbilities.containsKey(sanitized);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -734,6 +854,12 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
}
|
||||
|
||||
String sanitizedKey = FileNameHelper.sanitizeTextInput(key);
|
||||
if (!sanitizedKey.equals(key)) {
|
||||
Ability sanitized = customAbilities.get(sanitizedKey);
|
||||
if (sanitized != null) return sanitized.deepCopy();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -778,8 +904,14 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
|
||||
public boolean hasAbility(String key) {
|
||||
return builtAbilities.containsKey(key) || customAbilities.containsKey(key)
|
||||
|| customAbilitiesById.containsKey(key);
|
||||
if (builtAbilities.containsKey(key) || customAbilities.containsKey(key)
|
||||
|| customAbilitiesById.containsKey(key)) {
|
||||
return true;
|
||||
}
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(key);
|
||||
return !sanitized.equals(key)
|
||||
&& (builtAbilities.containsKey(sanitized) || customAbilities.containsKey(sanitized)
|
||||
|| customAbilitiesById.containsKey(sanitized));
|
||||
}
|
||||
|
||||
public Ability getCustomAbilityByUUID(String uuid) {
|
||||
@@ -787,7 +919,13 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
|
||||
public Ability getCustomAbilityByName(String name) {
|
||||
return customAbilities.get(name);
|
||||
Ability ability = customAbilities.get(name);
|
||||
if (ability != null) return ability;
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
if (!sanitized.equals(name)) {
|
||||
return customAbilities.get(sanitized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -821,6 +959,9 @@ public class AbilityController implements IAbilityHandler {
|
||||
if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) return true;
|
||||
}
|
||||
|
||||
String sanitizedKey = FileNameHelper.sanitizeTextInput(key);
|
||||
if (!sanitizedKey.equals(key) && customAbilities.containsKey(sanitizedKey)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1128,7 +1269,9 @@ public class AbilityController implements IAbilityHandler {
|
||||
|
||||
@Override
|
||||
public boolean hasCustomAbilityName(String name) {
|
||||
return customAbilities.containsKey(name);
|
||||
if (customAbilities.containsKey(name)) return true;
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
return !sanitized.equals(name) && customAbilities.containsKey(sanitized);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -7,6 +7,7 @@ import kamkeel.npcs.controllers.data.ability.data.AbilityIconData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.IAbilityAction;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.InvulnerableMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
|
||||
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
|
||||
@@ -21,6 +22,7 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityAbilityBarrier;
|
||||
import kamkeel.npcs.entity.EntityAbilityDome;
|
||||
import kamkeel.npcs.entity.EntityAbilityPanel;
|
||||
import kamkeel.npcs.util.FileNameHelper;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
@@ -85,6 +87,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
|
||||
// Interruption
|
||||
protected boolean interruptible = true;
|
||||
protected InvulnerableMode invulnerableMode = InvulnerableMode.NONE;
|
||||
|
||||
// Feedback
|
||||
protected LockMode lockMovement = LockMode.WINDUP;
|
||||
@@ -687,10 +690,24 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
).tab("General"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible)
|
||||
.hover("ability.hover.interruptible"),
|
||||
.hover("ability.hover.interruptible")
|
||||
.enabledWhen(() -> !invulnerableMode.invulnerableDuringWindup()),
|
||||
FieldDef.intField("ability.dazedTicks", this::getDazedTicks, this::setDazedTicks)
|
||||
.range(0, 1000).visibleWhen(this::isInterruptible)
|
||||
.range(0, 1000)
|
||||
.visibleWhen(() -> isInterruptible() && !invulnerableMode.invulnerableDuringWindup())
|
||||
).tab("General"));
|
||||
defs.add(FieldDef.stringEnumField("ability.invulnerable", InvulnerableMode.getDisplayKeys(),
|
||||
() -> this.getInvulnerableMode().getDisplayKey(),
|
||||
v -> {
|
||||
for (InvulnerableMode mode : InvulnerableMode.values()) {
|
||||
if (mode.getDisplayKey().equals(v)) {
|
||||
this.setInvulnerableMode(mode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.hover("ability.hover.invulnerable")
|
||||
.tab("General"));
|
||||
|
||||
// ── Burst section ────────────────────────────────────────────
|
||||
if (allowBurst()) {
|
||||
@@ -1174,6 +1191,9 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
if (!interruptible || (phase != AbilityPhase.WINDUP && phase != AbilityPhase.BURST_DELAY)) {
|
||||
return false;
|
||||
}
|
||||
if (isInvulnerableForCurrentPhase()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only direct physical hits can interrupt, not magic, fire, or other indirect damage
|
||||
if (source == null) {
|
||||
@@ -1332,6 +1352,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
nbt.setBoolean("syncWindup", syncWindupWithAnimation);
|
||||
nbt.setInteger("recovery", dazedTicks);
|
||||
nbt.setBoolean("interruptible", interruptible);
|
||||
nbt.setInteger("invulnerableMode", invulnerableMode.ordinal());
|
||||
nbt.setInteger("lockMovement", lockMovement.ordinal());
|
||||
nbt.setInteger("rotationMode", rotationMode.ordinal());
|
||||
nbt.setInteger("rotationPhase", rotationPhase.ordinal());
|
||||
@@ -1411,6 +1432,9 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
syncWindupWithAnimation = nbt.hasKey("syncWindup") ? nbt.getBoolean("syncWindup") : true;
|
||||
dazedTicks = nbt.hasKey("recovery") ? nbt.getInteger("recovery") : 80;
|
||||
interruptible = nbt.hasKey("interruptible") ? nbt.getBoolean("interruptible") : true;
|
||||
invulnerableMode = nbt.hasKey("invulnerableMode")
|
||||
? InvulnerableMode.fromOrdinal(nbt.getInteger("invulnerableMode"))
|
||||
: InvulnerableMode.NONE;
|
||||
lockMovement = LockMode.fromOrdinal(nbt.getInteger("lockMovement"));
|
||||
rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode"));
|
||||
rotationPhase = LockMode.fromOrdinal(nbt.getInteger("rotationPhase"));
|
||||
@@ -1501,7 +1525,12 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
if (builtIn) {
|
||||
this.name = name != null ? name : "";
|
||||
return;
|
||||
}
|
||||
String fallback = (this.name != null && !this.name.isEmpty()) ? this.name : "Ability";
|
||||
this.name = FileNameHelper.sanitizeName(name, fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1510,7 +1539,9 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
* Converts & color codes to § for rendering.
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
String result = (displayName != null && !displayName.isEmpty()) ? displayName : name;
|
||||
String result = (displayName != null && !displayName.isEmpty())
|
||||
? displayName
|
||||
: FileNameHelper.toDisplayName(name);
|
||||
return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : "";
|
||||
}
|
||||
|
||||
@@ -1664,6 +1695,22 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
this.interruptible = interruptible;
|
||||
}
|
||||
|
||||
public InvulnerableMode getInvulnerableMode() {
|
||||
return invulnerableMode;
|
||||
}
|
||||
|
||||
public void setInvulnerableMode(InvulnerableMode invulnerableMode) {
|
||||
this.invulnerableMode = invulnerableMode != null ? invulnerableMode : InvulnerableMode.NONE;
|
||||
}
|
||||
|
||||
public boolean isInvulnerableDuringWindup() {
|
||||
return invulnerableMode.invulnerableDuringWindup();
|
||||
}
|
||||
|
||||
public boolean isInvulnerableDuringActive() {
|
||||
return invulnerableMode.invulnerableDuringActive();
|
||||
}
|
||||
|
||||
public LockMode getLockMovement() {
|
||||
return lockMovement;
|
||||
}
|
||||
@@ -1796,6 +1843,20 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this ability grants invulnerability during its current execution phase.
|
||||
*/
|
||||
public boolean isInvulnerableForCurrentPhase() {
|
||||
return invulnerableMode.isInvulnerableInPhase(phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this ability grants invulnerability for a specific phase.
|
||||
*/
|
||||
public boolean isInvulnerableForPhase(AbilityPhase phase) {
|
||||
return invulnerableMode.isInvulnerableInPhase(phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rotation should be locked during the current phase.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import noppes.npcs.api.ability.IChainedAbility;
|
||||
import kamkeel.npcs.util.FileNameHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -77,7 +78,7 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction {
|
||||
}
|
||||
|
||||
public ChainedAbility(String name) {
|
||||
this.name = name != null ? name : "";
|
||||
setName(name);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -97,7 +98,8 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction {
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name != null ? name : "";
|
||||
String fallback = (this.name != null && !this.name.isEmpty()) ? this.name : "Chain";
|
||||
this.name = FileNameHelper.sanitizeName(name, fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +108,9 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction {
|
||||
* Converts & color codes to § for rendering.
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
String result = (displayName != null && !displayName.isEmpty()) ? displayName : name;
|
||||
String result = (displayName != null && !displayName.isEmpty())
|
||||
? displayName
|
||||
: FileNameHelper.toDisplayName(name);
|
||||
return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : "";
|
||||
}
|
||||
|
||||
@@ -406,7 +410,7 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction {
|
||||
|
||||
public void readNBT(NBTTagCompound nbt) {
|
||||
id = nbt.getString("Id");
|
||||
name = nbt.getString("Name");
|
||||
setName(nbt.getString("Name"));
|
||||
displayName = nbt.getString("DisplayName");
|
||||
enabled = !nbt.hasKey("Enabled") || nbt.getBoolean("Enabled");
|
||||
weight = nbt.hasKey("Weight") ? nbt.getInteger("Weight") : 10;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package kamkeel.npcs.controllers.data.ability.enums;
|
||||
|
||||
/**
|
||||
* Defines when a caster is invulnerable while an ability is executing.
|
||||
*/
|
||||
public enum InvulnerableMode {
|
||||
NONE,
|
||||
WINDUP,
|
||||
ACTIVE,
|
||||
BOTH;
|
||||
|
||||
public boolean invulnerableDuringWindup() {
|
||||
return this == WINDUP || this == BOTH;
|
||||
}
|
||||
|
||||
public boolean invulnerableDuringActive() {
|
||||
return this == ACTIVE || this == BOTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat BURST_DELAY as windup-like for interruption/damage immunity checks.
|
||||
*/
|
||||
public boolean isInvulnerableInPhase(AbilityPhase phase) {
|
||||
if (phase == null) return false;
|
||||
switch (phase) {
|
||||
case WINDUP:
|
||||
case BURST_DELAY:
|
||||
return invulnerableDuringWindup();
|
||||
case ACTIVE:
|
||||
return invulnerableDuringActive();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public String getDisplayKey() {
|
||||
switch (this) {
|
||||
case WINDUP:
|
||||
return "ability.invulnerable.windup";
|
||||
case ACTIVE:
|
||||
return "ability.invulnerable.active";
|
||||
case BOTH:
|
||||
return "ability.invulnerable.both";
|
||||
case NONE:
|
||||
default:
|
||||
return "ability.invulnerable.none";
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] getDisplayKeys() {
|
||||
return new String[]{
|
||||
"ability.invulnerable.none",
|
||||
"ability.invulnerable.windup",
|
||||
"ability.invulnerable.active",
|
||||
"ability.invulnerable.both"
|
||||
};
|
||||
}
|
||||
|
||||
public static InvulnerableMode fromOrdinal(int ordinal) {
|
||||
InvulnerableMode[] values = values();
|
||||
if (ordinal >= 0 && ordinal < values.length) {
|
||||
return values[ordinal];
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,8 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
|
||||
// Type-specific runtime state
|
||||
private transient DashDirection chosenDirection;
|
||||
private transient double preDashMotionX;
|
||||
private transient double preDashMotionZ;
|
||||
|
||||
public AbilityDash() {
|
||||
this.typeId = "ability.cnpc.dash";
|
||||
@@ -125,6 +127,8 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
preDashMotionX = caster.motionX;
|
||||
preDashMotionZ = caster.motionZ;
|
||||
initMovement(caster, dashDistance, dashSpeed);
|
||||
|
||||
if (dashMode == DashMode.DIRECTIONAL) {
|
||||
@@ -151,37 +155,46 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
if (checkTimeout(tick)) {
|
||||
stopMomentum(caster);
|
||||
finishDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (movementDirection == null) {
|
||||
finishDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkStall(caster, tick)) {
|
||||
stopMomentum(caster);
|
||||
finishDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
updatePrevPosition(caster);
|
||||
|
||||
if (getDistanceTraveledSq(caster) >= (double) dashDistance * dashDistance) {
|
||||
stopMomentum(caster);
|
||||
finishDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkBlocked(caster, dashSpeed)) {
|
||||
stopMomentum(caster);
|
||||
finishDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move caster (motionY left to gravity for skip arc)
|
||||
applyVelocity(caster, dashSpeed);
|
||||
// Move caster (motionY left to gravity for skip arc).
|
||||
// Directional mode layers the dash over incoming movement so momentum
|
||||
// entering/exiting the dash stays fluid.
|
||||
if (dashMode == DashMode.DIRECTIONAL) {
|
||||
applyHorizontalMomentum(caster,
|
||||
preDashMotionX + movementDirection.xCoord * dashSpeed,
|
||||
preDashMotionZ + movementDirection.zCoord * dashSpeed);
|
||||
} else {
|
||||
applyVelocity(caster, dashSpeed);
|
||||
}
|
||||
|
||||
if (!isPreview()) {
|
||||
// Trail particles
|
||||
@@ -191,24 +204,28 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
stopMomentum(caster);
|
||||
finishDash(caster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
stopMomentum(caster);
|
||||
finishDash(caster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
super.cleanup();
|
||||
chosenDirection = null;
|
||||
preDashMotionX = 0;
|
||||
preDashMotionZ = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetForBurst() {
|
||||
super.resetForBurst();
|
||||
chosenDirection = null;
|
||||
preDashMotionX = 0;
|
||||
preDashMotionZ = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -279,6 +296,14 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
return chosenDirection;
|
||||
}
|
||||
|
||||
private void finishDash(EntityLivingBase caster) {
|
||||
if (dashMode == DashMode.DIRECTIONAL) {
|
||||
applyHorizontalMomentum(caster, preDashMotionX, preDashMotionZ);
|
||||
} else {
|
||||
stopMomentum(caster);
|
||||
}
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
|
||||
@@ -226,7 +226,7 @@ public abstract class AbilityMovement extends Ability {
|
||||
* By sending S12 manually here — before friction — the client receives the full
|
||||
* velocity and moves at the correct speed.
|
||||
*/
|
||||
private void sendPlayerVelocity(EntityLivingBase caster) {
|
||||
protected void sendPlayerVelocity(EntityLivingBase caster) {
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
((EntityPlayerMP) caster).playerNetServerHandler.sendPacket(
|
||||
new S12PacketEntityVelocity(caster));
|
||||
@@ -234,11 +234,11 @@ public abstract class AbilityMovement extends Ability {
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply movement velocity to the caster (horizontal only, Y unchanged).
|
||||
* Apply horizontal momentum and sync to client.
|
||||
*/
|
||||
protected void applyVelocity(EntityLivingBase caster, float speed) {
|
||||
caster.motionX = movementDirection.xCoord * speed;
|
||||
caster.motionZ = movementDirection.zCoord * speed;
|
||||
protected void applyHorizontalMomentum(EntityLivingBase caster, double motionX, double motionZ) {
|
||||
caster.motionX = motionX;
|
||||
caster.motionZ = motionZ;
|
||||
if (!isPreview()) {
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
sendPlayerVelocity(caster);
|
||||
@@ -248,36 +248,27 @@ public abstract class AbilityMovement extends Ability {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply movement velocity to the caster (horizontal only, Y unchanged).
|
||||
*/
|
||||
protected void applyVelocity(EntityLivingBase caster, float speed) {
|
||||
applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply movement velocity with zero vertical motion.
|
||||
* Used for ground-locked movement like Charge.
|
||||
*/
|
||||
protected void applyVelocityFlat(EntityLivingBase caster, float speed) {
|
||||
caster.motionX = movementDirection.xCoord * speed;
|
||||
applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed);
|
||||
caster.motionY = 0;
|
||||
caster.motionZ = movementDirection.zCoord * speed;
|
||||
if (!isPreview()) {
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
sendPlayerVelocity(caster);
|
||||
} else {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all horizontal momentum.
|
||||
*/
|
||||
protected void stopMomentum(EntityLivingBase caster) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
if (!isPreview()) {
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
sendPlayerVelocity(caster);
|
||||
} else {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
applyHorizontalMomentum(caster, 0, 0);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.io.IOException;
|
||||
* bit 1: rotation locked (freeze yaw/pitch)
|
||||
* bit 2: has ability movement (ability controls motion — suppress WASD but don't zero)
|
||||
* bit 3: position locked (freeze position)
|
||||
* bit 5: ability is in ACTIVE phase
|
||||
*/
|
||||
public final class PlayerAbilityStatePacket extends AbstractPacket {
|
||||
public static final String packetName = "Data|PlayerAbilityState";
|
||||
@@ -31,6 +32,7 @@ public final class PlayerAbilityStatePacket extends AbstractPacket {
|
||||
public static final byte FLAG_HAS_ABILITY_MOVEMENT = 4;
|
||||
public static final byte FLAG_POSITION_LOCKED = 8;
|
||||
public static final byte FLAG_WAS_FLYING_AT_LOCK = 16;
|
||||
public static final byte FLAG_ACTIVE_PHASE = 32;
|
||||
|
||||
private byte flags;
|
||||
private float lockedYaw;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package kamkeel.npcs.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Utility helpers for filename-safe ability/chained names.
|
||||
* Allowed characters are letters, digits, underscore, and dash.
|
||||
*/
|
||||
public final class FileNameHelper {
|
||||
|
||||
private static final Set<String> WINDOWS_RESERVED_NAMES = new HashSet<String>(Arrays.asList(
|
||||
"CON", "PRN", "AUX", "NUL",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
|
||||
));
|
||||
|
||||
private FileNameHelper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a name to a filename-safe identifier.
|
||||
* Whitespace becomes underscores; unsupported chars are removed.
|
||||
*/
|
||||
public static String sanitizeName(String input, String fallback) {
|
||||
String sanitized = sanitizeInternal(input);
|
||||
if (sanitized.isEmpty()) {
|
||||
sanitized = sanitizeInternal(fallback);
|
||||
}
|
||||
if (sanitized.isEmpty()) {
|
||||
sanitized = "Ability";
|
||||
}
|
||||
if (isReservedWindowsName(sanitized)) {
|
||||
sanitized = "_" + sanitized;
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize textfield input without forcing a fallback.
|
||||
*/
|
||||
public static String sanitizeTextInput(String input) {
|
||||
return sanitizeInternal(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick check for compatibility with sanitized identifier rules.
|
||||
*/
|
||||
public static boolean isFileNameCompatible(String value) {
|
||||
if (value == null || value.isEmpty()) return false;
|
||||
if (isReservedWindowsName(value)) return false;
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char c = value.charAt(i);
|
||||
if (!Character.isLetterOrDigit(c) && c != '_' && c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable fallback display text for sanitized names.
|
||||
*/
|
||||
public static String toDisplayName(String value) {
|
||||
if (value == null || value.isEmpty()) return "";
|
||||
return value.replace('_', ' ');
|
||||
}
|
||||
|
||||
private static String sanitizeInternal(String raw) {
|
||||
if (raw == null) return "";
|
||||
String trimmed = raw.trim();
|
||||
if (trimmed.isEmpty()) return "";
|
||||
|
||||
StringBuilder out = new StringBuilder(trimmed.length());
|
||||
|
||||
for (int i = 0; i < trimmed.length(); i++) {
|
||||
char c = trimmed.charAt(i);
|
||||
if (Character.isLetterOrDigit(c)) {
|
||||
out.append(c);
|
||||
} else if (c == '_' || c == '-') {
|
||||
out.append(c);
|
||||
} else if (Character.isWhitespace(c)) {
|
||||
if (out.length() == 0) continue;
|
||||
char last = out.charAt(out.length() - 1);
|
||||
if (last == '_') continue;
|
||||
out.append('_');
|
||||
}
|
||||
}
|
||||
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static boolean isReservedWindowsName(String value) {
|
||||
return WINDOWS_RESERVED_NAMES.contains(value.toUpperCase());
|
||||
}
|
||||
}
|
||||
@@ -268,6 +268,15 @@ public abstract class AbstractDataAbilities {
|
||||
return currentAbility != null && currentAbility.isExecuting();
|
||||
}
|
||||
|
||||
/**
|
||||
* True when an executing ability grants invulnerability for its current phase.
|
||||
*/
|
||||
public boolean isCurrentAbilityInvulnerable() {
|
||||
return currentAbility != null
|
||||
&& currentAbility.isExecuting()
|
||||
&& currentAbility.isInvulnerableForCurrentPhase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently executing ability.
|
||||
*/
|
||||
|
||||
@@ -555,12 +555,15 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
* @return true if ability was interrupted
|
||||
*/
|
||||
public boolean onDamage(DamageSource source, float amount) {
|
||||
// Track hit for hit count condition
|
||||
recordHit();
|
||||
|
||||
if (currentAbility == null || !currentAbility.isExecuting()) {
|
||||
return false;
|
||||
}
|
||||
if (currentAbility.isInvulnerableForCurrentPhase()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Track hit for hit count condition
|
||||
recordHit();
|
||||
|
||||
net.minecraft.entity.Entity sourceEntity = source.getEntity();
|
||||
EntityLivingBase attacker = sourceEntity instanceof EntityLivingBase ? (EntityLivingBase) sourceEntity : null;
|
||||
|
||||
@@ -629,7 +629,12 @@ public class ScriptPlayerEventHandler {
|
||||
// Handle ability damage interactions (Guard counter, damage reduction)
|
||||
PlayerData pData = PlayerData.get((EntityPlayer) event.entityLiving);
|
||||
if (pData != null && pData.abilityData != null && pData.abilityData.isExecutingAbility()) {
|
||||
event.ammount = pData.abilityData.onDamage(event.source, event.ammount);
|
||||
if (pData.abilityData.isCurrentAbilityInvulnerable()) {
|
||||
event.ammount = 0;
|
||||
cancel = true;
|
||||
} else {
|
||||
event.ammount = pData.abilityData.onDamage(event.source, event.ammount);
|
||||
}
|
||||
}
|
||||
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.entityLiving);
|
||||
|
||||
@@ -16,6 +16,7 @@ public class ClientAbilityState {
|
||||
public static boolean hasAbilityMovement = false;
|
||||
public static boolean positionLocked = false;
|
||||
public static boolean wasFlyingAtLock = false;
|
||||
public static boolean activePhase = false;
|
||||
public static float lockedYaw = 0;
|
||||
public static float lockedPitch = 0;
|
||||
|
||||
@@ -28,6 +29,7 @@ public class ClientAbilityState {
|
||||
hasAbilityMovement = (flags & PlayerAbilityStatePacket.FLAG_HAS_ABILITY_MOVEMENT) != 0;
|
||||
positionLocked = (flags & PlayerAbilityStatePacket.FLAG_POSITION_LOCKED) != 0;
|
||||
wasFlyingAtLock = (flags & PlayerAbilityStatePacket.FLAG_WAS_FLYING_AT_LOCK) != 0;
|
||||
activePhase = (flags & PlayerAbilityStatePacket.FLAG_ACTIVE_PHASE) != 0;
|
||||
lockedYaw = yaw;
|
||||
lockedPitch = pitch;
|
||||
}
|
||||
@@ -56,6 +58,7 @@ public class ClientAbilityState {
|
||||
hasAbilityMovement = false;
|
||||
positionLocked = false;
|
||||
wasFlyingAtLock = false;
|
||||
activePhase = false;
|
||||
lockedYaw = 0;
|
||||
lockedPitch = 0;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ public class ClientTickHandler {
|
||||
private long buttonTime = 0L;
|
||||
private final int[] ignoreKeys = new int[]{157, 29, 54, 42, 184, 56, 220, 219};
|
||||
private boolean lastSpecialKeyDown = false;
|
||||
private boolean wasMovementSuppressed = false;
|
||||
|
||||
@SubscribeEvent(priority = EventPriority.LOWEST)
|
||||
public void onClientTick(TickEvent.ClientTickEvent event) {
|
||||
@@ -84,9 +85,15 @@ public class ClientTickHandler {
|
||||
lastSpecialKeyDown = specialKeyDown;
|
||||
}
|
||||
|
||||
boolean suppressMovement = mc.currentScreen == null && ClientAbilityState.shouldSuppressMovementInput();
|
||||
if (!suppressMovement && wasMovementSuppressed) {
|
||||
syncMovementKeyStates(mc);
|
||||
}
|
||||
wasMovementSuppressed = suppressMovement;
|
||||
|
||||
// Suppress player input during ability-controlled phases.
|
||||
// Only suppress when no GUI screen is open (screens already capture input).
|
||||
if (mc.currentScreen == null && ClientAbilityState.shouldSuppressMovementInput()) {
|
||||
if (suppressMovement) {
|
||||
// Unpress movement keybinds at the source BEFORE updatePlayerMoveState() reads them.
|
||||
KeyBinding.setKeyBindState(mc.gameSettings.keyBindForward.getKeyCode(), false);
|
||||
KeyBinding.setKeyBindState(mc.gameSettings.keyBindBack.getKeyCode(), false);
|
||||
@@ -301,6 +308,27 @@ public class ClientTickHandler {
|
||||
|| keyCode == mc.gameSettings.keyBindSprint.getKeyCode();
|
||||
}
|
||||
|
||||
private void syncMovementKeyStates(Minecraft mc) {
|
||||
syncKeyBindingState(mc.gameSettings.keyBindForward);
|
||||
syncKeyBindingState(mc.gameSettings.keyBindBack);
|
||||
syncKeyBindingState(mc.gameSettings.keyBindLeft);
|
||||
syncKeyBindingState(mc.gameSettings.keyBindRight);
|
||||
syncKeyBindingState(mc.gameSettings.keyBindJump);
|
||||
syncKeyBindingState(mc.gameSettings.keyBindSneak);
|
||||
syncKeyBindingState(mc.gameSettings.keyBindSprint);
|
||||
}
|
||||
|
||||
private void syncKeyBindingState(KeyBinding keyBinding) {
|
||||
int keyCode = keyBinding.getKeyCode();
|
||||
boolean pressed;
|
||||
if (keyCode < 0) {
|
||||
pressed = Mouse.isButtonDown(keyCode + 100);
|
||||
} else {
|
||||
pressed = Keyboard.isKeyDown(keyCode);
|
||||
}
|
||||
KeyBinding.setKeyBindState(keyCode, pressed);
|
||||
}
|
||||
|
||||
private boolean isIgnoredKey(int key) {
|
||||
int[] var2 = this.ignoreKeys;
|
||||
int var3 = var2.length;
|
||||
|
||||
@@ -84,8 +84,12 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface implements ISubGui
|
||||
@Override
|
||||
public void subGuiClosed(SubGuiInterface subgui) {
|
||||
if (subgui instanceof SubGuiDuplicateNameConfirm) {
|
||||
if (((SubGuiDuplicateNameConfirm) subgui).isConfirmed()) {
|
||||
SubGuiDuplicateNameConfirm confirm = (SubGuiDuplicateNameConfirm) subgui;
|
||||
if (confirm.isConfirmed()) {
|
||||
doSave();
|
||||
} else if (confirm.isBack()) {
|
||||
// Keep this save-confirm dialog open.
|
||||
return;
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
|
||||
@@ -10,7 +10,13 @@ import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
*/
|
||||
public class SubGuiDuplicateNameConfirm extends SubGuiInterface {
|
||||
|
||||
private boolean confirmed = false;
|
||||
public enum Result {
|
||||
CONTINUE,
|
||||
BACK,
|
||||
CANCEL
|
||||
}
|
||||
|
||||
private Result result = Result.CANCEL;
|
||||
|
||||
public SubGuiDuplicateNameConfirm() {
|
||||
setBackground("menubg.png");
|
||||
@@ -26,22 +32,38 @@ public class SubGuiDuplicateNameConfirm extends SubGuiInterface {
|
||||
addLabel(new GuiNpcLabel(0, "ability.duplicate.name", guiLeft + 10, y));
|
||||
|
||||
y += 30;
|
||||
addButton(new GuiNpcButton(0, guiLeft + 20, y, 80, 20, "gui.continue"));
|
||||
addButton(new GuiNpcButton(1, guiLeft + 120, y, 80, 20, "gui.cancel"));
|
||||
addButton(new GuiNpcButton(0, guiLeft + 10, y, 65, 20, "gui.continue"));
|
||||
addButton(new GuiNpcButton(1, guiLeft + 78, y, 65, 20, "gui.back"));
|
||||
addButton(new GuiNpcButton(2, guiLeft + 146, y, 65, 20, "gui.cancel"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buttonEvent(GuiButton guibutton) {
|
||||
if (guibutton.id == 0) {
|
||||
confirmed = true;
|
||||
result = Result.CONTINUE;
|
||||
close();
|
||||
} else if (guibutton.id == 1) {
|
||||
confirmed = false;
|
||||
result = Result.BACK;
|
||||
close();
|
||||
} else if (guibutton.id == 2) {
|
||||
result = Result.CANCEL;
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
public Result getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmed;
|
||||
return result == Result.CONTINUE;
|
||||
}
|
||||
|
||||
public boolean isBack() {
|
||||
return result == Result.BACK;
|
||||
}
|
||||
|
||||
public boolean isCancelled() {
|
||||
return result == Result.CANCEL;
|
||||
}
|
||||
}
|
||||
@@ -245,6 +245,9 @@ public class GuiFieldBuilder {
|
||||
case STRING: {
|
||||
String sVal = def.getValue() != null ? def.getValue().toString() : "";
|
||||
GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, sVal);
|
||||
if ("gui.name".equals(def.getLabel())) {
|
||||
tf.setFileNameSafe();
|
||||
}
|
||||
if (!def.isEnabled()) tf.setEnabled(false);
|
||||
if (hover != null) tf.setHoverText(hover);
|
||||
sw.addTextField(tf);
|
||||
|
||||
@@ -104,7 +104,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
|
||||
// ==================== PENDING SAVE ====================
|
||||
private Ability pendingSaveAbility = null;
|
||||
private boolean pendingNewCreation = false;
|
||||
|
||||
// ==================== PENDING VARIANT ====================
|
||||
private String pendingTypeId = null;
|
||||
@@ -511,13 +510,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
Ability newAbility = AbilityController.Instance.create(pendingTypeId);
|
||||
if (newAbility != null) {
|
||||
gui.getVariants().get(idx).apply(newAbility);
|
||||
if (hasDuplicateName(newAbility)) {
|
||||
pendingSaveAbility = newAbility;
|
||||
pendingNewCreation = true;
|
||||
pendingTypeId = null;
|
||||
setSubGui(new SubGuiDuplicateNameConfirm());
|
||||
return true;
|
||||
}
|
||||
pendingTypeId = null;
|
||||
openConfig(newAbility);
|
||||
return true;
|
||||
@@ -544,12 +536,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
if (variants.size() == 1) {
|
||||
variants.get(0).apply(newAbility);
|
||||
}
|
||||
if (hasDuplicateName(newAbility)) {
|
||||
pendingSaveAbility = newAbility;
|
||||
pendingNewCreation = true;
|
||||
setSubGui(new SubGuiDuplicateNameConfirm());
|
||||
return true;
|
||||
}
|
||||
openConfig(newAbility);
|
||||
return true;
|
||||
}
|
||||
@@ -561,27 +547,22 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
* @return true if a SubGui was opened (caller should return early, skipping initGui)
|
||||
*/
|
||||
private boolean handleDuplicateNameClosed(SubGuiDuplicateNameConfirm gui) {
|
||||
if (gui.isConfirmed() && pendingSaveAbility != null) {
|
||||
if (pendingNewCreation) {
|
||||
openConfig(pendingSaveAbility);
|
||||
pendingSaveAbility = null;
|
||||
pendingNewCreation = false;
|
||||
return true;
|
||||
} else {
|
||||
PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT()));
|
||||
pendingSaveAbility = null;
|
||||
}
|
||||
} else if (pendingSaveAbility != null) {
|
||||
if (pendingNewCreation) {
|
||||
pendingSaveAbility = null;
|
||||
pendingNewCreation = false;
|
||||
} else {
|
||||
Ability ability = pendingSaveAbility;
|
||||
pendingSaveAbility = null;
|
||||
setSubGui(ability.createConfigGui(this));
|
||||
return true;
|
||||
}
|
||||
if (pendingSaveAbility == null) return false;
|
||||
|
||||
if (gui.isConfirmed()) {
|
||||
PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT()));
|
||||
pendingSaveAbility = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gui.isBack()) {
|
||||
Ability ability = pendingSaveAbility;
|
||||
pendingSaveAbility = null;
|
||||
setSubGui(ability.createConfigGui(this));
|
||||
return true;
|
||||
}
|
||||
|
||||
pendingSaveAbility = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -590,7 +571,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
*/
|
||||
private boolean handlePendingSave() {
|
||||
if (hasDuplicateName(pendingSaveAbility)) {
|
||||
pendingNewCreation = false;
|
||||
setSubGui(new SubGuiDuplicateNameConfirm());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.packets.player.ability.AbilityHotbarSelectPacket;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import noppes.npcs.client.ClientProxy;
|
||||
import noppes.npcs.client.ClientAbilityState;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.client.gui.ScaledResolution;
|
||||
@@ -189,6 +190,7 @@ public class AbilityHotbarComponent extends HudComponent {
|
||||
|
||||
int centerX = overlayWidth / 2;
|
||||
int centerY = overlayHeight / 2;
|
||||
boolean activePhase = ClientAbilityState.activePhase;
|
||||
|
||||
// Compute text visibility: 0=Shown, 1=Hidden, 2=Held
|
||||
boolean showText;
|
||||
@@ -246,16 +248,16 @@ public class AbilityHotbarComponent extends HudComponent {
|
||||
|
||||
if (rawSlotIndex == -1) {
|
||||
// Deselect slot — draw empty marker
|
||||
drawDeselectSlot(cx, cy, scaledSize, isCenter, slotAlpha);
|
||||
drawDeselectSlot(cx, cy, scaledSize, isCenter, slotAlpha, activePhase);
|
||||
} else {
|
||||
slots[rawSlotIndex].drawCarousel(mc, sr, slotCooldown, cx, cy, scaledSize, isCenter, slotAlpha, showText);
|
||||
slots[rawSlotIndex].drawCarousel(mc, sr, slotCooldown, cx, cy, scaledSize, isCenter, slotAlpha, showText, activePhase);
|
||||
}
|
||||
}
|
||||
|
||||
GL11.glPopMatrix();
|
||||
}
|
||||
|
||||
private void drawDeselectSlot(int cx, int cy, int size, boolean isCenter, float alpha) {
|
||||
private void drawDeselectSlot(int cx, int cy, int size, boolean isCenter, float alpha, boolean activePhase) {
|
||||
float radius = size / 2f;
|
||||
float baseAlpha = isCenter ? 0.9f : 0.6f;
|
||||
float a = baseAlpha * alpha;
|
||||
@@ -268,7 +270,13 @@ public class AbilityHotbarComponent extends HudComponent {
|
||||
GL11.glEnable(GL11.GL_BLEND);
|
||||
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
if (isCenter) {
|
||||
if (activePhase) {
|
||||
if (isCenter) {
|
||||
GL11.glColor4f(0.65f, 0.14f, 0.14f, a);
|
||||
} else {
|
||||
GL11.glColor4f(0.38f, 0.10f, 0.10f, a);
|
||||
}
|
||||
} else if (isCenter) {
|
||||
GL11.glColor4f(0.5f, 0.2f, 0.2f, a);
|
||||
} else {
|
||||
GL11.glColor4f(0.3f, 0.3f, 0.3f, a);
|
||||
@@ -292,7 +300,13 @@ public class AbilityHotbarComponent extends HudComponent {
|
||||
}
|
||||
|
||||
// Draw X mark
|
||||
if (isCenter) {
|
||||
if (activePhase) {
|
||||
if (isCenter) {
|
||||
GL11.glColor4f(1.0f, 0.45f, 0.35f, alpha);
|
||||
} else {
|
||||
GL11.glColor4f(0.75f, 0.30f, 0.30f, alpha * 0.8f);
|
||||
}
|
||||
} else if (isCenter) {
|
||||
GL11.glColor4f(0.8f, 0.4f, 0.4f, alpha);
|
||||
} else {
|
||||
GL11.glColor4f(0.5f, 0.5f, 0.5f, alpha * 0.8f);
|
||||
|
||||
@@ -48,7 +48,7 @@ public class AbilityHotbarSlotRenderer extends Gui {
|
||||
|
||||
public void drawCarousel(Minecraft mc, ScaledResolution sr, float cooldownProgress,
|
||||
int cx, int cy, int size, boolean isCenter, float slotAlpha,
|
||||
boolean showText) {
|
||||
boolean showText, boolean activePhase) {
|
||||
float nameAlpha = this.nameAlpha;
|
||||
boolean isHorizontal = ConfigClient.AbilityHotbarHorizontal;
|
||||
if (size <= 0 || slotAlpha <= 0) return;
|
||||
@@ -61,21 +61,54 @@ public class AbilityHotbarSlotRenderer extends Gui {
|
||||
|
||||
boolean altTexture = ConfigClient.AbilityHotbarAltTexture;
|
||||
|
||||
if (altTexture) {
|
||||
drawRoundedRect(-radius, -radius, radius, radius, 0.6f, 0.6f, 0.6f, 0.75f * alpha, false);
|
||||
float fillR;
|
||||
float fillG;
|
||||
float fillB;
|
||||
float outlineR;
|
||||
float outlineG;
|
||||
float outlineB;
|
||||
|
||||
if (activePhase) {
|
||||
if (isCenter) {
|
||||
drawRoundedRect(-radius, -radius, radius, radius, 0.8f, 0.8f, 0.2f, alpha, true);
|
||||
fillR = 0.95f;
|
||||
fillG = 0.26f;
|
||||
fillB = 0.22f;
|
||||
outlineR = 1.0f;
|
||||
outlineG = 0.52f;
|
||||
outlineB = 0.28f;
|
||||
} else {
|
||||
drawRoundedRect(-radius, -radius, radius, radius, 0.4f, 0.4f, 0.4f, alpha * 0.8f, true);
|
||||
fillR = 0.36f;
|
||||
fillG = 0.14f;
|
||||
fillB = 0.14f;
|
||||
outlineR = 0.65f;
|
||||
outlineG = 0.22f;
|
||||
outlineB = 0.20f;
|
||||
}
|
||||
} else if (isCenter) {
|
||||
fillR = 0.8f;
|
||||
fillG = 0.8f;
|
||||
fillB = 0.2f;
|
||||
outlineR = 0.8f;
|
||||
outlineG = 0.8f;
|
||||
outlineB = 0.2f;
|
||||
} else {
|
||||
if (isCenter) {
|
||||
drawCircle(radius, 0.8f, 0.8f, 0.2f, 0.66f * alpha, false);
|
||||
drawCircle(radius, 0.8f, 0.8f, 0.2f, alpha, true);
|
||||
} else {
|
||||
drawCircle(radius, 1f, 1f, 1f, 0.66f * alpha, false);
|
||||
drawCircle(radius, 0.4f, 0.4f, 0.4f, alpha * 0.8f, true);
|
||||
}
|
||||
fillR = 0.4f;
|
||||
fillG = 0.4f;
|
||||
fillB = 0.4f;
|
||||
outlineR = 1f;
|
||||
outlineG = 1f;
|
||||
outlineB = 1f;
|
||||
}
|
||||
|
||||
if (altTexture) {
|
||||
float baseR = activePhase ? 0.30f : 0.6f;
|
||||
float baseG = activePhase ? 0.12f : 0.6f;
|
||||
float baseB = activePhase ? 0.12f : 0.6f;
|
||||
drawRoundedRect(-radius, -radius, radius, radius, baseR, baseG, baseB, 0.75f * alpha, false);
|
||||
drawRoundedRect(-radius, -radius, radius, radius, fillR, fillG, fillB, alpha * 0.8f, true);
|
||||
} else {
|
||||
drawCircle(radius, outlineR, outlineG, outlineB, 0.66f * alpha, false);
|
||||
drawCircle(radius, fillR, fillG, fillB, alpha * 0.8f, true);
|
||||
}
|
||||
|
||||
// Ability icon
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package noppes.npcs.client.gui.util;
|
||||
|
||||
import kamkeel.npcs.util.TextSplitter;
|
||||
import kamkeel.npcs.util.FileNameHelper;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
@@ -26,6 +27,7 @@ public class GuiNpcTextField extends GuiTextField {
|
||||
public float minFloat = 0, maxFloat = Float.MAX_VALUE, defFloat = 0;
|
||||
protected static GuiNpcTextField activeTextfield = null;
|
||||
public boolean canEdit = true;
|
||||
private boolean fileNameSafe = false;
|
||||
public String hoverableText = "";
|
||||
private boolean wasHovered = false;
|
||||
private int hoverCount = 0;
|
||||
@@ -71,6 +73,11 @@ public class GuiNpcTextField extends GuiTextField {
|
||||
public boolean textboxKeyTyped(char c, int i) {
|
||||
if (!canEdit || !isFocused())
|
||||
return false;
|
||||
|
||||
if (fileNameSafe && !GuiScreen.isCtrlKeyDown() && !isFileNameCharAllowed(c, i)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow Ctrl shortcuts (select all, copy, paste, cut) to bypass char filter
|
||||
if (GuiScreen.isCtrlKeyDown()) {
|
||||
// For Ctrl+V (paste) on numeric fields, filter clipboard to valid chars
|
||||
@@ -87,7 +94,17 @@ public class GuiNpcTextField extends GuiTextField {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.textboxKeyTyped(c, i);
|
||||
if (i == 47 && fileNameSafe) {
|
||||
String clipboard = GuiScreen.getClipboardString();
|
||||
if (clipboard != null) {
|
||||
String filtered = filterFileNameClipboard(clipboard);
|
||||
if (!filtered.isEmpty())
|
||||
writeText(filtered);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
boolean handled = super.textboxKeyTyped(c, i);
|
||||
return handled;
|
||||
}
|
||||
if (!charAllowed(c, i))
|
||||
return false;
|
||||
@@ -179,6 +196,9 @@ public class GuiNpcTextField extends GuiTextField {
|
||||
else if (getFloat() > maxFloat)
|
||||
setText(maxFloat + "");
|
||||
}
|
||||
if (fileNameSafe) {
|
||||
normalizeFileNameText();
|
||||
}
|
||||
setCursorPositionZero();
|
||||
if (listener != null)
|
||||
listener.unFocused(this);
|
||||
@@ -238,6 +258,51 @@ public class GuiNpcTextField extends GuiTextField {
|
||||
return this;
|
||||
}
|
||||
|
||||
public GuiNpcTextField setFileNameSafe() {
|
||||
fileNameSafe = true;
|
||||
normalizeFileNameText();
|
||||
return this;
|
||||
}
|
||||
|
||||
private boolean isSpecialNavigationKey(int keyCode) {
|
||||
for (int allowed : allowedSpecialChars) {
|
||||
if (allowed == keyCode) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isFileNameCharAllowed(char c, int keyCode) {
|
||||
if (isSpecialNavigationKey(keyCode) || c < 32) {
|
||||
return true;
|
||||
}
|
||||
return Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == ' ';
|
||||
}
|
||||
|
||||
private String filterFileNameClipboard(String clipboard) {
|
||||
StringBuilder filtered = new StringBuilder(clipboard.length());
|
||||
for (int idx = 0; idx < clipboard.length(); idx++) {
|
||||
char c = clipboard.charAt(idx);
|
||||
if (Character.isLetterOrDigit(c) || c == '_' || c == '-') {
|
||||
filtered.append(c);
|
||||
} else if (Character.isWhitespace(c)) {
|
||||
filtered.append(' ');
|
||||
}
|
||||
}
|
||||
return filtered.toString();
|
||||
}
|
||||
|
||||
private void normalizeFileNameText() {
|
||||
String current = getText();
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(current);
|
||||
if (sanitized.equals(current)) return;
|
||||
|
||||
int cursor = getCursorPosition();
|
||||
setText(sanitized);
|
||||
setCursorPosition(Math.min(cursor, sanitized.length()));
|
||||
}
|
||||
|
||||
public void setHoverText(String text) {
|
||||
this.hoverableText = StatCollector.translateToLocal(text);
|
||||
}
|
||||
|
||||
@@ -806,6 +806,9 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
|
||||
if (currentAbility == null || !currentAbility.isExecuting()) {
|
||||
return amount;
|
||||
}
|
||||
if (currentAbility.isInvulnerableForCurrentPhase()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
net.minecraft.entity.Entity sourceEntity = source.getEntity();
|
||||
EntityLivingBase attacker = sourceEntity instanceof EntityLivingBase ? (EntityLivingBase) sourceEntity : null;
|
||||
@@ -937,6 +940,9 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
|
||||
if (wasFlyingAtLock) {
|
||||
flags |= PlayerAbilityStatePacket.FLAG_WAS_FLYING_AT_LOCK;
|
||||
}
|
||||
if (currentAbility.getPhase() == AbilityPhase.ACTIVE) {
|
||||
flags |= PlayerAbilityStatePacket.FLAG_ACTIVE_PHASE;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
|
||||
@@ -659,6 +659,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
if (damagesource.damageType != null && damagesource.damageType.equals("outOfWorld") && isKilled()) {
|
||||
reset();
|
||||
}
|
||||
if (this.abilities != null && this.abilities.isCurrentAbilityInvulnerable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for custom weapon attack speed - bypass immunity if enough time has passed
|
||||
Entity sourceEntity = damagesource.getEntity();
|
||||
|
||||
Reference in new issue
Block a user