Pfeil, thanks for taking an interest! The relevant code is as follows:
Server path chooser; this is a
javafx.stage.FileChooser instance, which returns a
java.io.File object. The UI objects are retrieved from a map of controls by key and "file" is bound to the results of showing the file chooser dialog using the .showOpenDialog method (this is all that is in ui/show-chooser). This is just to keep the UI and the application logic separate. The file is then converted to an absolute path String using File.getCanonicalPath.
Code:
(defn server-choose-command
"### server-choose-command
This zero argument function displays the server chooser dialog and uses
the provided file to set the server path in the UI."
[]
(let [{:keys [server-chooser
server-path-lbl]} @state/controls
file (ui/show-chooser server-chooser)]
(when file
(ui/set-label server-path-lbl (.getCanonicalPath file)))))
A
javafx.beans.InvalidationListener has been set over the TextProperty of the
javafx.scene.control.Label which updates an atom (Clojure thread-safe mutable var) in order to store the server path as a globally accessible state variable.
Code:
(defn server-path-select
[]
(let [{:keys [server-path-lbl]} @state/controls
server-path (ui/get-text server-path-lbl)]
(if (= server-path "...")
(reset! state/server-path nil)
(reset! state/server-path server-path))))
The get-text function is below:
Code:
(defn get-text
"### get-text
This one argument function returns the text from the supplied control."
^String [control]
(.getText control))
This function sets the value of the mission path label from the
javafx.scene.control.TextField object, and is triggered by a
javafx.event.EventHandler set on the Select button.
Code:
(defn set-single-remote
[]
(let [{:keys [single-path-fld single-path-lbl]} @state/controls
mission-path (ui/get-text single-path-fld)]
(when (not (string/blank? mission-path))
(ui/set-label single-path-lbl mission-path))))
UI code which sets the label text below:
Code:
(defn set-label
"### set-label
This two argument function sets the text content of the supplied control to the
value of the supplied text argument."
[^Label label text]
(.setText label text))
Much like the server chooser, the single mission chooser function retrieves a File object from user input, turns it into an absolute path String and puts it into the single mission path Label object. The difference in this case is that the canonical path of the file is resolved against the content of the global server path atom.
Code:
(defn single-choose-command
[]
(let [{:keys [mis-chooser
single-path-lbl]} @state/controls
file (ui/show-chooser mis-chooser)]
(when file
(ui/set-label single-path-lbl
(get-resolved-path @state/server-path (.getCanonicalPath file))))))
The code to resolve the path transforms the input Strings into Path objects using Paths.get from
java.nio.file.Paths. It gets the parent folder of the server .exe file and then resolves this against "Missions" to get the root missions directory.
Then the path is relativized against the missions directory, normalised to remove any redundant elements and converted to a String object, before replacing all backward slash characters with forward slash characters. The Clojure function str calls the .toString method for x when it is given the single object x. The methods used for canonisation and relativisation can be found in
java.nio.file.Path.
Code:
(defn get-resolved-path
[root-path in-path]
(let [path (Paths/get in-path (into-array [""]))
server-path (Paths/get root-path (into-array [""]))
server-dir (.getParent server-path)
mis-dir (.resolve server-dir "Missions")]
(string/replace (->> path (.relativize mis-dir) .normalize str) "\\" "/")))
Now that the Label text has been set, the logic is
exactly the same for both routes.
This is the same global state atom update logic as with the server chooser, triggered by an InvalidationListener attached to the Label object:
Code:
(defn single-path-select
[]
(let [{:keys [single-path-lbl]} @state/controls
single-path (ui/get-text single-path-lbl)]
(if (= single-path "...")
(reset! state/mission-path nil)
(reset! state/mission-path single-path))))
To load the mission, we click the Load/Unload button, which calls the following function:
Code:
(defn load-unload-command
"### load-unload-command
This is a zero argument function which unloads the currently loaded mission if
it is loaded."
[]
(if @state/loaded
(server/unload-mission)
(when (= @state/mode "single")
(ui/toggle-prog-ind @state/controls true)
(server/load-mission @state/mission-path))))
When there is a mission loaded, we write "mission DESTROY" to the socket. We then enquire as to the mission state for the parser to pick up the new state (because the mission DESTROY command doesn't return any output to the console):
Code:
(defn unload-mission
"### unload-mission
This is a zero argument function which sends the command to the server console
which unloads the current mission. Because the unload command doesn't return
any output on completion, we also request the mission state so that the state
parsers can register the change in mission status."
[]
(write-socket "mission DESTROY")
(get-mission-state))
If the mission isn't loaded, then we write "mission LOAD <mission path>":
Code:
(defn load-mission
"### load-mission
This is a one argument function which sends the command to the server console
which loads a mission using the path described by the argument."
[path-to-mission]
(write-socket (str "mission LOAD " path-to-mission)))
Finally, the write-socket function which outputs to the
java.net.Socket output stream:
Code:
(defn write-socket
"### write-socket
This is a single argument function that simply calls the println method of the
object that is stored in the socket-out atom using the argument that we
provide.
This atom should contain the instance of the PrintWriter object that is
instantiated when we successfully connect to the server."
[text]
(.println ^PrintWriter @socket-out text))
Please note: I have also tried the following for the print command to match the server line endings:
Code:
(.print @socket-out (str text "\\n"))
(.flush @socket-out)
I even tried "\\n\\r\\n" as I noticed this in the server output quite frequently!
Output stream definition here:
Code:
(reset! socket-out (PrintWriter.
(BufferedWriter.
(OutputStreamWriter.
(.getOutputStream ^Socket @socket)
(Charset/forName "UTF-8")))
true))
I have tried a couple of different values for the character set for the OutputStreamWriter, including ISO-8859-1 and leaving it blank (which should be UTF-16).
Hope this is a decent enough outline of what goes on (I'm not cagey about the code as I intend to open the repo to all once the basic feature set is complete).
One of the methods that I used to try and detect any differences in the path was to set a comparator function on the single-choose-command function using
clojure.data/diff, which compared the text content of the single mission path TextField to the return value of the get-resolved-path function.
I loaded the mission using the TextField, loaded a different mission using the chooser (to trigger the bug), then loaded the original mission from the TextField using the FileChooser. In every instance it indicated that the value in the TextField (which worked every time) was the same as that returned by the FileChooser (which didn't), i.e. it returned (nil nil "Net/dogfight/DCG/dcgmission.mis").
The_WOZ, I will give your suggestion a try once I get home, thanks!