Convert all Ryzom character models and animations to .j3o format

EDIT: Added missing file, below!

Here’s a program I wrote that, along with the two other classes below, will allow you to get all the Ryzom project (which were released to Creative Commons) character models directly into j3o format, complete with working animations.

Here’s an example of the kind of high-quality models and animation the Ryzom assets contain:

The two other classes you need are my IQE Loader: Inter-Quake Export (IQE) Loader

And my Animation Retargeter: Using a Different Skeleton for Models and Animations

Here are the Ryzom assets in IQE format:

https://bitbucket.org/ccxvii/ryzom-assets

I found that I could only download the assets by doing a git shallow clone from BitBucket.

Running the program will produce a directory under assets/ryzom-assets/export that contains all of the models in j3o format. It will also contain 4 j3o files containing the the hundreds of animations: two male for male models and two for female models.

Why two files per sex? That’s because Ryzom basically used two different base skeletons for their races and animations. The good news is that you can use any animation with any model, provided you listen carefully.

By default, each of the j3o format models this program produces is set to work with the animations in the files starting with “ca_”. But under the “ryzom_alternative” user data on each model is another version of the model targeted to work with the animations in the “ge_” files.

Some caveats:

  1. Almost all the models came through okay, but not all of them. I believe the problem is in the original IQE file.
  2. The j3o files only seem to load in JMonkey version 3.1.0-beta2 and later, which means they won’t load in most (all?) versions of the SDK, at this point.
  3. The models currently use the Unshaded material definition. If someone who knows something about model textures wants to explain to me how I can incorporate Ryzom’s “.s” textures or any of the more advanced texturing that I skipped over, I would be glad to do that.

Any feedback or technical suggestions are appreciated.

Here’s the license:

import java.io.File;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;

import static java.util.logging.Level.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.apache.commons.io.FileUtils;

import com.jme3.animation.AnimControl;
import com.jme3.animation.Skeleton;
import com.jme3.animation.SkeletonControl;
import com.jme3.app.BasicProfilerState;
import com.jme3.app.DebugKeysAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.app.StatsAppState;
import com.jme3.asset.AssetManager;
import com.jme3.asset.TextureKey;
import com.jme3.asset.plugins.ClasspathLocator;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.material.MatParamTexture;
import com.jme3.material.Material;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.SceneGraphVisitor;
import com.jme3.scene.Spatial;
import com.jme3.system.JmeContext;
import com.jme3.texture.Texture;

import net.bithaven.jme.IQELoader;
import net.bithaven.jme.ryzom.Ryzom.ModelPart;

/**
 * Copyright Alweth on hub.jmonkeyengine.org forums 2017
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software (the "Software"), to 
 * use, and modify the Software 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 or any of its derivatives shall not be sold or distributed independently of the expressed intention of 
 * the author.
 * 
 * Any distribution of any of the output of the Software or its derivatives, or any derivative of such output shall 
 * include with it acknowledgement of the contribution of the author of this software, as above, in providing this 
 * software for use, free of charge.
 * 
 * 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.
 *
 * @author Alweth on hub.jmonkeyengine.org forums
 *
 */
public final class RyzomConverter extends SimpleApplication {
	private static final Logger logger = Logger.getLogger(RyzomConverter.class.getName());

	private static AssetManager am;
	private static String ryzomRoot;
	private static Path ryzomRootPath;
	private static String actorsDir = "ryzom-assets/actors";
	private static String actorsRoot;
	private static boolean noAnimations = false;
	private static boolean noModels = false;

	public static void main(String[] args) {
		if (args.length < 1) {
			System.err.println("Error: The path to the directory containing the Ryzom directory \"ryzom-assets\" must be passed as the first argument.");
			return;
		} else {
			ryzomRoot = args[0];
			ryzomRootPath = FileSystems.getDefault().getPath(ryzomRoot);
			actorsRoot = ryzomRoot + "/" + actorsDir;
			for (int i = 1; i < args.length; i++) {
				if (args[i].equals("noanimations")) noAnimations = true;
				if (args[i].equals("nomodels")) noModels = true;
			}
			RyzomConverter app = new RyzomConverter();
			app.start(JmeContext.Type.Headless);
		}
	}

	private RyzomConverter () {
		super(new StatsAppState(), new DebugKeysAppState(), new BasicProfilerState(false));
	}

	/**
	 * Initializes the client app. Inherited from JMonkey; called by JMonkey; don't call.
	 */
	@Override
	public void simpleInitApp() {

		// init stuff that is independent of whether state is PAUSED or RUNNING
		am = getAssetManager();
		am.registerLocator(ryzomRoot, FileLocator.class);
		am.registerLoader(IQELoader.class, "iqe");

		am.registerLocator("assets", FileLocator.class);
		am.unregisterLocator("/", ClasspathLocator.class);
		am.registerLocator("/", ClasspathLocator.class);

	}

	/**
	 * Inherited from JMonkey; called by JMonkey; don't call.
	 * @param tpf The number of seconds that has past since this was last called. Usually much less than 
	 * {@code 1f}.
	 * 
	 */
	@Override
	public void simpleUpdate(float tpf) {
		if (!noModels) {
			for (ModelPart part : ModelPart.values()) {
				DirectoryStream<Path> ds = null;
				try {
					ds = Files.newDirectoryStream(
							FileSystems.getDefault().getPath(actorsRoot + "/" + part.dir), 
							part.fileFilter);
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				for (Path p : ds) {
					logger.log(INFO, "Loading " + part.toString() + ": " + p.getFileName().toString());
					List<Spatial> variants = Ryzom.generateSpatialsFromPath(am, p, ryzomRootPath);
					logger.log(INFO, "Exporting " + variants.size() + " variants.");
					for (Spatial s : variants) {
						s.setName(s.getName() + "@" + s.getUserData("ryzom_skin"));
						exportSpatial(s);
					}
				}
				try {
					ds.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}

		if (!noAnimations) {
			HashMap<String,Node> nodes = new HashMap<String,Node>();
			HashMap<String,AnimControl> animControls = new HashMap<String,AnimControl>();
			animControls.put("ca_hof", null);
			animControls.put("ca_hom", null);
			animControls.put("ge_hof", null);
			animControls.put("ge_hom", null);
			for (Map.Entry<String,AnimControl> entry : animControls.entrySet()) {
				String code = entry.getKey();
				Skeleton sk = Ryzom.getSkeleton(am, code);
				AnimControl aControl = new AnimControl(sk);
				entry.setValue(aControl);
				Node node = new Node("animations_" + code);
				node.addControl(aControl);
				SkeletonControl sc = new SkeletonControl(sk);
				//sc.setHardwareSkinningPreferred(true);
				node.addControl(sc);
				nodes.put(code, node);		
			}

			DirectoryStream<Path> animationsIn = null;
			try {
				animationsIn = Files.newDirectoryStream(
						FileSystems.getDefault().getPath(actorsRoot + "/anims"), "*.iqe");
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

			for (Path p : animationsIn) {
				String fileName = p.getFileName().toString();
				logger.log(INFO, "Loading animation: " + fileName);
				String name = fileName.substring(0, fileName.length() - 4);
				String code = Ryzom.extractSkeletonCode(fileName);
				AnimControl ac = animControls.get(code);
				Ryzom.addAnim(am, ac, name, code);
				//code = Ryzom.switchSkeletonCodeRace(code);
				//ac = animControls.get(code);
				//Ryzom.addAnim(am, ac, name, code);
			}

			Object[] keys = nodes.keySet().toArray();
			for (Object k : keys) {
				logger.log(INFO, "Exporting animations: " + nodes.get(k).getName());
				exportSpatial(nodes.get(k));
				nodes.remove(k);
			}
		}

		stop();
	}

	public void exportSpatial (Spatial s) {
		String code = s.getUserData("ryzom_skeleton");
		if (code != null && !code.startsWith("ca")) {
			Spatial o = s.getUserData("ryzom_alternate");
			if (o != null) {
				s = o;
			}
		}
		s = s.deepClone();
		Path p = FileSystems.getDefault().getPath("assets", "ryzom-assets", "export");
		Path tex = p.resolve("textures");
		try {
			if (!Files.exists(p)) {
				Files.createDirectory(p);
			}
			if (!Files.exists(tex)) {
				Files.createDirectory(tex);
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		s.depthFirstTraversal(new SceneGraphVisitor() {
			@Override
			public void visit(Spatial spatial) {
				if (spatial instanceof Geometry) {
					Material m = ((Geometry) spatial).getMaterial();
					if (m != null) {
						MatParamTexture param = m.getTextureParam("ColorMap");
						if (param != null) {
							Texture t = param.getTextureValue();
							if (t != null) {
								TextureKey key = (TextureKey)t.getKey();
								File from = new File(ryzomRoot + "/" + key.getName());
								Path toPath = tex.resolve(from.getName());
								File to = toPath.toFile();
								try {
									FileUtils.copyFile(from, to);
								} catch (IOException e) {
									// TODO Auto-generated catch block
									e.printStackTrace();
								}
								key = new TextureKey(toPath.subpath(1, toPath.getNameCount()).toString(), key.isFlipY());
								t = am.loadTexture(key);
								m.setTexture("ColorMap", t);
							}
						}
					}
				}
			}
		});
		File out = new File(p.toString(), s.getName() + ".j3o");
		try {
			BinaryExporter.getInstance().save(s, out);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

Here’s the other necessary file (net.bithaven.jme.ryzom.Ryzom.java):

package net.bithaven.jme.ryzom;

import static java.util.logging.Level.*;
import java.util.logging.Logger;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import com.jme3.animation.AnimControl;
import com.jme3.asset.AssetManager;
import com.jme3.asset.TextureKey;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.SceneGraphVisitor;
import com.jme3.scene.Spatial;

import net.bithaven.jme.AnimationRetargeter;

import com.jme3.animation.Animation;
import com.jme3.animation.Skeleton;
import com.jme3.animation.SkeletonControl;

/**
 * Copyright Alweth on hub.jmonkeyengine.org forums 2017
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software (the "Software"), to 
 * use, and modify the Software 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 or any of its derivatives shall not be sold or distributed independently of the expressed intention of 
 * the author.
 * 
 * Any distribution of any of the output of the Software or its derivatives, or any derivative of such output shall 
 * include with it acknowledgement of the contribution of the author of this software, as above, in providing this 
 * software for use, free of charge.
 * 
 * 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.
 *
 * @author Alweth on hub.jmonkeyengine.org forums
 *
 */
public class Ryzom {
	private static final Logger logger = Logger.getLogger(Ryzom.class.getName());
	public static String ryzomAssets = "ryzom-assets";
	private static HashMap<String,Spatial> animationAtlas = new HashMap<String,Spatial>();
	private static HashMap<String,Skeleton> skeletonAtlas = new HashMap<String,Skeleton>();
	
	private Ryzom () {};
	
	public static void main (String[] args) {
		
	}
	
	private static Spatial loadAnim (AssetManager assetManager, String animationName) {
		Spatial animation = animationAtlas.get(animationName);
		if (animation == null) {
			animation = assetManager.loadModel(ryzomAssets + "/actors/anims/" + animationName + ".iqe");
			AnimControl ac2 = animation.getControl(AnimControl.class);
			Animation a = ac2.getAnim(animationName);
			Skeleton sk = Ryzom.getSkeleton(assetManager, extractSkeletonCode(animationName));
			AnimationRetargeter.retarget(a, ac2.getSkeleton(), sk);
			animation.removeControl(ac2);
			animation.removeControl(SkeletonControl.class);
			AnimControl ac3 = new AnimControl(sk);
			ac3.addAnim(a);
			animation.addControl(ac3);
			animation.addControl(new SkeletonControl(sk));
			
			animationAtlas.put(animationName, animation);
		}
		return animation;
	}
	
	public static void addAnim (AssetManager assetManager, AnimControl animControl, String animationName) {
		animControl.addAnim(loadAnim(assetManager, animationName).getControl(AnimControl.class).getAnim(animationName));
	}
	
	public static void addAnim (AssetManager assetManager, AnimControl animControl, String animationName, String code) {
		Spatial animation = animationAtlas.get(animationName + ":" + code);
		if (animation == null) {
			Spatial old = loadAnim(assetManager, animationName);
			animation = new Node(animationName + ":" + code);
			AnimControl ac2 = old.getControl(AnimControl.class);
			Animation a = ac2.getAnim(animationName).clone();
			Skeleton sk = Ryzom.getSkeleton(assetManager, code);
			AnimationRetargeter.retarget(a, ac2.getSkeleton(), sk);
			AnimControl ac3 = new AnimControl(sk);
			ac3.addAnim(a);
			animation.addControl(ac3);
			animation.addControl(new SkeletonControl(sk));
			
			animationAtlas.put(animationName + ":" + code, animation);
		}
		animControl.addAnim(animation.getControl(AnimControl.class).getAnim(animationName));
	}
	
	public static void fixSkeleton (AssetManager assetManager, Spatial toFix) {
		String name = toFix.getName();
		if (name == null) {
			return;
		}
		String prefix = extractSkeletonCode(name);
		
		AnimationRetargeter.retarget(toFix, getSkeleton(assetManager, prefix));
	}
	
	private static Pattern skeletonCodePattern = Pattern.compile("(ca|fy|ma|tr|zo|ge)_ho[fm]");
	private static Pattern skeletonCodePattern2 = Pattern.compile("(ca|fy|ma|tr|zo|ge).*_[fh]_");
	private static Pattern skeletonCodePattern3 = Pattern.compile("ho[fm]");
	public static @Nullable String extractSkeletonCode(@Nonnull String name) {
		Matcher m = skeletonCodePattern.matcher(name);
		if (!m.find()) {
			m = skeletonCodePattern2.matcher(name);
			if (!m.find()) {
				m = skeletonCodePattern3.matcher(name);
				if (!m.find()) {
					return null;
				} else {
					return  "ge_" + m.group();
				}
			} else {
				String code = m.group();
				int c = code.charAt(code.length() - 2);
				String sex = c == 'f' ? "_hof" : "_hom";
				String race = code.substring(0, 2);
				if (!race.equals("ca")) {
					race = "ge";
				}
				return race + sex;
			}
		} else {
			String code = m.group();
			String sex = code.substring(2);
			String race = code.substring(0, 2);
			if (!race.equals("ca")) {
				race = "ge";
			}
			return race + sex;
		}
	}
	
	public static final String switchSkeletonCodeRace (String code) {
		if ("ca".equals(code.substring(0, 2))) {
			return "ge" + code.substring(2);
		} else {
			return "ca" + code.substring(2);
		}
	}

	public static Skeleton getSkeleton(AssetManager assetManager, String prefix) {
		Skeleton s = skeletonAtlas.get(prefix + "_skel");
		if (s == null) {
				Spatial sModel = assetManager.loadModel(ryzomAssets + "/actors/" + prefix + "_skel.iqe");
				s = sModel.getControl(SkeletonControl.class).getSkeleton();
				skeletonAtlas.put(prefix + "_skel", s);
		}
		return s;
	}
	
	public static enum ModelPart {
		HAIR("cheveux", "cheveux"),
		FACE("visage", "visage"),
		ARMOR_HELMET("armor", "casque"),
		ARMOR_CHEST("armor", "gilet", "torse"),
		ARMOR_ARMPADS("armor", "armpad"),
		GAUNTLET("gauntlets", "gauntlet"),
		ARMOR_HANDS("armor", "hand"),
		ARMOR_PANTS("armor", "pantabottes", "pantabotte", "dress"),
		ARMOR_BOOTS("armor", "bottes", "botte");
		
		public final String dir;
		public final String[] codes;

		private ModelPart (String dir, String... codes) {
			this.dir = dir;
			this.codes = codes;
		}
		
		public final DirectoryStream.Filter<Path> fileFilter = new DirectoryStream.Filter<Path>() {
			@Override
			public boolean accept(Path path) throws IOException {
				String name = path.getFileName().toString();
				if (!name.endsWith(".iqe")) {
					return false;
				}
				if (checkModelPart(name) != null) {
					return true;
				} else {
					return false;
				}
			}			
		};
		
		public @Nullable String checkModelPart (@Nonnull String name) {
			for (String code : codes) {
				if (name.indexOf("_" + code) != -1) {
					return code;
				}
			}
			return null;
		}
	}

	public static enum Sex {
		FEMALE("hof"),
		MALE("hom");
		
		public final String code;

		private Sex (String code) {
			this.code = code;
		}
		
		
		
		public boolean is (String test) {
			char c;
			if (this == MALE) {
				c = 'h';
			} else {
				c = 'f';
			}
			return test.indexOf("_" + code + "_") != -1 || test.indexOf("_" + c + "_") != -1;
		}
		
		public String changeTo (String s) {
			if (this == MALE) {
				return s.replaceAll("_hof_", "_hom_").replaceAll("_f_", "_h_");
			} else {
				return s.replaceAll("_hom_", "_hof_").replaceAll("_h_", "_f_");
			}
		}
	}
	
	public static LinkedList<Spatial> generateSpatialsFromPath (AssetManager am, Path p, Path ryzomRootPath) {
		LinkedList<Spatial> outNodes = new LinkedList<Spatial>();
		Spatial s = generateSpatialFromPath(am, p, ryzomRootPath);
		outNodes.add(s);
		
		HashSet<String> skins = new HashSet<String>();
		
		getSkinInfo(p, s, skins);

		generateAlternateRaceVersion(am, s);
		
		String skin = s.getUserData("ryzom_skin");
		skins.remove(skin);
		for (String skin2 : skins) {
			Spatial clone = s.clone(true);
			changeSkin(am, ryzomRootPath, p, clone, skin2);
			clone.setUserData("ryzom_skin", skin2);
			generateAlternateRaceVersion(am, clone);
			outNodes.add(clone);
		}
		
		logger.log(INFO, "Generated " + outNodes.size() + " skin variants.");
		
		return outNodes;
	}

	private static void generateAlternateRaceVersion(AssetManager am, Spatial s) {
		String code = extractSkeletonCode(s.getName());
		Spatial o = s.deepClone();
		s.setUserData("ryzom_skeleton", code);
		String code2 = switchSkeletonCodeRace(code);
		o.setUserData("ryzom_skeleton", code2);
		AnimationRetargeter.retarget(o, getSkeleton(am, code), getSkeleton(am, code2));
		s.setUserData("ryzom_alternate", o);
		o.setUserData("ryzom_alternate", s);
	}

	private static void changeSkin(AssetManager am, Path ryzomRootPath, Path p, Spatial s, String skin) {
		if (s instanceof Geometry) {
			String materialName = (String)s.getUserData("IQEMaterial");
			if (materialName != null) {
				Path dir = p.getParent().resolve("textures");
				Iterator<String> iter = materialNameVariants(materialName, skin);
				while (iter.hasNext()) {
					String testName = iter.next();
					if (Files.exists(dir.resolve(testName + ".png"))) {
						s.setUserData("IQEMaterial", testName);
						((Geometry) s).getMaterial().setTexture("ColorMap", am.loadTexture(
							new TextureKey(	ryzomRootPath.relativize(dir).toString() + "/" + testName + ".png", 
											false)));
						break;
					}
				}
				String newName = materialName;
				if (skin.charAt(0) != '-')
					newName = variantPattern.matcher(newName).replaceAll(skin.substring(0, 2));
				if (!skin.endsWith("-"))
					newName = colorPattern.matcher(newName)
						.replaceAll(skin.substring(skin.indexOf(":") + 1));
			}
		}
		if (s instanceof Node) {
			for (Spatial ss : ((Node) s).getChildren()) {
				changeSkin(am, ryzomRootPath, p, ss, skin);
			}
		}
	}

	private static Pattern variantPattern = Pattern.compile("\\d\\d");
	private static Pattern colorPattern = Pattern.compile("_(c\\d|com|off)");
	
	private static void getSkinInfo(Path p, Spatial s, HashSet<String> skins) {
		String material = (String)s.getUserData("IQEMaterial");
		String skin = null;
		if (material != null) {
			skin = skinFromMaterialName(material);
			skins.add(skin);
			s.setUserData("ryzom_skin", skin);
			getMaterialInfo(p, material, skins);
		}
		
		if (s instanceof Node) {
			for (Spatial ss : ((Node) s).getChildren()) {
				getSkinInfo(p, ss, skins);
				if (skin == null) {
					skin = (String)ss.getUserData("ryzom_skin");
					s.setUserData("ryzom_skin", skin);
				}
			}
		}
	}

	private static void getMaterialInfo(Path path, String material, HashSet<String> skins) {
		Pattern matRegex = Pattern.compile(regexFromMaterialName(material));

		Path textureDir = path.getParent().resolve("textures");
		try {
			DirectoryStream<Path> ds = Files.newDirectoryStream(textureDir);
			for (Path p : ds) {
				String name = p.getFileName().toString();
				if (matRegex.matcher(name).find()) {
					skins.add(skinFromMaterialName(name));
				}
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	private static String regexFromMaterialName(String material) {
		Matcher m = variantPattern.matcher(material);
		if (m.find()) {
			material = material.replace(m.group(), variantPattern.pattern());
		}
		m = colorPattern.matcher(material);
		if (m.find()) {
			material = material.replace(m.group(), colorPattern.pattern());
		} else {
			material += "(" + colorPattern.pattern() + ")?";
		}
		return "^" + material + "\\.png$";
	}

	public static Spatial generateSpatialFromPath (AssetManager am, Path p, Path ryzomRootPath) {
		String name = p.getFileName().toString();
		name = name.substring(0, name.length() - 4); // Remove the extension.
		Spatial s = am.loadModel(ryzomRootPath.relativize(p).toString());
		fixSkeleton(am, s);
		
		fixIrregularities(s);
		
		s.setName(name);
		String path = ryzomRootPath.relativize(p).toString();
		s.setUserData("path", path);
		ModelPart part = null;
		String code = null;

		for (ModelPart check : ModelPart.values()) {
			code = check.checkModelPart(name);
			if (code != null) {
				part = check;
				break;
			}
		}
		if (part == null) {
			return s;
		} 

		s.setUserData("ryzom_part", part.toString());
		s.setUserData("ryzom_part_name", code);
		s.setUserData("ryzom_style", name.substring(0, name.indexOf(code)));
		s.setUserData("ryzom_substyle", name.substring(name.indexOf(code) + code.length()));
		
		return s;
	}

	//TODO: There's really no need to traverse every subspatial just to fix this one problem.
	private static void fixIrregularities(Spatial s) {
		//Hacks to fix irregularities in the Ryzom naming.
		s.depthFirstTraversal(new SceneGraphVisitor() {
			@Override
			public void visit(Spatial spatial) {
				if ("ca_hof_armor_01_epaule_c1".equals((String)spatial.getUserData("IQEMaterial"))) {
					 //Make it consistent with how the other skins are named so that variants can be found.
					spatial.setUserData("IQEMaterial", "ca_hof_armor01_epaule_c1");
				}
			}
		});
	}
	
	private static String skinFromMaterialName (String name) {
		String variant = "-";
		String color = "-";
		Matcher m = variantPattern.matcher(name);
		if (m.find()) {
			variant = m.group();
		}
		m = colorPattern.matcher(name);
		if (m.find()) {
			color = m.group();
		}
		return variant + ":" + color;
	}
	
	private static Iterator<String> materialNameVariants (String name, String skin) {
		return new Iterator<String>() {
			int mask = (skin.charAt(0) == '-' ?  0b00 : 0b10) + (skin.endsWith("-") ? 0b0 : 0b1);
			int next = mask;

			@Override
			public boolean hasNext() {
				return next >= 0;
			}

			@Override
			public String next() {
				String out = applySkinToName(name, skin, next);
				next--;
				while (((next & mask) ^ next) != 0b00 && next >= 0) {
					next--;
				}
				return out;
			}
		};
	}
	
	private static String applySkinToName(String name, String skin, int version) {
		Matcher m;
		if ((version & 0b10) == 0b10) {
			m = variantPattern.matcher(name);
			if (m.find()) {
				name = name.replace(m.group(), skin.substring(0, 2));
			}
		}
		if ((version & 0b1) == 0b1) {
			m = colorPattern.matcher(name);
			if (m.find()) {
				name = name.replace(m.group(), skin.substring(skin.indexOf(":") + 1));
			} else {
				name += skin.substring(skin.indexOf(":") + 1);
			}
		}
		return name;
	}

	private static class Skin implements Comparable<Skin> {
		final String variant;
		final String color;
		
		public static Skin fromMaterialName (String name) {
			String variant = null;
			String color = null;
			Matcher m = variantPattern.matcher(name);
			if (m.find()) {
				variant = m.group();
			}
			m = colorPattern.matcher(name);
			if (m.find()) {
				color = m.group();
			}
			if (variant == null && color == null) {
				return null;
			}
			return new Skin(variant, color);
		}
		
		public Skin (String variant, String color) {
			this.variant = variant;
			this.color = color;
		}
		
		@Override
		public int compareTo(Skin o) {
			int c1 = variant.compareTo(o.variant);
			if (c1 != 0) {
				return c1;
			} else {
				return color.compareTo(o.color);
			}
		}
		
		@Override
		public boolean equals(Object obj) {
			if (obj == null) {
				return false;
			} else if (!Skin.class.isAssignableFrom(obj.getClass())) {
				return compareTo((Skin)obj) == 0;
			} else {
				return false;
			}
		}
	}
}
10 Likes

Thank you, thank you, thank you !

I’ve played the game, and used some of the texture when i found the repository, but could not use the models. Trees and plants have a definitely alien look, as well as some of the monsters.

regards

1 Like

Here’s a thread explaining how to bring in (maybe) all non-character assets:

I don’t find ModelPart or Ryzom.getSkeleton. Are those classes in a separate library or package?.

1 Like

So sorry. I’ve added that file to the original post.

2 Likes

Hi, Alweth!.
It’s good to see you here.
I’ve been out for a few months too because of my move and my real job, which is a gigantic time-consuming beast.
Thanks for

I’m unclear what the license allows.

I’m assembling a Gradle project (including your source code with minor modifications) to simplify the conversion. It would include your copyright notice and license. It would acknowledge you as the original author of the code.

Would it be OK to upload the project to GitHub as a public repo? Or would such distribution be “independently of the expressed intention of the author”?

1 Like

Thanks for asking. You have my permission to distribute it on GitHub as you’ve described. Please link to the distribution here.

3 Likes

Thank you for the prompt reply.

The new GitHub repository is at GitHub - stephengold/RyzomConverter: Adapt models from the Ryzom Asset Repository for use with jMonkeyEngine.

I’m still figuring how to use the J3O files in an application. I’ll add a screenshot and expand the instructions once I do. Other than that, I’m open to any feedback on the new repo.

4 Likes

I fixed a bug or two. Also, it now has a page at JMonkeyStore:
View Page - jMonkeyEngine - Store

5 Likes