Plus Porter (1.1.0.1)

Jan 25, 2015 at 2:38 AM
In my body, in my head
Forum Moderator
"Life begins and ends with Nu."
Join Date: Aug 28, 2009
Location: The Purple Zone
Posts: 5998
it's written in java.....................................
 
Jan 26, 2015 at 12:27 AM
Based Member
"Life begins and ends with Nu."
Join Date: Dec 31, 2011
Location: United States
Posts: 2314
Age: 27
Like the others have said, this program is written in Java and therefore should theoretically be able to run on any operating system that has a Java Virtual machine written for it, which includes Mac an Linux. I've made enough finishing touches to the point where I can consider looking into making this program better support other operating systems, so if there are any issues getting this to work on Linux, let me know. I'm not quite sure how the Linux version of the game is structured, though, so there may be some problems trying to access the files in the game's installation.
 
Jan 27, 2015 at 8:33 AM
Junior Member
"Fresh from the Bakery"
Join Date: Apr 25, 2011
Location: Smack in the center of America.
Posts: 18
Age: 29
duncathan said:
In case you don't see noxid's point, he means that it's (theoretically?) already cross-compatible.

I do see his point. I'm not the complete idiot you seem to think I am.

However, it's not cross-compatible. I've tried. The download is a Windows/DOS executable, and I can't get Java to install in Wine. Is there a .jar I can use to run it natively in Linux?
 
Jan 27, 2015 at 8:41 AM
In my body, in my head
Forum Moderator
"Life begins and ends with Nu."
Join Date: Aug 28, 2009
Location: The Purple Zone
Posts: 5998
ah, hayden, why you go n' do that for
 
Jan 27, 2015 at 2:31 PM
Been here way too long...
Discord Group Admin
Org Discord Moderator
"Life begins and ends with Nu."
Join Date: Oct 18, 2011
Location:
Posts: 2337
Thunderchin said:
I do see his point. I'm not the complete idiot you seem to think I am.
Well my fucking bad then, I guess. Sorry for providing helpful information in case you needed it.

@Hayden yeah giving us a jar rather than exe would be a good first step towards cross-compatibility support
 
Jan 27, 2015 at 3:35 PM
Based Member
"Life begins and ends with Nu."
Join Date: Dec 31, 2011
Location: United States
Posts: 2314
Age: 27
Ah, sorry about that. The program doesn't work right unless I change a certain JVM property, and in order to have it hard-coded into the runnable file I had to wrap the jar into an exe. I'll have to look into alternative methods when I get the time.

Noxid said:
ah, hayden, why you go n' do that for
You're one to talk.
 
Last edited by a moderator:
Jan 27, 2015 at 3:47 PM
In my body, in my head
Forum Moderator
"Life begins and ends with Nu."
Join Date: Aug 28, 2009
Location: The Purple Zone
Posts: 5998
well the key difference is I provided it as an option, not the sole source of the program, since as is apparent it kind of kills Java's key benefit of being cross-platform-ish
what JVM option are you tweaking
 
Jan 27, 2015 at 4:28 PM
Based Member
"Life begins and ends with Nu."
Join Date: Dec 31, 2011
Location: United States
Posts: 2314
Age: 27
Noxid said:
well the key difference is I provided it as an option, not the sole source of the program
Fair enough.

Noxid said:
what JVM option are you tweaking
I increased the stack size. With the default stack size, the NICALiS graphics filtering process will freeze if you give it an image with large enough clumps of pixels that match sections from NICALiS sprite sheets.
 
Jan 27, 2015 at 8:21 PM
In my body, in my head
Forum Moderator
"Life begins and ends with Nu."
Join Date: Aug 28, 2009
Location: The Purple Zone
Posts: 5998
yeah, what dunc said would work
And, I reckon there's probably more efficient ways of doing that. Like, breaking the image up into 16x16 / 32x32 chunks and comparing them sequentially, since the coords have to be the same on each end anyway and all the sprites are multiples of 8 or 16 at least
 
Jan 27, 2015 at 11:15 PM
Lvl 1
Forum Moderator
"Life begins and ends with Nu."
Join Date: May 28, 2008
Location: PMMM MMO
Posts: 3713
Age: 31
HaydenStudios said:
I increased the stack size. With the default stack size, the NICALiS graphics filtering process will freeze if you give it an image with large enough clumps of pixels that match sections from NICALiS sprite sheets.
I think you'll need to explain how you're doing this, or maybe pastebin some code, because I have no idea how you're using the entire stack with this process (this smells like bad code).
 
Jan 28, 2015 at 3:36 AM
Based Member
"Life begins and ends with Nu."
Join Date: Dec 31, 2011
Location: United States
Posts: 2314
Age: 27
Noxid said:
yeah, what dunc said would work
That's what I was thinking, I just haven't found out what the precise commandline syntax for that is yet. I probably won't have to do much digging, though.

Noxid said:
And, I reckon there's probably more efficient ways of doing that. Like, breaking the image up into 16x16 / 32x32 chunks and comparing them sequentially, since the coords have to be the same on each end anyway and all the sprites are multiples of 8 or 16 at least
The coordinates won't always be the same on each end, though. And we're not just scanning for character sprites, but also patterns from 2x res backgrounds, face pictures, and credit images as well. Those generally aren't neatly divided into discernible sections.


GIRakaCHEEZER said:
I think you'll need to explain how you're doing this, or maybe pastebin some code, because I have no idea how you're using the entire stack with this process (this smells like bad code).
When an image is loaded for filtering, it first is scanned for any clumps of pixels that are too small to be considered worthwhile matches. It then loads the first file on the list of 2x res images to compare it to. Before it begins scanning for matching patterns, it first goes through some filters. The first filter analyzes all the colors present in the 2 images. If the number of colors they share is any less than 2, then the image is skipped, and the image being filtered is then compared with the next file on the list of images in the CS+ directory. If they have enough colors in common, the process will continue. The program then sees if there are any colors that the two images don't have in common, and if so then it makes sure that the locations of those colors in the images will be ignored. It then begins the main scanning phase which takes place as follows:
Code:
Start at the first pixel of image 1:
{
    If for some reason this pixel is one we've been told to ignore:
        Move onto the next pixel in image 1
    Start at the first pixel of image 2:
    {
        If for some reason this pixel is one we've been told to ignore:
            Move onto the next pixel in image 2
        If said pixel in image 1 is the same color as said pixel in image 2:
        {
            See if the corresponding pixels of the images match in the 8 surrounding pixels
            If so, then see if it matches the 8 pixels around that as well, and keep going until you reach a dead end
            If the pattern has enough pixels/colors, then the clump is declared as filtered; it will then ignore all pixels involved
        }
        If said pixel in image 1 is NOT the same color as said pixel in image 2:
            Move on to the next pixel in image 2
    }
    When you've reached the last pixel of image 2, move onto the next pixel of image 1, and start back at the first pixel of image 2
}
When you've reached the last pixel of image 1, the scanning process is complete. It now loads a new image to compare image 1 to.
The algorithm for scanning the 8 pixels around it is recursive, which is why the stack gets really large when you're scanning images with large clumps.

Now, obviously, this technique is extremely inefficient, which is why, if you are replacing an existing image, the program suspects that a good portion of the image getting filtered matches the corresponding pixels of the image it's replacing, and therefore does this check before it does anything else:
Code:
Set the image getting replaced as image 2
Start at the first pixel:
{
    If said pixel from image 1 matches said pixel from image 2, set said pixel as a filtered point that will be ignored when scanning
    If said pixel from image 1 does NOT match said pixel from image 2, then do nothing
    Move onto the next pixel
}
This quick check usually knocks out most of the image to scan, so ideally that more lengthy algorithm is only needed to mop up the section of the image that was changed. After doing this check, it will sift through all the other images like normal to find any copyrighted patterns in the image being scanned. If you're adding a spritesheet or other image resource without replacing an existing one, then the filtering wizard will rely solely on the less efficient algorithm since it won't know to bank on any particular image having a strong resemblance to the one being filtered.
 
Jan 28, 2015 at 4:59 AM
In my body, in my head
Forum Moderator
"Life begins and ends with Nu."
Join Date: Aug 28, 2009
Location: The Purple Zone
Posts: 5998
I don't really understand what this process is trying to accomplish. What do you do with the filtered pixels? what happens to the not filtered ones?
What do typical input/outputs look like?
 
Jan 28, 2015 at 11:20 AM
Based Member
"Life begins and ends with Nu."
Join Date: Dec 31, 2011
Location: United States
Posts: 2314
Age: 27
The purpose is to take an image, and find all instances of NICALiS graphics in it that match areas of any of the 2x res spritesheets, tilesets, backgrounds, face pictures, or credit images present in a Cave Story+ directory. After finding all the instances of NICALiS graphics, it writes the locations of the pixels that use NICALiS graphics to a text document, as well as the place in the 2x res images to take those pixels from. It then changes those pixels in the filtered image to black. That way, you've still got the data for the image but you can legally distribute it. Here's an example of what an image will look like after getting filtered:

ItemImage_zpsrjbmmpdn.png

This here is ItemImage.bmp from my fourth ending mod. The main change is that the beast fang is changed to Booster's glasses. What you see here will remain in this image until it's time to apply the port.

When Plus Porter applies a port, it opens the list of reference points for each image, gets each pixel from the referenced images, and puts them back into the filtered image.
 
Jan 28, 2015 at 3:32 PM
In my body, in my head
Forum Moderator
"Life begins and ends with Nu."
Join Date: Aug 28, 2009
Location: The Purple Zone
Posts: 5998
Ok so, these port packs need to be installed by your program to recombine the graphics with existing assets, I assume? Like, you can't just drop them into the mods folder and take off running.
 
Jan 28, 2015 at 3:40 PM
Based Member
"Life begins and ends with Nu."
Join Date: Dec 31, 2011
Location: United States
Posts: 2314
Age: 27
Noxid said:
Ok so, these port packs need to be installed by your program to recombine the graphics with existing assets, I assume?
That's right.

Noxid said:
Like, you can't just drop them into the mods folder and take off running.
If said mod has filtered images, then that's right; some of the graphics will look like they just came out of a shredder if you try to do that. If the port doesn't have any filtered images, though, then just dropping the folder in there and modifying mods.txt will work fine.
 
Jan 28, 2015 at 4:01 PM
Lvl 1
Forum Moderator
"Life begins and ends with Nu."
Join Date: May 28, 2008
Location: PMMM MMO
Posts: 3713
Age: 31
Can I actually see the code instead of your pseudo-code representation of this comparison function, since this really some terrible pseudo-code/I can't follow this well at all.
 
Jan 28, 2015 at 4:11 PM
In my body, in my head
Forum Moderator
"Life begins and ends with Nu."
Join Date: Aug 28, 2009
Location: The Purple Zone
Posts: 5998
ok, well, I think you're overthinking your problem. Can you give me a counter-case to this algorithm:
Code:
	public static void main(String... args) throws IOException {
		File canon = new File("faceCanon.bmp"), modded = new File("face1.bmp"), filtered = new File("filtered.png");
		File result = new File("final.bmp");
		filter(canon,modded,filtered);
		applyFilter(canon, filtered, result);
	}
	
	public static void filter(File canonfile, File moddedFile, File output) throws IOException {
		BufferedImage canon = ImageIO.read(canonfile);
		BufferedImage modded = ImageIO.read(moddedFile);
		BufferedImage out = new BufferedImage(modded.getWidth(), modded.getHeight(), BufferedImage.TYPE_INT_ARGB);
		
		for (int y = 0; y < modded.getHeight(); y++) {
			for (int x = 0; x < modded.getWidth(); x++) {
				if ((x >= canon.getWidth() && y >= canon.getHeight()) 
						|| canon.getRGB(x, y) != modded.getRGB(x, y)) {
					out.setRGB(x, y, modded.getRGB(x, y));
				}
			}
		}
		
		ImageIO.write(out, "png", output);
	}
	
	public static void applyFilter(File canonFile, File filterFile, File output) throws IOException {
		BufferedImage canon = ImageIO.read(canonFile);
		BufferedImage filter = ImageIO.read(filterFile);
		BufferedImage out = new BufferedImage(filter.getWidth(), filter.getHeight(), BufferedImage.TYPE_INT_RGB);
		
		Graphics g = out.getGraphics();
		g.drawImage(canon, 0, 0, null);
		g.drawImage(filter, 0, 0, null);
		g.dispose();
		
		ImageIO.write(out, "bmp", output);
	}
 
Jan 28, 2015 at 7:41 PM
Based Member
"Life begins and ends with Nu."
Join Date: Dec 31, 2011
Location: United States
Posts: 2314
Age: 27
Can I actually see the code instead of your pseudo-code representation of this comparison function, since this really some terrible pseudo-code/I can't follow this well at all.
All right. I hesitated to paste it at first because there's a bunch of extra stuff in it that takes some other things into account, but here's a condensed version of the algorithm:
Code:
for(int y1 = 0; y1 < image1.getHeight(); y1++)
    for(int x1 = 0; x1 < image1.getWidth(); x1++)
        if(!tempBlankPixels1[y1][x1] && !linkedPixels[y1][x1])
            for(int y2 = 0; y2 < image2.getHeight(); y2++)
	        for(int x2 = 0; x2 < image2.getWidth(); x2++)
                    if(!blankPixels2[y2][x2] && skipLinked)
                        if(new Color(image1.getRGB(x1, y1)).equals(new Color(image2.getRGB(x2, y2))))
                            TestMatch(image1, image2, x1, y1, x2, y2, true);

private static void TestMatch(BufferedImage image1, BufferedImage image2, int x1, int y1, int x2, int y2, boolean source)
{
    if( ( ((x1 >= 0 && y1 >= 0) && (x2 >= 0 && y2 >= 0)) && (x1 <= image1.getWidth()-1 && x2 <= image2.getWidth()-1)) && ((y1 <= image1.getHeight()-1) && (y2 <= image2.getHeight()-1)) )
    {
        TestMatch(image1, image2, x1 + 1, y1, x2 + 1, y2, false);
        TestMatch(image1, image2, x1 + 1, y1 + 1, x2 + 1, y2 + 1, false);
        TestMatch(image1, image2, x1, y1 + 1, x2, y2 + 1, false);
        TestMatch(image1, image2, x1 - 1, y1 + 1, x2 - 1, y2 + 1, false);
        TestMatch(image1, image2, x1 - 1, y1, x2 - 1, y2, false);
        TestMatch(image1, image2, x1 - 1, y1 - 1, x2 - 1, y2 - 1, false);
        TestMatch(image1, image2, x1, y1 - 1, x2, y2 - 1, false);
        TestMatch(image1, image2, x1 + 1, y1 - 1, x2 + 1, y2 - 1, false);
    }
}
ok, well, I think you're overthinking your problem. Can you give me a counter-case to this algorithm:
Code:
public static void main(String... args) throws IOException {
		File canon = new File("faceCanon.bmp"), modded = new File("face1.bmp"), filtered = new File("filtered.png");
		File result = new File("final.bmp");
		filter(canon,modded,filtered);
		applyFilter(canon, filtered, result);
	}
	
	public static void filter(File canonfile, File moddedFile, File output) throws IOException {
		BufferedImage canon = ImageIO.read(canonfile);
		BufferedImage modded = ImageIO.read(moddedFile);
		BufferedImage out = new BufferedImage(modded.getWidth(), modded.getHeight(), BufferedImage.TYPE_INT_ARGB);
		
		for (int y = 0; y < modded.getHeight(); y++) {
			for (int x = 0; x < modded.getWidth(); x++) {
				if ((x >= canon.getWidth() && y >= canon.getHeight()) 
						|| canon.getRGB(x, y) != modded.getRGB(x, y)) {
					out.setRGB(x, y, modded.getRGB(x, y));
				}
			}
		}
		
		ImageIO.write(out, "png", output);
	}
	
	public static void applyFilter(File canonFile, File filterFile, File output) throws IOException {
		BufferedImage canon = ImageIO.read(canonFile);
		BufferedImage filter = ImageIO.read(filterFile);
		BufferedImage out = new BufferedImage(filter.getWidth(), filter.getHeight(), BufferedImage.TYPE_INT_RGB);
		
		Graphics g = out.getGraphics();
		g.drawImage(canon, 0, 0, null);
		g.drawImage(filter, 0, 0, null);
		g.dispose();
		
		ImageIO.write(out, "bmp", output);
	}
Easily. In the example, just pretend that we're using the 2x res images. This here is the modified PrtSand tileset image for my fourth ending mod:
PrtSand_zpsycmz6t6z.png

Notice the 3 tiles in the bottom row. Those are necessary in order to pull off that tile-shooting trick at the beginning of the Sand Zone.

If I've read your algorithm right, this would be the output filtered image:
PrtSandCopy_zpsag45cyzu.png

With this being double resolution, this would have the NICALiS double resolution version of the tiles, and that would be what gets distributed in the port. It had a direct match from the image it was comparing it to, but it was in a different location and thus missed it.


Here's another example. This is the modified PrtJail tileset image for my fourth ending mod. Again, pretend it's double resolution:
PrtJail_zpsyrrucesf.png

Notice the images of King and his sword in the bottom right corner. I was unable to get King's entity to swing his sword the way I wanted him to, so I modified the tileset like so and used some <SMP wizardry to make it look like he did.

If we used your algorithm, I believe the outputted image to be distributed would look like this:
PrtJailCopy_zpslmndia0m.png

A big problem is that King and his Blade are still there. No such patterns exist in the original PrtJail tileset, but such patterns do exist in some other spritesheets in the Cave Story+ directory. Your algorithm would have missed King because it was only scanning 1 image.


The reason your algorithm wouldn't work in all cases is because it makes 2 assumptions:
1) It assumes that it will only need to scan 1 image in order to get all of the copyrighted content out of it
2) It assumes that all of the copyrighted content in the image being filtered is at the exact same location as the one being scanned

In order to find all the copyrighted graphics in all cases, it has to scan all of the images in the Cave Story+ installation, and anticipate that any part of the image being filtered could be any part of an existing NICALiS image.

I'm open to hearing ways that I can make my code better, but first I want to make sure you fully understand how complex the problem is. I'm getting the impression that neither you nor GIR have tried Plus Porter out. If you're this interested in helping me optimize my code (which I appreciate that you are, by the way), taking the program out for a spin yourself would probably go a long way towards helping you understand exactly what you're trying to help me optimize.
 
Top