|
package main |
|
|
|
import ( |
|
"bufio" |
|
"fmt" |
|
"image" |
|
"image/color" |
|
_ "image/jpeg" |
|
_ "image/png" |
|
"io/ioutil" |
|
"math" |
|
"os" |
|
"path" |
|
"path/filepath" |
|
"strings" |
|
) |
|
|
|
/** |
|
* v1.1.0 |
|
* |
|
* Original Python script by Dark Revenant. |
|
* Transcoded to Golang and edited to show more info by Wisp. |
|
* |
|
* Dependencies: |
|
*/ |
|
|
|
var dirPath, errorGettingDir = os.Getwd() |
|
var debug = false |
|
|
|
func main() { |
|
finalReadout := "" |
|
totalBytes := 0 |
|
|
|
check(errorGettingDir) |
|
|
|
modFolders, err := ioutil.ReadDir(dirPath) |
|
check(err) |
|
fmt.Println("Current dir: " + dirPath) |
|
|
|
// Iterate over mod folders |
|
for _, modFolder := range modFolders { |
|
fmt.Println("Mod: " + modFolder.Name()) |
|
var fileRelativePathList []string |
|
|
|
err := filepath.Walk(modFolder.Name(), |
|
func(filePath string, info os.FileInfo, err error) error { |
|
|
|
if !info.IsDir() { |
|
fileRelativePathList = append(fileRelativePathList, filePath) |
|
} |
|
|
|
return nil |
|
}) |
|
|
|
check(err) |
|
|
|
totalBytesForMod := float64(0) |
|
//didAlreadyCountBackgroundImage := false |
|
|
|
for _, relativeFilePath := range fileRelativePathList { |
|
absoluteFilePath := path.Join(dirPath, relativeFilePath) |
|
|
|
if strings.Contains(relativeFilePath, "_CURRENTLY_UNUSED") { |
|
fmt.Printf("Skipping unused file %s\n", relativeFilePath) |
|
continue |
|
} |
|
|
|
reader, err := os.Open(absoluteFilePath) |
|
check(err) |
|
img, _, err := image.Decode(reader) |
|
|
|
if err != nil { |
|
if debug { |
|
fmt.Printf("Skipping %s (%s)\n", relativeFilePath, err) |
|
} |
|
continue |
|
} |
|
|
|
width, height := img.Bounds().Max.X, img.Bounds().Max.Y |
|
bitsPerChannel := 0 |
|
numColorChannels := 0 |
|
doesModContainABackgroundImage := false |
|
hasAlpha := !Opaque(img) |
|
bytesToSubtract := 0 |
|
|
|
switch img.ColorModel() { |
|
case |
|
color.Alpha16Model: |
|
bitsPerChannel = 16 |
|
numColorChannels = 0 |
|
case color.Gray16Model: |
|
bitsPerChannel = 16 |
|
numColorChannels = 1 |
|
case color.GrayModel: |
|
bitsPerChannel = 8 |
|
numColorChannels = 1 |
|
case color.AlphaModel: |
|
bitsPerChannel = 8 |
|
numColorChannels = 0 |
|
case color.CMYKModel: |
|
bitsPerChannel = 8 |
|
numColorChannels = 4 |
|
case color.NRGBAModel: |
|
bitsPerChannel = 8 |
|
numColorChannels = 3 |
|
case color.NYCbCrAModel: |
|
bitsPerChannel = 8 |
|
numColorChannels = 3 |
|
case color.RGBAModel: |
|
bitsPerChannel = 8 |
|
numColorChannels = 3 |
|
case color.NRGBA64Model: |
|
bitsPerChannel = 16 |
|
numColorChannels = 3 |
|
case color.RGBA64Model: |
|
bitsPerChannel = 16 |
|
numColorChannels = 3 |
|
case color.YCbCrModel: |
|
bitsPerChannel = 8 |
|
numColorChannels = 3 |
|
} |
|
|
|
numChannels := numColorChannels |
|
|
|
if bitsPerChannel == 0 || numColorChannels == 0 { |
|
fmt.Printf("\nUnknown image format: %s\n", absoluteFilePath) |
|
// Use most common values so we can continue |
|
bitsPerChannel = 8 |
|
numColorChannels = 3 |
|
} |
|
|
|
// Note: This is not entirely correct. |
|
// If the image has an alpha channel, but doesn't use it (image entirely opaque) |
|
// then the alpha channel won't be counted, but it should be. |
|
// In practice, this only seems to cause mod counts to be off by a few kilobytes. |
|
if hasAlpha { |
|
numChannels = numChannels + 1 |
|
} |
|
|
|
// tex height and width rounds the image width + height up to the next power of two |
|
texWidth := 1 |
|
for texWidth < width { |
|
texWidth = texWidth * 2 |
|
} |
|
|
|
texHeight := 1 |
|
for texHeight < height { |
|
texHeight = texHeight * 2 |
|
} |
|
|
|
// Increase memory use due to mipmapping |
|
multiplier := float64(4) / 3 |
|
|
|
if strings.Contains(relativeFilePath, "backgrounds") { |
|
// If mod adds a larger-than-vanilla background, count it as increasing VRAM, but only once |
|
// because only one background is loaded at a time |
|
if doesModContainABackgroundImage { |
|
fmt.Printf("Skipping backgrounds after the first in the mod %s\n", relativeFilePath) |
|
continue |
|
} |
|
|
|
// If file is a standard size background, then it should not increase VRAM use |
|
// because only one background is loaded at a time |
|
if width <= 2048 { |
|
fmt.Printf("Skipped vanilla-sized (%dpx width) background %s\n", width, relativeFilePath) |
|
continue |
|
} |
|
|
|
// Number of bytes in a vanilla background image |
|
// Only count any excess toward the mod's VRAM hit |
|
bytesToSubtract = 12582912 |
|
multiplier = float64(1) |
|
} |
|
|
|
bytesPerPixel := float64(bitsPerChannel*numChannels) / float64(8) |
|
bytes := int(math.Ceil(float64(texWidth)*float64(texHeight)*bytesPerPixel*multiplier)) - bytesToSubtract |
|
totalBytesForMod = float64(totalBytesForMod + float64(bytes)) |
|
totalBytes = totalBytes + bytes |
|
|
|
fmt.Printf("%s: TexHeight: %d, TexWidth: %d, Channels (alpha: %t): %d (Mult=%f)\n"+ |
|
" --> %d * %d * %d * %f = %d bytes\n", |
|
relativeFilePath, texHeight, texWidth, |
|
hasAlpha, numChannels, multiplier, |
|
texHeight, texWidth, numChannels, multiplier, bytes) |
|
} |
|
|
|
if totalBytesForMod == 0 { |
|
continue |
|
} |
|
|
|
modPrintout := fmt.Sprintf("\n%s\n", modFolder.Name()) |
|
modPrintout = modPrintout + getSizeBreakdown(totalBytesForMod) |
|
|
|
finalReadout = finalReadout + modPrintout |
|
} |
|
|
|
fmt.Println(finalReadout) |
|
fmt.Printf("-------------\nTotal Modlist\n%s", getSizeBreakdown(float64(totalBytes))) |
|
bufio.NewReader(os.Stdin).ReadString('\n') |
|
} |
|
|
|
func check(e error) { |
|
if e != nil { |
|
bufio.NewReader(os.Stdin).ReadString('\n') |
|
panic(e) |
|
} |
|
} |
|
|
|
func getSizeBreakdown(bytes float64) string { |
|
result := fmt.Sprintf("%.0f bytes\n", bytes) |
|
|
|
if bytes >= 1024 { |
|
result = result + fmt.Sprintf("%.3f kiB\n", bytes/1024) |
|
} |
|
|
|
if bytes >= 1048576 { |
|
result = result + fmt.Sprintf("%.3f MiB\n", bytes/1048576) |
|
} |
|
|
|
if bytes >= 1073741824 { |
|
result = result + fmt.Sprintf("%.3f GiB\n", bytes/1073741824) |
|
} |
|
|
|
return result |
|
} |
|
|
|
// from https://stackoverflow.com/a/44105659/1622788 |
|
func Opaque(im image.Image) bool { |
|
// Check if image has Opaque() method: |
|
if oim, ok := im.(interface { |
|
Opaque() bool |
|
}); ok { |
|
return oim.Opaque() // It does, call it and return its result! |
|
} |
|
|
|
// No Opaque() method, we need to loop through all pixels and check manually: |
|
rect := im.Bounds() |
|
for y := rect.Min.Y; y < rect.Max.Y; y++ { |
|
for x := rect.Min.X; x < rect.Max.X; x++ { |
|
if _, _, _, a := im.At(x, y).RGBA(); a != 0xffff { |
|
return false // Found a non-opaque pixel: image is non-opaque (transparent) |
|
} |
|
} |
|
} |
|
return true // All pixels are opaque, so is the image |
|
} |