package tk.captainsplexx.Mod;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.UUID;
import tk.captainsplexx.Game.Core;
import tk.captainsplexx.Resource.FileHandler;
import tk.captainsplexx.Resource.ResourceHandler.LinkBundleType;
import tk.captainsplexx.Resource.ResourceHandler.ResourceType;
import tk.captainsplexx.Resource.CAS.CasCatEntry;
import tk.captainsplexx.Resource.CAS.CasCatManager;
import tk.captainsplexx.Resource.CAS.CasDataReader;
import tk.captainsplexx.Resource.CAS.CasManager;
import tk.captainsplexx.Resource.ITEXTURE.ITexture;
import tk.captainsplexx.Resource.ITEXTURE.ITextureConverter;
import tk.captainsplexx.Resource.TOC.ConvertedSBpart;
import tk.captainsplexx.Resource.TOC.ConvertedTocFile;
import tk.captainsplexx.Resource.TOC.ResourceLink;
import tk.captainsplexx.Resource.TOC.TocConverter;
import tk.captainsplexx.Resource.TOC.TocCreator;
import tk.captainsplexx.Resource.TOC.TocFile;
import tk.captainsplexx.Resource.TOC.TocManager;
import tk.captainsplexx.Resource.TOC.TocSBLink;
public class ModTools {
public ArrayList<Mod> mods;
public ArrayList<Package> packages;
public static final String RESOURCEFOLDER = "/resources/";
public static final String PACKAGEFOLDER = "/packages/";
public static final String PACKTYPE = ".pack";
public ModTools() {
init();
}
public void init(/*AKA. RESET*/){
this.mods = new ArrayList<>();
this.packages = new ArrayList<>();
fetchMods();
Core.getJavaFXHandler().getMainWindow().updateModsList();
}
public void fetchMods(){
File modsDir = new File("mods/");
for (File f : modsDir.listFiles()){
if (f.isDirectory()){
Mod mod = new Mod();
mod.setPath(FileHandler.normalizePath(f.getAbsolutePath()));
String[] splitPath = mod.getPath().split("/");
mod.setFolderName(splitPath[splitPath.length-1]);
File info = new File(f.getAbsolutePath()+"\\info.txt");
if (info.exists()){
readModInfo(mod, info);
}
if (mod.getAuthor() != null){
String[] split = Core.gamePath.split("/");
int length = split.length;
if (Core.gamePath.endsWith("/")){
length--;
}
String destFolderPath = "";
for (int i=0; i<length-1;i++){
destFolderPath +=split[i]+"/";
}
destFolderPath += mod.getGame()+"_"+mod.getFolderName();
mod.setDestFolderPath(destFolderPath);
mods.add(mod);
}
}
}
}
void readModInfo(Mod mod, File info){
try {
FileReader fr = new FileReader(info);
BufferedReader br = new BufferedReader(fr);
mod.setName(br.readLine());
mod.setAuthor(br.readLine());
mod.setGame(br.readLine());
String line = "";
String text = "";
while ((line = br.readLine()) != null){
text += line+"\n";
}
mod.setDesc(text);
br.close();
fr.close();
}catch (Exception e){
System.err.println("Could not read Info from Mod. "+info.getAbsolutePath());
}
}
//TODO Extract sources from zip once. delete zip. sharing mod-> compress.
public void fetchPackages(){
int entries = 0;
ArrayList<File> files = FileHandler.listf(Core.getGame().getCurrentMod().getPath()+PACKAGEFOLDER, ".pack");
for (File f : files){
if (!f.isDirectory()){
Package pack = readPackageInfo(f);
if (pack!=null){
packages.add(pack);
entries+=pack.getEntries().size();
}
}
}
System.out.println(packages.size()+" Packages where found in current mod with a total of "+entries+" entries.");
}
public Package readPackageInfo(File file){
try {
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
Package pack = new Package(FileHandler.normalizePath(file.getAbsolutePath()).replace(Core.getGame().getCurrentMod().getPath()+"/packages", "").replace(PACKTYPE, ""));
String line = "";
while ((line = br.readLine()) != null){
if (!line.startsWith("#")){
String[] parts = line.split("\\|");
/*| is treated as an OR in RegEx. So you need to escape it:
* String[] separated = line.split("\\|");
*/
if (parts.length<4||parts.length>5){continue;}
PackageEntry entry = new PackageEntry(
LinkBundleType.valueOf(parts[0]),
parts[1],
ResourceType.valueOf(parts[2]),
parts[3]
);
if (parts.length==5){//additional
entry.setTargetPath(parts[4]);
}
pack.getEntries().add(entry);
}
}
br.close();
fr.close();
return pack;
}catch (Exception e){
System.err.println("Could not read Info from Package. "+file);
return null;
}
}
public boolean writePackage(Package pack, File file){
if (file.exists()){
file.delete();
}
ArrayList<String> entries = new ArrayList<>();
for (PackageEntry entry : pack.getEntries()){
String fullEntry = entry.getBundleType()+"|"+entry.getSubPackage()+"|"+entry.getResType()+"|"+entry.getResourcePath();
if (entry.getTargetPath()!=null){
fullEntry += "|"+entry.getTargetPath();
}
entries.add(fullEntry);
}
Collections.sort(entries);
if (!FileHandler.writeLine(entries, file)){
return false;
}
return true;
}
public boolean writePackages(){
if (Core.getGame().getCurrentMod()==null){
return false;
}
for (Package pack : packages){
if (!writePackage(pack, new File(Core.getGame().getCurrentMod().getPath()+"/packages/"+pack.getName()+PACKTYPE))){
return false;
}
}
return false;
}
public Package getPackage(String name){
for (Package pack : packages){
if (pack.getName().equals(name)){
return pack;
}
}
Package pack = new Package(name);
packages.add(pack);
return null;
}
public boolean playMod(boolean recompile){
if (recompile){
String path = Client.cloneClient(Core.gamePath+"/", Core.getGame().getCurrentMod().getGame()+"_"+Core.getGame().getCurrentMod().getFolderName(), true);
/*String path = Core.getGame().getCurrentMod().getGame()+"_"+Core.getGame().getCurrentMod().getFolderName();*/
if (path!=null){
System.out.println("Compile Client...");
String casCatPath = path+"/Update/Patch/Data/cas_99.cas";
CasCatManager manPatched = Core.getGame().getResourceHandler().getPatchedCasCatManager();
CasManager.createCAS(casCatPath);
Mod currentMod = Core.getGame().getCurrentMod();
//TODO MOD CLIENT LOGIC! multi subpackages dont work ;(
for (Package pack : packages){
Core.getGame().setCurrentFile(FileHandler.normalizePath((Core.gamePath+"/"+pack.getName())));
TocFile toc = TocManager.readToc(Core.getGame().getCurrentFile());
ConvertedTocFile convToc = TocConverter.convertTocFile(toc);
//SORT
HashMap<String, ArrayList<PackageEntry>> sorted = new HashMap<>();
for (PackageEntry entry : pack.getEntries()){
ArrayList<PackageEntry> subPackageEntries = sorted.get(entry.getSubPackage());
if (subPackageEntries==null){
subPackageEntries = new ArrayList<PackageEntry>();
sorted.put(entry.getSubPackage(), subPackageEntries);
entry.setSubPackage(null);//why not free up some memory here :)
}
subPackageEntries.add(entry);
}
//PROCC
for (String subPackageName : sorted.keySet()){
ConvertedSBpart currentSBpart = null;
for (TocSBLink link : convToc.getBundles()){
if (link.getID().equals(subPackageName)){
//link.setSbPath(sbPath); change the sb path once one subpackage is already done
TocFile part = link.getLinkedSBPart();
currentSBpart = TocConverter.convertSBpart(part);
break;
}
}
if (currentSBpart==null){
System.err.println("Mod.ModTools.playMod can't handle new subpackages at this time. ");
return false;
}
int originalSize = 0;
ArrayList<PackageEntry> subPackageEntries = sorted.get(subPackageName);
byte[] data = null;
CasCatEntry casCatEntry;
CasCatEntry casCatEntryChunk;
String chunkID;
for (PackageEntry sortedEntry : subPackageEntries){
casCatEntry = null;
casCatEntryChunk = null;
chunkID = null;
switch(sortedEntry.getResType()){
case ANIMTRACKDATA:
break;
case ANT:
break;
case CHUNK:
break;
case EBX:
data = FileHandler.readFile(currentMod.getPath()+RESOURCEFOLDER+sortedEntry.getResourcePath());
originalSize = data.length;
casCatEntry = CasManager.extendCAS(data, new File(casCatPath), manPatched);
break;
case ENLIGHTEN:
break;
case GFX:
break;
case HKDESTRUCTION:
break;
case HKNONDESTRUCTION:
break;
case ITEXTURE:
byte[] ddsFileBytes = /*DSS FILE*/FileHandler.readFile(currentMod.getPath()+RESOURCEFOLDER+sortedEntry.getResourcePath());
chunkID = UUID.randomUUID().toString().replace("-", "");
String[] split = sortedEntry.getResourcePath().split("\\.");
byte[] originalHeaderBytes = readOrignalData(split[0], currentSBpart.getRes());
if (originalHeaderBytes!=null){
FileHandler.writeFile("output/debug/originalHeaderBytes", originalHeaderBytes);
ITexture newITexture = ITextureConverter.getITextureHeader(ddsFileBytes, new ITexture(originalHeaderBytes, null), chunkID);
/*Temp debug test
System.err.println("Texture Replacement does not work at the moment :(\n"
+ "use originalHeaderBytes instead!");
ITexture oldITexture = new ITexture(originalHeaderBytes, new FileSeeker());
newITexture.setChunkID(oldITexture.getChunkID());*/
data = newITexture.toBytes();
FileHandler.writeFile("output/debug/newITexture", data);
originalSize = data.length;
casCatEntry = CasManager.extendCAS(data, new File(casCatPath), manPatched);
byte[] blockData = ITextureConverter.getBlockData(ddsFileBytes);
casCatEntryChunk = CasManager.extendCAS(blockData, new File(casCatPath), manPatched);
modifyChunkEntry(casCatEntryChunk, chunkID, blockData.length, newITexture.getNameHash(), currentSBpart, true /*isNew*/);
}else{
System.err.println("ITexture could not get applied!");
}
break;
case LIGHTINGSYSTEM:
break;
case LUAC:
break;
case MESH:
break;
case OCCLUSIONMESH:
break;
case PROBESET:
break;
case RAGDOLL:
break;
case SHADERDATERBASE:
break;
case SHADERDB:
break;
case SHADERPROGRAMDB:
break;
case STATICENLIGHTEN:
break;
case STREAIMINGSTUB:
break;
case UNDEFINED:
break;
default:
break;
}
if (sortedEntry.getResType()==ResourceType.EBX){
modifyResourceLink(sortedEntry, casCatEntry, originalSize, currentSBpart.getEbx());
}else if (sortedEntry.getResType()==ResourceType.ITEXTURE||sortedEntry.getResType()==ResourceType.MESH){
modifyResourceLink(sortedEntry, casCatEntry, originalSize, currentSBpart.getRes());
}else{
System.err.println(sortedEntry.getResType()+" isn't defined in (Mod.ModTools.playMod) for modifyResourceL1nk!");
}
if (casCatEntryChunk!=null){
manPatched.getEntries().add(casCatEntryChunk);
}
if (casCatEntry!=null){
manPatched.getEntries().add(casCatEntry);
}
}
//TODO convToc.setTotalSize(totalSize);
String newPath = ((String) Core.getGame().getCurrentFile()+".sb").replace(Core.gamePath, path);
TocCreator.createModifiedSBFile(convToc, currentSBpart, false/*TODO*/, newPath, true/*delete first*/);
}
byte[] tocBytes = TocCreator.createTocFile(convToc);
File newTocFile = new File(((String) Core.getGame().getCurrentFile()+".toc").replace(Core.gamePath, path));
if (newTocFile.exists()){
newTocFile.delete();//delete do remove hardlink.
}
FileHandler.writeFile(newTocFile.getAbsolutePath(), tocBytes);
}
//CREATE CAS.CAT
byte[] patchedCasCatBytes = manPatched.getCat();
File casCatFile = new File(path+"/Update/Patch/Data/cas.cat");
if (casCatFile.exists()){
casCatFile.delete();
}
FileHandler.writeFile(casCatFile.getAbsolutePath(), patchedCasCatBytes);
//DONE OPEN FOLDER!
FileHandler.openFolder(path);
//Core.getJavaFXHandler().getDialogBuilder().showInfo("INFO", "Ready to Play!\nOrigin DRM Files needs to be replaced manually!");
//Core.getJavaFXHandler().getMainWindow().toggleModLoaderVisibility();
Core.keepAlive = false;
return true;
}
Core.getJavaFXHandler().getDialogBuilder().showError("ERROR", "Something went wrong :(", null, null);
return false;
}else{
FileHandler.openFolder(Core.getGame().getCurrentMod().getDestFolderPath());
Core.getJavaFXHandler().getDialogBuilder().showInfo("INFO", "Have fun =)");
return false;
}
}
/*public ResourceLink modifyChunkEntry(CasCatEntry chunkCatEntry, String chunkGuid, Integer chunkSize, ConvertedSBpart convertedSBpart, boolean isNew){
return modifyChunkEntry(chunkCatEntry, chunkGuid, chunkSize, 0, convertedSBpart, isNew);
}*/
public boolean modifyChunkEntry(CasCatEntry chunkCatEntry, String chunkGuid, Integer chunkSize, int h32NameHash, ConvertedSBpart convertedSBpart, boolean isNew){
if (isNew){
//Confirmed! Everything working fine!!
ResourceLink chunkLink = new ResourceLink();
/*we are going for patched default without mip settings
* and set the itexture starting map to 0 or 1. (whatever its starts at)*/
chunkLink.setId(chunkGuid);
chunkLink.setSha1(chunkCatEntry.getSHA1());
chunkLink.setLogicalOffset(0);//start at 0 or first mip
chunkLink.setLogicalSize(chunkSize);
/*chunkLink.setRangeStart(0); we not need mip mapping
chunkLink.setRangeEnd(chunkSize);*/
chunkLink.setCasPatchType(1);//patch it using patched cas.
/* We need to put in the compressed size.
* We not using any compression but have compression headers
* The file is bigger as one block. So we have to calculate the
* total number of blocks to get the header size we can add to the
* raw dds block data size.*/
int sizeHeaders = CasManager.calculateNumberOfBlocks(chunkSize) * CasManager.blockHeaderNumBytes;
chunkLink.setSize(sizeHeaders+chunkSize);
/*
SBENTRY-CHUNK:
ID: 01 10 96 C2 D2 DA DF 9B 39 31 23 20 14 07 C1 E7
SHA1: 78 3A 38 9D E8 F9 E8 FE E6 F3 35 C1 D9 D5 7A C9 0A 9C A9 33
the 3 following offset are based on the compressed entries.
Thats kinda wierd....
SIZE: DE 77 10 00 00 00 00 00 == 1.079.262 ---> Same as SBENTRY(RANGEEND)
--> Compressed size with headers
RANGESTART: 02 6E 0F 00 == 1.011.202 ---> Same as MipONEnedoffset from ITEXTURE!
--> I've checked this value. its the position inside compressed block array.
So the offset contains the block header too!
--> the delta is 0x109DC aka. 68.060... the offset is 0x91FE smaller as the size of (3rd counting form 1.) mip
RANGEEND: DE 77 10 00 == 1.079.262 ---> Same as SBENTRY(SIZE)
-->range end to logical offset has a space of 0x40000-0x77DE which is the delta from range end to the next mip ??
LOGICALOFFSET: 00 00 14 00 == 1.310.720 ---> first mip size + second mip size
LOGICALSIZE: 68 55 01 00 == 87.400 --->
has no idata or h32 or meta
ITEXTURE:
FirstMip: 02
MipONEnedoffset: 3B 52 0C 00 == 807.483 --> as chunk's rangestart its the compressed offset with block
headers
MipTWOendoffset: 02 6E 0F 00 == 1.011.202 ---> Same as SBENTRY(RANGESTART) || This is the first MipMap Level -2-
ChunkSize: 68 55 15 00 == 1.398.120 ---> SBENTRY(LOGICALOFFSET) + SBENTRY(LOGICALOFFSET) == this
*This seems to be the default one
id
sha1
size
logicalOffset
logicalSize
-----------------
*And i guess this is the patched default one.
id
sha1
size
logicalOffset
logicalSize
casPatchType
-----------------
*This resource can be extracted by given range arguments.
*ITexture is prob. using this for starting on a lower mip map
id
sha1
size
rangeStart
rangeEnd
logicalOffset
logicalSize
-----------------
*the patched one with range
id
sha1
size
rangeStart
rangeEnd
logicalOffset
logicalSize
casPatchType
*/
/*if (Game is DragonAge){
chunkLink.setH32(h32NameHash);
chunkLink.setMeta(new byte[] {8, 102, 105, 114, 115, 116, 77, 105, 112, 0, 0, 0, 0, 0, 0});
}*/
convertedSBpart.getChunks().add(chunkLink);
return true;
}else{
System.err.println("TODO modify Chunk Entry that already exist!");
}
return false;
}
public boolean modifyResourceLink(PackageEntry packEntry, CasCatEntry casCatEntry, int originalSize, ArrayList<ResourceLink> targetList){
/*ALREADY EXIST*/
for (ResourceLink link : targetList){
String targetObject = packEntry.getTargetPath();//has a special targetPath defined, use this.
if (targetObject==null){
targetObject = packEntry.getResourcePath();//otherwise use the resourcePath as target.
}
if (link.getName().equals(targetObject.replace(".", "-").split("-")[0])){
link.setCasPatchType(1);//Patching using data from update cas
//link.setResType(resType);
//link.setLogicalOffset(logicalOffset);
link.setSha1(casCatEntry.getSHA1().toLowerCase());
link.setBaseSha1(null);
link.setDeltaSha1(null);
link.setSize(casCatEntry.getProcSize());
link.setOriginalSize(originalSize);
return true;
}
}
/*NEW ONE*/
String targetObject = packEntry.getTargetPath();//has a special targetPath defined, use this.
if (targetObject==null){
targetObject = packEntry.getResourcePath();//otherwise use the resourcePath as target.
}
ResourceLink link = new ResourceLink();
link.setName(targetObject.replace(".", "-").split("-")[0]);
link.setType(packEntry.getResType());
//link.setResType(resType);
//link.setLogicalOffset(logicalOffset);
link.setBaseSha1(null);
link.setDeltaSha1(null);
link.setCasPatchType(1);//Patching using data from update cas
link.setSha1(casCatEntry.getSHA1().toLowerCase());
link.setSize(casCatEntry.getProcSize());
link.setOriginalSize(originalSize);
targetList.add(link);
return false;
}
public boolean extendCurrentPackage(LinkBundleType bundle, String sbPart, ResourceType type, String path){
String currentTocName = Core.getGame().getCurrentToc().getName();
Package currentPackage = Core.getModTools().getPackage(currentTocName);
if (currentPackage!=null){
return extendPackage(bundle, sbPart, type, path, currentPackage);
}
return false;
}
public boolean extendPackage(LinkBundleType bundle, String sbPart, ResourceType type, String path, Package pack){
return extendPackage(bundle, sbPart, type, path, null, pack);
}
public boolean extendPackage(LinkBundleType bundle, String sbPart, ResourceType type, String path, String targetPath, Package pack){
PackageEntry entry = new PackageEntry(bundle, sbPart, type, path);
if (targetPath!=null){
/*Additional, if someone whats to have a diffrent resources for a package with the same path.*/
entry.setTargetPath(targetPath);
}
for (PackageEntry pEntry: pack.getEntries()){
if (pEntry.getBundleType()==bundle && pEntry.getSubPackage().equals(sbPart) &&
pEntry.getResourcePath().equals(path) && pEntry.getResType()==type &&
pEntry.getTargetPath()==targetPath){
//entry does already exist.
System.err.println("Entry does allready exist. So we dont have to extend the package!");
return true;
}
}
pack.getEntries().add(entry);
return true;
}
public byte[] readOrignalData(String resourceName, ArrayList<ResourceLink> resourceList){
for (ResourceLink link : resourceList){
if (link.getName().equalsIgnoreCase(resourceName)){
return CasDataReader.readCas(link.getBaseSha1(), link.getDeltaSha1(), link.getSha1(), link.getCasPatchType());
}
}
System.err.println("Original Data could not get found for "+resourceName);
return null;
}
//i dont really have to create a function for removing one line from a ".pack" file? huh.
//Getter n Setter
public ArrayList<Mod> getMods() {
return mods;
}
public ArrayList<Package> getPackages() {
return packages;
}
}