ModernFix/src/main/java/org/embeddedt/modernfix/ModernFixClient.java
2023-04-28 14:59:18 -04:00

258 lines
11 KiB
Java

package org.embeddedt.modernfix;
import com.mojang.blaze3d.platform.InputConstants;
import com.mojang.datafixers.util.Pair;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.DebugScreenOverlay;
import net.minecraft.client.gui.screens.ConnectScreen;
import net.minecraft.client.gui.screens.TitleScreen;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.world.entity.Entity;
import net.minecraftforge.client.event.*;
import net.minecraftforge.client.gui.ForgeIngameGui;
import net.minecraftforge.client.settings.KeyConflictContext;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.TagsUpdatedEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.world.WorldEvent;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.ExtensionPoint;
import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.client.registry.ClientRegistry;
import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.server.FMLServerStartingEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.network.NetworkEvent;
import org.embeddedt.modernfix.core.ModernFixMixinPlugin;
import org.embeddedt.modernfix.core.config.ModernFixConfig;
import org.embeddedt.modernfix.packet.EntityIDSyncPacket;
import org.embeddedt.modernfix.screen.ModernFixConfigScreen;
import org.embeddedt.modernfix.world.IntegratedWatchdog;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Supplier;
public class ModernFixClient {
public static long worldLoadStartTime;
private static int numRenderTicks;
public static float gameStartTimeSeconds = -1;
private static boolean recipesUpdated, tagsUpdated = false;
private String brandingString = null;
public ModernFixClient() {
// clear reserve as it's not needed
ObfuscationReflectionHelper.setPrivateValue(Minecraft.class, null, new byte[0], "field_71444_a");
if(ModernFixMixinPlugin.instance.isOptionEnabled("feature.branding.F3Screen")) {
Optional<? extends ModContainer> mfContainer = ModList.get().getModContainerById("modernfix");
if(mfContainer.isPresent())
brandingString = "ModernFix " + mfContainer.get().getModInfo().getVersion().toString();
}
FMLJavaModLoadingContext.get().getModEventBus().addListener(this::clientSetup);
ModLoadingContext.get().registerExtensionPoint(
ExtensionPoint.CONFIGGUIFACTORY,
() -> (mc, screen) -> new ModernFixConfigScreen(screen)
);
}
private KeyMapping configKey;
private void clientSetup(FMLClientSetupEvent event) {
configKey = new KeyMapping("key.modernfix.config", KeyConflictContext.UNIVERSAL, InputConstants.UNKNOWN, "key.modernfix");
ClientRegistry.registerKeyBinding(configKey);
}
@SubscribeEvent
public void onConfigKey(TickEvent.ClientTickEvent event) {
if(event.phase == TickEvent.Phase.START && configKey.consumeClick()) {
Minecraft.getInstance().setScreen(new ModernFixConfigScreen(Minecraft.getInstance().screen));
}
}
public void resetWorldLoadStateMachine() {
numRenderTicks = 0;
worldLoadStartTime = -1;
recipesUpdated = false;
tagsUpdated = false;
}
@SubscribeEvent(priority = EventPriority.LOWEST)
public void onMultiplayerConnect(GuiScreenEvent.InitGuiEvent.Pre event) {
if(event.getGui() instanceof ConnectScreen && !event.isCanceled()) {
worldLoadStartTime = System.nanoTime();
} else if (event.getGui() instanceof TitleScreen && gameStartTimeSeconds < 0) {
gameStartTimeSeconds = ManagementFactory.getRuntimeMXBean().getUptime() / 1000f;
ModernFix.LOGGER.warn("Game took " + gameStartTimeSeconds + " seconds to start");
}
}
@SubscribeEvent(priority = EventPriority.LOW)
public void onRecipesUpdated(RecipesUpdatedEvent event) {
recipesUpdated = true;
}
@SubscribeEvent(priority = EventPriority.LOW)
public void onTagsUpdated(TagsUpdatedEvent event) {
tagsUpdated = true;
}
@SubscribeEvent
public void onRenderTickEnd(TickEvent.RenderTickEvent event) {
if(event.phase == TickEvent.Phase.END
&& recipesUpdated
&& tagsUpdated
&& worldLoadStartTime != -1
&& Minecraft.getInstance().player != null
&& numRenderTicks++ >= 10) {
float timeSpentLoading = ((float)(System.nanoTime() - worldLoadStartTime) / 1000000000f);
ModernFix.LOGGER.warn("Time from main menu to in-game was " + timeSpentLoading + " seconds");
ModernFix.LOGGER.warn("Total time to load game and open world was " + (timeSpentLoading + gameStartTimeSeconds) + " seconds");
resetWorldLoadStateMachine();
if(ModernFix.worldLoadSemaphore != null)
ModernFix.worldLoadSemaphore.countDown();
}
}
@SubscribeEvent(priority = EventPriority.HIGHEST)
public void onRenderOverlay(RenderGameOverlayEvent.Text event) {
if(brandingString != null && Minecraft.getInstance().options.renderDebug) {
event.getLeft().add("");
event.getLeft().add(brandingString);
}
}
@SubscribeEvent
public void onDisconnect(WorldEvent.Unload event) {
if(event.getWorld().isClientSide()) {
DebugScreenOverlay overlay = ObfuscationReflectionHelper.getPrivateValue(ForgeIngameGui.class, (ForgeIngameGui)Minecraft.getInstance().gui, "debugOverlay");
if(overlay != null) {
Minecraft.getInstance().tell(overlay::clearChunkCache);
}
}
}
/**
* Check if the IDs match and remap them if not.
* @return true if ID remap was needed
*/
private static boolean compareAndSwitchIds(Class<? extends Entity> eClass, String fieldName, EntityDataAccessor<?> accessor, int newId) {
if(accessor.id != newId) {
ModernFix.LOGGER.warn("Corrected ID mismatch on {} field {}. Client had {} but server wants {}.",
eClass,
fieldName,
accessor.id,
newId);
accessor.id = newId;
return true;
} else {
ModernFix.LOGGER.debug("{} {} ID fine: {}", eClass, fieldName, newId);
return false;
}
}
/**
* Horrendous hack to allow tracking every synced entity data manager.
*
* This is to ensure we can perform ID fixup on already constructed managers.
*/
public static final Set<SynchedEntityData> allEntityDatas = Collections.newSetFromMap(new WeakHashMap<>());
private static final Field entriesArrayField;
static {
Field field;
try {
field = SynchedEntityData.class.getDeclaredField("entriesArray");
field.setAccessible(true);
} catch(ReflectiveOperationException e) {
field = null;
}
entriesArrayField = field;
}
/**
* Extremely hacky method to detect and correct mismatched entity data parameter IDs on the client and server.
*
* The technique is far from ideal, but it should detect reliably and also not break already constructed entities.
*/
public static void handleEntityIDSync(EntityIDSyncPacket packet, Supplier<NetworkEvent.Context> context) {
Map<Class<? extends Entity>, List<Pair<String, Integer>>> info = packet.getFieldInfo();
context.get().enqueueWork(() -> {
boolean fixNeeded = false;
for(Map.Entry<Class<? extends Entity>, List<Pair<String, Integer>>> entry : info.entrySet()) {
Class<? extends Entity> eClass = entry.getKey();
for(Pair<String, Integer> field : entry.getValue()) {
String fieldName = field.getFirst();
int newId = field.getSecond();
try {
Field f = eClass.getDeclaredField(fieldName);
f.setAccessible(true);
EntityDataAccessor<?> accessor = (EntityDataAccessor<?>)f.get(null);
if(compareAndSwitchIds(eClass, fieldName, accessor, newId))
fixNeeded = true;
} catch(NoSuchFieldException e) {
ModernFix.LOGGER.warn("Couldn't find field on {}: {}", eClass, fieldName);
} catch(ReflectiveOperationException e) {
throw new RuntimeException("Unexpected exception", e);
}
}
}
/* Now the ID mappings on synced entity data instances are probably all wrong. Fix that. */
List<SynchedEntityData> dataEntries;
synchronized (allEntityDatas) {
if(fixNeeded) {
dataEntries = new ArrayList<>(allEntityDatas);
for(SynchedEntityData manager : dataEntries) {
Int2ObjectOpenHashMap<SynchedEntityData.DataItem<?>> fixedMap = new Int2ObjectOpenHashMap<>();
List<SynchedEntityData.DataItem<?>> items = new ArrayList<>(manager.itemsById.values());
for(SynchedEntityData.DataItem<?> item : items) {
fixedMap.put(item.getAccessor().id, item);
}
manager.lock.writeLock().lock();
try {
manager.itemsById.replaceAll((id, parameter) -> fixedMap.get((int)id));
if(entriesArrayField != null) {
try {
SynchedEntityData.DataItem<?>[] dataArray = new SynchedEntityData.DataItem[items.size()];
for(int i = 0; i < dataArray.length; i++) {
dataArray[i] = fixedMap.get(i);
}
entriesArrayField.set(manager, dataArray);
} catch(ReflectiveOperationException e) {
ModernFix.LOGGER.error(e);
}
}
} finally {
manager.lock.writeLock().unlock();
}
}
}
allEntityDatas.clear();
}
});
context.get().setPacketHandled(true);
}
@SubscribeEvent
public void onServerStarted(FMLServerStartingEvent event) {
if(ModernFixConfig.INTEGRATED_SERVER_WATCHDOG.get()) {
IntegratedWatchdog watchdog = new IntegratedWatchdog(event.getServer());
watchdog.start();
}
}
}