changeset 65:8b8b0bade520

Fixed support for mounts using the new MountJournal and mount IDs (no conversion of old profiles at the moment).
author Jerome Vuarand <jerome.vuarand@gmail.com>
date Thu, 23 Oct 2014 13:44:59 +0100
parents 49ae7191821f
children 06167f9ed2ac
files .pkgmeta CyborgMMO7.lua CyborgMMO7.toc MountMap.lua RatPageController.lua WowObjects.lua support/casc.lua support/casc/bin.lua support/casc/blte.lua support/casc/dbc.lua support/casc/jenkins96.lua support/casc/platform.lua support/gen-mount-db.lua
diffstat 13 files changed, 1374 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.pkgmeta	Thu Oct 23 13:44:59 2014 +0100
@@ -0,0 +1,4 @@
+ignore:
+    - support
+
+# vi: ft=yaml ts=4 sts=4 sw=4 et
--- a/CyborgMMO7.lua	Thu Oct 23 12:17:02 2014 +0100
+++ b/CyborgMMO7.lua	Thu Oct 23 13:44:59 2014 +0100
@@ -331,6 +331,10 @@
 		if not CyborgMMO7SaveData then
 			CyborgMMO7SaveData = {}
 		end
+		-- cleanup the local mount cache
+		for mount in pairs(CyborgMMO_MountMap) do
+			CyborgMMO_LocalMountMap[mount] = nil
+		end
 		PreLoad(CyborgMMO7SaveData)
 	elseif event == "CYBORGMMO_ASYNC_DATA_LOADED" then
 		AsyncDataLoaded = true
--- a/CyborgMMO7.toc	Thu Oct 23 12:17:02 2014 +0100
+++ b/CyborgMMO7.toc	Thu Oct 23 13:44:59 2014 +0100
@@ -4,8 +4,9 @@
 ## Version: @project-version@
 ## LoadOnDemand: 0
 ## SavedVariablesPerCharacter: CyborgMMO7SaveData
-## SavedVariables: CyborgMMO_ProfileKeyBindings
+## SavedVariables: CyborgMMO_ProfileKeyBindings CyborgMMO_LocalMountMap
 
+MountMap.lua
 Localisation.lua
 OptionView.lua
 CallbackFactory.lua
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MountMap.lua	Thu Oct 23 13:44:59 2014 +0100
@@ -0,0 +1,501 @@
+-- this is a generated file, do not edit, see support/gen-mount-db.lua
+CyborgMMO_MountMap = {
+	[6] = 458,
+	[7] = 459,
+	[8] = 468,
+	[9] = 470,
+	[11] = 472,
+	[12] = 578,
+	[13] = 579,
+	[14] = 580,
+	[15] = 581,
+	[17] = 5784,
+	[18] = 6648,
+	[19] = 6653,
+	[20] = 6654,
+	[21] = 6777,
+	[22] = 6896,
+	[24] = 6898,
+	[25] = 6899,
+	[26] = 8394,
+	[27] = 8395,
+	[28] = 8980,
+	[31] = 10789,
+	[34] = 10793,
+	[35] = 10795,
+	[36] = 10796,
+	[38] = 10799,
+	[39] = 10873,
+	[40] = 10969,
+	[41] = 13819,
+	[42] = 15779,
+	[43] = 15780,
+	[45] = 16055,
+	[46] = 16056,
+	[50] = 16080,
+	[51] = 16081,
+	[52] = 16082,
+	[53] = 16083,
+	[54] = 16084,
+	[55] = 17229,
+	[56] = 17450,
+	[57] = 17453,
+	[58] = 17454,
+	[62] = 17459,
+	[63] = 17460,
+	[64] = 17461,
+	[65] = 17462,
+	[66] = 17463,
+	[67] = 17464,
+	[68] = 17465,
+	[69] = 17481,
+	[70] = 18363,
+	[71] = 18989,
+	[72] = 18990,
+	[73] = 18991,
+	[74] = 18992,
+	[75] = 22717,
+	[76] = 22718,
+	[77] = 22719,
+	[78] = 22720,
+	[79] = 22721,
+	[80] = 22722,
+	[81] = 22723,
+	[82] = 22724,
+	[83] = 23161,
+	[84] = 23214,
+	[85] = 23219,
+	[87] = 23221,
+	[88] = 23222,
+	[89] = 23223,
+	[90] = 23225,
+	[91] = 23227,
+	[92] = 23228,
+	[93] = 23229,
+	[94] = 23238,
+	[95] = 23239,
+	[96] = 23240,
+	[97] = 23241,
+	[98] = 23242,
+	[99] = 23243,
+	[100] = 23246,
+	[101] = 23247,
+	[102] = 23248,
+	[103] = 23249,
+	[104] = 23250,
+	[105] = 23251,
+	[106] = 23252,
+	[107] = 23338,
+	[108] = 23509,
+	[109] = 23510,
+	[110] = 24242,
+	[111] = 24252,
+	[116] = 25863,
+	[117] = 25953,
+	[118] = 26054,
+	[119] = 26055,
+	[120] = 26056,
+	[121] = 26655,
+	[122] = 26656,
+	[123] = 28828,
+	[125] = 30174,
+	[129] = 32235,
+	[130] = 32239,
+	[131] = 32240,
+	[132] = 32242,
+	[133] = 32243,
+	[134] = 32244,
+	[135] = 32245,
+	[136] = 32246,
+	[137] = 32289,
+	[138] = 32290,
+	[139] = 32292,
+	[140] = 32295,
+	[141] = 32296,
+	[142] = 32297,
+	[145] = 33630,
+	[146] = 33660,
+	[147] = 34406,
+	[149] = 34767,
+	[150] = 34769,
+	[151] = 34790,
+	[152] = 34795,
+	[153] = 34896,
+	[154] = 34897,
+	[155] = 34898,
+	[156] = 34899,
+	[157] = 35018,
+	[158] = 35020,
+	[159] = 35022,
+	[160] = 35025,
+	[161] = 35027,
+	[162] = 35028,
+	[163] = 35710,
+	[164] = 35711,
+	[165] = 35712,
+	[166] = 35713,
+	[167] = 35714,
+	[168] = 36702,
+	[169] = 37015,
+	[170] = 39315,
+	[171] = 39316,
+	[172] = 39317,
+	[173] = 39318,
+	[174] = 39319,
+	[176] = 39798,
+	[177] = 39800,
+	[178] = 39801,
+	[179] = 39802,
+	[180] = 39803,
+	[183] = 40192,
+	[185] = 41252,
+	[186] = 41513,
+	[187] = 41514,
+	[188] = 41515,
+	[189] = 41516,
+	[190] = 41517,
+	[191] = 41518,
+	[196] = 42776,
+	[197] = 42777,
+	[199] = 43688,
+	[201] = 43899,
+	[202] = 43900,
+	[203] = 43927,
+	[204] = 44151,
+	[205] = 44153,
+	[206] = 44317,
+	[207] = 44744,
+	[211] = 46197,
+	[212] = 46199,
+	[213] = 46628,
+	[219] = 48025,
+	[220] = 48027,
+	[221] = 48778,
+	[222] = 48954,
+	[223] = 49193,
+	[224] = 49322,
+	[225] = 49378,
+	[226] = 49379,
+	[230] = 51412,
+	[236] = 54729,
+	[237] = 54753,
+	[238] = 55164,
+	[240] = 55531,
+	[241] = 58615,
+	[242] = 58819,
+	[243] = 58983,
+	[246] = 59567,
+	[247] = 59568,
+	[248] = 59569,
+	[249] = 59570,
+	[250] = 59571,
+	[251] = 59572,
+	[253] = 59650,
+	[254] = 59785,
+	[255] = 59788,
+	[256] = 59791,
+	[257] = 59793,
+	[258] = 59797,
+	[259] = 59799,
+	[262] = 59961,
+	[263] = 59976,
+	[264] = 59996,
+	[265] = 60002,
+	[266] = 60021,
+	[267] = 60024,
+	[268] = 60025,
+	[269] = 60114,
+	[270] = 60116,
+	[271] = 60118,
+	[272] = 60119,
+	[273] = 60136,
+	[274] = 60140,
+	[275] = 60424,
+	[276] = 61229,
+	[277] = 61230,
+	[278] = 61294,
+	[279] = 61309,
+	[280] = 61425,
+	[284] = 61447,
+	[285] = 61451,
+	[286] = 61465,
+	[287] = 61467,
+	[288] = 61469,
+	[289] = 61470,
+	[291] = 61996,
+	[292] = 61997,
+	[293] = 62048,
+	[294] = 63232,
+	[295] = 63635,
+	[296] = 63636,
+	[297] = 63637,
+	[298] = 63638,
+	[299] = 63639,
+	[300] = 63640,
+	[301] = 63641,
+	[302] = 63642,
+	[303] = 63643,
+	[304] = 63796,
+	[305] = 63844,
+	[306] = 63956,
+	[307] = 63963,
+	[308] = 64656,
+	[309] = 64657,
+	[310] = 64658,
+	[311] = 64659,
+	[312] = 64731,
+	[313] = 64927,
+	[314] = 64977,
+	[317] = 65439,
+	[318] = 65637,
+	[319] = 65638,
+	[320] = 65639,
+	[321] = 65640,
+	[322] = 65641,
+	[323] = 65642,
+	[324] = 65643,
+	[325] = 65644,
+	[326] = 65645,
+	[327] = 65646,
+	[328] = 65917,
+	[329] = 66087,
+	[330] = 66088,
+	[331] = 66090,
+	[332] = 66091,
+	[333] = 66122,
+	[334] = 66123,
+	[335] = 66124,
+	[336] = 66846,
+	[337] = 66847,
+	[338] = 66906,
+	[339] = 66907,
+	[340] = 67336,
+	[341] = 67466,
+	[342] = 68056,
+	[343] = 68057,
+	[344] = 68187,
+	[345] = 68188,
+	[349] = 69395,
+	[350] = 69820,
+	[351] = 69826,
+	[352] = 71342,
+	[358] = 71810,
+	[363] = 72286,
+	[364] = 72807,
+	[365] = 72808,
+	[366] = 73313,
+	[367] = 73629,
+	[368] = 73630,
+	[371] = 74856,
+	[372] = 74918,
+	[373] = 75207,
+	[375] = 75596,
+	[376] = 75614,
+	[382] = 75973,
+	[386] = 84751,
+	[388] = 87090,
+	[389] = 87091,
+	[391] = 88331,
+	[392] = 88335,
+	[393] = 88718,
+	[394] = 88741,
+	[395] = 88742,
+	[396] = 88744,
+	[397] = 88746,
+	[398] = 88748,
+	[399] = 88749,
+	[400] = 88750,
+	[401] = 88990,
+	[403] = 90621,
+	[404] = 92155,
+	[405] = 92231,
+	[406] = 92232,
+	[407] = 93326,
+	[408] = 93623,
+	[409] = 93644,
+	[410] = 96491,
+	[411] = 96499,
+	[412] = 96503,
+	[413] = 97359,
+	[415] = 97493,
+	[416] = 97501,
+	[417] = 97560,
+	[418] = 97581,
+	[419] = 98204,
+	[420] = 98718,
+	[421] = 98727,
+	[422] = 100332,
+	[423] = 100333,
+	[424] = 101282,
+	[425] = 101542,
+	[426] = 101573,
+	[428] = 101821,
+	[429] = 102346,
+	[430] = 102349,
+	[431] = 102350,
+	[432] = 102488,
+	[433] = 102514,
+	[434] = 103081,
+	[435] = 103195,
+	[436] = 103196,
+	[439] = 107203,
+	[440] = 107516,
+	[441] = 107517,
+	[442] = 107842,
+	[443] = 107844,
+	[444] = 107845,
+	[445] = 110039,
+	[446] = 110051,
+	[447] = 113120,
+	[448] = 113199,
+	[449] = 118089,
+	[450] = 118737,
+	[451] = 120043,
+	[452] = 120395,
+	[453] = 120822,
+	[455] = 121820,
+	[456] = 121836,
+	[457] = 121837,
+	[458] = 121838,
+	[459] = 121839,
+	[460] = 122708,
+	[462] = 123182,
+	[463] = 123886,
+	[464] = 123992,
+	[465] = 123993,
+	[466] = 124408,
+	[467] = 124550,
+	[468] = 124659,
+	[469] = 126507,
+	[470] = 126508,
+	[471] = 127154,
+	[472] = 127156,
+	[473] = 127158,
+	[474] = 127161,
+	[475] = 127164,
+	[476] = 127165,
+	[477] = 127169,
+	[478] = 127170,
+	[479] = 127174,
+	[480] = 127176,
+	[481] = 127177,
+	[484] = 127209,
+	[485] = 127213,
+	[486] = 127216,
+	[487] = 127220,
+	[488] = 127271,
+	[492] = 127286,
+	[493] = 127287,
+	[494] = 127288,
+	[495] = 127289,
+	[496] = 127290,
+	[497] = 127293,
+	[498] = 127295,
+	[499] = 127302,
+	[500] = 127308,
+	[501] = 127310,
+	[503] = 129552,
+	[504] = 129918,
+	[505] = 129932,
+	[506] = 129934,
+	[507] = 129935,
+	[508] = 130086,
+	[509] = 130092,
+	[510] = 130137,
+	[511] = 130138,
+	[515] = 130965,
+	[516] = 130985,
+	[517] = 132036,
+	[518] = 132117,
+	[519] = 132118,
+	[520] = 132119,
+	[521] = 133023,
+	[522] = 134359,
+	[523] = 134573,
+	[526] = 135416,
+	[527] = 135418,
+	[528] = 136163,
+	[529] = 136164,
+	[530] = 136400,
+	[531] = 136471,
+	[532] = 136505,
+	[533] = 138423,
+	[534] = 138424,
+	[535] = 138425,
+	[536] = 138426,
+	[537] = 138640,
+	[538] = 138641,
+	[539] = 138642,
+	[540] = 138643,
+	[541] = 139407,
+	[542] = 139442,
+	[543] = 139448,
+	[544] = 139595,
+	[545] = 140249,
+	[546] = 140250,
+	[547] = 142073,
+	[548] = 142266,
+	[549] = 142478,
+	[550] = 142641,
+	[551] = 142878,
+	[554] = 146615,
+	[555] = 146622,
+	[557] = 148392,
+	[558] = 148396,
+	[559] = 148417,
+	[560] = 148428,
+	[561] = 148476,
+	[562] = 148618,
+	[563] = 148619,
+	[564] = 148620,
+	[565] = 148626,
+	[566] = 148970,
+	[567] = 148972,
+	[568] = 149801,
+	[571] = 153489,
+	[593] = 163024,
+	[594] = 163025,
+	[600] = 155741,
+	[603] = 169952,
+	[606] = 170347,
+	[607] = 171436,
+	[609] = 171617,
+	[612] = 171620,
+	[613] = 171621,
+	[614] = 171622,
+	[615] = 171623,
+	[616] = 171624,
+	[617] = 171625,
+	[618] = 171626,
+	[619] = 171627,
+	[621] = 171629,
+	[623] = 171632,
+	[625] = 171634,
+	[626] = 171635,
+	[627] = 171636,
+	[628] = 171637,
+	[629] = 171638,
+	[630] = 171824,
+	[632] = 171825,
+	[634] = 171828,
+	[635] = 171829,
+	[636] = 171830,
+	[637] = 171831,
+	[638] = 171832,
+	[639] = 171833,
+	[640] = 171834,
+	[641] = 171835,
+	[642] = 171836,
+	[644] = 171838,
+	[645] = 171839,
+	[647] = 171841,
+	[648] = 171842,
+	[649] = 171843,
+	[650] = 171844,
+	[651] = 171845,
+	[654] = 171848,
+	[655] = 171849,
+	[657] = 171851,
+	[664] = 175700,
+}
+CyborgMMO_LocalMountMap = {}
--- a/RatPageController.lua	Thu Oct 23 12:17:02 2014 +0100
+++ b/RatPageController.lua	Thu Oct 23 13:44:59 2014 +0100
@@ -48,6 +48,36 @@
 function RatPageController_methods:GetCursorObject()
 	local type,a,b,c = GetCursorInfo()
 	ClearCursor()
+	
+	-- special case for unknown mounts (do it here since we're sure the cursor is free)
+	if type=='mount' then
+		local mountID = a
+		-- if the mount is unknown
+		if mountID~=0xFFFFFFF and not CyborgMMO_MountMap[mountID] and not CyborgMMO_LocalMountMap[mountID] then
+			-- build a reverse index of known mount spells
+			local reverse = {}
+			for mount,spell in pairs(CyborgMMO_MountMap) do reverse[spell] = mount end
+			for mount,spell in pairs(CyborgMMO_LocalMountMap) do reverse[spell] = mount end
+			-- iterate over mount journal
+			for i=1,C_MountJournal.GetNumMounts() do
+				local _,spell = C_MountJournal.GetMountInfo(i)
+				-- if the mount has no known mount ID
+				if not reverse[spell] then
+					-- pickup the mount
+					C_MountJournal.Pickup(i)
+					-- get the mount id from the cursor
+					local _,mount = GetCursorInfo()
+					ClearCursor()
+					-- save that to avoid spamming the cursor too often
+					if mount then
+						CyborgMMO_LocalMountMap[mount] = spell
+						reverse[spell] = mount
+					end
+				end
+			end
+		end
+	end
+	
 	if type=='item' then
 		local id,link = a,b
 		return CyborgMMO_CreateWowObject('item', id)
@@ -65,6 +95,9 @@
 	elseif type=='battlepet' then
 		local petID = a
 		return CyborgMMO_CreateWowObject('battlepet', petID)
+	elseif type=='mount' then
+		local mountID = a
+		return CyborgMMO_CreateWowObject('mount', mountID)
 	elseif type=='equipmentset' then
 		local name = a
 		return CyborgMMO_CreateWowObject('equipmentset', name)
--- a/WowObjects.lua	Thu Oct 23 12:17:02 2014 +0100
+++ b/WowObjects.lua	Thu Oct 23 13:44:59 2014 +0100
@@ -357,6 +357,68 @@
 
 ------------------------------------------------------------------------------
 
+local function GetMountInfoEx(mountID)
+	-- special case for random mount
+	if mountID == 0xFFFFFFF then
+		return 0,"Interface/ICONS/ACHIEVEMENT_GUILDPERK_MOUNTUP"
+	end
+	
+	local spellID = CyborgMMO_MountMap[mountID] or CyborgMMO_LocalMountMap[mountID]
+	if not spellID then return nil,"not in database" end
+	
+	local mountIndex
+	for i=1,C_MountJournal.GetNumMounts() do
+		local _,spell,texture = C_MountJournal.GetMountInfo(i) -- :FIXME: this may fail too early in the session (like when loading saved data)
+		if spell==spellID then
+			return i,texture
+		end
+	end
+	
+	return nil,"not in journal"
+end
+
+local WowMount_methods = setmetatable({}, {__index=WowObject_methods})
+local WowMount_mt = {__index=WowMount_methods}
+
+local function WowMount(mountID)
+	local mountIndex,texture = GetMountInfoEx(mountID)
+	if not mountIndex then
+		-- the mount might have been removed from the game
+		return nil
+	end
+	
+	local self = WowObject("mount", mountID)
+	CyborgMMO_DPrint("creating mount binding:", mountID, texture)
+
+	self.mountID = mountID
+	self.texture = texture
+
+	setmetatable(self, WowMount_mt)
+
+	return self
+end
+
+function WowMount_methods:DoAction()
+	local mountIndex = GetMountInfoEx(self.mountID)
+	if not mountIndex then return end
+	
+	C_MountJournal.Summon(mountIndex)
+end
+
+function WowMount_methods:Pickup()
+	local mountIndex = GetMountInfoEx(self.mountID)
+	if not mountIndex then return end
+	
+	return C_MountJournal.Pickup(mountIndex)
+end
+
+function WowMount_methods:SetBinding(key)
+	local buttonFrame,parentFrame,name = CyborgMMO_CallbackFactory:AddCallback(function() self:DoAction() end)
+	SetOverrideBindingClick(parentFrame, true, key, name, "LeftButton")
+end
+
+------------------------------------------------------------------------------
+
 -- this class is used by pre-defined icons in the corner of the Rat page
 CyborgMMO_CallbackIcons = {
 	new = function(self)
@@ -416,6 +478,8 @@
 		object = WowEquipmentSet(...)
 	elseif type == "battlepet" then
 		object = WowBattlePet(...)
+	elseif type == "mount" then
+		object = WowMount(...)
 	elseif type == "callback" then
 		object = WowCallback(...)
 	else
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/support/casc.lua	Thu Oct 23 13:44:59 2014 +0100
@@ -0,0 +1,342 @@
+local M, plat, bin = {}, require("casc.platform"), require("casc.bin")
+local jenkins96, blte = require("casc.jenkins96"), require("casc.blte")
+
+local uint32_le, uint32_be, uint40_be, uint16_le, to_be40, to_le32, to_bin, to_hex, ssub
+	= bin.uint32_le, bin.uint32_be, bin.uint40_be, bin.uint16_le, bin.to_be40, bin.to_le32, bin.to_bin, bin.to_hex, string.sub
+
+M.locale = {} do
+	for k,v in pairs({US=0x02, KR=0x04, FR=0x10, DE=0x20, CN=0x40, ES=0x80, TW=0x0100, GB=0x0200, MX=0x1000, RU=0x2000, BR=0x4000, IT=0x8000, PT=0x010000}) do
+		local isCN, m = k == "cn", v * 2
+		M.locale[k] = function(loc, tf)
+			return loc % m >= v and ((tf % 16 > 7) == isCN and 2 or 1) or nil
+		end
+	end
+end
+local function defaultLocale(loc, tf, cdn)
+	return (loc % 0x400 < 0x200 and 0 or 2) + (tf % 16 < 8 and 1 or 0) - (cdn and 4 or 0)
+end
+
+local function check_args(name, idx, v, t1, t2, ...)
+	if t1 then
+		local tt = type(v)
+		if tt ~= t1 and tt ~= t2 then
+			error('Invalid argument #' .. idx .. " to " .. name .. ": " .. t1 .. " expected; got " .. tt, 3)
+		end
+		return check_args(name, idx+1, ...)
+	end
+end
+
+local function readFile(...)
+	local path = plat.path(...)
+	if path then
+		local h, err = io.open(path, "rb")
+		if h then
+			local c = h:read("*a")
+			h:close()
+			return c
+		end
+		return h, err
+	end
+end
+local function readURL(...)
+	local url = plat.url(...)
+	if url then
+		return plat.http(url)
+	end
+end
+local function prefixHash(h)
+	local a, b, c = h:match("((%x%x)(%x%x).+)")
+	return b, c, a
+end
+local function cachedRead3(cpath, lpath, url)
+	local ret = cpath and readFile(cpath) or lpath and readFile(lpath)
+	if not ret and url then
+		ret = readURL(url)
+		if ret and cpath then
+			local h = io.open(cpath, "wb")
+			if h then
+				h:write(ret)
+				h:close()
+			end
+		end
+	end
+	return ret
+end
+local function cachedRead(casc, ...)
+	local ckey = plat.url(...)
+	ckey = ckey and casc.cachebase and plat.path(casc.cachebase, (ckey:gsub("[/\\]", "-")))
+	return cachedRead3(ckey, plat.path(casc.base, ...), plat.url(casc.cdnbase, ...))
+end
+
+local function parseInfoData(data)
+	local hname, htype, hn, ret, ln, last = {}, {}, 1, {}, 1
+	local i, s, line = data:gmatch("[^\n]+")
+	line = i(s, line)
+	for e in line:gmatch("[^|]+") do
+		hn, hname[hn], htype[hn] = hn + 1, e:match("^([^!]+)!([^:]+)")
+	end
+	
+	for e in i,s,line do
+		local l, ln = {}, 1
+		for f in e:gmatch("|?([^|]*)") do
+			if f ~= "" then
+				l[hname[ln]] = htype[ln] == "DEC" and tonumber(f) or f
+			end
+			ln = ln + 1
+		end
+		if ln > 1 then
+			ret[#ret+1] = l
+		end
+	end
+
+	return ret
+end
+local function parseConfigData(data, into)
+	local ret, ln, last = type(into) == "table" and into or {}, 1
+	for l in data:gmatch("[^\r\n]+") do
+		if l:match("^%s*[^#].-%S") then
+			local name, args = l:match("^%s*(%S+)%s*=(.*)$")
+			if name then
+				last, ln = {}, 1
+				ret[name] = last
+			end
+			for s in (args or l):gmatch("%S+") do
+				last[ln], ln = s, ln + 1
+			end
+		end
+	end
+	return ret
+end
+local function parseLocalIndexData(data, into)
+	local pos, sub, len = 8 + uint32_le(data), data.sub
+	pos = pos + ((16 - pos % 16) % 16)
+	len, pos = uint32_le(data, pos), pos + 9
+	assert(len % 18 == 0, "Index data block length parity check")
+	
+	for i=1, len/18 do
+		into[sub(data, pos, pos + 8)], pos = uint40_be(data, pos+8), pos + 18
+	end
+
+	return len/18
+end
+local function parseCDNIndexData(name, data, into)
+	local dlen, p = #data-28, 0
+	for i=1, math.floor(dlen/4100) - math.floor(dlen/844600) do
+		for pos=p, p+4072, 24 do
+			local len = uint32_be(data, pos+16)
+			if len > 0 then
+				local ofs = uint32_be(data, pos+20)
+				into[ssub(data, pos+1, pos+9)] = ofs .. "-" .. (ofs+len) .. ":" .. name
+			end
+		end
+		p = p + 4096
+	end
+end
+local function parseRootData(data, ed)
+	local ret, pos, dl = {}, 1, #data
+	while pos < dl do
+		local n, tflag, loc = uint32_le(data, pos-1), uint32_le(data, pos+3), uint32_le(data, pos+7)
+		pos = pos + 12 + 4*n
+		for i=1,n do
+			local nhash = ssub(data, pos+16, pos+23)
+			local t, j = ret[nhash] or {}
+			ret[nhash], j = t, #t
+			t[j+1], t[j+2], t[j+3], pos = ssub(data, pos,pos+15), tflag, loc, pos + 24
+		end
+	end
+	return ret
+end
+local function parseEncodingData(data)
+	local h, locale, n = {}, data:sub(1,2), uint32_be(data, 9)
+	local p2 = 22 + uint32_be(data, 18) + 32*n
+	for i=1, n do
+		local c = uint16_le(data, p2)
+		while c > 0 do
+			local chsh = ssub(data, p2+7, p2+22)
+			h[chsh] = {size=uint32_be(data, p2+2)}
+			p2 = p2 + 6+16
+			for i=1, c do
+				h[chsh][i], p2 = ssub(data, p2+1, p2+16), p2 + 16
+			end
+			c = uint16_le(data, p2)
+		end
+		p2 = data:match("()%z%Z", p2)
+	end
+	return h, locale
+end
+
+local function getFileByLoc(casc, loc, lcache)
+	local cnt = lcache and readFile(lcache)
+	if cnt then
+	elseif type(loc) == "number" and casc.base then
+		cnt = blte.readArchive(plat.path(casc.base, "data", ("data.%03d"):format(loc / 2^30)), loc % 2^30)
+	elseif type(loc) == "string" and casc.cdnbase then
+		local range, name = loc:match("(%d+%-%d+):(.+)")
+		cnt = blte.readData(plat.http(plat.url(casc.cdnbase, "data", prefixHash(name or loc)), range and {Range="bytes=" .. range}))
+		if lcache and cnt then
+			local h = io.open(lcache, "wb")
+			if h then
+				h:write(cnt)
+				h:close()
+			end
+		end
+	end
+	return cnt
+end
+local function getLocByFileKey(casc, fileKey)
+	local key, i, i2 = fileKey:sub(1,9), casc.index, casc.indexCDN
+	return i and i[key] or i2 and i2[key]
+end
+local function getFileByKey(casc, fileKey, lcache)
+	return getFileByLoc(casc, getLocByFileKey(casc, #fileKey == 32 and to_bin(fileKey) or fileKey), lcache)
+end
+local function getLocFromRootEntry(casc, e, rateFunc)
+	local score, ret = -math.huge
+	for i=1,e and #e or 0, 3 do
+		local keys = casc.encoding[e[i] or e]
+		local loc = keys and keys[1] and getLocByFileKey(casc, keys[1])
+		if loc then
+			local lscore = rateFunc(e[i+2], e[i+1], type(loc) == "string")
+			if lscore and lscore > score then
+				score, ret = lscore, loc
+			end
+		end
+	end
+	return ret
+end
+local function addCDNKey(casc, key)
+	if casc.indexCDN then
+		local hkey = #key == 32 and key or to_hex(key)
+		local rkey = #key == 16 and key or to_bin(key)
+		casc.indexCDN[rkey:sub(1,9)] = hkey
+	end
+	return key
+end
+
+local handle = {}
+local handle_mt = {__index=handle}
+function handle:readFile(path, lang)
+	lang = M.locale[lang] or lang
+	check_args("cascHandle:readFile", 1, path, "string", "string", lang, "function", "nil")
+
+	local h = jenkins96.hash_path(path)
+	return getFileByLoc(self, getLocFromRootEntry(self, self.root and self.root[h], lang or self.locale))
+end
+function handle:readFileByEncodingHash(hash)
+	check_args("cascHandle:readFileByEncodingHash", 1, hash, "string", "string")
+
+	return getFileByKey(self, #hash == 32 and to_bin(hash) or hash)
+end
+function handle:readFileByContentHash(hash)
+	check_args("cascHandle:readFileByContentHash", 1, hash, "string", "string")
+	
+	local k = self.encoding[#hash == 32 and to_bin(hash) or hash]
+	return k and getFileByKey(self, k[1])
+end
+function handle:setLocale(locale)
+	check_args("cascHandle:setLocale", 1, M.locale[locale] or locale, "function", "nil")
+	
+	self.locale = M.locale[locale] or locale or defaultLocale
+end
+
+function M.open(localBase, buildKey, cdnBase, cdnKey, cacheBase)
+	if buildKey == nil then error('Syntax: cac.open("localBase" or nil, "buildKey", "cdnBase" or nil, "cdnKey" or nil[, "cacheBase"])') end
+	check_args("casc.open", 1, localBase, "string", "nil", buildKey, "string", nil, cdnBase, "string", "nil", cdnKey, "string", "nil", cacheBase, "string", "nil")
+	assert(type(localBase) == "string" or type(cdnBase) == "string", 'invalid arguments: at least one of localBase, cdnBase must not be nil')
+	assert(type(cdnBase) ~= "string" or type(cdnKey) == "string", 'invalid arguments: if cdnBase is specified, cdnKey must be a string')
+
+	local casc = setmetatable({base=localBase, buildKey=buildKey, cdnbase=cdnBase, cdnKey=cdnKey, cachebase=cacheBase, locale=defaultLocale}, handle_mt)
+	casc.conf = parseConfigData(cachedRead(casc, "config", prefixHash(buildKey)))
+	if cdnKey then
+		parseConfigData(cachedRead(casc, "config", prefixHash(cdnKey)), casc.conf)
+		casc.indexCDN = {}
+		for k,v in pairs(casc.conf.archives) do
+			parseCDNIndexData(v, cachedRead3(
+				plat.path(cacheBase, "index." .. v),
+				plat.path(localBase, "indices", v .. ".index"),
+				plat.url(cdnBase, "data", prefixHash(v .. ".index"))
+			), casc.indexCDN)
+		end
+	end
+	if localBase then
+		casc.index = {}
+		for _, f in plat.files(plat.path(localBase, "data"), "*.idx") do
+			parseLocalIndexData(assert(readFile(f)), casc.index)
+		end
+	end
+
+	local ekey = addCDNKey(casc, casc.conf.encoding[#casc.conf.encoding])
+	casc.encoding = parseEncodingData(getFileByKey(casc, ekey, plat.path(cacheBase, "encoding." .. ekey)))
+	local rkey = addCDNKey(casc, casc.encoding[to_bin(casc.conf.root[#casc.conf.root])][1])
+	casc.root = parseRootData(getFileByKey(casc, rkey, plat.path(cacheBase, "root." .. to_hex(rkey))))
+	
+	return casc
+end
+
+function M.cdnbuild(patchBase, region)
+	check_args("casc.cdnbuild", 1, patchBase, "string", "string", region, "string", "nil")
+	
+	local versions = parseInfoData(plat.http(plat.url(patchBase, "versions")))
+	local cdns = parseInfoData(plat.http(plat.url(patchBase, "cdns")))
+	local reginfo = {}
+	for i=1,#versions do
+		local v = versions[i]
+		reginfo[v.Region] = {cdnKey = v.CDNConfig, buildKey=v.BuildConfig, build=v.BuildId, version=v.VersionsName}
+	end
+	for i=1,#cdns do
+		local c = cdns[i]
+		if reginfo[c.Name] then
+			reginfo[c.Name].cdnBase = c.Hosts and c.Path and plat.url("http://", c.Hosts:match("%S+"), c.Path)
+		end
+	end
+	if reginfo[region] and reginfo[region].cdnBase then
+		local r = reginfo[region]
+		return r.buildKey, r.cdnBase, r.cdnKey, r.version, r.build
+	elseif region == nil then
+		return reginfo, versions, cdns
+	end
+end
+function M.localbuild(buildInfoPath, selectBuild)
+	check_args("casc.localbuild", 1, buildInfoPath, "string", "string", selectBuild, "function", "nil")
+	
+	local info = parseInfoData(assert(readFile(buildInfoPath)))
+	if type(selectBuild) == "function" and info then
+		local ii = info[selectBuild(info)]
+		if ii then
+			return ii["Build Key"], plat.url("http://", ii["CDN Hosts"]:match("%S+"), ii["CDN Path"]), ii["CDN Key"],  ii["Version"]
+		end
+	elseif info then
+		local branches = {}
+		for i=1,#info do
+			local ii = info[i]
+			local curl = plat.url("http://", ii["CDN Hosts"]:match("%S+"), ii["CDN Path"])
+			branches[ii.Branch] = {cdnKey=ii["CDN Key"], buildKey=ii["Build Key"], version=ii["Version"], cdnBase=curl}
+		end
+		return branches, info
+	end
+end
+
+function M.selectActiveBuild(buildInfo)
+	check_args("casc.selectActiveBuild", 1, buildInfo, "table", "table")
+	
+	for i=1,#buildInfo do
+		if buildInfo[i].Active == 1 then
+			return i
+		end
+	end
+end
+function M.selectUserBuild(buildInfo)
+	check_args("casc.selectUserBuild", 1, buildInfo, "table", "table")
+	
+	print("Available builds:")
+	for i=1,#buildInfo do
+		print(("%d. [%s] %s (%s)%s"):format(i, buildInfo[i]["Build Key"], buildInfo[i].Version, buildInfo[i].Branch, buildInfo[i].Active == 1 and " (active)" or ""))
+	end
+	io.stdout:write("Enter index: ")
+	io.stdout:flush()
+	
+	local c = tonumber(io.stdin:read("*l"))
+	return buildInfo[c] and c
+end
+
+return M
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/support/casc/bin.lua	Thu Oct 23 13:44:59 2014 +0100
@@ -0,0 +1,55 @@
+local M, sbyte, schar, sgsub, sformat = {}, string.byte, string.char, string.gsub, string.format
+local inf, nan = math.huge, math.huge-math.huge
+
+local hexbin = {} for i=0,255 do
+	local h, b = sformat("%02x", i), schar(i)
+	hexbin[sformat("%02X", i)], hexbin[h], hexbin[b] = b, b, h
+end
+
+function M.uint32_le(s, pos)
+	local d, c, b, a = sbyte(s, (pos or 0)+1, (pos or 0) + 4)
+	return a*256^3 + b*256^2 + c*256 + d
+end
+function M.uint16_le(s, pos)
+	local b,a = sbyte(s, (pos or 0)+1, (pos or 0) + 2)
+	return a*256 + b
+end
+function M.uint32_be(s, pos)
+	local a,b,c,d = sbyte(s, (pos or 0)+1, (pos or 0) + 4)
+	return a*256^3 + b*256^2 + c*256 + d
+end
+function M.uint40_be(s, pos)
+	local a, b, c, d, e = sbyte(s, (pos or 0)+1, (pos or 0) + 5)
+	return a*256^4 + b*256^3 + c*256^2 + d*256 + e
+end
+function M.float32_le(s, pos)
+	local a, b, c, d = sbyte(s, (pos or 0) + 1, (pos or 0) + 4)
+	local s, e, f = d > 127 and -1 or 1, (d % 128)*2 + (c > 127 and 1 or 0), a + b*256 + (c % 128)*256^2
+	if e > 0 and e < 255 then
+		return s * (1+f/2^23) * 2^(e-127)
+	else
+		return e == 0 and (s * f/2^23 * 2^-126) or f == 0 and (s * inf) or nan
+	end
+end
+function M.int32_le(s, pos)
+	local d, c, b, a = sbyte(s, (pos or 0)+1, (pos or 0) + 4)
+	return a*256^3 + b*256^2 + c*256 + d - (a < 128 and 0 or 2^32)
+end
+
+function M.to_le32(n)
+	local n = n % 2^32
+	return schar(n % 256, (n / 256) % 256, (n / 256^2) % 256, (n / 256^3) % 256)
+end
+function M.to_be40(n)
+	local n = n % 2^40
+	return schar((n / 256^4) % 256, (n / 256^3) % 256, (n / 256^2) % 256, (n / 256) % 256, n % 256)
+end
+
+function M.to_bin(hs)
+	return hs and sgsub(hs, "%x%x", hexbin)
+end
+function M.to_hex(bs)
+	return bs and sgsub(bs, ".", hexbin)
+end
+
+return M
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/support/casc/blte.lua	Thu Oct 23 13:44:59 2014 +0100
@@ -0,0 +1,64 @@
+local M, plat, bin = {}, require("casc.platform"), require("casc.bin")
+local open, tconcat, assert, error = io.open, table.concat, assert, error
+local uint32_le, uint32_be = bin.uint32_le, bin.uint32_be
+
+local string_cursor do
+	local function read(self, n)
+		local p = self.pos
+		self.pos = p + n
+		return self.str:sub(p, p+n-1)
+	end
+	function string_cursor(s)
+		return {str=s, read=read, pos=1}
+	end
+end
+
+local function decodeChunk(chunk)
+	local format = chunk:sub(1,1)
+	if format == 'N' then
+		return chunk:sub(2)
+	elseif format == 'Z' then
+		return plat.decompress(chunk:sub(2))
+	else
+		error('Unknown chunk format: ' .. tostring(format))
+	end
+end
+local function parseBLTE(h, dataSize)
+	local header = h:read(8)
+	assert(header:sub(1,4) == 'BLTE', 'BLTE file magic signature')
+	local ofs = uint32_be(header, 4)
+
+	local chunks, ret = ofs > 0 and {}
+	if ofs > 0 then
+		local n = uint32_be(h:read(4)) % 2^16
+		local buf, p = h:read(n*24), 0
+		for i=1, n do
+			chunks[i], p = uint32_be(buf, p), p + 24
+		end
+		for i=1, #chunks do
+			chunks[i] = decodeChunk(h:read(chunks[i]))
+		end
+		ret = tconcat(chunks, "")
+	else
+		ret = decodeChunk(h:read(dataSize))
+	end
+	return ret
+end
+
+function M.readArchive(path, offset)
+	assert(type(path) == "string" and type(offset) == "number", 'Syntax: "content" = casc.blte.readArchive("path", offset)')
+
+	local h = open(path, "rb")
+	h:seek("set", offset)
+	local blockHead = h:read(30)
+	local ret = parseBLTE(h, uint32_le(blockHead, 16)-30)
+	h:close()
+	
+	return ret
+end
+function M.readData(str)
+	assert(type(str) == "string", 'Syntax: "content" = casc.blte.readContent("str")')
+	return parseBLTE(string_cursor(str), #str)
+end
+
+return M
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/support/casc/dbc.lua	Thu Oct 23 13:44:59 2014 +0100
@@ -0,0 +1,85 @@
+local M, bin = {}, require("casc.bin")
+local assert, loadstring, smatch = assert, loadstring or load, string.match
+
+local uint32_le, int32_le, float32_le = bin.uint32_le, bin.int32_le, bin.float32_le
+
+local function unpacker(data, format, rows, stride, hsize, sbase, tfunc)
+	tfunc = type(tfunc) == "function" and tfunc or nil
+	
+	local skip, p, pe = 0, [=[-- casc.dbc:iterator
+		local smatch, uint32_le, int32_le, float32_le, tfunc, data, rows, stride, sbase, rpos, i = ...
+		return function()
+			if i < rows then
+				rpos, i = rpos + stride, i + 1
+				return ]=] .. (tfunc and "tfunc(i" or "i"), (tfunc and ")" or "") .. '\nend\nend'
+	
+	for r, t in format:gmatch("(%d*)(.)") do
+		r = tonumber(r) or 1
+		for i=1,r do
+			if t == '.' then
+				skip = skip + 4 * r
+				break
+			elseif t == 'u' then
+				p, skip = p .. ', uint32_le(data, rpos+' .. skip .. ')', skip + 4
+			elseif t == 'i' then
+				p, skip = p .. ', int32_le(data, rpos+' .. skip .. ')', skip + 4
+			elseif t == 'f' then
+				p, skip = p .. ', float32_le(data, rpos+' .. skip .. ')', skip + 4
+			elseif t == 's' then
+				assert(sbase, "invalid signature: 's' requires a string block")
+				p, skip = p .. ', smatch(data, "%Z*", sbase + uint32_le(data,rpos+' .. skip .. '))', skip + 4
+			else
+				error('Unknown signature field type "' .. t .. '"')
+			end
+		end
+	end
+
+	return loadstring(p .. pe)(smatch, uint32_le, int32_le, float32_le,
+		tfunc, data, rows, stride, sbase, hsize - stride, 0), skip
+end
+
+local header do
+	local function dbc(data)
+		assert(data:sub(1,4) == "WDBC", "DBC magic signature")
+		local rows, fields, stride, stringSize = uint32_le(data, 4), uint32_le(data, 8), uint32_le(data, 12), uint32_le(data, 16)
+		assert(20 + rows*stride + stringSize <= #data, "Data too short")
+	
+		return rows, fields, stride, 20, 21 + rows * stride
+	end
+	local function db2(data)
+		local hsize = 48
+		local rows, fields, stride, stringSize = uint32_le(data, 4), uint32_le(data, 8), uint32_le(data, 12), uint32_le(data, 16)
+		local build, minId, maxId, locale, rid = uint32_le(data, 24), uint32_le(data, 32), uint32_le(data, 36), uint32_le(data, 40)
+	
+		if maxId > 0 then
+			local n, p = maxId-minId + 1, hsize
+			rid, hsize = {}, hsize + 6 * n
+			for i=1,n do
+				rid[i], p = uint32_le(data, p), p + 6
+			end
+		end
+		assert(hsize + rows*stride + stringSize <= #data, "Data too short")
+	
+		return rows, fields, stride, hsize, hsize + 1 + rows * stride, rid, minId, maxId, build, locale
+	end
+	header = {WDBC=dbc, WDB2=db2, WCH2=db2}
+end
+
+function M.header(data)
+	assert(type(data) == "string", 'Syntax: casc.dbc.header("data")')
+	local fourCC = data:sub(1,4)
+	return assert(header[fourCC], "Unsupported format")(data)
+end
+
+function M.rows(data, sig, loose)
+	assert(type(data) == "string" and type(sig) == "string", 'Syntax: casc.dbc.rows("data", "rowSignature"[, loose])')
+	
+	local rows, _, stride, hsize, sbase, rid = M.header(data)
+	local iter, skip = unpacker(data, sig, rows, stride, hsize, sbase, rid and function(i, ...) return rid[i], ... end)
+	assert(skip <= stride, 'signature exceeds stride')
+	assert(loose or skip == stride, 'signature/stride mismatch')
+
+	return iter
+end
+
+return M
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/support/casc/jenkins96.lua	Thu Oct 23 13:44:59 2014 +0100
@@ -0,0 +1,38 @@
+local M, plat, bin = {}, require("casc.platform"), require("casc.bin")
+
+local rot, xor, uint32_le, to_le32 = plat.rol, plat.bxor, bin.uint32_le, bin.to_le32
+
+function M.hash(k)
+	assert(type(k) == "string", 'Syntax: casc.jenkins96.hash("key")')
+	
+	if #k == 0 then return 0xdeadbeef, 0xdeadbeef end
+	local a = 0xdeadbeef + #k
+	local b, c, k = a, a, k .. (k ~= "" and ("\0"):rep((12 - #k % 12) % 12) or "")
+	for i=0, #k-13, 12 do
+		a, b, c = a + uint32_le(k, i), b + uint32_le(k, i+4), c + uint32_le(k, i+8)
+		a = xor(a-c, rot(c, 4)); c = c + b
+	   b = xor(b-a, rot(a, 6)); a = a + c
+	   c = xor(c-b, rot(b, 8)); b = b + a
+	   a = xor(a-c, rot(c,16)); c = c + b
+	   b = xor(b-a, rot(a,19)); a = a + c
+	   c = xor(c-b, rot(b, 4)); b = b + a
+	end
+	local i = #k - 12
+	a, b, c = a + uint32_le(k, i), b + uint32_le(k, i+4), c + uint32_le(k, i+8)
+	c = xor(c, b) - rot(b,14)
+	a = xor(a, c) - rot(c,11)
+	b = xor(b, a) - rot(a,25)
+	c = xor(c, b) - rot(b,16)
+	a = xor(a, c) - rot(c,04)
+	b = xor(b, a) - rot(a,14)
+	c = xor(c, b) - rot(b,24)
+	return c, b
+end
+
+function M.hash_path(path)
+	assert(type(path) == "string", 'Syntax: casc.jenkins96.hash_path("path")')
+	local c, b = M.hash((path:upper():gsub('/', '\\')))
+	return to_le32(b) .. to_le32(c)
+end
+
+return M
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/support/casc/platform.lua	Thu Oct 23 13:44:59 2014 +0100
@@ -0,0 +1,148 @@
+local M, assert, getenv = {}, assert, os.getenv
+
+local function maybe(m)
+	local ok, v = pcall(require, m)
+	return ok and v
+end
+local lfs = maybe("lfs") -- LuaFileSystem; http://keplerproject.github.io/luafilesystem/
+local zlib = maybe("zlib") -- lzlib; https://github.com/LuaDist/lzlib
+local bit = maybe("bit") -- Lua BitOp; http://bitop.luajit.org
+local socket = maybe("socket.http") -- LuaSocket; http://w3.impa.br/~diego/software/luasocket/home.html
+
+local function shellEscape(s)
+	return '"' .. s:gsub('"', '"\\\\""') .. '"'
+end
+local function readAndDeleteFile(path)
+	local h, err = io.open(path, "rb")
+	if h then
+		local c = h:read("*a")
+		h:close()
+		h, err = c, nil
+	end
+	os.remove(path)
+	return h, err
+end
+
+local dir_sep = package and package.config and package.config:sub(1,1) or "/"
+do -- M.path(a, b, ...)
+	M.path = function(a, b, ...)
+		if a and b then
+			return M.path(a .. (a:sub(-1) ~= dir_sep and dir_sep or "") .. b, ...)
+		end
+		return a
+	end
+end
+M.url = function(a, b, ...)
+	if a and b then
+		return M.url(a .. ((a:sub(-1) == "/" or b:sub(1,1) == "/") and "" or "/") .. b, ...)
+	end
+	return a
+end
+
+M.commands =
+	dir_sep == '/' and {toDevNull=' 2>/dev/null', ls='ls %s', mkdir='mkdir -p %s', gzip='gzip -dcq %s'} or
+	dir_sep == '\\' and {toDevNull=' 2>NUL', ls='(for %%a in (%s) do @echo %%~fa)', mkdir='mkdir %s', gzip='gzip -dcq %s', TMP=os.getenv('TMP') or os.getenv('TEMP')}
+
+M.tmpname = function()
+	local tn = os.tmpname()
+	return (M.commands and M.commands.TMP or "") .. tn
+end
+
+M.decompress = zlib and zlib.decompress or function(compressed)
+	assert(type(compressed) == "string", 'Syntax: casc.platform.decompress("compressed")')
+	assert(M.commands and M.commands.gzip and M.commands.toDevNull, 'unsupported platform')
+	
+	local f, f2 = M.tmpname(), M.tmpname()
+	local h = io.open(f, "wb")
+	h:write('\31\139\8\0\0\0\0\0')
+	h:write(compressed)
+	h:close()
+	
+	os.execute(M.commands.gzip:format(shellEscape(f)) .. " 1>" .. f2 .. " " .. M.commands.toDevNull)
+	os.remove(f)
+	
+	return readAndDeleteFile(f2)
+end
+
+M.mkdir = lfs and lfs.mkdir or function(path)
+	assert(type(path) == 'string', 'Syntax: casc.platform.mkdir("path")')
+	assert(M.commands and M.commands.mkdir, 'unsupported platform')
+	
+	return os.execute(M.commands.mkdir:format(shellEscape(path)))
+end
+
+M.files = lfs and function(dir, glob)
+	assert(type(dir) == "string" and type(glob) == 'string', 'Syntax: casc.platform.files("dir", "glob")')
+	local pat = "^" .. glob:gsub("%.%-%+", "%%%0"):gsub("%*", ".*") .. "$"
+	local t, ni = {}, 1
+	for f in lfs.dir(dir) do
+		if f ~= "." and f ~= ".." and f:match(pat) then
+			t[ni], ni = M.path(dir, f), ni + 1
+		end
+	end
+	return pairs(t)
+end or function(dir, glob)
+	assert(type(dir) == "string" and type(glob) == 'string', 'Syntax: casc.platform.files("dir", "glob")')
+	assert(M.commands and M.commands.ls, 'unsupported platform')
+	
+	local dir, files = glob:match("^(.-)([^" .. dir_sep .. "]+)$")
+	local t, ni, h = {}, 1, io.popen(M.commands.ls:format(shellEscape(dir) .. files), "r")
+	for l in h:lines() do
+		t[ni], ni = l, ni + 1
+	end
+	h:close()
+	return pairs(t)
+end
+
+local floor = math.floor
+M.rol = bit and bit.rol or function(n, b)
+	local n, e2 = n % 2^32, 2^(32-b)
+	return ((n % e2) * 2^b + floor(n/e2)) % 2^32
+end
+
+M.bxor = bit and bit.bxor or function(a, b)
+   local out, m, lm = 0, 1
+	for i=1,32 do
+		m, lm = m+m, m
+		local am, bm = a % m, b % m
+		out, a, b = out + (am == bm and 0 or lm), a-am, b-bm
+		if a == 0 or b == 0 then
+			return (out + b + a) % 2^32
+		end
+	end
+	return out
+end
+
+if socket then
+	socket.USERAGENT, socket.TIMEOUT = "lcasc/1.1", 5
+	local ltn12, RETRIES = require("ltn12"), 3
+	M.http = function(url, h)
+		for i=1,RETRIES do
+			local sink = {}
+			local ok, status, head = socket.request({url=url, sink=ltn12.sink.table(sink), headers=h})
+			if ok then
+				local cnt = table.concat(sink, "")
+				return status >= 200 and status < 300 and cnt, status, head, cnt
+			elseif i == RETRIES then
+				error("HTTP request failed: " .. tostring(status) .. "\nURL: " .. tostring(url))
+			end
+		end
+	end
+else
+	M.http = function(url, h)
+		local c, of = "curl -s -S -A 'luacasc/1.1+curl'", M.tmpname()
+		if type(h) == "table" then
+			for k,v in pairs(h) do
+				c = c .. ' -H ' .. shellEscape(k .. ": " .. v)
+			end
+		end
+		c = c .. ' -o ' .. shellEscape(of)
+		c = c .. ' ' .. shellEscape(url)
+		if os.execute(c) == 0 then
+			return readAndDeleteFile(of)
+		end
+		os.remove(of)
+	end
+end
+
+return M
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/support/gen-mount-db.lua	Thu Oct 23 13:44:59 2014 +0100
@@ -0,0 +1,34 @@
+local dump = require 'dump'
+local path = require 'path'
+local registry = require 'registry'
+local casc = require 'casc'
+local dbc = require 'casc.dbc'
+
+
+local root =
+	registry.HKLM.SOFTWARE['Blizzard Entertainment']['World of Warcraft'] 'InstallPath' or
+	registry.HKLM.SOFTWARE.Wow6432Node['Blizzard Entertainment']['World of Warcraft'] 'InstallPath'
+root = path.split(root)
+
+-- load local build config
+local buildKey,cdnBase,cdnKey = assert(casc.localbuild(tostring(root / '.build.info'), casc.selectActiveBuild))
+
+-- open the CASC
+local handle = casc.open(tostring(root / 'Data'), buildKey, cdnBase, cdnKey)
+
+-- read the mount database
+local Mount = handle:readFile("DBFilesClient/Mount.db2")
+
+-- create a mapping from mount ID to spell ID
+local map = {}
+for _,mount,spell in dbc.rows(Mount, 'u7.u.') do
+	map[mount] = spell
+end
+
+-- save
+local file = assert(io.open("../MountMap.lua", 'wb'))
+assert(file:write("-- this is a generated file, do not edit, see support/gen-mount-db.lua\n"))
+assert(file:write("CyborgMMO_MountMap = "..dump.tostring(map).."\n"))
+assert(file:write("CyborgMMO_LocalMountMap = {}\n"))
+assert(file:close())
+