diff --git a/build.gradle b/build.gradle index 118b974..c083bd9 100644 --- a/build.gradle +++ b/build.gradle @@ -9,12 +9,15 @@ repositories { maven { url "https://raw.githubusercontent.com/acmi/Serializer/mvn-repo" } maven { url "https://raw.githubusercontent.com/acmi/L2crypt/mvn-repo" } maven { url "https://raw.githubusercontent.com/acmi/L2unreal/mvn-repo" } - maven { url "https://raw.githubusercontent.com/acmi/pec/mvn-repo" } + //maven { url "https://raw.githubusercontent.com/acmi/pec/mvn-repo" } maven { url "https://raw.githubusercontent.com/acmi/AutoCompleteComboBox/mvn-repo" } } dependencies { - compile group: 'acmi.l2.clientmod', name: 'pec', version: '1.3.+' + //compile group: 'acmi.l2.clientmod', name: 'pec', version: '1.3.+' + compile project(':pec') + + compile files('lib/jsquish.jar') } jar { diff --git a/lib/jsquish.jar b/lib/jsquish.jar new file mode 100644 index 0000000..a5216ad Binary files /dev/null and b/lib/jsquish.jar differ diff --git a/settings.gradle b/settings.gradle index 1679eab..e200458 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,4 @@ rootProject.name = 'L2pe' +include ':pec' +project(':pec').projectDir = new File(settingsDir, '../pec') \ No newline at end of file diff --git a/src/main/java/acmi/l2/clientmod/l2pe/Controllers.java b/src/main/java/acmi/l2/clientmod/l2pe/Controllers.java new file mode 100644 index 0000000..e8d1b57 --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/Controllers.java @@ -0,0 +1,228 @@ +package acmi.l2.clientmod.l2pe; + +import static acmi.l2.clientmod.l2pe.Util.showException; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.Charset; +import java.util.logging.Level; +import java.util.logging.Logger; + +import acmi.l2.clientmod.io.UnrealPackage; +import acmi.l2.clientmod.l2pe.view.SMView; +import javafx.application.Platform; +import javafx.fxml.FXMLLoader; +import javafx.fxml.JavaFXBuilderFactory; +import javafx.scene.Scene; +import javafx.stage.Stage; + +/** + * @author PointerRage + * + */ +public class Controllers { + private final static Logger log = Logger.getLogger(Controllers.class.getName()); + + private static MainWndController mainWndController; + + private static Stage importWndStage; + private static ImportWndController importWndController; + + private static Stage nameWndStage; + private static NameWndController nameWndController; + + private static Stage viewWndStage; + private static SMView viewWndController; + + private Controllers() { + throw new RuntimeException(); + } + + public static MainWndController getMainWndController() { + return mainWndController; + } + + public static void setMainWndController(MainWndController _mainWndController) { + mainWndController = _mainWndController; + } + + public static Stage getImportWndStage() { + return importWndStage; + } + + public static void setImportWndStage(Stage _importWndStage) { + importWndStage = _importWndStage; + } + + public static ImportWndController getImportWndController() { + return importWndController; + } + + public static void setImportWndController(ImportWndController _importWndController) { + importWndController = _importWndController; + } + + public static NameWndController getNameWndController() { + return nameWndController; + } + + public static void setNameWndController(NameWndController _nameWndController) { + nameWndController = _nameWndController; + } + + public static Stage getNameWndStage() { + return nameWndStage; + } + + public static void setNameWndStage(Stage _nameWndStage) { + nameWndStage = _nameWndStage; + } + + public static SMView getViewWndController() { + return viewWndController; + } + + public static void setViewWndController(SMView _viewWndController) { + viewWndController = _viewWndController; + } + + public static Stage getViewWndStage() { + return viewWndStage; + } + + public static void setViewWndStage(Stage _viewWndStage) { + viewWndStage = _viewWndStage; + } + + public static void setLoading(boolean value) { + mainWndController.setVisibleLoading(value); + if(importWndController != null) { + importWndController.setVisibleLoading(value); + } + + if(nameWndController != null) { + nameWndController.setVisibleLoading(value); + } + } + + public static void die() { + Platform.exit(); + } + + public static void showImportWnd() { + Stage stage = getImportWndStage(); + if (stage != null) { + if (!stage.isShowing()) { + stage.show(); + } + + stage.requestFocus(); + return; + } + + try { + FXMLLoader loader = new FXMLLoader(Controllers.class.getResource("importwnd.fxml"), null, new JavaFXBuilderFactory(), null, Charset.forName(FXMLLoader.DEFAULT_CHARSET_NAME)); + setImportWndStage(stage = new Stage()); + stage.setScene(new Scene(loader.load())); + setImportWndController(loader.getController()); + stage.setTitle("L2PE Import Editor"); + stage.show(); + } catch (Exception e) { + log.log(Level.SEVERE, "Failed to show importwnd", e); + showException("Failed to show importwnd", e); + } + } + + public static void showNameWnd() { + Stage stage = getNameWndStage(); + if (stage != null) { + if (!stage.isShowing()) { + stage.show(); + } + + stage.requestFocus(); + return; + } + + try { + FXMLLoader loader = new FXMLLoader(Controllers.class.getResource("namewnd.fxml"), null, new JavaFXBuilderFactory(), null, Charset.forName(FXMLLoader.DEFAULT_CHARSET_NAME)); + setNameWndStage(stage = new Stage()); + stage.setScene(new Scene(loader.load())); + setNameWndController(loader.getController()); + stage.setTitle("L2PE Name Editor"); + stage.show(); + } catch (Exception e) { + log.log(Level.SEVERE, "Failed to show namewnd", e); + showException("Failed to show namewnd", e); + } + } + + public static void showMeshView() { + Stage stage = getViewWndStage(); + + if (stage != null) { + if(!stage.isShowing()) { + stage.show(); + } + + stage.requestFocus(); + return; + } + + try { + final FXMLLoader loader = new FXMLLoader(Controllers.class.getResource("view/smview.fxml"), null, new JavaFXBuilderFactory(), null, Charset.forName(FXMLLoader.DEFAULT_CHARSET_NAME)); + final Scene scene = new Scene(loader.load()); + setViewWndController(loader.getController()); + setViewWndStage(stage = new Stage()); + stage.setScene(scene); + stage.setTitle("L2PE View"); + stage.show(); + } catch (Exception e) { + log.log(Level.SEVERE, "Failed to view mesh", e); + showException("Failed to view mesh", e); + } + } + + public static void showMesh(UnrealPackage.Entry mesh) { + showMeshView(); + + final File packageFile; + try { + packageFile = Util.getPackageFile(mesh); + } catch (IOException e) { + log.log(Level.SEVERE, "Failed to view mesh", e); + showException("Failed to view mesh", e); + return; + } + + final SMView controller = getViewWndController(); + try { + controller.setStaticmesh(packageFile, mesh.getObjectFullName()); + } catch (UncheckedIOException e) { + showException("Couldnt show mesh", e); + return; + } + } + + public static void showTexture(UnrealPackage.Entry tex) { + showMeshView(); + + final File packageFile; + try { + packageFile = Util.getPackageFile(tex); + } catch (IOException e) { + log.log(Level.SEVERE, "Failed to view mesh", e); + showException("Package not found", e); + return; + } + + final SMView controller = getViewWndController(); + try { + controller.setTexture(packageFile, tex.getObjectFullName()); + } catch(UncheckedIOException e) { + showException("Couldnt show viewer", e); + return; + } + } +} diff --git a/src/main/java/acmi/l2/clientmod/l2pe/ImportWndController.java b/src/main/java/acmi/l2/clientmod/l2pe/ImportWndController.java new file mode 100644 index 0000000..9404986 --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/ImportWndController.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2016 acmi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package acmi.l2.clientmod.l2pe; + +import static acmi.l2.clientmod.l2pe.Util.createBackup; +import static acmi.l2.clientmod.l2pe.Util.showException; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Collections; +import java.util.ResourceBundle; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; + +import acmi.l2.clientmod.io.UnrealPackage; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.TextField; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.GridPane; +import javafx.util.Pair; + +/** + * @author PointerRage + * + */ +public class ImportWndController implements Initializable { + private final static Logger log = Logger.getLogger(ImportWndController.class.getName()); + private ExecutorService executor = Executors.newSingleThreadExecutor(r -> new Thread(r, "L2pe ImportWndExecutor") { + { + setDaemon(true); + } + }); + + @FXML private Button addImport; + @FXML private Button editImport; + @FXML private Button deleteImport; + @FXML private Button sortImports; + + @FXML private ListView imports; + @FXML private ProgressIndicator loading; + + private boolean isNeedSort = false; + + public ImportWndController() { + } + + public void setVisibleLoading(boolean value) { + loading.setVisible(value); + } + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + imports.setCellFactory(list -> new ListCell() { + @Override + protected void updateItem(UnrealPackage.ImportEntry e, boolean bln) { + super.updateItem(e, bln); + if(e == null) { + return; + } + + setText(e.getObjectFullName() + " [" + e.getFullClassName() + "]"); + } + }); + imports.setOnMouseClicked(this::importAction); + + update(null); + } + + public synchronized void update(UnrealPackage _up) { + final MainWndController mainWnd = Controllers.getMainWndController(); + + final UnrealPackage up; + if(_up == null) { + if (!mainWnd.isPackageSelected()) { + return; + } + + up = mainWnd.getUnrealPackage(); + } else { + up = _up; + } + + executor.execute(() -> { + loading.setVisible(true); + try { + imports.getSelectionModel().clearSelection(); + Platform.runLater(() -> imports.getItems().setAll(up.getImportTable())); + sortIfNeeded(); + } finally { + loading.setVisible(false); + } + }); + } + + private void importAction(MouseEvent ev) { + if(ev.getClickCount() < 2) { + return; + } + + final MainWndController mainWnd = Controllers.getMainWndController(); + if (!mainWnd.isPackageSelected()) { + return; + } + + final UnrealPackage.ImportEntry selected = imports.getSelectionModel().getSelectedItem(); + if(selected == null) { + return; + } + + if(selected.getFullClassName().equals("Engine.Texture")) { + Controllers.showTexture(selected); + } else if(selected.getFullClassName().equals("Engine.StaticMesh")) { + Controllers.showMesh(selected); + } else if(selected.getFullClassName().equals("Engine.ColorModifier")) { + File packageFile; + try { + packageFile = Util.getPackageFile(selected); + } catch (IOException e) { + log.log(Level.SEVERE, "Failed to view mesh", e); + showException("Failed to view mesh", e); + return; + } + + Controllers.showMeshView(); + Controllers.getViewWndController().setColorModifier(packageFile, selected.getObjectFullName()); + } + } + + public void addImport() { + final MainWndController mainWnd = Controllers.getMainWndController(); + + if (!mainWnd.isPackageSelected()) + return; + + Dialog> dialog = new Dialog<>(); + dialog.setTitle("Create import entry"); + dialog.setHeaderText(null); + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + grid.setPadding(new Insets(20, 150, 10, 10)); + + TextField name = new TextField(); + name.setPromptText("Package.Name"); + TextField clazz = new TextField(); + clazz.setPromptText("Core.Class"); + + grid.add(new Label("Name:"), 0, 0); + grid.add(name, 1, 0); + grid.add(new Label("Class:"), 0, 1); + grid.add(clazz, 1, 1); + + dialog.getDialogPane().setContent(grid); + dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + dialog.setResultConverter(dialogButton -> { + if (dialogButton == ButtonType.OK) { + return new Pair<>(name.getText(), clazz.getText()); + } + return null; + }); + + dialog.showAndWait().ifPresent(nameClass -> executor.execute(() -> { + Controllers.setLoading(true); + + createBackup(); + try { + try (UnrealPackage up = new UnrealPackage(mainWnd.getUnrealPackage().getFile().openNewSession(false))) { + up.addImportEntries(Collections.singletonMap(nameClass.getKey(), nameClass.getValue())); + Platform.runLater(() -> mainWnd.setUnrealPackage(up)); + mainWnd.getEnvironment().markInvalid(up.getPackageName()); + Platform.runLater(() -> update(up)); + } + + mainWnd.reselectEntry(); + } catch (Exception e) { + log.log(Level.SEVERE, e, () -> "Couldn't add import entry"); + showException("Couldn't add import entry", e); + } finally { + Controllers.setLoading(false); + } + })); + } + + public void editImport() { + final MainWndController mainWnd = Controllers.getMainWndController(); + + if (!mainWnd.isPackageSelected()) { + return; + } + + final UnrealPackage.ImportEntry selected = imports.getSelectionModel().getSelectedItem(); + if(selected == null) { + return; + } + + Dialog dialog = new Dialog<>(); + dialog.setTitle("Create import entry"); + dialog.setHeaderText(null); + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + grid.setPadding(new Insets(20, 150, 10, 10)); + + TextField textName = new TextField(); + textName.setPromptText("Package.Name"); + textName.setText(selected.getObjectFullName()); + + grid.add(new Label("Name:"), 0, 0); + grid.add(textName, 1, 0); + + dialog.getDialogPane().setContent(grid); + dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + dialog.setResultConverter(dialogButton -> { + if (dialogButton == ButtonType.OK) { + return textName.getText(); + } + return null; + }); + + dialog.showAndWait().ifPresent(name -> executor.execute(() -> { + Controllers.setLoading(true); + + createBackup(); + try { + try (UnrealPackage up = new UnrealPackage(mainWnd.getUnrealPackage().getFile().openNewSession(false))) { + final int index = up.getImportTable().indexOf(selected); + if(index < 0) { + throw new Exception("Negative index!"); + } + up.renameImport(index, name); + Platform.runLater(() -> mainWnd.setUnrealPackage(up)); + mainWnd.getEnvironment().markInvalid(up.getPackageName()); + Platform.runLater(() -> update(up)); + } + + mainWnd.reselectEntry(); + } catch (Exception e) { + log.log(Level.SEVERE, e, () -> "Couldn't add import entry"); + showException("Couldn't add import entry", e); + } finally { + Controllers.setLoading(false); + } + })); + } + + public void deleteImport() { + final MainWndController mainWnd = Controllers.getMainWndController(); + + final UnrealPackage.ImportEntry selected = imports.getSelectionModel().getSelectedItem(); + if(selected == null) { + return; + } + + executor.execute(() -> { + Controllers.setLoading(true); + + createBackup(); + + try { + final File selectedFile = Controllers.getMainWndController().getSelectedPackage(); + if (selectedFile == null) { + return; + } + + try (UnrealPackage up = new UnrealPackage(selectedFile, false)) { + up.updateImportTable(table -> table.remove(selected)); + Platform.runLater(() -> mainWnd.setUnrealPackage(up)); + mainWnd.getEnvironment().markInvalid(up.getPackageName()); + Platform.runLater(() -> update(up)); + } catch (Exception e) { + log.log(Level.SEVERE, "Failed to delete selected import entry", e); + showException("Failed to delete selected import entry", e); + } + + mainWnd.reselectEntry(); + } finally { + Controllers.setLoading(false); + } + }); + } + + public void sortImports() { + isNeedSort = !isNeedSort; + sortIfNeeded(); + if(!isNeedSort) { + Platform.runLater(() -> update(null)); + } + } + + private void sortIfNeeded() { + if(!isNeedSort) { + return; + } + + Util.sort(imports); + } +} diff --git a/src/main/java/acmi/l2/clientmod/l2pe/L2PE.java b/src/main/java/acmi/l2/clientmod/l2pe/L2PE.java index a54f9bf..95ff4e4 100644 --- a/src/main/java/acmi/l2/clientmod/l2pe/L2PE.java +++ b/src/main/java/acmi/l2/clientmod/l2pe/L2PE.java @@ -21,16 +21,6 @@ */ package acmi.l2.clientmod.l2pe; -import javafx.application.Application; -import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.binding.Bindings; -import javafx.fxml.FXMLLoader; -import javafx.geometry.Rectangle2D; -import javafx.scene.Scene; -import javafx.stage.Screen; -import javafx.stage.Stage; - import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -43,11 +33,31 @@ import java.util.logging.Logger; import java.util.prefs.Preferences; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.binding.Bindings; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Rectangle2D; +import javafx.scene.Scene; +import javafx.stage.Screen; +import javafx.stage.Stage; + public class L2PE extends Application { private static final Logger log = Logger.getLogger(L2PE.class.getName()); - + private static L2PE instance; + + public static L2PE getInstance() { + return instance; + } + private Stage stage; private String version; + + public L2PE() { + super(); + instance = this; + } Stage getStage() { return stage; @@ -65,8 +75,9 @@ public void start(Stage stage) throws Exception { FXMLLoader loader = new FXMLLoader(getClass().getResource("main.fxml")); stage.setScene(new Scene(loader.load())); - Controller controller = loader.getController(); + MainWndController controller = loader.getController(); controller.setApplication(this); + stage.setOnCloseRequest(we -> Controllers.die()); stage.titleProperty().bind(Bindings.createStringBinding(() -> (controller.getEnvironment() != null ? controller.getEnvironment().getStartDir().getAbsolutePath() + " - " : "") + "L2PE " + version, controller.environmentProperty())); stage.setWidth(Double.parseDouble(windowPrefs().get("width", String.valueOf(stage.getWidth())))); diff --git a/src/main/java/acmi/l2/clientmod/l2pe/Controller.java b/src/main/java/acmi/l2/clientmod/l2pe/MainWndController.java similarity index 73% rename from src/main/java/acmi/l2/clientmod/l2pe/Controller.java rename to src/main/java/acmi/l2/clientmod/l2pe/MainWndController.java index b54a542..a1630e7 100644 --- a/src/main/java/acmi/l2/clientmod/l2pe/Controller.java +++ b/src/main/java/acmi/l2/clientmod/l2pe/MainWndController.java @@ -21,10 +21,36 @@ */ package acmi.l2.clientmod.l2pe; +import static acmi.l2.clientmod.l2pe.Util.SAVE_DEFAULTS; +import static acmi.l2.clientmod.l2pe.Util.selectItem; +import static acmi.l2.clientmod.l2pe.Util.showException; +import static acmi.l2.clientmod.unreal.UnrealSerializerFactory.IS_STRUCT; +import static acmi.util.AutoCompleteComboBox.getSelectedItem; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + import acmi.l2.clientmod.io.ObjectOutput; import acmi.l2.clientmod.io.ObjectOutputStream; import acmi.l2.clientmod.io.UnrealPackage; import acmi.l2.clientmod.properties.control.PropertiesEditor; +import acmi.l2.clientmod.properties.control.skin.edit.ObjectEdit; import acmi.l2.clientmod.unreal.Environment; import acmi.l2.clientmod.unreal.UnrealRuntimeContext; import acmi.l2.clientmod.unreal.core.Class; @@ -43,31 +69,30 @@ import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.geometry.Insets; -import javafx.scene.control.*; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Dialog; +import javafx.scene.control.DialogPane; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.Menu; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.Separator; +import javafx.scene.control.TextField; +import javafx.scene.control.TextInputDialog; import javafx.scene.layout.GridPane; -import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.StageStyle; -import javafx.util.Pair; import javafx.util.StringConverter; -import java.io.*; -import java.net.URL; -import java.util.*; -import java.util.function.Consumer; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import static acmi.l2.clientmod.unreal.UnrealSerializerFactory.IS_STRUCT; -import static acmi.util.AutoCompleteComboBox.getSelectedItem; - -public class Controller extends ControllerBase implements Initializable { - private static final Logger log = Logger.getLogger(Controller.class.getName()); - - private static final boolean SAVE_DEFAULTS = System.getProperty("L2pe.saveDefaults", "false").equalsIgnoreCase("true"); - private static final boolean SHOW_STACKTRACE = System.getProperty("L2pe.showStackTrace", "false").equalsIgnoreCase("true"); +public class MainWndController extends ControllerBase implements Initializable { + private static final Logger log = Logger.getLogger(MainWndController.class.getName()); @FXML private Menu packageMenu; @@ -86,17 +111,23 @@ public class Controller extends ControllerBase implements Initializable { @FXML private ComboBox entrySelector; @FXML - private Button addName; + private Button showNameWnd; @FXML - private Button addImport; + private Button showImportWnd; @FXML private Button addExport; @FXML private Button save; @FXML private Button copy; + @FXML + private Button delete; + @FXML + private Button update; @FXML private CheckMenuItem showAllProperties; + @FXML + private CheckMenuItem makeBackups; @FXML private PropertiesEditor properties; @FXML @@ -120,6 +151,25 @@ public ObjectProperty initialDirectoryProperty() { public void setInitialDirectory(File initialDirectory) { this.initialDirectory.set(initialDirectory); } + + public boolean isBackupEnable() { + return makeBackups.isSelected(); + } + + public void reselectEntry() { + final UnrealPackage.ExportEntry selected = getEntry(); + if (selected != null) { + Platform.runLater(() -> selectItem(entrySelector, selected)); + } + } + + public void setVisibleLoading(boolean isLoading) { + loading.setVisible(isLoading); + } + + public File getSelectedPackage() { + return packageSelector.getValue(); + } @Override protected void execute(Task task, Consumer exceptionHandler) { @@ -140,6 +190,7 @@ private Task wrap(Task task) { @Override public void initialize(URL url, ResourceBundle resourceBundle) { + Controllers.setMainWndController(this); setInitialDirectory(new File(L2PE.getPrefs().get("initialDirectory", System.getProperty("user.dir")))); initialDirectoryProperty().addListener((observable, oldVal, newVal) -> { if (newVal != null) @@ -203,6 +254,16 @@ public File fromString(String string) { execute(() -> { try (UnrealPackage up = new UnrealPackage(newValue, true)) { Platform.runLater(() -> setUnrealPackage(up)); + + final ImportWndController iwc = Controllers.getImportWndController(); + if(iwc != null) { + iwc.update(up); + } + + final NameWndController nwc = Controllers.getNameWndController(); + if(nwc != null) { + nwc.update(up); + } } }, e -> { log.log(Level.SEVERE, e, () -> "Couldn't load: " + newValue); @@ -213,8 +274,8 @@ public File fromString(String string) { packageMenu.disableProperty().bind(packageSelected().not()); entrySeparator.visibleProperty().bind(packageSelected()); - addName.visibleProperty().bind(packageSelected()); - addImport.visibleProperty().bind(packageSelected()); + showNameWnd.visibleProperty().bind(packageSelected()); + showImportWnd.visibleProperty().bind(packageSelected()); addExport.visibleProperty().bind(packageSelected()); entrySelector.visibleProperty().bind(packageSelected()); unrealPackageProperty().addListener((observable, oldValue, newValue) -> { @@ -263,115 +324,96 @@ public File fromString(String string) { entryMenu.disableProperty().bind(entrySelected().not()); save.visibleProperty().bind(entrySelected()); copy.visibleProperty().bind(Bindings.createBooleanBinding(() -> canCopy(getObject()), objectProperty())); - + delete.visibleProperty().bind(entrySelected()); + update.visibleProperty().bind(entrySelected()); + + makeBackups.setSelected(true); + + ObjectEdit.getInstance().addElement(context -> { + String type = ((acmi.l2.clientmod.unreal.core.ObjectProperty) context.getTemplate()).type.getFullName(); + if(!type.equals("Engine.StaticMesh")) { + return null; + } + + Button viewButton = new Button("View"); + viewButton.setMinWidth(Region.USE_PREF_SIZE); + viewButton.setOnAction(e -> { + final ComboBox cb = (ComboBox) context.getEditorNode(); + final UnrealPackage.Entry entry = cb.getSelectionModel().getSelectedItem(); + if(entry == null) { + return; + } + + Controllers.showMesh(entry); + }); + return viewButton; + }); + + ObjectEdit.getInstance().addElement(context -> { + String type = ((acmi.l2.clientmod.unreal.core.ObjectProperty) context.getTemplate()).type.getFullName(); + if(!type.equals("Engine.Texture")) { + return null; + } + + Button viewButton = new Button("View"); + viewButton.setMinWidth(Region.USE_PREF_SIZE); + viewButton.setOnAction(e -> { + final ComboBox cb = (ComboBox) context.getEditorNode(); + final UnrealPackage.Entry entry = cb.getSelectionModel().getSelectedItem(); + if(entry == null) { + return; + } + + Controllers.showTexture(entry); + }); + return viewButton; + }); + loading.setVisible(false); + + final Map namedParams = L2PE.getInstance().getParameters().getNamed(); + String value = namedParams.get("ini"); + if(value != null) { + final File file = new File(value); + if(!file.exists() || !file.isFile()) { + showException("l2.ini not found", new Exception()); + } + setIni(file); + } } public void selectL2ini() { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle("Open l2.ini"); - fileChooser.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("L2.ini", "L2.ini"), - new FileChooser.ExtensionFilter("All files", "*.*")); - - if (getInitialDirectory() != null && - getInitialDirectory().exists() && - getInitialDirectory().isDirectory()) - fileChooser.setInitialDirectory(getInitialDirectory()); - - File selected = fileChooser.showOpenDialog(application.getStage()); - if (selected == null) - return; - - setInitialDirectory(selected.getParentFile()); - - try { - setEnvironment(Environment.fromIni(selected)); - } catch (Exception e) { - log.log(Level.SEVERE, e, () -> "Couldn't load L2.ini"); - - showException("Couldn't load L2.ini", e); - return; - } - - execute(() -> getSerializerFactory().getOrCreateObject("Engine.Actor", IS_STRUCT), e -> { - log.log(Level.SEVERE, e, () -> "Couldn't load Engine.Actor"); - - showException("Couldn't load Engine.Actor", e); - }); - } - - public void addName() { - if (!isPackageSelected()) - return; - - TextInputDialog dialog = new TextInputDialog(); - dialog.setTitle("Create name entry"); - dialog.setHeaderText(null); - dialog.setContentText("Name string:"); - dialog.showAndWait() - .ifPresent(name -> execute(() -> { - try (UnrealPackage up = new UnrealPackage(getUnrealPackage().getFile().openNewSession(false))) { - up.addNameEntries(name); - Platform.runLater(() -> setUnrealPackage(up)); - - getEnvironment().markInvalid(up.getPackageName()); - } - }, e -> { - log.log(Level.SEVERE, e, () -> "Couldn't add name entry"); - - showException("Couldn't add name entry", e); - } - )); - } - - public void addImport() { - if (!isPackageSelected()) - return; - - Dialog> dialog = new Dialog<>(); - dialog.setTitle("Create import entry"); - dialog.setHeaderText(null); - - GridPane grid = new GridPane(); - grid.setHgap(10); - grid.setVgap(10); - grid.setPadding(new Insets(20, 150, 10, 10)); - - TextField name = new TextField(); - name.setPromptText("Package.Name"); - TextField clazz = new TextField(); - clazz.setPromptText("Core.Class"); - - grid.add(new Label("Name:"), 0, 0); - grid.add(name, 1, 0); - grid.add(new Label("Class:"), 0, 1); - grid.add(clazz, 1, 1); - - dialog.getDialogPane().setContent(grid); - dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); - - dialog.setResultConverter(dialogButton -> { - if (dialogButton == ButtonType.OK) { - return new Pair<>(name.getText(), clazz.getText()); - } - return null; - }); - - dialog.showAndWait() - .ifPresent(nameClass -> execute(() -> { - try (UnrealPackage up = new UnrealPackage(getUnrealPackage().getFile().openNewSession(false))) { - up.addImportEntries(Collections.singletonMap(nameClass.getKey(), nameClass.getValue())); - Platform.runLater(() -> setUnrealPackage(up)); - - getEnvironment().markInvalid(up.getPackageName()); - } - }, e -> { - log.log(Level.SEVERE, e, () -> "Couldn't add import entry"); - - showException("Couldn't add import entry", e); - })); - } + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Open l2.ini"); + fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("L2.ini", "L2.ini"), new FileChooser.ExtensionFilter("All files", "*.*")); + + if (getInitialDirectory() != null && getInitialDirectory().exists() && getInitialDirectory().isDirectory()) + fileChooser.setInitialDirectory(getInitialDirectory()); + fileChooser.setInitialFileName("l2.ini"); + + File selected = fileChooser.showOpenDialog(application.getStage()); + if (selected == null) + return; + + setIni(selected); + } + + private void setIni(File file) { + setInitialDirectory(file.getParentFile()); + + try { + setEnvironment(Environment.fromIni(file)); + } catch (Exception e) { + log.log(Level.SEVERE, e, () -> "Couldn't load L2.ini"); + showException("Couldn't load L2.ini", e); + return; + } + + execute(() -> getSerializerFactory().getOrCreateObject("Engine.Actor", IS_STRUCT), e -> { + log.log(Level.SEVERE, e, () -> "Couldn't load Engine.Actor"); + showException("Couldn't load Engine.Actor", e); + }); + } public void addExport() { if (!isPackageSelected()) @@ -412,6 +454,8 @@ public void addExport() { dialog.showAndWait() .ifPresent(nameClass -> execute(() -> { + Util.createBackup(); + try (UnrealPackage up = new UnrealPackage(getUnrealPackage().getFile().openNewSession(false))) { String objName = nameClass[1]; int flags = UnrealPackage.DEFAULT_OBJECT_FLAGS; @@ -448,6 +492,8 @@ public void save() { return; execute(() -> { + Util.createBackup(); + try (UnrealPackage up = new UnrealPackage(getUnrealPackage().getFile().openNewSession(false))) { UnrealPackage.ExportEntry entry = up.getExportTable().get(selected.getIndex()); Object object = getSerializerFactory().getOrCreateObject(entry); @@ -506,6 +552,8 @@ public void copy() { dialog.setHeaderText(null); dialog.setContentText("New name:"); dialog.showAndWait().ifPresent(name -> execute(() -> { + Util.createBackup(); + try (UnrealPackage up = new UnrealPackage(getUnrealPackage().getFile().openNewSession(false))) { if (object.getClass() == Object.class) { up.addExportEntry(name, @@ -534,6 +582,60 @@ public void copy() { showException("Couldn't copy entry", e); })); } + + public void delete() { + if (!isEntrySelected()) + return; + + UnrealPackage.ExportEntry selected = getSelectedItem(entrySelector); + if (selected == null) + return; + + execute(() -> { + Util.createBackup(); + + try (UnrealPackage up = new UnrealPackage(getUnrealPackage().getFile().openNewSession(false))) { + up.updateExportTable(exportTable -> { + exportTable.remove(selected); + up.getFile().setPosition(up.getDataEndOffset().orElseThrow(IllegalStateException::new)); + }); + + entrySelector.getSelectionModel().clearSelection(); + Platform.runLater(() -> setUnrealPackage(up)); + getEnvironment().markInvalid(up.getPackageName()); + } + }, e -> { + log.log(Level.SEVERE, e, () -> "Couldn't delete entry"); + showException("Couldn't delete entry", e); + }); + } + + public void update() { + UnrealPackage.ExportEntry selected = getSelectedItem(entrySelector); + if (selected == null) { + return; + } + + execute(() -> { + try (UnrealPackage up = new UnrealPackage(getUnrealPackage().getFile().openNewSession(false))) { + Platform.runLater(() -> setUnrealPackage(up)); + getEnvironment().markInvalid(up.getPackageName()); + } + + Platform.runLater(() -> selectItem(entrySelector, selected)); + }, e -> { + log.log(Level.SEVERE, e, () -> "Couldn't update entry"); + showException("Couldn't update entry", e); + }); + } + + public void showImportWnd() { + Controllers.showImportWnd(); + } + + public void showNameWnd() { + Controllers.showNameWnd(); + } public void exportProperties() { if (!isEntrySelected()) @@ -576,7 +678,7 @@ public void exportProperties() { } public void about() { - Dialog dialog = new Dialog(); + Dialog dialog = new Dialog<>(); dialog.initStyle(StageStyle.UTILITY); dialog.setTitle("About"); @@ -600,58 +702,6 @@ public void about() { } public void exit() { - Platform.exit(); - } - - private void showException(String text, Throwable ex) { - Platform.runLater(() -> { - if (SHOW_STACKTRACE) { - Alert alert = new Alert(Alert.AlertType.ERROR); - alert.setTitle("Error"); - alert.setHeaderText(null); - alert.setContentText(text); - - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - ex.printStackTrace(pw); - String exceptionText = sw.toString(); - - Label label = new Label("Exception stacktrace:"); - - TextArea textArea = new TextArea(exceptionText); - textArea.setEditable(false); - textArea.setWrapText(true); - - textArea.setMaxWidth(Double.MAX_VALUE); - textArea.setMaxHeight(Double.MAX_VALUE); - GridPane.setVgrow(textArea, Priority.ALWAYS); - GridPane.setHgrow(textArea, Priority.ALWAYS); - - GridPane expContent = new GridPane(); - expContent.setMaxWidth(Double.MAX_VALUE); - expContent.add(label, 0, 0); - expContent.add(textArea, 0, 1); - - alert.getDialogPane().setExpandableContent(expContent); - - alert.showAndWait(); - } else { - //noinspection ThrowableResultOfMethodCallIgnored - Throwable t = getTop(ex); - - Alert alert = new Alert(Alert.AlertType.ERROR); - alert.setTitle(t.getClass().getSimpleName()); - alert.setHeaderText(text); - alert.setContentText(t.getMessage()); - - alert.showAndWait(); - } - }); - } - - private static Throwable getTop(Throwable t) { - while (t.getCause() != null) - t = t.getCause(); - return t; + Controllers.die(); } } diff --git a/src/main/java/acmi/l2/clientmod/l2pe/NameWndController.java b/src/main/java/acmi/l2/clientmod/l2pe/NameWndController.java new file mode 100644 index 0000000..0a8709e --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/NameWndController.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2016 acmi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package acmi.l2.clientmod.l2pe; + +import static acmi.l2.clientmod.l2pe.Util.createBackup; +import static acmi.l2.clientmod.l2pe.Util.showException; + +import java.net.URL; +import java.util.ResourceBundle; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; + +import acmi.l2.clientmod.io.UnrealPackage; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Button; +import javafx.scene.control.ListView; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.TextInputDialog; + +/** + * @author PointerRage + * + */ +public class NameWndController implements Initializable { + private final static Logger log = Logger.getLogger(NameWndController.class.getName()); + private ExecutorService executor = Executors.newSingleThreadExecutor(r -> new Thread(r, "L2pe NameWndExecutor") { + { + setDaemon(true); + } + }); + + @FXML private Button addName; + @FXML private Button editName; + @FXML private Button deleteName; + @FXML private Button sortName; + + @FXML private ListView names; + @FXML private ProgressIndicator loading; + + private boolean isNeedSort = false; + + public NameWndController() { + } + + public void setVisibleLoading(boolean value) { + loading.setVisible(value); + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + update(null); + } + + public void update(UnrealPackage _up) { + final MainWndController mainWnd = Controllers.getMainWndController(); + + final UnrealPackage up; + if(_up == null) { + if (!mainWnd.isPackageSelected()) { + return; + } + + up = mainWnd.getUnrealPackage(); + } else { + up = _up; + } + + executor.execute(() -> { + loading.setVisible(true); + try { + names.getSelectionModel().clearSelection(); + Platform.runLater(() -> names.getItems().setAll(up.getNameTable())); + sortIfNeeded(); + } finally { + loading.setVisible(false); + } + }); + } + + public void addName() { + final MainWndController mainWnd = Controllers.getMainWndController(); + if (!mainWnd.isPackageSelected()) { + return; + } + + TextInputDialog dialog = new TextInputDialog(); + dialog.setTitle("Create name entry"); + dialog.setHeaderText(null); + dialog.setContentText("Name string:"); + dialog.showAndWait().ifPresent(name -> executor.execute(() -> { + Controllers.setLoading(true); + + createBackup(); + + try { + try (UnrealPackage up = new UnrealPackage(mainWnd.getUnrealPackage().getFile().openNewSession(false))) { + up.addNameEntries(name); + Platform.runLater(() -> mainWnd.setUnrealPackage(up)); + mainWnd.getEnvironment().markInvalid(up.getPackageName()); + Platform.runLater(() -> update(up)); + } + + mainWnd.reselectEntry(); + } catch(Exception e) { + log.log(Level.SEVERE, e, () -> "Couldn't add name entry"); + showException("Couldn't add name entry", e); + } finally { + Controllers.setLoading(false); + } + })); + } + + public void editName() { + final MainWndController mainWnd = Controllers.getMainWndController(); + if (!mainWnd.isPackageSelected()) { + return; + } + + final UnrealPackage.NameEntry selected = names.getSelectionModel().getSelectedItem(); + if(selected == null) { + return; + } + + TextInputDialog dialog = new TextInputDialog(); + dialog.setTitle("Create name entry"); + dialog.setHeaderText(null); + dialog.setContentText("Name string:"); + dialog.showAndWait().ifPresent(name -> executor.execute(() -> { + Controllers.setLoading(true); + + createBackup(); + + try { + try (UnrealPackage up = new UnrealPackage(mainWnd.getUnrealPackage().getFile().openNewSession(false))) { + up.updateNameEntry(selected.getIndex(), name, selected.getFlags()); + Platform.runLater(() -> mainWnd.setUnrealPackage(up)); + mainWnd.getEnvironment().markInvalid(up.getPackageName()); + Platform.runLater(() -> update(up)); + } + + mainWnd.reselectEntry(); + } catch(Exception e) { + log.log(Level.SEVERE, e, () -> "Couldn't edit name entry"); + showException("Couldn't edit name entry", e); + } finally { + Controllers.setLoading(false); + } + })); + } + + public void deleteName() { + final MainWndController mainWnd = Controllers.getMainWndController(); + if (!mainWnd.isPackageSelected()) + return; + + final UnrealPackage.NameEntry selected = names.getSelectionModel().getSelectedItem(); + if(selected == null) { + return; + } + + executor.execute(() -> { + Controllers.setLoading(true); + + createBackup(); + + try { + try (UnrealPackage up = new UnrealPackage(mainWnd.getUnrealPackage().getFile().openNewSession(false))) { + up.updateNameTable(table -> table.remove(selected.getIndex())); + Platform.runLater(() -> mainWnd.setUnrealPackage(up)); + mainWnd.getEnvironment().markInvalid(up.getPackageName()); + Platform.runLater(() -> update(up)); + } + + mainWnd.reselectEntry(); + } catch(Exception e) { + log.log(Level.SEVERE, e, () -> "Couldn't delete name entry"); + showException("Couldn't delete name entry", e); + } finally { + Controllers.setLoading(false); + } + }); + } + + public void sortName() { + isNeedSort = !isNeedSort; + sortIfNeeded(); + if(!isNeedSort) { + Platform.runLater(() -> update(null)); + } + } + + private void sortIfNeeded() { + if(!isNeedSort) { + return; + } + + Util.sort(names); + } +} diff --git a/src/main/java/acmi/l2/clientmod/l2pe/Util.java b/src/main/java/acmi/l2/clientmod/l2pe/Util.java index 0f657e6..444203b 100644 --- a/src/main/java/acmi/l2/clientmod/l2pe/Util.java +++ b/src/main/java/acmi/l2/clientmod/l2pe/Util.java @@ -1,20 +1,51 @@ package acmi.l2.clientmod.l2pe; -import acmi.l2.clientmod.io.*; -import acmi.l2.clientmod.unreal.UnrealRuntimeContext; -import acmi.l2.clientmod.unreal.UnrealSerializerFactory; -import acmi.l2.clientmod.unreal.properties.L2Property; -import acmi.l2.clientmod.unreal.properties.PropertiesUtil; +import static acmi.l2.clientmod.io.UnrealPackage.ObjectFlag.HasStack; +import static acmi.l2.clientmod.io.UnrealPackage.ObjectFlag.Standalone; +import static acmi.l2.clientmod.unreal.UnrealSerializerFactory.IS_STRUCT; import java.io.ByteArrayOutputStream; -import java.util.*; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.stream.Stream; -import static acmi.l2.clientmod.io.UnrealPackage.ObjectFlag.HasStack; -import static acmi.l2.clientmod.io.UnrealPackage.ObjectFlag.Standalone; -import static acmi.l2.clientmod.unreal.UnrealSerializerFactory.IS_STRUCT; +import acmi.l2.clientmod.io.ObjectOutput; +import acmi.l2.clientmod.io.ObjectOutputStream; +import acmi.l2.clientmod.io.RandomAccess; +import acmi.l2.clientmod.io.RandomAccessFile; +import acmi.l2.clientmod.io.UnrealPackage; +import acmi.l2.clientmod.unreal.UnrealRuntimeContext; +import acmi.l2.clientmod.unreal.UnrealSerializerFactory; +import acmi.l2.clientmod.unreal.properties.L2Property; +import acmi.l2.clientmod.unreal.properties.PropertiesUtil; +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.TextArea; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; public class Util { + private Util() { + throw new RuntimeException(); + } + + public static final boolean SAVE_DEFAULTS = System.getProperty("L2pe.saveDefaults", "false").equalsIgnoreCase("true"); + public static final boolean SHOW_STACKTRACE = System.getProperty("L2pe.showStackTrace", "false").equalsIgnoreCase("true"); + public static void createClass(UnrealSerializerFactory serializer, UnrealPackage up, String objName, String objSuperClass, int flags, List properties) { flags |= Standalone.getMask(); @@ -97,4 +128,112 @@ public static void createObject(UnrealSerializerFactory serializer, UnrealPackag entry.setObjectRawData(baos.toByteArray()); } + + public static void createBackup() { + final MainWndController mainWnd = Controllers.getMainWndController(); + if(!mainWnd.isBackupEnable() || !mainWnd.isPackageSelected()) { + return; + } + + final RandomAccess ra = mainWnd.getUnrealPackage().getFile(); + if (ra instanceof RandomAccessFile) { + final RandomAccessFile raf = (RandomAccessFile) ra; + final File source = new File(raf.getPath()); + final String time = DateTimeFormatter.ofPattern("yyyy_MM_dd-HH_mm_ss").format(LocalDateTime.now()); + String path = source.getPath(); + path = path.substring(0, path.lastIndexOf(File.separatorChar)); + final File backup = new File(path, source.getName() + "_" + time); + + try { + Files.copy(source.toPath(), backup.toPath()); + } catch(IOException e) { + showException("Failed to create backup", e); + } + } + } + + public static File getPackageFile(UnrealPackage.Entry entry) throws IOException { + final MainWndController mainWnd = Controllers.getMainWndController(); + final File root = mainWnd.getInitialDirectory().getParentFile(); + final String entryPath = entry.getObjectFullName(); + final String packageName = entryPath.substring(0, entryPath.indexOf('.')); + + return Files.walk(root.toPath()) + .filter(p -> p.toFile().isFile()) + .map(p -> p.toFile()) + .filter(f -> f.getName().contains(".") && f.getName().substring(0, f.getName().lastIndexOf('.')).equalsIgnoreCase(packageName)) + .findAny() + .orElseThrow(IOException::new); + } + + public static void showException(String text, Throwable ex) { + Platform.runLater(() -> { + if (SHOW_STACKTRACE) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText(null); + alert.setContentText(text); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + ex.printStackTrace(pw); + String exceptionText = sw.toString(); + + Label label = new Label("Exception stacktrace:"); + + TextArea textArea = new TextArea(exceptionText); + textArea.setEditable(false); + textArea.setWrapText(true); + + textArea.setMaxWidth(Double.MAX_VALUE); + textArea.setMaxHeight(Double.MAX_VALUE); + GridPane.setVgrow(textArea, Priority.ALWAYS); + GridPane.setHgrow(textArea, Priority.ALWAYS); + + GridPane expContent = new GridPane(); + expContent.setMaxWidth(Double.MAX_VALUE); + expContent.add(label, 0, 0); + expContent.add(textArea, 0, 1); + + alert.getDialogPane().setExpandableContent(expContent); + + alert.showAndWait(); + } else { + Throwable t = getTop(ex); + + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle(t.getClass().getSimpleName()); + alert.setHeaderText(text); + alert.setContentText(t.getMessage()); + + alert.showAndWait(); + } + }); + } + + private static Throwable getTop(Throwable t) { + while (t.getCause() != null) + t = t.getCause(); + return t; + } + + public static void sort(ListView list) { + Platform.runLater(() -> Collections.sort(list.getItems(), new Comparator() { + @Override + public int compare(T o1, T o2) { + return o1.toString().compareTo(o2.toString()); + } + })); + } + + public static void selectItem(ComboBox comboBox, T select) { + for(T item : comboBox.getItems()) { + if(!item.equals(select)) { + continue; + } + + comboBox.getSelectionModel().select(item); + break; + } + } } diff --git a/src/main/java/acmi/l2/clientmod/l2pe/view/PerspectiveCameraWrap.java b/src/main/java/acmi/l2/clientmod/l2pe/view/PerspectiveCameraWrap.java new file mode 100644 index 0000000..e0e7d9e --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/view/PerspectiveCameraWrap.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016 acmi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package acmi.l2.clientmod.l2pe.view; + +import javafx.beans.NamedArg; +import javafx.scene.PerspectiveCamera; + +public class PerspectiveCameraWrap extends PerspectiveCamera { + public PerspectiveCameraWrap() { + } + + public PerspectiveCameraWrap(@NamedArg("fixedEyeAtCameraZero") boolean fixedEyeAtCameraZero) { + super(fixedEyeAtCameraZero); + } +} diff --git a/src/main/java/acmi/l2/clientmod/l2pe/view/SMView.java b/src/main/java/acmi/l2/clientmod/l2pe/view/SMView.java new file mode 100644 index 0000000..09085ce --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/view/SMView.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016 acmi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package acmi.l2.clientmod.l2pe.view; + +import java.io.File; +import java.net.URL; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.stream.Collectors; + +import javafx.beans.binding.Bindings; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.SubScene; +import javafx.scene.control.Label; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Pane; + +public class SMView implements Initializable { + @FXML + private Pane root; + @FXML + private SubScene view3dScene; + @FXML + private View3D view3dController; + @FXML + private GridPane properties; + @FXML + private Label vertex; + @FXML + private Label triangles; + @FXML + private Label sections; + @FXML + private Label materials; + + public void setStaticmesh(File packageFile, String obj) { + view3dController.setStaticmesh(packageFile, obj); + } + + public void setTexture(File packageFile, String obj) { + view3dController.setTexture(packageFile, obj); + } + + public void setColorModifier(File packageFile, String objName) { + view3dController.setColorModifier(packageFile, objName); + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + view3dScene.widthProperty().bind(root.widthProperty()); + view3dScene.heightProperty().bind(root.heightProperty()); + view3dScene.setCamera(view3dController.getCamera()); + + properties.setVisible(true); + vertex.textProperty().bind(view3dController.pointsProperty().asString()); + triangles.textProperty().bind(view3dController.trianglesProperty().asString()); + sections.textProperty().bind(Bindings.createStringBinding(() -> view3dController.getMaterials() == null ? "0" : String.valueOf(view3dController.getMaterials().size()), view3dController.materialsProperty())); + materials.textProperty().bind(Bindings.createStringBinding(() -> view3dController.getMaterials() == null ? "" : view3dController.getMaterials().stream().filter(Objects::nonNull).distinct().sorted(String.CASE_INSENSITIVE_ORDER).collect(Collectors.joining("\n")), view3dController.materialsProperty())); + } + + public void onMousePressed(MouseEvent me) { + view3dController.onMousePressed(me); + } + + public void onMouseDragged(MouseEvent me) { + view3dController.onMouseDragged(me); + } + +// public void onMouseWheel(ZoomEvent ze) { +// view3dController.onMouseWheel(ze); +// } +} diff --git a/src/main/java/acmi/l2/clientmod/l2pe/view/View3D.java b/src/main/java/acmi/l2/clientmod/l2pe/view/View3D.java new file mode 100644 index 0000000..adde17f --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/view/View3D.java @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2016 acmi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package acmi.l2.clientmod.l2pe.view; + +import static acmi.l2.clientmod.io.BufferUtil.getCompactInt; +import static acmi.l2.clientmod.l2pe.view.helpers.Util.find; +import static acmi.l2.clientmod.l2pe.view.helpers.Util.iterateProperties; +import static acmi.l2.clientmod.l2pe.view.helpers.Util.nameFilter; +import static javafx.scene.shape.VertexFormat.POINT_NORMAL_TEXCOORD; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import acmi.l2.clientmod.io.UnrealPackage; +import acmi.l2.clientmod.l2pe.Controllers; +import acmi.l2.clientmod.l2pe.view.helpers.ImageUtil; +import acmi.l2.clientmod.l2pe.view.model.StaticMesh; +import acmi.l2.clientmod.unreal.properties.L2Property; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ListProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableFloatArray; +import javafx.collections.ObservableList; +import javafx.embed.swing.SwingFXUtils; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.PerspectiveCamera; +import javafx.scene.input.MouseEvent; +import javafx.scene.paint.Color; +import javafx.scene.paint.PhongMaterial; +import javafx.scene.shape.Box; +import javafx.scene.shape.CullFace; +import javafx.scene.shape.MeshView; +import javafx.scene.shape.TriangleMesh; +import javafx.util.Pair; + +public class View3D implements Initializable { + private static final double CAMERA_INITIAL_X_ANGLE = 135; + private static final double CAMERA_INITIAL_Z_ANGLE = -45; + private static final double ROTATION_SPEED = 0.2; + private static final double ZOOM_SPEED = 0.0025; + + @FXML + private Xform staticmeshGroup; + @FXML + private PerspectiveCamera camera; + @FXML + private Xform cameraXform; + @FXML + private Xform cameraXform2; + @FXML + private Xform cameraXform3; + + private IntegerProperty points = new SimpleIntegerProperty(this, "points"); + private IntegerProperty triangles = new SimpleIntegerProperty(this, "triangles"); + private ListProperty materials = new SimpleListProperty<>(this, "materials"); + + private double mousePosX; + private double mousePosY; + private double mouseOldX; + private double mouseOldY; + + private double zoomSpeed = 1.0; + + public PerspectiveCamera getCamera() { + return camera; + } + + public int getPoints() { + return points.get(); + } + + public ReadOnlyIntegerProperty pointsProperty() { + return points; + } + + public int getTriangles() { + return triangles.get(); + } + + public ReadOnlyIntegerProperty trianglesProperty() { + return triangles; + } + + public ObservableList getMaterials() { + return materials.get(); + } + + public ListProperty materialsProperty() { + return materials; + } + + public void setColorModifier(File packageFile, String obj) { + try(UnrealPackage up = new UnrealPackage(packageFile, true)) { + up.getExportTable().parallelStream() + .filter(e -> e.getObjectInnerFullName().equalsIgnoreCase(obj) || e.getObjectFullName().equalsIgnoreCase(obj)) + .filter(e -> e.getFullClassName().equalsIgnoreCase("Engine.ColorModifier")) + .findAny() + .ifPresent(export -> { + staticmeshGroup.getChildren().clear(); + + acmi.l2.clientmod.unreal.core.Object colorModifier = Controllers.getMainWndController().getSerializerFactory().getOrCreateObject(export); + for(L2Property prop : colorModifier.properties) { + if(!prop.getName().equals("Color")) { + continue; + } + + @SuppressWarnings("unchecked") + final List colorStruct = (List) prop.getAt(0); //color struct + Function func = name -> (Integer) colorStruct.stream() + .filter(e -> true) + .findAny() + .get() + .getAt(0); + int a = func.apply("A"); + int r = func.apply("R"); + int g = func.apply("G"); + int b = func.apply("B"); + Color fxColor = Color.rgb(r, g, b, a / 255.); + + final Box box = new Box(128, 128, 1); + final PhongMaterial material = new PhongMaterial(); + material.setDiffuseColor(fxColor); + box.setMaterial(material); + + points.set(8); + triangles.set(2*6); + materials.set(FXCollections.observableArrayList("Color")); + camera.setTranslateZ(-128 * 2); + zoomSpeed = 128 * ZOOM_SPEED; + + staticmeshGroup.getChildren().add(box); + } + } + ); + } + } + + public void setTexture(File packageFile, String obj) { + try (UnrealPackage up = new UnrealPackage(packageFile, true)) { + up.getExportTable().parallelStream() + .filter(e -> e.getObjectInnerFullName().equalsIgnoreCase(obj) || e.getObjectFullName().equalsIgnoreCase(obj)) + .filter(e -> e.getFullClassName().equalsIgnoreCase("Engine.Texture")) + .findAny() + .ifPresent(entry -> { + staticmeshGroup.getChildren().clear(); + + BufferedImage image = ImageUtil.getImage(entry.getObjectRawData(), up); + if(image == null) { + return; + } + + final Box box = new Box(image.getWidth(), image.getHeight(), 1); + final PhongMaterial material = new PhongMaterial(); + image = ImageUtil.removeAlpha(image); + material.setDiffuseMap(SwingFXUtils.toFXImage(image, null)); + box.setMaterial(material); + + points.set(8); + triangles.set(2*6); + materials.set(FXCollections.observableArrayList(entry.getObjectFullName())); + camera.setTranslateZ(-Math.max(image.getWidth(), image.getHeight()) * 2); + zoomSpeed = Math.max(image.getWidth(), image.getHeight()) * ZOOM_SPEED; + + staticmeshGroup.getChildren().add(box); + } + ); + } + } + + public void setStaticmesh(File packageFile, String obj) { + try (UnrealPackage up = new UnrealPackage(packageFile, true)) { + up.getExportTable().parallelStream() + .filter(e -> e.getObjectInnerFullName().equalsIgnoreCase(obj) || e.getObjectFullName().equalsIgnoreCase(obj)) + .filter(e -> e.getFullClassName().equalsIgnoreCase("Engine.StaticMesh")) + .findAny() + .ifPresent(entry -> { + staticmeshGroup.getChildren().clear(); + + StaticMesh staticMesh = StaticMesh.readStaticMesh(entry); + + points.set(staticMesh.vertexStream.vert.length); + triangles.set(staticMesh.indexStream1.indices.length / 3); + materials.set(staticMesh.materials.stream().map(pair -> pair == null ? + null : + String.format("%s'%s'", pair.getValue(), pair.getKey())).collect(Collectors.toCollection(FXCollections::observableArrayList))); + + camera.setTranslateZ(-staticMesh.boundingSphere.r * 2); + zoomSpeed = staticMesh.boundingSphere.r * ZOOM_SPEED; + + ObservableFloatArray points = FXCollections.observableFloatArray(); + ObservableFloatArray normals = FXCollections.observableFloatArray(); + ObservableFloatArray texCoords = FXCollections.observableFloatArray(); + for (int vertexIndex = 0; vertexIndex < staticMesh.vertexStream.vert.length; vertexIndex++) { + StaticMesh.StaticMeshVertex vertex = staticMesh.vertexStream.vert[vertexIndex]; + StaticMesh.MeshUVFloat uv = staticMesh.UVStream[0].data[vertexIndex]; + + points.addAll(vertex.pos.x, vertex.pos.y, vertex.pos.z); + normals.addAll(vertex.normal.x, vertex.normal.y, vertex.normal.z); + texCoords.addAll(uv.u, uv.v); + } + + List sections = new ArrayList<>(staticMesh.sections.length); + for (int j = 0; j < staticMesh.sections.length; j++) { + StaticMesh.StaticMeshSection staticMeshSection = staticMesh.sections[j]; + MeshView meshView = new MeshView(); + + TriangleMesh mesh = new TriangleMesh(POINT_NORMAL_TEXCOORD); + mesh.getPoints().setAll(points); + mesh.getNormals().setAll(normals); + mesh.getTexCoords().setAll(texCoords); + + mesh.getFaces().addAll( + IntStream.range(staticMeshSection.firstIndex, staticMeshSection.firstIndex + staticMeshSection.numFaces * 3) + .map(i -> staticMesh.indexStream1.indices[i]) + .flatMap(i -> IntStream.of(i, i, i)) + .toArray() + ); + + meshView.setCullFace(CullFace.FRONT); + meshView.setMesh(mesh); + + PhongMaterial material = new PhongMaterial(); + Pair materialRef = staticMesh.materials.get(j); + BufferedImage image = resolveMaterial(packageFile.getParentFile().getParentFile(), materialRef); //client root folder + if (image != null) { + image = ImageUtil.removeAlpha(image); + material.setDiffuseMap(SwingFXUtils.toFXImage(image, null)); + } + meshView.setMaterial(material); + + sections.add(meshView); + } + + staticmeshGroup.getChildren().addAll(sections); + } + ); + } + } + + private static final String[] TEXTURE_FOLDER_NAMES = new String[]{"textures", "systextures"}; + + private static BufferedImage resolveMaterial(File gameFolder, Pair mat) { + if (mat == null) + return null; + + for (String folderName : TEXTURE_FOLDER_NAMES) { + File folder = find(gameFolder, File::isDirectory, nameFilter(folderName)); + if (folder == null) + continue; + + File pack = find(folder, nameFilter(mat.getKey().substring(0, mat.getKey().indexOf('.')) + ".utx"), File::isFile); + if (pack == null) + continue; + + try (UnrealPackage up = new UnrealPackage(pack, true)) { + UnrealPackage.ExportEntry entry = up.getExportTable() + .stream() + .filter(e -> e.getObjectFullName().equalsIgnoreCase(mat.getKey())) + .filter(e -> e.getFullClassName().equalsIgnoreCase(mat.getValue())) + .findAny() + .orElse(null); + if (entry != null) { + if (entry.getFullClassName().equalsIgnoreCase("Engine.Texture")) { + return ImageUtil.getImage(entry.getObjectRawData(), up); + } else if (entry.getFullClassName().equalsIgnoreCase("Engine.Combiner")) { + return resolveMaterial(gameFolder, prop(entry, "Material1")); + } else if (entry.getFullClassName().equalsIgnoreCase("Engine.Shader")) { + return resolveMaterial(gameFolder, prop(entry, "Diffuse")); + } else { + return resolveMaterial(gameFolder, prop(entry, "Material")); + } + } + } + } + + return null; + } + + private static Pair prop(UnrealPackage.ExportEntry entry, String propName) { + UnrealPackage up = entry.getUnrealPackage(); + ByteBuffer buffer = ByteBuffer.wrap(entry.getObjectRawData()); + buffer.order(ByteOrder.LITTLE_ENDIAN); + AtomicReference> reference = new AtomicReference<>(); + iterateProperties(buffer, up, (name, offset, data) -> { + if (name.equalsIgnoreCase(propName)) { + UnrealPackage.Entry material = up.objectReference(getCompactInt(data)); + if (material != null) + reference.set(new Pair<>(material.getObjectFullName(), material.getFullClassName())); + } + }); + return reference.get(); + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + cameraXform3.setRotateZ(180.0); + + cameraXform.rz.setAngle(CAMERA_INITIAL_Z_ANGLE); + cameraXform.rx.setAngle(CAMERA_INITIAL_X_ANGLE); + } + + public void onMousePressed(MouseEvent me) { + mousePosX = me.getSceneX(); + mousePosY = me.getSceneY(); + mouseOldX = me.getSceneX(); + mouseOldY = me.getSceneY(); + } + + public void onMouseDragged(MouseEvent me) { + mouseOldX = mousePosX; + mouseOldY = mousePosY; + mousePosX = me.getSceneX(); + mousePosY = me.getSceneY(); + double mouseDeltaX = (mousePosX - mouseOldX); + double mouseDeltaY = (mousePosY - mouseOldY); + + if (me.isPrimaryButtonDown()) { + cameraXform.rz.setAngle(cameraXform.rz.getAngle() - mouseDeltaX * ROTATION_SPEED); + cameraXform.rx.setAngle(cameraXform.rx.getAngle() + mouseDeltaY * ROTATION_SPEED); + } else if (me.isSecondaryButtonDown()) { + double z = camera.getTranslateZ(); + double newZ = z - mouseDeltaY * zoomSpeed; + camera.setTranslateZ(newZ); + } else if (me.isMiddleButtonDown()) { + cameraXform2.t.setX(cameraXform2.t.getX() + mouseDeltaX * zoomSpeed); + cameraXform2.t.setY(cameraXform2.t.getY() + mouseDeltaY * zoomSpeed); + } + } + +// public void onMouseWheel(ZoomEvent ze) { +// final double zoomFactor = ze.getZoomFactor(); +// final double z = camera.getTranslateZ(); +// +// double newZ = z - zoomFactor * zoomSpeed; +// camera.setTranslateZ(newZ); +// } +} diff --git a/src/main/java/acmi/l2/clientmod/l2pe/view/Xform.java b/src/main/java/acmi/l2/clientmod/l2pe/view/Xform.java new file mode 100644 index 0000000..0f14946 --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/view/Xform.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2016 acmi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package acmi.l2.clientmod.l2pe.view; + +import javafx.scene.Group; +import javafx.scene.transform.Rotate; +import javafx.scene.transform.Scale; +import javafx.scene.transform.Translate; + +public class Xform extends Group { + + public enum RotateOrder { + XYZ, XZY, YXZ, YZX, ZXY, ZYX + } + + public Translate t = new Translate(); + public Translate p = new Translate(); + public Translate ip = new Translate(); + public Rotate rx = new Rotate(); + + { + rx.setAxis(Rotate.X_AXIS); + } + + public Rotate ry = new Rotate(); + + { + ry.setAxis(Rotate.Y_AXIS); + } + + public Rotate rz = new Rotate(); + + { + rz.setAxis(Rotate.Z_AXIS); + } + + public Scale s = new Scale(); + + public Xform() { + super(); + getTransforms().addAll(t, rz, ry, rx, s); + } + + public Xform(RotateOrder rotateOrder) { + super(); + // choose the order of rotations based on the rotateOrder + switch (rotateOrder) { + case XYZ: + getTransforms().addAll(t, p, rz, ry, rx, s, ip); + break; + case XZY: + getTransforms().addAll(t, p, ry, rz, rx, s, ip); + break; + case YXZ: + getTransforms().addAll(t, p, rz, rx, ry, s, ip); + break; + case YZX: + getTransforms().addAll(t, p, rx, rz, ry, s, ip); // For Camera + break; + case ZXY: + getTransforms().addAll(t, p, ry, rx, rz, s, ip); + break; + case ZYX: + getTransforms().addAll(t, p, rx, ry, rz, s, ip); + break; + } + } + + public void setTranslate(double x, double y, double z) { + t.setX(x); + t.setY(y); + t.setZ(z); + } + + public void setTranslate(double x, double y) { + t.setX(x); + t.setY(y); + } + + // Cannot override these methods as they are final: + // public void setTranslateX(double x) { t.setX(x); } + // public void setTranslateY(double y) { t.setY(y); } + // public void setTranslateZ(double z) { t.setZ(z); } + // Use these methods instead: + public void setTx(double x) { + t.setX(x); + } + + public void setTy(double y) { + t.setY(y); + } + + public void setTz(double z) { + t.setZ(z); + } + + public void setRotate(double x, double y, double z) { + rx.setAngle(x); + ry.setAngle(y); + rz.setAngle(z); + } + + public void setRotateX(double x) { + rx.setAngle(x); + } + + public void setRotateY(double y) { + ry.setAngle(y); + } + + public void setRotateZ(double z) { + rz.setAngle(z); + } + + public void setRx(double x) { + rx.setAngle(x); + } + + public void setRy(double y) { + ry.setAngle(y); + } + + public void setRz(double z) { + rz.setAngle(z); + } + + public void setScale(double scaleFactor) { + s.setX(scaleFactor); + s.setY(scaleFactor); + s.setZ(scaleFactor); + } + + public void setScale(double x, double y, double z) { + s.setX(x); + s.setY(y); + s.setZ(z); + } + + // Cannot override these methods as they are final: + // public void setScaleX(double x) { s.setX(x); } + // public void setScaleY(double y) { s.setY(y); } + // public void setScaleZ(double z) { s.setZ(z); } + // Use these methods instead: + public void setSx(double x) { + s.setX(x); + } + + public void setSy(double y) { + s.setY(y); + } + + public void setSz(double z) { + s.setZ(z); + } + + public void setPivot(double x, double y, double z) { + p.setX(x); + p.setY(y); + p.setZ(z); + ip.setX(-x); + ip.setY(-y); + ip.setZ(-z); + } + + public void reset() { + t.setX(0.0); + t.setY(0.0); + t.setZ(0.0); + rx.setAngle(0.0); + ry.setAngle(0.0); + rz.setAngle(0.0); + s.setX(1.0); + s.setY(1.0); + s.setZ(1.0); + p.setX(0.0); + p.setY(0.0); + p.setZ(0.0); + ip.setX(0.0); + ip.setY(0.0); + ip.setZ(0.0); + } + + public void resetTSP() { + t.setX(0.0); + t.setY(0.0); + t.setZ(0.0); + s.setX(1.0); + s.setY(1.0); + s.setZ(1.0); + p.setX(0.0); + p.setY(0.0); + p.setZ(0.0); + ip.setX(0.0); + ip.setY(0.0); + ip.setZ(0.0); + } + + @Override + public String toString() { + return "Xform[t = (" + + t.getX() + ", " + + t.getY() + ", " + + t.getZ() + ") " + + "r = (" + + rx.getAngle() + ", " + + ry.getAngle() + ", " + + rz.getAngle() + ") " + + "s = (" + + s.getX() + ", " + + s.getY() + ", " + + s.getZ() + ") " + + "p = (" + + p.getX() + ", " + + p.getY() + ", " + + p.getZ() + ") " + + "ip = (" + + ip.getX() + ", " + + ip.getY() + ", " + + ip.getZ() + ")]"; + } +} diff --git a/src/main/java/acmi/l2/clientmod/l2pe/view/helpers/ImageUtil.java b/src/main/java/acmi/l2/clientmod/l2pe/view/helpers/ImageUtil.java new file mode 100644 index 0000000..b439987 --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/view/helpers/ImageUtil.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016 acmi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package acmi.l2.clientmod.l2pe.view.helpers; + +import static acmi.l2.clientmod.l2pe.view.helpers.Util.iterateProperties; +import static gr.zdimensions.jsquish.Squish.decompressImage; + +import java.awt.image.BufferedImage; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import acmi.l2.clientmod.io.DataInput; +import acmi.l2.clientmod.io.UnrealPackage; +import acmi.l2.clientmod.unreal.engine.Material; +import gr.zdimensions.jsquish.Squish; + +public class ImageUtil { + public static BufferedImage getImage(byte[] bytes, UnrealPackage up) { + ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + AtomicReference format = new AtomicReference<>(Format.NODATA); + AtomicInteger width = new AtomicInteger(); + AtomicInteger height = new AtomicInteger(); + iterateProperties(buffer, up, (name, offset, data) -> { + if (name.equalsIgnoreCase("Format")) { + format.set(Format.values()[data.get() & 0xFF]); + } else if (name.equalsIgnoreCase("USize")) { + width.set(data.getInt()); + } else if (name.equalsIgnoreCase("VSize")) { + height.set(data.getInt()); + } + }); + DataInput dataInput = DataInput.dataInput(buffer, UnrealPackage.getDefaultCharset()); + new Material().readUnk(dataInput, up.getVersion(), up.getLicense()); + dataInput.readCompactInt(); + dataInput.readInt(); + byte[] data = dataInput.readByteArray(); + + switch (format.get()) { + case DXT1: + case DXT3: + case DXT5: + byte[] decompressed = decompressImage(null, width.get(), height.get(), data, Squish.CompressionType.valueOf(format.get().name())); + BufferedImage bi = new BufferedImage(width.get(), height.get(), BufferedImage.TYPE_4BYTE_ABGR); + bi.getRaster().setDataElements(0, 0, width.get(), height.get(), decompressed); + return bi; + default: + return null; + } + } + + public static BufferedImage removeAlpha(BufferedImage img) { + BufferedImage copy = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB); + for (int x = 0; x < img.getWidth(); x++) + for (int y = 0; y < img.getHeight(); y++) + copy.setRGB(x, y, img.getRGB(x, y)); + return copy; + } + + private enum Format { + P8, + RGBA7, + RGB16, + DXT1, + RGB8, + RGBA8, + NODATA, + DXT3, + DXT5, + L8, + G16, + RRRGGGBBB + } +} diff --git a/src/main/java/acmi/l2/clientmod/l2pe/view/helpers/StaticMeshActorUtil.java b/src/main/java/acmi/l2/clientmod/l2pe/view/helpers/StaticMeshActorUtil.java new file mode 100644 index 0000000..904e062 --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/view/helpers/StaticMeshActorUtil.java @@ -0,0 +1,593 @@ +/* + * Copyright (c) 2016 acmi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package acmi.l2.clientmod.l2pe.view.helpers; + +import static acmi.l2.clientmod.io.BufferUtil.getCompactInt; +import static acmi.l2.clientmod.io.BufferUtil.putCompactInt; +import static acmi.l2.clientmod.io.ByteUtil.compactIntToByteArray; +import static acmi.l2.clientmod.io.UnrealPackage.ObjectFlag.HasStack; +import static acmi.l2.clientmod.io.UnrealPackage.ObjectFlag.LoadForEdit; +import static acmi.l2.clientmod.io.UnrealPackage.ObjectFlag.LoadForServer; +import static acmi.l2.clientmod.io.UnrealPackage.ObjectFlag.Transactional; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import acmi.l2.clientmod.io.UnrealPackage; +import acmi.l2.clientmod.l2pe.view.model.Offsets; +import acmi.l2.clientmod.unreal.properties.PropertiesUtil.Type; + +public class StaticMeshActorUtil { + public static Offsets getOffsets(byte[] staticMeshActor, UnrealPackage unrealPackage) throws BufferUnderflowException { + Offsets offsets = new Offsets(); + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor); + buffer.order(ByteOrder.LITTLE_ENDIAN); + Util.readStateFrame(buffer); + Util.iterateProperties(buffer, unrealPackage, (name, offset, obj) -> { + switch (name) { + case "StaticMesh": + offsets.mesh = offset; + offsets.meshSize = obj.limit(); + break; + case "Location": + offsets.location = offset; + break; + case "Rotation": + offsets.rotation = offset; + break; + case "SwayRotationOrig": + offsets.swayRotationOrig = offset; + break; + case "ColLocation": + offsets.colLocation = offset; + break; + case "BasePos": + offsets.basePos = offset; + break; + case "BaseRot": + offsets.baseRot = offset; + break; + case "DrawScale": + offsets.drawScale = offset; + break; + case "DrawScale3D": + offsets.drawScale3D = offset; + break; + case "RotationRate": + offsets.rotationRate = offset; + break; + case "ZoneRenderState": + offsets.zoneRenderState = offset; + offsets.zoneRenderStateCount = getCompactInt(obj); + break; + } + }); + return offsets; + } + + public static int getSize(int size, ByteBuffer buffer) throws BufferUnderflowException { + switch (size) { + case 0: + return 1; + case 1: + return 2; + case 2: + return 4; + case 3: + return 12; + case 4: + return 16; + case 5: + return buffer.get() & 0xFF; + case 6: + return buffer.getShort() & 0xFFFF; + case 7: + return buffer.getInt(); + } + throw new RuntimeException("invalid size " + size); + } + + public static int getByteCount(int v) { + return Math.abs(v) > 63 ? 2 : 1; + } + + public static int getStaticMesh(byte[] staticMeshActor, Offsets offsets) { + if (offsets.mesh == 0) { + return 0; + } + return getCompactInt((ByteBuffer) ByteBuffer.wrap(staticMeshActor).order(ByteOrder.LITTLE_ENDIAN).position(offsets.mesh)); + } + + public static byte[] setStaticMesh(byte[] staticMeshActor, Offsets offsets, int staticMeshIndex) { + if (offsets.meshSize != getByteCount(staticMeshIndex)) { + byte[] bytes = new byte[staticMeshActor.length + getByteCount(staticMeshIndex) - offsets.meshSize]; + ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.put(staticMeshActor, 0, offsets.mesh - 1); + buffer.put((byte) (getByteCount(staticMeshIndex) == 2 ? 21 : 5)); + putCompactInt(buffer, staticMeshIndex); + buffer.put(staticMeshActor, offsets.mesh + offsets.meshSize, staticMeshActor.length - (offsets.mesh + offsets.meshSize)); + if (offsets.location != 0) { + offsets.location += getByteCount(staticMeshIndex) - offsets.meshSize; + } + if (offsets.rotation != 0) { + offsets.rotation += getByteCount(staticMeshIndex) - offsets.meshSize; + } + if (offsets.swayRotationOrig != 0) { + offsets.swayRotationOrig += getByteCount(staticMeshIndex) - offsets.meshSize; + } + if (offsets.colLocation != 0) { + offsets.colLocation += getByteCount(staticMeshIndex) - offsets.meshSize; + } + offsets.meshSize = getByteCount(staticMeshIndex); + return bytes; + } + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor); + buffer.position(offsets.mesh); + putCompactInt(buffer, staticMeshIndex); + return staticMeshActor; + } + + public static float[] getLocation(byte[] staticMeshActor, Offsets offsets) { + if ((offsets.location == 0) && (offsets.colLocation == 0)) { + return null; + } + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.position(offsets.location != 0 ? offsets.location : offsets.colLocation); + return new float[]{buffer.getFloat(), buffer.getFloat(), buffer.getFloat()}; + } + + public static void setLocation(byte[] staticMeshActor, Offsets offsets, float x, float y, float z) { + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor); + buffer.order(ByteOrder.LITTLE_ENDIAN); + if (offsets.location != 0) { + buffer.position(offsets.location); + buffer.putFloat(x); + buffer.putFloat(y); + buffer.putFloat(z); + } + if (offsets.colLocation != 0) { + buffer.position(offsets.colLocation); + buffer.putFloat(x); + buffer.putFloat(y); + buffer.putFloat(z); + } + if (offsets.basePos != 0) { + buffer.position(offsets.basePos); + buffer.putFloat(x); + buffer.putFloat(y); + buffer.putFloat(z); + } + } + + public static int[] getRotation(byte[] staticMeshActor, Offsets offsets) { + if ((offsets.rotation == 0) && (offsets.swayRotationOrig == 0)) { + return null; + } + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.position(offsets.rotation != 0 ? offsets.rotation : offsets.swayRotationOrig); + return new int[]{buffer.getInt(), buffer.getInt(), buffer.getInt()}; + } + + public static void setRotation(byte[] staticMeshActor, Offsets offsets, int p, int y, int r) { + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor); + buffer.order(ByteOrder.LITTLE_ENDIAN); + if (offsets.rotation != 0) { + buffer.position(offsets.rotation); + buffer.putInt(p); + buffer.putInt(y); + buffer.putInt(r); + } + if (offsets.swayRotationOrig != 0) { + buffer.position(offsets.swayRotationOrig); + buffer.putInt(p); + buffer.putInt(y); + buffer.putInt(r); + } + if (offsets.baseRot != 0) { + buffer.position(offsets.baseRot); + buffer.putInt(p); + buffer.putInt(y); + buffer.putInt(r); + } + } + + public static int[] getRotationRate(byte[] staticMeshActor, Offsets offsets) { + if (offsets.rotationRate == 0) { + return null; + } + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.position(offsets.rotationRate); + return new int[]{buffer.getInt(), buffer.getInt(), buffer.getInt()}; + } + + public static void setRotationRate(byte[] staticMeshActor, Offsets offsets, int p, int y, int r) { + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor); + buffer.order(ByteOrder.LITTLE_ENDIAN); + if (offsets.rotationRate != 0) { + buffer.position(offsets.rotationRate); + buffer.putInt(p); + buffer.putInt(y); + buffer.putInt(r); + } + } + + public static Float getDrawScale(byte[] staticMeshActor, Offsets offsets) { + if (offsets.drawScale == 0) { + return null; + } + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor).order(ByteOrder.LITTLE_ENDIAN); + return buffer.getFloat(offsets.drawScale); + } + + public static void setDrawScale(byte[] staticMeshActor, Offsets offsets, float scale) { + if (offsets.drawScale == 0) { + return; + } + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor).order(ByteOrder.LITTLE_ENDIAN); + buffer.putFloat(offsets.drawScale, scale); + } + + public static float[] getDrawScale3D(byte[] staticMeshActor, Offsets offsets) { + if (offsets.drawScale3D == 0) { + return null; + } + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor).order(ByteOrder.LITTLE_ENDIAN); + buffer.position(offsets.drawScale3D); + return new float[]{buffer.getFloat(), buffer.getFloat(), buffer.getFloat()}; + } + + public static void setDrawScale3D(byte[] staticMeshActor, Offsets offsets, float scaleX, float scaleY, float scaleZ) { + if (offsets.drawScale3D == 0) { + return; + } + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor).order(ByteOrder.LITTLE_ENDIAN); + buffer.position(offsets.drawScale3D); + buffer.putFloat(scaleX); + buffer.putFloat(scaleY); + buffer.putFloat(scaleZ); + } + + public static int[] getZoneRenderState(byte[] staticMeshActor, Offsets offsets) { + if (offsets.zoneRenderState == 0) + return null; + + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor).order(ByteOrder.LITTLE_ENDIAN); + buffer.position(offsets.zoneRenderState); + int[] zrs = new int[getCompactInt(buffer)]; + for (int i = 0; i < zrs.length; i++) + zrs[i] = buffer.getInt(); + return zrs; + } + + public static byte[] setZoneRenderState(byte[] staticMeshActor, Offsets offsets, int... states) { + if (offsets.zoneRenderState == 0) + return staticMeshActor; + + byte[] zrs = new byte[getIntArraySizeByCount(states.length)]; + ByteBuffer tmp = ByteBuffer.wrap(zrs).order(ByteOrder.LITTLE_ENDIAN); + putCompactInt(tmp, states.length); + for (int zs : states) + tmp.putInt(zs); + + if (states.length != offsets.zoneRenderStateCount) { + byte[] newBytes = new byte[staticMeshActor.length - getIntArraySizeByCount(offsets.zoneRenderStateCount) + zrs.length]; + System.arraycopy(staticMeshActor, 0, newBytes, 0, offsets.zoneRenderState - 1); + newBytes[offsets.zoneRenderState - 1] = (byte) zrs.length; + System.arraycopy(zrs, 0, newBytes, offsets.zoneRenderState, zrs.length); + System.arraycopy(staticMeshActor, offsets.zoneRenderState + getIntArraySizeByCount(offsets.zoneRenderStateCount), newBytes, offsets.zoneRenderState + zrs.length, newBytes.length - (offsets.zoneRenderState + zrs.length)); + return newBytes; + } else { + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor); + buffer.position(offsets.zoneRenderState); + buffer.put(zrs); + return staticMeshActor; + } + } + + private static int getIntArraySizeByCount(int count) { + return count * 4 + (count > 63 ? 2 : 1); + } + + public static byte[] createActor(UnrealPackage up, String clazz, int staticMeshRef, + boolean rotating, boolean zoneRenderState) { + ByteBuffer buffer = ByteBuffer.allocate(0x100).order(ByteOrder.LITTLE_ENDIAN); + + byte[] tmp = compactIntToByteArray(up.objectReferenceByName(clazz, c -> true)); + buffer.put(tmp); + buffer.put(tmp); + buffer.putLong(-1); + buffer.putInt(0); + buffer.put(compactIntToByteArray(-1)); + + //Properties + + if (zoneRenderState) { + buffer.put(compactIntToByteArray(up.nameReference("ZoneRenderState"))); + buffer.put((byte) 0x59); + buffer.put((byte) 0x05); + buffer.put((byte) 0x01); + buffer.putInt(1); + + buffer.put(compactIntToByteArray(up.nameReference("bDynamicActorFilterState"))); + buffer.put((byte) 0xD3); + buffer.put((byte) 0x00); + } + + if (rotating) { + buffer.put(compactIntToByteArray(up.nameReference("Physics"))); + buffer.put((byte) 0x01); + buffer.put((byte) 0x05); + } + + tmp = compactIntToByteArray(staticMeshRef); + buffer.put(compactIntToByteArray(up.nameReference("StaticMesh"))); + buffer.put((byte) (Type.OBJECT.ordinal() | ((tmp.length - 1) << 4))); + buffer.put(tmp); + + if (rotating) { + buffer.put(compactIntToByteArray(up.nameReference("bStatic"))); + buffer.put((byte) 0x53); + buffer.put((byte) 0x0); + } + + tmp = compactIntToByteArray(up.objectReferenceByName("LevelInfo0", c -> c.equalsIgnoreCase("Engine.LevelInfo"))); + buffer.put(compactIntToByteArray(up.nameReference("Level"))); + buffer.put((byte) (Type.OBJECT.ordinal() | ((tmp.length - 1) << 4))); + buffer.put(tmp); + + //Region + + buffer.put(compactIntToByteArray(up.nameReference("bSunAffect"))); + buffer.put((byte) 0xd3); + buffer.put((byte) 0); + + tmp = compactIntToByteArray(up.nameReference("StaticMeshActor")); + buffer.put(compactIntToByteArray(up.nameReference("Tag"))); + buffer.put((byte) (Type.NAME.ordinal() | ((tmp.length - 1) << 4))); + buffer.put(tmp); + + //PhysicsVolume + + buffer.put(compactIntToByteArray(up.nameReference("Location"))); + buffer.put((byte) 0x3a); + buffer.put(compactIntToByteArray(up.nameReference("Vector"))); + buffer.putFloat(0); + buffer.putFloat(0); + buffer.putFloat(0); + buffer.put(compactIntToByteArray(up.nameReference("ColLocation"))); + buffer.put((byte) 0x3a); + buffer.put(compactIntToByteArray(up.nameReference("Vector"))); + buffer.putFloat(0); + buffer.putFloat(0); + buffer.putFloat(0); + + buffer.put(compactIntToByteArray(up.nameReference("Rotation"))); + buffer.put((byte) 0x3a); + buffer.put(compactIntToByteArray(up.nameReference("Rotator"))); + buffer.putInt(0); + buffer.putInt(0); + buffer.putInt(0); + buffer.put(compactIntToByteArray(up.nameReference("SwayRotationOrig"))); + buffer.put((byte) 0x3a); + buffer.put(compactIntToByteArray(up.nameReference("Rotator"))); + buffer.putInt(0); + buffer.putInt(0); + buffer.putInt(0); + + if (rotating) { + buffer.put(compactIntToByteArray(up.nameReference("bFixedRotationDir"))); + buffer.put((byte) 0xd3); + buffer.put((byte) 0x0); + buffer.put(compactIntToByteArray(up.nameReference("RotationRate"))); + buffer.put((byte) 0x3a); + buffer.put(compactIntToByteArray(up.nameReference("Rotator"))); + buffer.putInt(0); + buffer.putInt(0); + buffer.putInt(0); + } + + buffer.put(compactIntToByteArray(up.nameReference("DrawScale"))); + buffer.put((byte) 0x24); + buffer.putFloat(1); + + buffer.put(compactIntToByteArray(up.nameReference("DrawScale3D"))); + buffer.put((byte) 0x3a); + buffer.put(compactIntToByteArray(up.nameReference("Vector"))); + buffer.putFloat(1); + buffer.putFloat(1); + buffer.putFloat(1); + + //TexModifyInfo + + buffer.put(compactIntToByteArray(up.nameReference("None"))); + + buffer.flip(); + byte[] actor = new byte[buffer.limit()]; + buffer.get(actor); + return actor; + } + + public static final int STATIC_MESH_ACTOR_FLAGS = UnrealPackage.ObjectFlag.getFlags( + Transactional, LoadForServer, LoadForEdit, HasStack); + + /** + * @param staticMesh object ref + * @return object ref + * @throws IOException + */ + public static int addStaticMeshActor(UnrealPackage up, int staticMesh, String clazz, boolean rotating, boolean zoneState) throws UncheckedIOException { + Map names = new HashMap<>(); +// "StaticMesh", +// //"Region", "Zone", "iLeaf", "ZoneNumber", +// "bSunAffect", "Tag", "StaticMeshActor", +// //"PhysicsVolume", +// "Location", "ColLocation", "Vector", +// "Rotation", "SwayRotationOrig", "Rotator", +// "DrawScale", "DrawScale3D" +// //"TexModifyInfo", "bUseModify", "bTwoSide", "bAlphaBlend", +// //"bDummy", "Color", "AlphaOp", "ColorOp" + names.put("StaticMesh", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("bSunAffect", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("Tag", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("StaticMeshActor", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("Location", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("ColLocation", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("Vector", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("Rotation", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("SwayRotationOrig", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("Rotator", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("DrawScale", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("DrawScale3D", UnrealPackage.DEFAULT_OBJECT_FLAGS); + if (rotating) { + names.put("RotationRate", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("Physics", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("bStatic", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("bFixedRotationDir", UnrealPackage.DEFAULT_OBJECT_FLAGS); + } + if (zoneState) { + names.put("ZoneRenderState", UnrealPackage.DEFAULT_OBJECT_FLAGS); + names.put("bDynamicActorFilterState", UnrealPackage.DEFAULT_OBJECT_FLAGS); + } + + up.addNameEntries(names); + + if (up.objectReferenceByName(clazz, c -> c.equalsIgnoreCase("Core.Class")) == 0) + up.addImportEntries(Collections.singletonMap(clazz, "Core.Class")); + + up.getNameTable(); + up.getImportTable(); + up.getExportTable(); + + String cName = clazz.substring(clazz.lastIndexOf(".") + 1); + String name = cName + sm(cName, up); + up.addExportEntry(name, clazz, null, StaticMeshActorUtil.createActor( + up, clazz, staticMesh, rotating, zoneState), + STATIC_MESH_ACTOR_FLAGS + ); + + up.getNameTable(); + up.getImportTable(); + up.getExportTable(); + + int newActorInd = up.objectReferenceByName(name, c -> c.equalsIgnoreCase(clazz)); + + //System.out.println("0x"+Integer.toHexString(newActorInd-1)+" "+name+" added"); + + byte[] compact = compactIntToByteArray(newActorInd); + + UnrealPackage.ExportEntry level = (UnrealPackage.ExportEntry) up.objectReference(up.objectReferenceByName("myLevel", c -> c.equalsIgnoreCase("Engine.Level"))); + ByteBuffer levelBuffer = ByteBuffer.wrap(level.getObjectRawData()).order(ByteOrder.LITTLE_ENDIAN); + + byte[] newBytes = new byte[levelBuffer.capacity() + compact.length]; + + getCompactInt(levelBuffer); + levelBuffer.getInt(); + int count = levelBuffer.getInt(); + for (int i = 0; i < count; i++) + getCompactInt(levelBuffer); + + int countPos = levelBuffer.position(); + + levelBuffer.getInt(); + count = levelBuffer.getInt(); + for (int i = 0; i < count; i++) + getCompactInt(levelBuffer); + + levelBuffer.putInt(countPos, count + 1); + levelBuffer.putInt(countPos + 4, count + 1); + + System.arraycopy(levelBuffer.array(), 0, newBytes, 0, levelBuffer.position()); + System.arraycopy(compact, 0, newBytes, levelBuffer.position(), compact.length); + System.arraycopy(levelBuffer.array(), levelBuffer.position(), newBytes, levelBuffer.position() + compact.length, levelBuffer.capacity() - levelBuffer.position()); + + level.setObjectRawData(newBytes); + + return newActorInd; + } + + public static int copyStaticMeshActor(UnrealPackage up, int ind) throws IOException { + UnrealPackage.ExportEntry entry = up.getExportTable().get(ind); + + String name = entry.getObjectClass().getObjectName().getName() + sm(entry.getObjectClass().getObjectName().getName(), up); + up.addExportEntry(name, entry.getObjectClass().getObjectFullName(), null, entry.getObjectRawData(), entry.getObjectFlags()); + + up.getNameTable(); + up.getImportTable(); + up.getExportTable(); + + int newActorInd = up.objectReferenceByName(name, c -> c.equalsIgnoreCase(entry.getObjectClass().getObjectFullName())); + + //System.out.println("0x"+Integer.toHexString(newActorInd-1)+" "+name+" added"); + + byte[] compact = compactIntToByteArray(newActorInd); + + UnrealPackage.ExportEntry level = (UnrealPackage.ExportEntry) up.objectReference(up.objectReferenceByName("myLevel", c -> c.equalsIgnoreCase("Engine.Level"))); + ByteBuffer levelBuffer = ByteBuffer.wrap(level.getObjectRawData()).order(ByteOrder.LITTLE_ENDIAN); + + byte[] newBytes = new byte[levelBuffer.capacity() + compact.length]; + + getCompactInt(levelBuffer); + levelBuffer.getInt(); + int count = levelBuffer.getInt(); + for (int i = 0; i < count; i++) + getCompactInt(levelBuffer); + + int countPos = levelBuffer.position(); + + levelBuffer.getInt(); + count = levelBuffer.getInt(); + for (int i = 0; i < count; i++) + getCompactInt(levelBuffer); + + levelBuffer.putInt(countPos, count + 1); + levelBuffer.putInt(countPos + 4, count + 1); + + System.arraycopy(levelBuffer.array(), 0, newBytes, 0, levelBuffer.position()); + System.arraycopy(compact, 0, newBytes, levelBuffer.position(), compact.length); + System.arraycopy(levelBuffer.array(), levelBuffer.position(), newBytes, levelBuffer.position() + compact.length, levelBuffer.capacity() - levelBuffer.position()); + + level.setObjectRawData(newBytes); + + return newActorInd; + } + + private static int sm(String clazz, UnrealPackage up) { + Pattern pattern = Pattern.compile(clazz + "\\d+"); + return 1 + up.getExportTable() + .stream() + .map(e -> e.getObjectName().getName()) + .filter(name -> pattern.matcher(name).matches()) + .mapToInt(name -> Integer.parseInt(name.substring(clazz.length()))) + .max() + .orElse(-1); + } +} diff --git a/src/main/java/acmi/l2/clientmod/l2pe/view/helpers/TriConsumer.java b/src/main/java/acmi/l2/clientmod/l2pe/view/helpers/TriConsumer.java new file mode 100644 index 0000000..a421b42 --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/view/helpers/TriConsumer.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2016 acmi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package acmi.l2.clientmod.l2pe.view.helpers; + +public interface TriConsumer { + void accept(T t, U u, V v); +} diff --git a/src/main/java/acmi/l2/clientmod/l2pe/view/helpers/Util.java b/src/main/java/acmi/l2/clientmod/l2pe/view/helpers/Util.java new file mode 100644 index 0000000..011a071 --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/view/helpers/Util.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2016 acmi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package acmi.l2.clientmod.l2pe.view.helpers; + +import static acmi.l2.clientmod.io.BufferUtil.getCompactInt; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import acmi.l2.clientmod.io.UnrealPackage; +import acmi.l2.clientmod.unreal.properties.PropertiesUtil.Type; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.TextField; + +public class Util { + public static final FileFilter MAP_FILE_FILTER = pathname -> + (pathname != null) && (pathname.isFile()) && (pathname.getName().endsWith(".unr")); + public static final FileFilter STATICMESH_FILE_FILTER = pathname -> + (pathname != null) && (pathname.isFile()) && (pathname.getName().endsWith(".usx")); + + public static Float getFloat(TextField textField, Float defaultValue) { + try { + return Float.valueOf(textField.getText()); + } catch (NumberFormatException nfe) { + return defaultValue; + } + } + + public static Integer getInt(TextField textField, Integer defaultValue) { + try { + return Integer.valueOf(textField.getText()); + } catch (NumberFormatException nfe) { + return defaultValue; + } + } + + public static double range(float[] loc, Double x, Double y, Double z) { + double s = 0.0; + if (x != null) + s += Math.pow(loc[0] - x, 2.0); + if (y != null) + s += Math.pow(loc[1] - y, 2.0); + if (z != null) + s += Math.pow(loc[2] - z, 2.0); + return Math.sqrt(s); + } + + public static Double getDoubleOrClearTextField(TextField textField) { + try { + return Double.valueOf(textField.getText()); + } catch (NumberFormatException nfe) { + if (!textField.getText().equals("-")) + textField.setText(""); + return null; + } + } + + public static void showAlert(Alert.AlertType alertType, String title, String header, String content) { + Alert alert = new Alert(alertType); + alert.setTitle(title); + alert.setHeaderText(header); + alert.setContentText(content); + alert.show(); + } + + public static boolean showConfirm(Alert.AlertType alertType, String title, String header, String content) { + Alert alert = new Alert(alertType); + alert.setTitle(title); + alert.setHeaderText(header); + alert.setContentText(content); + alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO); + return ButtonType.YES == alert.showAndWait().orElse(ButtonType.NO); + } + + public static void readStateFrame(ByteBuffer buffer) throws BufferUnderflowException { + getCompactInt(buffer); + getCompactInt(buffer); + buffer.getLong(); + buffer.getInt(); + getCompactInt(buffer); + } + + public static void iterateProperties(ByteBuffer buffer, UnrealPackage up, TriConsumer func) throws BufferUnderflowException { + String name; + while (!"None".equals(name = up.getNameTable().get(getCompactInt(buffer)).getName())) { + byte info = buffer.get(); + Type type = Type.values()[info & 15]; + int size = (info & 112) >> 4; + boolean array = (info & 128) == 128; + if (type == Type.STRUCT) { + getCompactInt(buffer); + } + + size = StaticMeshActorUtil.getSize(size, buffer); + if (array && type != Type.BOOL) { + buffer.get(); + } + + byte[] obj = new byte[size]; + int pos = buffer.position(); + buffer.get(obj); + + func.accept(name, pos, ByteBuffer.wrap(obj).order(ByteOrder.LITTLE_ENDIAN)); + } + } + + public static int getXY(File mapDir, String mapName) throws IOException { + int[] m = new int[2]; + + try (UnrealPackage up = new UnrealPackage(new File(mapDir, mapName), true)) { + List infos = up.getExportTable().stream() + .filter(e -> e.getObjectClass().getObjectFullName().equals("Engine.TerrainInfo")) + .collect(Collectors.toList()); + for (UnrealPackage.ExportEntry e : infos) { + byte[] staticMeshActor = e.getObjectRawData(); + ByteBuffer buffer = ByteBuffer.wrap(staticMeshActor); + buffer.order(ByteOrder.LITTLE_ENDIAN); + readStateFrame(buffer); + iterateProperties(buffer, up, (name, pos, obj) -> { + switch (name) { + case "MapX": + m[0] = obj.getInt(); + break; + case "MapY": + m[1] = obj.getInt(); + break; + } + }); + } + } + + return m[0] | (m[1] << 8); + } + + public static CharSequence tab(int indent) { + StringBuilder sb = new StringBuilder(indent); + for (int i = 0; i < indent; i++) + sb.append('\t'); + return sb; + } + + public static CharSequence newLine(int indent) { + StringBuilder sb = new StringBuilder("\r\n"); + sb.append(tab(indent)); + return sb; + } + + public static CharSequence newLine() { + return newLine(0); + } + + public static Throwable getTop(Throwable t) { + while (t.getCause() != null) + t = t.getCause(); + return t; + } + + public static Predicate nameFilter(String name) { + return f -> f.getName().equalsIgnoreCase(name); + } + + @SafeVarargs + public static File find(File folder, Predicate... filters) { + if (folder == null) + return null; + + File[] children = folder.listFiles(); + if (children == null) + return null; + + Stream stream = Arrays.stream(children); + for (Predicate filter : filters) + stream = stream.filter(filter); + return stream + .findAny() + .orElse(null); + } +} diff --git a/src/main/java/acmi/l2/clientmod/l2pe/view/model/Offsets.java b/src/main/java/acmi/l2/clientmod/l2pe/view/model/Offsets.java new file mode 100644 index 0000000..fd34678 --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/view/model/Offsets.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016 acmi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package acmi.l2.clientmod.l2pe.view.model; + +public class Offsets implements Cloneable { + public int mesh; + public int meshSize; + + public int location; + public int colLocation; + public int basePos; + + public int drawScale; + public int drawScale3D; + + public int rotation; + public int swayRotationOrig; + public int baseRot; + + public int rotationRate; + + public int zoneRenderState; + public int zoneRenderStateCount; + + @Override + public String toString() { + return "Offsets{" + + "mesh=0x" + Integer.toHexString(mesh) + + ", meshSize=" + meshSize + + ", location=0x" + Integer.toHexString(location) + + ", rotation=0x" + Integer.toHexString(rotation) + + ", swayRotationOrig=0x" + Integer.toHexString(swayRotationOrig) + + ", colLocation=0x" + Integer.toHexString(colLocation) + + ", basePos=0x" + Integer.toHexString(basePos) + + ", baseRot=0x" + Integer.toHexString(baseRot) + + ", drawScale=0x" + Integer.toHexString(drawScale) + + ", rotationRate=0x" + Integer.toHexString(rotationRate) + + ", zoneRenderState=0x" + Integer.toHexString(zoneRenderState) + + ", zoneRenderStateCount=" + zoneRenderStateCount + + '}'; + } + + @Override + protected Offsets clone() { + try { + return (Offsets) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/acmi/l2/clientmod/l2pe/view/model/StaticMesh.java b/src/main/java/acmi/l2/clientmod/l2pe/view/model/StaticMesh.java new file mode 100644 index 0000000..0fef6dc --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/view/model/StaticMesh.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2016 acmi + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package acmi.l2.clientmod.l2pe.view.model; + +import static acmi.l2.clientmod.io.BufferUtil.getCompactInt; +import static acmi.l2.clientmod.l2pe.view.helpers.Util.iterateProperties; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import acmi.l2.clientmod.io.DataInput; +import acmi.l2.clientmod.io.ObjectInput; +import acmi.l2.clientmod.io.ReflectionSerializerFactory; +import acmi.l2.clientmod.io.UnrealPackage; +import acmi.l2.clientmod.io.annotation.UShort; +import javafx.util.Pair; + +public class StaticMesh { + public transient List> materials; + public Box boundingBox; + public Sphere boundingSphere; + public StaticMeshSection[] sections; + public Box boundingBox2; + public StaticMeshVertexStream vertexStream; + public RawColorStream colorStream1; + public RawColorStream colorStream2; + public StaticMeshUVStream[] UVStream; + public RawIndexBuffer indexStream1; + public RawIndexBuffer indexStream2; + + public static StaticMesh readStaticMesh(UnrealPackage.ExportEntry entry) { + return readStaticMesh(entry.getObjectRawData(), entry.getUnrealPackage()); + } + + public static StaticMesh readStaticMesh(byte[] bytes, UnrealPackage up) { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.order(ByteOrder.LITTLE_ENDIAN); + List materials = new ArrayList<>(); + iterateProperties(buffer, up, (name, pos, data) -> { + if ("Materials".equalsIgnoreCase(name)) { + int size = getCompactInt(data); + for (int i = 0; i < size; i++) { + iterateProperties(data, up, (matName, matPos, matData) -> { + if ("Material".equalsIgnoreCase(matName)) { + materials.add(up.objectReference(getCompactInt(matData))); + } + }); + } + } + }); + StaticMesh staticMesh = (StaticMesh) ObjectInput + .objectInput(DataInput.dataInput(buffer, null), new ReflectionSerializerFactory(), null) + .readObject(StaticMesh.class); + staticMesh.materials = materials + .stream() + .map(e -> e != null ? new Pair<>(e.getObjectFullName(), e.getFullClassName()) : null) + .collect(Collectors.toList()); + + return staticMesh; + } + + public static class Vector { + public float x, y, z; + } + + public static class Box { + public Vector min; + public Vector max; + public byte isValid; + } + + public static class Sphere extends Vector { + public float r; + } + + public static class StaticMeshSection { + public int f4; + @UShort + public int firstIndex; + @UShort + public int firstVertex; + @UShort + public int lastVertex; + @UShort + public int fE; + @UShort + public int numFaces; + } + + public static class StaticMeshVertexStream { + public StaticMeshVertex[] vert; + public int revision; + } + + public static class StaticMeshVertex { + public Vector pos; + public Vector normal; + } + + public static class RawColorStream { + public Color[] color; + public int revision; + } + + public static class StaticMeshUVStream { + public MeshUVFloat[] data; + public int f10; + public int f1C; + } + + public static class MeshUVFloat { + public float u; + public float v; + } + + public static class RawIndexBuffer { + @UShort + public int[] indices; + public int revision; + } + + public static class Color { + public byte r, g, b, a; + } +} diff --git a/src/main/resources/acmi/l2/clientmod/l2pe/importwnd.fxml b/src/main/resources/acmi/l2/clientmod/l2pe/importwnd.fxml new file mode 100644 index 0000000..13af4f4 --- /dev/null +++ b/src/main/resources/acmi/l2/clientmod/l2pe/importwnd.fxml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +