From fe5018254b1fd486ccfb07b3d8f76995486475da Mon Sep 17 00:00:00 2001 From: PointerRage Date: Wed, 28 Sep 2016 20:40:34 +0300 Subject: [PATCH 1/2] Supports to view imported texture/mesh/color. Added new windows: names window, imports window. New buttons: update & delete. --- build.gradle | 7 +- lib/jsquish.jar | Bin 0 -> 38693 bytes settings.gradle | 2 + .../acmi/l2/clientmod/l2pe/Controllers.java | 228 +++++++ .../clientmod/l2pe/ImportWndController.java | 312 +++++++++ .../java/acmi/l2/clientmod/l2pe/L2PE.java | 35 +- ...Controller.java => MainWndController.java} | 414 ++++++------ .../l2/clientmod/l2pe/NameWndController.java | 201 ++++++ .../java/acmi/l2/clientmod/l2pe/Util.java | 157 ++++- .../l2pe/view/PerspectiveCameraWrap.java | 34 + .../acmi/l2/clientmod/l2pe/view/SMView.java | 93 +++ .../acmi/l2/clientmod/l2pe/view/View3D.java | 371 +++++++++++ .../acmi/l2/clientmod/l2pe/view/Xform.java | 240 +++++++ .../l2pe/view/helpers/ImageUtil.java | 94 +++ .../view/helpers/StaticMeshActorUtil.java | 593 ++++++++++++++++++ .../l2pe/view/helpers/TriConsumer.java | 26 + .../l2/clientmod/l2pe/view/helpers/Util.java | 207 ++++++ .../l2/clientmod/l2pe/view/model/Offsets.java | 70 +++ .../clientmod/l2pe/view/model/StaticMesh.java | 147 +++++ .../acmi/l2/clientmod/l2pe/importwnd.fxml | 48 ++ .../acmi/l2/clientmod/l2pe/main.fxml | 19 +- .../acmi/l2/clientmod/l2pe/namewnd.fxml | 48 ++ .../acmi/l2/clientmod/l2pe/view/3dview.fxml | 71 +++ .../acmi/l2/clientmod/l2pe/view/smview.css | 4 + .../acmi/l2/clientmod/l2pe/view/smview.fxml | 66 ++ 25 files changed, 3269 insertions(+), 218 deletions(-) create mode 100644 lib/jsquish.jar create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/Controllers.java create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/ImportWndController.java rename src/main/java/acmi/l2/clientmod/l2pe/{Controller.java => MainWndController.java} (73%) create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/NameWndController.java create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/view/PerspectiveCameraWrap.java create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/view/SMView.java create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/view/View3D.java create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/view/Xform.java create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/view/helpers/ImageUtil.java create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/view/helpers/StaticMeshActorUtil.java create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/view/helpers/TriConsumer.java create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/view/helpers/Util.java create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/view/model/Offsets.java create mode 100644 src/main/java/acmi/l2/clientmod/l2pe/view/model/StaticMesh.java create mode 100644 src/main/resources/acmi/l2/clientmod/l2pe/importwnd.fxml create mode 100644 src/main/resources/acmi/l2/clientmod/l2pe/namewnd.fxml create mode 100644 src/main/resources/acmi/l2/clientmod/l2pe/view/3dview.fxml create mode 100644 src/main/resources/acmi/l2/clientmod/l2pe/view/smview.css create mode 100644 src/main/resources/acmi/l2/clientmod/l2pe/view/smview.fxml 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 0000000000000000000000000000000000000000..a5216ad0eeecf526fefdaa3a6c4404d6763329b5 GIT binary patch literal 38693 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

f91G}MnbfG2L9aAo+52&Z{!AI?qP-Z*s1 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 m0ro&aZxka8T-S}$KzI{5z?&6#QyK$ 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..e7c504d --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/ImportWndController.java @@ -0,0 +1,312 @@ +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..2a6f2d0 --- /dev/null +++ b/src/main/java/acmi/l2/clientmod/l2pe/NameWndController.java @@ -0,0 +1,201 @@ +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..da505ff --- /dev/null +++ b/src/main/resources/acmi/l2/clientmod/l2pe/importwnd.fxml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +