/*******************************************************************************
* Rhythos Editor is a game editor and project management tool for making RPGs on top of the Rhythos Game system.
*
* Copyright (C) 2013 David Maletz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package mrpg.script;
import java.awt.Desktop;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import mrpg.editor.MapEditor;
import mrpg.editor.resource.Folder;
import mrpg.editor.resource.Image;
import mrpg.editor.resource.Media;
import mrpg.editor.resource.Project;
import mrpg.editor.resource.Resource;
import mrpg.export.Export;
import mrpg.export.NekoExport;
import mrpg.export.SWFExport;
public class HaxeCompiler {
private static Process host, test; private static int port = 0; private static String neko_exe = null;
private static class close implements MapEditor.OnClose {public void onClose(){destroy();}}
static {MapEditor.on_close.add(new close());}
private static boolean isClosed(Process p){
try{p.exitValue(); return true;} catch(Exception e){return false;}
}
private static void launchHost() throws Exception {
if(host == null || isClosed(host)){
ServerSocket s = new ServerSocket(0); port = s.getLocalPort(); s.close();
host = Runtime.getRuntime().exec("haxe --wait "+port);
}
}
public static String[] TARGETS = {"flash", "windows -neko", "mac -neko", "linux -neko", "linux -neko -64", "html5", "android", "ios", "blackberry", "webos"};
//TODO: Enable (& test) targets Mac, Linux, Linux 64 and HTML5. Also, I *think* it is safe to delete nme.ndll, test this.
public static String[] TARGET_NAMES = {"Flash SWF", "Windows", "Mac", "Linux", "Linux 64-bit"/*, "HTML5"*/};
public static String defaultTarget(){return "flash";}
private static String getProjectOut(Project p){return p.getName();}
private static int getProjectWidth(Project p){return 400;}
private static int getProjectHeight(Project p){return 300;}
private static String createNMML(Project p){
StringBuilder b = new StringBuilder();
b.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
b.append("<project>");
b.append("<meta title=\""); b.append(p.getName());
b.append("\" package=\"com.rhythos.core\" version=\"1.0.0\" company=\"Rhythos RPG Builder\" />");
b.append("<app main=\"com.rhythos.core.Main\" file=\"");
b.append(getProjectOut(p)); b.append("\" path=\".\" />");
b.append("<window background=\"#ffffff\" fps=\"24\" />");
b.append("<window width=\""); b.append(getProjectWidth(p)); b.append("\" height=\"");
b.append(getProjectHeight(p)); b.append("\" unless=\"mobile\" />");
b.append("<window orientation=\"landscape\" vsync=\"true\" antialiasing=\"0\" if=\"neko\"/>");
b.append("<window require-shaders=\"true\" if=\"neko\"/>");
b.append("<source path=\"../\" />");
b.append("<haxelib name=\"nme\" />");
b.append("<ndll name=\"std\" />");
b.append("<ndll name=\"regexp\" />");
b.append("<ndll name=\"zlib\" />");
b.append("<ndll name=\"nme\" haxelib=\"nme\" />");
b.append("<haxeflag name=\"--dead-code-elimination\" if=\"html5\" />");
b.append("<haxeflag name=\"--js-modern\" if=\"html5\" />");
b.append("</project>");
return b.toString();
}
public static class Result {
public final String target; public final boolean isNeko; public final long time; public final ArrayList<String> messages;
public Result(String t, boolean n, ArrayList<String> m){
target = t; isNeko = n; time = System.currentTimeMillis(); messages = m;
}
}
private static void updateApplicationMainFl(File f) throws Exception {
BufferedWriter out = new BufferedWriter(new FileWriter(new File(f, "ApplicationMain.hx"), true));
out.newLine(); out.write("import AssetList;"); out.newLine(); out.flush(); out.close();
}
private static void generateAssetListFl(Project p, File f) throws Exception {
BufferedWriter out = new BufferedWriter(new FileWriter(new File(f, "AssetList.hx")));
out.write("class AssetList {}"); out.newLine();
for(String type : p.getResourceTypes()){
boolean image = type.equals(Export.IMAGE), sound = type.equals(Export.SOUND);
for(Resource rs : p.getResources(type)){
out.write("class A"); out.write(type); out.write(Long.toHexString(rs.getId()));
if(image) out.write(" extends nme.display.BitmapData { public function new () { super (0, 0); } }");
else if(sound) out.write(" extends nme.media.Sound { }");
else out.write(" extends nme.utils.ByteArray { }"); out.newLine();
}
} out.flush(); out.close();
}
private static Result inner_parse(Project p, File fbase, File fbin) throws Exception {
if(!fbin.exists()) fbin.mkdir(); String target = p.getTarget(); boolean flash = target.equals("flash");
File project = new File(fbin, "project.nmml");
if(!project.exists() || project.lastModified() < new File(fbase, ".project").lastModified()){
BufferedWriter out = new BufferedWriter(new FileWriter(new File(fbin, "project.nmml")));
out.write(createNMML(p)); out.flush(); out.close();
Process _p = Runtime.getRuntime().exec("haxelib run nme update \""+project.getAbsolutePath()+"\" "+target);
_p.waitFor(); if(flash) updateApplicationMainFl(new File(new File(fbin, target), "haxe"));
} File hxml; boolean neko = false; int i = target.indexOf(" -neko"); String t = target; if(i != -1){
neko = true; t = target.substring(0, i); if(target.indexOf("-64") != -1) t += "64";
hxml = new File(fbin, t); hxml = new File(hxml, "neko");
} else hxml = new File(fbin, t); hxml = new File(hxml, "haxe"); hxml = new File(hxml, "release.hxml");
if(!hxml.exists() || !new File(hxml.getParentFile(), "bin").exists()){
Process _p = Runtime.getRuntime().exec("haxelib run nme update \""+project.getAbsolutePath()+"\" "+target);
_p.waitFor(); if(!hxml.exists()) throw new Exception(); if(flash) updateApplicationMainFl(hxml.getParentFile());
} if(flash) generateAssetListFl(p, hxml.getParentFile());
String s = "--cwd \""+fbin.getAbsolutePath()+"\"\n\""+hxml.getAbsolutePath()+"\"\n\000"; byte b[] = s.getBytes("UTF-8");
Socket socket = new Socket("127.0.0.1", port); OutputStream out = socket.getOutputStream(); out.write(b); out.flush();
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
Result r = new Result(t, neko, new ArrayList<String>());
String line; while((line = in.readLine()) != null) r.messages.add(line);
socket.close(); return r;
}
public static Result parse(Project p) throws Exception {
launchHost(); File fbase = p.getFile(), fbin = new File(fbase, Folder.OUT_DIR);
return inner_parse(p, fbase, fbin);
}
private static void deleteAll(File f, boolean subdir) throws Exception {
for(File c : f.listFiles()) if(subdir || !c.isDirectory()){
if(c.isDirectory()) deleteAll(c, subdir); c.delete();
}
}
private static byte NEKO[] = {(byte)'N',(byte)'E',(byte)'K',(byte)'O'};
public static void compile(Project p) throws Exception {
launchHost(); File fbase = p.getFile(), fbin = new File(fbase, Folder.OUT_DIR);
Result r = inner_parse(p, fbase, fbin); p.lastCompile = r; ScriptEditor.compiled(p);
if(r.messages.size() > 0){System.out.println(r.messages); /*TODO: Show error log*/ throw new Exception();}
Export export; String t = r.target;
if(r.isNeko){
File _target = new File(new File(fbin, t), "neko");
File bindir = new File(_target, "bin"); deleteAll(bindir, false); if(t.equals("mac")){
File app = new File(bindir, getProjectOut(p)+".app"); deleteAll(app, true); app.delete();
BufferedWriter command = new BufferedWriter(new FileWriter(new File(bindir, getProjectOut(p)+".command")));
command.write("#!/bin/bash\ncd \"${0%/*}\"\n./"+getProjectOut(p)+"\n"); command.flush(); command.close();
} Resource.copyDir(new File("neko", t), bindir); boolean windows = t.equals("windows"); String ext = ((windows)?".exe":"");
File out = new File(bindir, p.getName()+ext); new File(bindir, "neko"+ext).renameTo(out);
FileOutputStream exe = new FileOutputStream(out, true); int len = (int)exe.getChannel().size();
Export.writeAll(new FileInputStream(new File(new File(_target, "obj"), "ApplicationMain.n")), exe);
exe.write(NEKO); ByteBuffer bb = ByteBuffer.allocate(4); bb.order(ByteOrder.LITTLE_ENDIAN);
bb.putInt(len); exe.write(bb.array()); exe.flush(); exe.close();
export = new NekoExport(new File(new File(_target, "bin"), "assets"));
} else if(t.equals("flash")) export = new SWFExport(new File(new File(new File(fbin, t), "bin"), getProjectOut(p)+".swf"));
else throw new Exception();
for(String type : p.getResourceTypes()){
boolean image = type.equals(Export.IMAGE), sound = type.equals(Export.SOUND);
for(Resource rs : p.getResources(type)){
if(image) export.addImage((Image)rs); else if(sound) export.addMedia((Media)rs); else {
BufferedInputStream in = new BufferedInputStream(new FileInputStream(rs.getFile()));
in.skip(rs.getHeaderSize()); export.addData(in, type, rs.getId(), rs.getFile().lastModified());
}
}
} export.finish();
}
public static void run(Project p) throws Exception {
if(test != null && !isClosed(test)){test.destroy(); test = null;} String t = p.lastCompile.target;
File fbase = p.getFile(), fbin = new File(fbase, Folder.OUT_DIR); fbin = new File(fbin, t);
if(p.lastCompile.isNeko){
fbin = new File(fbin, "neko"); File dir = new File(fbin, "bin");
fbin = new File(fbin, "obj"); fbin = new File(fbin, "ApplicationMain.n");
if(neko_exe == null){
String os = System.getProperty("os.name").toLowerCase();
if(os.contains("windows")) neko_exe = "neko\\windows\\neko.exe";
else if(os.contains("linux")) neko_exe = "neko/linux/neko";
else if(os.contains("mac")) neko_exe = "neko/mac/neko";
else throw new Exception();
//TODO: check for linux 64?
}
test = Runtime.getRuntime().exec(neko_exe+" \""+fbin.getAbsolutePath()+"\"", null, dir);
//TODO: open log window showing errors from test
} else if(t.equals("flash")){
fbin = new File(fbin, "bin"); fbin = new File(fbin, getProjectOut(p)+".swf");
Desktop.getDesktop().open(fbin);
}
}
public static void destroy(){
try{if(host != null){host.destroy(); host = null;}} catch(Exception e){}
try{if(test != null){test.destroy(); test = null;}} catch(Exception e){}
}
}