File System
Monkey Forums/Monkey Code/File System
| ||
Here's a basic pseudo file system for Monkey; tested and working on Android devices (LG Optimus One), iOS emulator, HTML5 and Flash targets. Self explanatory for the most part and I've only added the features I need for myself right now. Basically all the Write<whatever> functions add data into a string corresponding to the filename opened with WriteFile. Data is not persistent until you call filesystem.SaveAll(). [edit 13-Sep-11] Updated to fix a couple of issues as well as now avoiding having ASCII 0 in the string (which caused a load of problems) - Thanks muddy_shoes for the String/Int conversion. Also I've added in Little Endian string/int conversion code in the conversion class. These are useful if you've converted something from Blitzmax to Monkey, as you can load your level data with LoadString, and give the conversion method a four-byte string which it will then convert to an integer. test.monkey Strict Import filesystem Function Main:Int() New testApp Return 0 End Function Class testApp Extends App Field fileHandler:FileSystem Method OnCreate:Int() Self.fileHandler = New FileSystem Local stream:FileStream Local n:int stream = Self.fileHandler.WriteFile("test/test.bin") stream.WriteString("Hello") stream.WriteInt(1234536343) stream.WriteString("Bye!") Self.fileHandler.SaveAll() Self.fileHandler.ListDir() stream = Self.fileHandler.ReadFile("test/test.bin") if stream Print stream.ReadString() Print stream.ReadInt() Print stream.ReadString() EndIf Return 0 End Method End Class filesystem.monkey Strict Import mojo Class FileSystem Extends DataConversion Private Field _header:String = "MKYDATA" Field fileData:String Field index:StringMap<FileStream> Public Method New() Self.LoadAll() End Method WriteFile:FileStream(filename:String) Local f:FileStream = new FileStream f.filename = filename.ToLower() f.fileptr = 0 Self.index.Insert(f.filename.ToLower(),f) Return f End Method ReadFile:FileStream(filename:String) filename = filename.ToLower() Local f:FileStream f = Self.index.ValueForKey(filename) f.fileptr = 0 Return f End Method FileExists:Bool(filename:String) filename = filename.ToLower() if Self.index.Contains(filename) Return True Else Return False End Return False End Method ListDir:Void() Local filename:String Local stream:FileStream Print "Directory Listing:" For filename = EachIn Self.index.Keys() stream = Self.index.ValueForKey(filename) Print filename + " " + stream.data.Length()+" byte(s)." Next End Method DeleteFile:Void(filename:String) filename = filename.ToLower() if Self.index.Contains(filename) Self.index.Remove(filename) End End Method SaveAll:Void() Local f:FileStream Self.fileData = Self._header'header self.fileData+= Self.IntToString(Self.index.Count())'number of files in index if Self.index.Count() > 0 For f = EachIn Self.index.Values() 'store filename Self.fileData+= Self.IntToString(f.filename.Length()) if f.filename.Length() > 0 Self.fileData+= f.filename End 'store data Self.fileData+= Self.IntToString(f.data.Length()) if f.data.Length() > 0 Self.fileData+= f.data End Next End SaveState(Self.fileData) End Method LoadAll:Void() Local numFiles:Int Local stream:FileStream Local len:Int Local ptr:Int Self.fileData = LoadState() self.index = New StringMap<FileStream> if Self.fileData.Length() > 0 if Self.fileData.StartsWith(Self._header) Self.index.Clear() ptr+=Self._header.Length() numFiles = Self.StringToInt(Self.fileData[ptr..ptr+3]) ptr+=3 if numFiles > 0 For Local n:Int = 1 to numFiles stream = New FileStream 'filename len = Self.StringToInt(Self.fileData[ptr..ptr+3]) ptr+=3 if len > 0 stream.filename = Self.fileData[ptr..ptr+len] ptr+=len End 'data len = Self.StringToInt(Self.fileData[ptr..ptr+3]) ptr+=3 if len > 0 stream.data = Self.fileData[ptr..ptr+len] ptr+=len End Self.index.Insert(stream.filename,stream) Next End End Else SaveState("")'save empty file and indicate no files stored End End End Class FileStream Extends DataConversion Field filename:String Field fileptr:Int Private Field data:String Public Method ReadInt:Int() Local result:string result = Self.data[Self.fileptr..self.fileptr+3] Self.fileptr+=3 Return Self.StringToInt(result) End Method WriteInt:Void(val:Int) Self.data+=Self.IntToString(val) End Method ReadString:String() Local result:String Local strLen:Int = self.StringToInt(self.data[self.fileptr..self.fileptr+3]) Self.fileptr+=3 if strLen > 0 result = Self.data[Self.fileptr..self.fileptr+strLen] Self.fileptr+=strLen End Return result End Method WriteString:Void(val:String) Self.data+=Self.IntToString(val.Length()) if val.Length() > 0 Self.data+=val End End Method ReadFloat:Float() Local result:float Local s:String Local strLen:Int = self.StringToInt(self.data[self.fileptr..self.fileptr+3]) Self.fileptr+=3 s = Self.data[Self.fileptr..self.fileptr+strLen] result = Self.StringToFloat(s) Self.fileptr+=strLen Return result End Method WriteFloat:Void(val:Float) Local s:String = self.FloatToString(val) Self.data+=Self.IntToString(s.Length()) Self.data+=s End Method ReadBool:Bool() Local result:Bool result = Bool(Self.data[Self.fileptr]) Self.fileptr+=1 Return result End Method Method WriteBool:Void(val:Bool) Self.data+=String.FromChar(val) End Method End Class DataConversion Method LittleEndianIntToString:String(val:Int) Local result:String result = String.FromChar((val) & $FF) result+= String.FromChar((val Shr 8) & $FF) result+= String.FromChar((val Shr 16) & $FF) result+= String.FromChar((val Shr 24) & $FF) Return result End Method StringToLittleEndianInt:Int(val:String) Local result:Int result = (val[0]) result|= (val[1] Shl 8) result|= (val[2] Shl 16) result|= (val[3] Shl 24) Return result End Method IntToString:String(val:Int) Local result:String result = String.FromChar($F000 | ((val Shr 20) & $0FFF) ) result += String.FromChar($F000 | ((val Shr 8) & $0FFF)) result += String.FromChar($F000 | (val & $00FF)) Return result End Method StringToInt:Int(val:String) Return ((val[0]&$0FFF) Shl 20) | ((val[1]&$0FFF) Shl 8) |(val[2]&$00FF) End Method FloatToString:String(val:Float) Return String(val) End Method StringToFloat:Float(val:String) Return Float(val) End End |
| ||
Awesome! Somebody owes you a beverage of your choice! :) |
| ||
Fantastic! i think we all owe you a beverage :) |
| ||
Cool... can I make one suggestion though, stick to a standard in your coding, you've sometimes got PascalCase methods and sometimes camelCase methods - and since Monkey is case-sensitive it makes sense to keep everything the same. For the docs: Monkey naming convention The standard Monkey modules use a simple naming convention: All-caps case (eg: 'ALLCAPS' ): * Constants Pascal case (eg: 'PascalCase' ): * Classes * Globals * Functions, methods and properties. Camel case (eg: 'camelCase' ): * Fields * Locals and function parameters Also would mind if I stick this in Diddy? ;) |
| ||
As I pointed out in your other thread, your int conversion is using twice as many characters as necessary. More efficient versions are at the bottom of the float conversion code I posted. |
| ||
Cool... can I make one suggestion though, stick to a standard in your coding, you've sometimes got PascalCase methods and sometimes camelCase I normally use camelCase! For this, though, I used the PascalCase for the duplicates of the Blitzmax commands (because that's how they are in Blitzmax!) so I guess I did it that way for familiarity. Admittedly there are a couple of rogue ones in there though. Also would mind if I stick this in Diddy? ;) Don't mind at all. |
| ||
Ta, just committed it to Diddy (after a quick tidy up): http://code.google.com/p/diddy/source/browse/trunk/src/diddy/filesystem.monkey Have you tested muddy_shoes suggestions? |
| ||
Not yet but feel free. [Edit] it says its 4 or 5 times slower so i think I'll pass. up to you for diddy tho. |
| ||
Cheers for testing it Dave... 4 or 5 times slower is a hugh amount... think Diddy will stay with your version.. |
| ||
Cheers for testing it Dave I don't get the impression any testing was done. He's just noting what I wrote about my testing and seems to have misinterpreted. The float conversion is "up to" 4-5 times slower. In other words, it's the worst case from my tests, the average case is not that bad as shown by the Android figures that I gave. The trade off is the reduction in string size which seemed to be the concern being voiced about using SaveState. The integer conversion is no slower, in fact I'd expect it to be faster as well as more space efficient as there are fewer operations involved in the packing and unpacking. |
| ||
No I didn't test it - I was just going on what you said. I won't get around to it for a few hours tho, if at all today, cos I'm busy on something right now. If anybody wants to update and test the code in the meantime with something they think might be more efficient, knock yourselves out. That's what I put it here for! |
| ||
Okay, so here are some test results using the Diddy module as a base. The numbers after the operation descriptions are in ms. Note that the byte values are what the module prints out based on the assumption that 1 char = 1 byte. That assumption is actually incorrect but the number is okay for comparing storage requirements. Starting with writing and reading 20,000 floats and 20,000 ints on Flash, which appears to have the worst performance of the PC-based targets for me. Current code: Write floats: 165 Directory Listing: test.bin 464780 byte(s). Read floats: 32 Write ints: 30 Directory Listing: test.bin 80000 byte(s). Read ints: 7 Alternative: Write floats: 204 Directory Listing: test.bin 141565 byte(s). Read floats: 89 Write ints: 18 Directory Listing: test.bin 40000 byte(s). Read ints: 1 My conversion code is slightly slower when writing floats and 3-4 times as slow when reading. However, the actual difference that the user would experience when reading a level or similar would be unnoticeable and the storage requirements are about a third of the current implementation. As I expected, the integer read and write times are about halved along with the storage used. Now on Android, using 1000 floats and ints. Current code: Write floats: 14914 Directory Listing: test.bin 14544 byte(s). Read floats: 149 Write ints: 1803 Directory Listing: test.bin 4000 byte(s). Read ints: 66 Alternative code: Write floats: 4945 Directory Listing: test.bin 5011 byte(s). Read floats: 491 Write ints: 917 Directory Listing: test.bin 2000 byte(s). Read ints: 3 The reduction in overall string length means that writing is much faster with the alternative methods. As with Flash the read cost for floats is 3-4 times higher. My altered version of the Diddy file is below. There are still a number of tweaks that could improve matters further and I'm also wondering if a Monkey StringBuilder/StringBuffer type class backed by native implementations would make a big difference. |
| ||
Well... finished what I was doing but its Sunday and I've been on the sauce so I'm not quite sure what planet I'm on right now... I'll have to check this stuff out tomorrow when I arrive back on Earth. |
| ||
Sorry I misread what Dave put... Can you post your test app muddy? |
| ||
Sure. It's probably been changed a little from the runs above, but it was pretty much this: |
| ||
Just having a look at your int/string conversion code and I'm not sure it will work all the time. When I was doing my code I did some test or other and could not get FromChar() to return anything higher than 60599 - potentially you'd want anything up to 65535 to be returned. This is why I ended up doing single byte conversions. Maybe I got it wrong though. Is your code tested to the maximum range? |
| ||
Added Boolean support. Add this code into the FileStream class:Method ReadBool:Bool() Local result:Bool result = Bool(Self.data[Self.fileptr]) Self.fileptr+=1 Return result End Method Method WriteBool:Void(val:Bool) Self.data+=String.FromChar(val) End Method |
| ||
It's rather difficult to define "maximum range" for a Monkey Integer or Float as the eventual type varies across the targets. All the tests I ran showed that charcodes are 16-bit values and the methods I posted work for 32-bit signed integer ranges. I don't have any iOS or MacOS devices, so they are untested. It's certainly possible that some environments could have smaller chars, but it would be a little odd considering the need to support unicode. |
| ||
I think this doesn't work on iOS Devices. I have a file created with BlitzMax, when I try to load the Ressource with LoadString I always get an empty result. I think it's because as it's a binary file the following line doesn't work: NSString *str=[NSString stringWithContentsOfURL:url usedEncoding:&enc error:nil]; Do you have any suggestions on how I get this working without converting the binary file into a text-based format? When I read out the error message it links to the following error code: NSFileReadUnknownStringEncodingError = 264, // Read error (string encoding of file contents could not be determined) |
| ||
Ok, here is a small fix to make everything work on ios devices. Instead of LoadString, use the following function:String LoadBinaryString(String fpath) { NSString *rpath=pathForResource( fpath ); if( !rpath ) return ""; const char *filename = [rpath UTF8String]; FILE *fp = fopen(filename, "r"); String ret = String::Load(fp); fclose(fp); return ret; } |