2012-08-31 14:46:33 -06:00
-- Interface powered item editor.
local gui = require ' gui '
2012-09-07 08:25:39 -06:00
local dialog = require ' gui.dialogs '
2012-11-28 12:13:42 -07:00
local widgets = require ' gui.widgets '
2014-04-26 12:50:52 -06:00
local guiScript = require ' gui.script '
2012-09-23 14:45:19 -06:00
local args = { ... }
2012-11-28 12:13:42 -07:00
local keybindings = {
offset = { key = " CUSTOM_ALT_O " , desc = " Show current items offset " } ,
find = { key = " CUSTOM_F " , desc = " Find a value by entering a predicate " } ,
lua_set = { key = " CUSTOM_ALT_S " , desc = " Set by using a lua function " } ,
insert = { key = " CUSTOM_ALT_I " , desc = " Insert a new value to the vector " } ,
delete = { key = " CUSTOM_ALT_D " , desc = " Delete selected entry " } ,
2014-04-26 12:50:52 -06:00
reinterpret = { key = " CUSTOM_ALT_R " , desc = " Open selected entry as something else " } ,
2015-02-01 10:58:49 -07:00
start_filter = { key = " CUSTOM_S " , desc = " Start typing filter, Enter to finish " } ,
2012-11-28 12:13:42 -07:00
help = { key = " HELP " , desc = " Show this help " } ,
2015-06-12 15:02:12 -06:00
displace = { key = " STRING_A093 " , desc = " Open reference offseted by index " } ,
2015-09-12 17:55:38 -06:00
NOT_USED = { key = " SEC_SELECT " , desc = " Edit selected entry as a number (for enums) " } , --not a binding...
2012-11-28 12:13:42 -07:00
}
2012-09-23 14:45:19 -06:00
function getTargetFromScreens ( )
local my_trg
if dfhack.gui . getCurFocus ( ) == ' item ' then
my_trg = dfhack.gui . getCurViewscreen ( ) . item
elseif dfhack.gui . getCurFocus ( ) == ' joblist ' then
local t_screen = dfhack.gui . getCurViewscreen ( )
my_trg = t_screen.jobs [ t_screen.cursor_pos ]
elseif dfhack.gui . getCurFocus ( ) == ' createquota ' then
local t_screen = dfhack.gui . getCurViewscreen ( )
my_trg = t_screen.orders [ t_screen.sel_idx ]
elseif dfhack.gui . getCurFocus ( ) == ' dwarfmode/LookAround/Flow ' then
local t_look = df.global . ui_look_list.items [ df.global . ui_look_cursor ]
my_trg = t_look.flow
2012-08-31 14:46:33 -06:00
2012-09-23 14:45:19 -06:00
elseif dfhack.gui . getSelectedUnit ( true ) then
my_trg = dfhack.gui . getSelectedUnit ( true )
elseif dfhack.gui . getSelectedItem ( true ) then
my_trg = dfhack.gui . getSelectedItem ( true )
elseif dfhack.gui . getSelectedJob ( true ) then
my_trg = dfhack.gui . getSelectedJob ( true )
else
qerror ( " No valid target found " )
end
return my_trg
2012-08-31 14:46:33 -06:00
end
2012-09-23 14:22:14 -06:00
GmEditorUi = defclass ( GmEditorUi , gui.FramedScreen )
GmEditorUi.ATTRS = {
2012-08-31 14:46:33 -06:00
frame_style = gui.GREY_LINE_FRAME ,
frame_title = " GameMaster's editor " ,
2015-02-14 20:53:06 -07:00
}
2012-11-28 12:13:42 -07:00
function GmEditorUi : onHelp ( )
self.subviews . pages : setSelected ( 2 )
end
function burning_red ( input ) -- todo does not work! bug angavrilov that so that he would add this, very important!!
local col = COLOR_LIGHTRED
return { text = input , pen = dfhack.pen . parse { fg = COLOR_LIGHTRED , bg = 0 } }
end
2012-11-28 17:26:10 -07:00
function Disclaimer ( tlb )
local dsc = { " Association Of " , { text = " Psychic " , pen = dfhack.pen . parse { fg = COLOR_YELLOW , bg = 0 } } ,
" Dwarves (AOPD) is not responsible for all the damage " , NEWLINE , " that this tool can (and will) cause to you and your loved dwarves " , NEWLINE , " and/or saves.Please use with caution. " , NEWLINE , { text = " Magma not included. " , pen = dfhack.pen . parse { fg = COLOR_LIGHTRED , bg = 0 } } }
if tlb then
for _ , v in ipairs ( dsc ) do
table.insert ( tlb , v )
end
end
return dsc
end
2015-02-01 10:58:49 -07:00
2012-09-23 14:22:14 -06:00
function GmEditorUi : init ( args )
self.stack = { }
self.item_count = 0
self.keys = { }
2012-11-28 12:13:42 -07:00
local helptext = { { text = " Help " } , NEWLINE , NEWLINE }
for k , v in pairs ( keybindings ) do
2015-09-12 17:55:38 -06:00
table.insert ( helptext , { text = v.desc , key = v.key , key_sep = ' : ' } )
2012-11-28 12:13:42 -07:00
table.insert ( helptext , NEWLINE )
end
table.insert ( helptext , NEWLINE )
2012-11-28 17:26:10 -07:00
Disclaimer ( helptext )
2015-02-14 20:53:06 -07:00
2012-11-28 12:13:42 -07:00
local helpPage = widgets.Panel {
subviews = { widgets.Label { text = helptext , frame = { l = 1 , t = 1 , yalign = 0 } } } }
local mainList = widgets.List { view_id = " list_main " , choices = { } , frame = { l = 1 , t = 3 , yalign = 0 } , on_submit = self : callback ( " editSelected " ) ,
2015-09-12 17:55:38 -06:00
on_submit2 = self : callback ( " editSelectedRaw " ) ,
2012-11-28 12:13:42 -07:00
text_pen = dfhack.pen . parse { fg = COLOR_DARKGRAY , bg = 0 } , cursor_pen = dfhack.pen . parse { fg = COLOR_YELLOW , bg = 0 } }
local mainPage = widgets.Panel {
subviews = {
mainList ,
2015-06-12 14:55:55 -06:00
widgets.Label { text = { { text = " <no item> " , id = " name " } , { gap = 1 , text = " Help " , key = keybindings.help . key , key_sep = ' () ' } } , view_id = ' lbl_current_item ' , frame = { l = 1 , t = 1 , yalign = 0 } } ,
2015-09-12 17:55:38 -06:00
widgets.Label { text = { { text = " Search " , key = keybindings.start_filter . key , key_sep = ' () ' } , { text = " : " } } , frame = { l = 1 , t = 2 } } ,
2015-06-12 14:55:55 -06:00
widgets.EditField { frame = { l = 12 , t = 2 } , active = false , on_change = self : callback ( ' text_input ' ) , on_submit = self : callback ( " enable_input " , false ) , view_id = " filter_input " } ,
2012-11-28 12:13:42 -07:00
--widgets.Label{text="BLAH2"}
}
, view_id = ' page_main ' }
2015-02-14 20:53:06 -07:00
2012-11-28 12:13:42 -07:00
local pages = widgets.Pages { subviews = { mainPage , helpPage } , view_id = " pages " }
self : addviews {
pages
}
self : pushTarget ( args.target )
2012-09-23 14:22:14 -06:00
end
2015-02-01 10:58:49 -07:00
function GmEditorUi : text_input ( new_text )
self : updateTarget ( true , true )
end
function GmEditorUi : enable_input ( enable )
self.subviews . filter_input.active = enable
end
2012-10-07 11:44:18 -06:00
function GmEditorUi : find ( test )
2015-02-14 20:53:06 -07:00
local trg = self : currentTarget ( )
2012-11-28 12:13:42 -07:00
if test == nil then
dialog.showInputPrompt ( " Test function " , " Input function that tests(k,v as argument): " , COLOR_WHITE , " " , dfhack.curry ( self.find , self ) )
return
end
2015-02-14 20:53:06 -07:00
2012-11-28 12:13:42 -07:00
local e , what = load ( " return function(k,v) return " .. test .. " end " )
if e == nil then
dialog.showMessage ( " Error! " , " function failed to compile \n " .. what , COLOR_RED )
end
2015-02-14 20:53:06 -07:00
2012-10-07 11:44:18 -06:00
if trg.target and trg.target . _kind and trg.target . _kind == " container " then
2015-02-14 20:53:06 -07:00
2012-10-07 11:44:18 -06:00
for k , v in pairs ( trg.target ) do
if e ( ) ( k , v ) == true then
self : pushTarget ( v )
return
end
end
2012-11-28 12:13:42 -07:00
else
local i = 1
for k , v in pairs ( trg.target ) do
if e ( ) ( k , v ) == true then
self.subviews . list_main : setSelected ( i )
return
end
i = i + 1
end
2012-10-07 11:44:18 -06:00
end
end
2012-09-23 14:22:14 -06:00
function GmEditorUi : insertNew ( typename )
local tp = typename
if typename == nil then
2012-11-28 12:13:42 -07:00
dialog.showInputPrompt ( " Class type " , " Input class type: " , COLOR_WHITE , " " , self : callback ( " insertNew " ) )
2012-09-23 14:22:14 -06:00
return
end
local ntype = df [ tp ]
if ntype == nil then
dialog.showMessage ( " Error! " , " Type ' " .. tp .. " not found " , COLOR_RED )
return
end
2015-02-14 20:53:06 -07:00
local trg = self : currentTarget ( )
2012-09-23 14:22:14 -06:00
if trg.target and trg.target . _kind and trg.target . _kind == " container " then
local thing = ntype : new ( )
2012-11-28 12:13:42 -07:00
dfhack.call_with_finalizer ( 1 , false , df.delete , thing , function ( tscreen , target , to_insert )
target : insert ( " # " , to_insert ) ; tscreen : updateTarget ( true , true ) ; end , self , trg.target , thing )
2015-02-14 20:53:06 -07:00
2012-09-23 14:22:14 -06:00
end
end
2012-11-28 12:13:42 -07:00
function GmEditorUi : deleteSelected ( key )
2012-09-23 14:22:14 -06:00
local trg = self : currentTarget ( )
if trg.target and trg.target . _kind and trg.target . _kind == " container " then
2012-11-28 12:13:42 -07:00
trg.target : erase ( key )
self : updateTarget ( true , true )
2012-09-23 14:22:14 -06:00
end
end
2012-11-28 12:13:42 -07:00
function GmEditorUi : getSelectedKey ( )
return self : currentTarget ( ) . keys [ self.subviews . list_main : getSelected ( ) ]
end
2012-09-23 14:22:14 -06:00
function GmEditorUi : currentTarget ( )
return self.stack [ # self.stack ]
end
2015-09-12 17:55:38 -06:00
function GmEditorUi : getSelectedEnumType ( )
2014-03-08 01:09:53 -07:00
local trg = self : currentTarget ( )
2015-09-12 17:55:38 -06:00
local trg_key = trg.keys [ self.subviews . list_main : getSelected ( ) ]
if trg.target . _field == nil then return nil end
2015-09-20 09:10:19 -06:00
if trg.target : _field ( trg_key ) == nil then return nil end
2014-03-08 01:09:53 -07:00
local enum = trg.target : _field ( trg_key ) . _type
if enum._kind == " enum-type " then
2015-09-12 17:55:38 -06:00
return enum
else
return nil
end
end
function GmEditorUi : editSelectedEnum ( index , choice )
local enum = self : getSelectedEnumType ( )
if enum then
local trg = self : currentTarget ( )
local trg_key = self : getSelectedKey ( )
2014-03-08 01:09:53 -07:00
local list = { }
for i = enum._first_item , enum._last_item do
2015-09-12 17:55:38 -06:00
table.insert ( list , { text = ( ' %s (%i) ' ) : format ( tostring ( enum [ i ] ) , i ) , value = i } )
2014-03-08 01:09:53 -07:00
end
guiScript.start ( function ( )
2015-09-12 17:55:38 -06:00
local ret , idx , choice = guiScript.showListPrompt ( " Choose item: " , nil , 3 , list , nil , true )
2014-03-08 01:09:53 -07:00
if ret then
trg.target [ trg_key ] = choice.value
self : updateTarget ( true )
end
end )
2015-02-14 20:53:06 -07:00
2014-03-08 01:09:53 -07:00
else
qerror ( " not an enum " )
end
end
2014-04-26 12:50:52 -06:00
function GmEditorUi : openReinterpret ( key )
local trg = self : currentTarget ( )
dialog.showInputPrompt ( tostring ( trg_key ) , " Enter new type: " , COLOR_WHITE ,
" " , function ( choice )
local ntype = df [ tp ]
self : pushTarget ( df.reinterpret_cast ( ntype , trg.target [ key ] ) )
end )
end
2015-06-12 14:55:55 -06:00
function GmEditorUi : openOffseted ( index , choice )
local trg = self : currentTarget ( )
local trg_key = trg.keys [ index ]
dialog.showInputPrompt ( tostring ( trg_key ) , " Enter offset: " , COLOR_WHITE , " " ,
function ( choice )
self : pushTarget ( trg.target [ trg_key ] : _displace ( tonumber ( choice ) ) )
end )
end
2015-09-12 17:55:38 -06:00
function GmEditorUi : editSelectedRaw ( index , choice )
self : editSelected ( index , choice , { raw = true } )
end
function GmEditorUi : editSelected ( index , choice , opts )
opts = opts or { }
2012-09-23 14:22:14 -06:00
local trg = self : currentTarget ( )
2012-11-28 12:13:42 -07:00
local trg_key = trg.keys [ index ]
2012-09-23 14:22:14 -06:00
if trg.target and trg.target . _kind and trg.target . _kind == " bitfield " then
2012-11-28 12:13:42 -07:00
trg.target [ trg_key ] = not trg.target [ trg_key ]
self : updateTarget ( true )
2012-09-23 14:22:14 -06:00
else
--print(type(trg.target[trg.keys[trg.selected]]),trg.target[trg.keys[trg.selected]]._kind or "")
2012-11-28 12:13:42 -07:00
local trg_type = type ( trg.target [ trg_key ] )
2015-09-12 17:55:38 -06:00
if self : getSelectedEnumType ( ) and not opts.raw then
self : editSelectedEnum ( )
elseif trg_type == ' number ' or trg_type == ' string ' then --ugly TODO: add metatable get selected
2013-01-13 13:07:10 -07:00
dialog.showInputPrompt ( tostring ( trg_key ) , " Enter new value: " , COLOR_WHITE ,
2012-11-28 12:13:42 -07:00
tostring ( trg.target [ trg_key ] ) , self : callback ( " commitEdit " , trg_key ) )
2015-02-14 20:53:06 -07:00
2015-05-24 09:43:05 -06:00
elseif trg_type == ' boolean ' then
trg.target [ trg_key ] = not trg.target [ trg_key ]
2012-11-28 12:13:42 -07:00
self : updateTarget ( true )
2015-05-24 09:43:05 -06:00
elseif trg_type == ' userdata ' or trg_type == ' table ' then
2012-11-28 12:13:42 -07:00
self : pushTarget ( trg.target [ trg_key ] )
2015-07-24 11:58:09 -06:00
elseif trg_type == ' nil ' or trg_type == ' function ' then
-- ignore
2012-09-23 14:22:14 -06:00
else
2015-05-24 09:43:05 -06:00
print ( " Unknown type: " .. trg_type )
pcall ( function ( ) print ( " Subtype: " .. tostring ( trg.target [ trg_key ] . _kind ) ) end )
2012-09-23 14:22:14 -06:00
end
end
end
2012-11-28 12:13:42 -07:00
function GmEditorUi : commitEdit ( key , value )
2012-09-23 14:22:14 -06:00
local trg = self : currentTarget ( )
2012-11-28 12:13:42 -07:00
if type ( trg.target [ key ] ) == ' number ' then
trg.target [ key ] = tonumber ( value )
elseif type ( trg.target [ key ] ) == ' string ' then
trg.target [ key ] = value
2012-09-23 14:22:14 -06:00
end
2012-11-28 12:13:42 -07:00
self : updateTarget ( true )
2012-11-28 08:40:37 -07:00
end
2012-11-28 12:13:42 -07:00
function GmEditorUi : set ( key , input )
2015-02-14 20:53:06 -07:00
local trg = self : currentTarget ( )
2012-11-28 08:40:37 -07:00
if input == nil then
2012-11-28 12:13:42 -07:00
dialog.showInputPrompt ( " Set to what? " , " Lua code to set to (v cur target): " , COLOR_WHITE , " " , self : callback ( " set " , key ) )
2012-11-28 08:40:37 -07:00
return
end
local e , what = load ( " return function(v) return " .. input .. " end " )
if e == nil then
dialog.showMessage ( " Error! " , " function failed to compile \n " .. what , COLOR_RED )
2012-11-28 12:13:42 -07:00
return
2012-11-28 08:40:37 -07:00
end
2012-11-28 12:13:42 -07:00
trg.target [ key ] = e ( ) ( trg )
self : updateTarget ( true )
2012-09-23 14:22:14 -06:00
end
2012-11-28 12:13:42 -07:00
function GmEditorUi : onInput ( keys )
if keys.LEAVESCREEN then
2015-02-03 10:17:33 -07:00
if self.subviews . filter_input.active then
self : enable_input ( false )
return
end
2012-11-28 12:13:42 -07:00
if self.subviews . pages : getSelected ( ) == 2 then
self.subviews . pages : setSelected ( 1 )
else
2012-09-23 14:22:14 -06:00
self : popTarget ( )
end
2015-02-01 10:58:49 -07:00
end
2015-02-01 11:07:28 -07:00
2015-02-01 10:58:49 -07:00
if self.subviews . filter_input.active then
self.super . onInput ( self , keys )
return
end
if keys [ keybindings.offset . key ] then
2012-11-28 12:13:42 -07:00
local trg = self : currentTarget ( )
2012-12-15 04:27:16 -07:00
local _ , stoff = df.sizeof ( trg.target )
local size , off = df.sizeof ( trg.target : _field ( self : getSelectedKey ( ) ) )
dialog.showMessage ( " Offset " , string.format ( " Size hex=%x,%x dec=%d,%d \n Relative hex=%x dec=%d " , size , off , size , off , off - stoff , off - stoff ) , COLOR_WHITE )
2015-06-12 14:55:55 -06:00
elseif keys [ keybindings.displace . key ] then
self : openOffseted ( self.subviews . list_main : getSelected ( ) )
2012-11-28 12:13:42 -07:00
elseif keys [ keybindings.find . key ] then
self : find ( )
elseif keys [ keybindings.lua_set . key ] then
self : set ( self : getSelectedKey ( ) )
elseif keys [ keybindings.insert . key ] then --insert
self : insertNew ( )
elseif keys [ keybindings.delete . key ] then --delete
self : deleteSelected ( self : getSelectedKey ( ) )
2015-02-14 20:53:06 -07:00
elseif keys [ keybindings.reinterpret . key ] then
2014-04-26 12:50:52 -06:00
self : openReinterpret ( self : getSelectedKey ( ) )
2015-02-01 10:58:49 -07:00
elseif keys [ keybindings.start_filter . key ] then
self : enable_input ( true )
return
2012-11-28 12:13:42 -07:00
end
self.super . onInput ( self , keys )
end
2014-03-08 01:09:53 -07:00
function getStringValue ( trg , field )
local obj = trg.target
2015-02-14 20:53:06 -07:00
2014-03-08 01:09:53 -07:00
local text = tostring ( obj [ field ] )
pcall ( function ( )
if obj._field ~= nil then
local enum = obj : _field ( field ) . _type
if enum._kind == " enum-type " then
2015-09-12 17:55:38 -06:00
text = text .. " ( " .. tostring ( enum [ obj [ field ] ] ) .. " ) "
2014-03-08 01:09:53 -07:00
end
end
end )
return text
end
2012-11-28 12:13:42 -07:00
function GmEditorUi : updateTarget ( preserve_pos , reindex )
local trg = self : currentTarget ( )
2015-02-01 10:58:49 -07:00
local filter = self.subviews . filter_input.text
2012-11-28 12:13:42 -07:00
if reindex then
trg.keys = { }
for k , v in pairs ( trg.target ) do
2015-02-01 10:58:49 -07:00
if filter ~= " " then
local ok , ret = dfhack.pcall ( string.match , tostring ( k ) , filter )
if not ok then
table.insert ( trg.keys , k )
elseif ret then
table.insert ( trg.keys , k )
end
else
table.insert ( trg.keys , k )
end
2012-09-23 14:22:14 -06:00
end
end
2012-11-28 12:13:42 -07:00
self.subviews . lbl_current_item : itemById ( ' name ' ) . text = tostring ( trg.target )
local t = { }
for k , v in pairs ( trg.keys ) do
2015-02-14 20:53:06 -07:00
table.insert ( t , { text = { { text = string.format ( " %-25s " , tostring ( v ) ) } , { gap = 1 , text = getStringValue ( trg , v ) } } } )
2012-11-28 12:13:42 -07:00
end
local last_pos
if preserve_pos then
last_pos = self.subviews . list_main : getSelected ( )
end
self.subviews . list_main : setChoices ( t )
if last_pos then
self.subviews . list_main : setSelected ( last_pos )
else
2012-12-03 12:48:23 -07:00
self.subviews . list_main : setSelected ( trg.selected )
2012-11-28 12:13:42 -07:00
end
2012-09-23 14:22:14 -06:00
end
function GmEditorUi : pushTarget ( target_to_push )
local new_tbl = { }
new_tbl.target = target_to_push
new_tbl.keys = { }
new_tbl.selected = 1
2015-02-01 11:07:28 -07:00
new_tbl.filter = " "
2012-12-03 12:48:23 -07:00
if self : currentTarget ( ) ~= nil then
self : currentTarget ( ) . selected = self.subviews . list_main : getSelected ( )
2015-02-01 11:07:28 -07:00
self.stack [ # self.stack ] . filter = self.subviews . filter_input.text
2012-12-03 12:48:23 -07:00
end
2012-09-23 14:22:14 -06:00
for k , v in pairs ( target_to_push ) do
table.insert ( new_tbl.keys , k )
end
new_tbl.item_count =# new_tbl.keys
table.insert ( self.stack , new_tbl )
2015-02-01 11:07:28 -07:00
self.subviews . filter_input.text = " "
2012-11-28 12:13:42 -07:00
self : updateTarget ( )
2012-09-23 14:22:14 -06:00
end
function GmEditorUi : popTarget ( )
table.remove ( self.stack ) --removes last element
if # self.stack == 0 then
self : dismiss ( )
2012-11-28 12:13:42 -07:00
return
2012-09-23 14:22:14 -06:00
end
2015-02-01 11:07:28 -07:00
self.subviews . filter_input.text = self.stack [ # self.stack ] . filter --restore filter
2012-11-28 12:13:42 -07:00
self : updateTarget ( )
2012-09-23 14:22:14 -06:00
end
2012-09-23 14:45:19 -06:00
function show_editor ( trg )
2015-07-24 11:58:09 -06:00
if not trg then
qerror ( ' Target not found ' )
end
2012-09-23 14:45:19 -06:00
local screen = GmEditorUi { target = trg }
screen : show ( )
end
2015-07-24 11:58:09 -06:00
eval_env = { }
setmetatable ( eval_env , { __index = function ( _ , k )
if k == ' scr ' or k == ' screen ' then
return dfhack.gui . getCurViewscreen ( )
elseif k == ' bld ' or k == ' building ' then
return dfhack.gui . getSelectedBuilding ( )
elseif k == ' item ' then
return dfhack.gui . getSelectedItem ( )
elseif k == ' job ' then
return dfhack.gui . getSelectedJob ( )
elseif k == ' wsjob ' or k == ' workshop_job ' then
return dfhack.gui . getSelectedWorkshopJob ( )
elseif k == ' unit ' then
return dfhack.gui . getSelectedUnit ( )
else
return _G [ k ]
end
end } )
function eval ( s )
local f , err = load ( " return " .. s , " expression " , " t " , eval_env )
if err then qerror ( err ) end
return f ( )
end
2012-09-23 14:45:19 -06:00
if # args ~= 0 then
if args [ 1 ] == " dialog " then
function thunk ( entry )
2015-07-24 11:58:09 -06:00
show_editor ( eval ( entry ) )
2012-09-23 14:45:19 -06:00
end
dialog.showInputPrompt ( " Gm Editor " , " Object to edit: " , COLOR_GRAY , " " , thunk )
2014-04-26 12:50:52 -06:00
elseif args [ 1 ] == " free " then
show_editor ( df.reinterpret_cast ( df [ args [ 2 ] ] , args [ 3 ] ) )
2012-09-23 14:45:19 -06:00
else
2015-07-24 11:58:09 -06:00
show_editor ( eval ( args [ 1 ] ) )
2012-09-23 14:45:19 -06:00
end
else
show_editor ( getTargetFromScreens ( ) )
end
2012-08-31 14:46:33 -06:00