aboutsummaryrefslogtreecommitdiff
path: root/captain
diff options
context:
space:
mode:
authorJeffrey Armstrong <jeff@approximatrix.com>2021-04-01 11:38:06 -0400
committerJeffrey Armstrong <jeff@approximatrix.com>2021-04-01 11:38:06 -0400
commitaf084b8acb5ed7e883e422c4626a596eb32fba25 (patch)
tree323cb1c5c0de5ad919d0c932718efaff14a5566b /captain
parent9d19023f554dee7d8656a18dd81479decc03b3ee (diff)
downloadlevitating-af084b8acb5ed7e883e422c4626a596eb32fba25.tar.gz
levitating-af084b8acb5ed7e883e422c4626a596eb32fba25.zip
Added support for viewing individual instruction pages, assigning players, and launching instruction jobs
Diffstat (limited to 'captain')
-rw-r--r--captain/db.f90283
-rw-r--r--captain/external.f90307
-rw-r--r--captain/launch.f9014
-rw-r--r--captain/levitating-captain.prj85
-rw-r--r--captain/sql/create.sql3
5 files changed, 634 insertions, 58 deletions
diff --git a/captain/db.f90 b/captain/db.f90
index c61ffc7..1060dbc 100644
--- a/captain/db.f90
+++ b/captain/db.f90
@@ -5,6 +5,13 @@ implicit none
integer, parameter::PLAYER_NAME_LENGTH = 128
integer, parameter::FILENAME_NAME_LENGTH = 1024
+ integer, parameter::JOB_STATUS_SUCCESS = 1
+ integer, parameter::JOB_STATUS_FAILURE = 2
+ integer, parameter::JOB_STATUS_WORKING = 3
+
+ integer, parameter::PLAYER_STATUS_BUSY = JOB_STATUS_WORKING
+ integer, parameter::PLAYER_STATUS_IDLE = 100
+
character(1024)::database_file
type(c_ptr)::db
@@ -18,6 +25,11 @@ implicit none
end type
+ interface is_player_busy
+ module procedure is_player_busy_by_id
+ module procedure is_player_busy_by_name
+ end interface is_player_busy
+
contains
subroutine initialize_db(filename)
@@ -185,6 +197,277 @@ contains
end function get_instruction_names
+ function get_jobs_count()
+ implicit none
+
+ type(sqlite3_stmt)::stmt
+ integer::get_jobs_count
+
+ get_jobs_count = 0
+ if(stmt%prepare(db, "SELECT COUNT(*) FROM jobs") == SQLITE_OK) then
+ if(stmt%step() == SQLITE_ROW) then
+ get_jobs_count = stmt%column_int(0)
+ end if
+ end if
+ call stmt%finalize()
+
+ end function get_jobs_count
+
+ subroutine get_player_name(id, str)
+ implicit none
+
+ integer, intent(in)::id
+ character(*), intent(out)::str
+
+ type(sqlite3_stmt)::stmt
+
+ str = " "
+
+ if(stmt%prepare(db, "SELECT name FROM players WHERE id=?") == SQLITE_OK) then
+ if(stmt%bind_int(1, id) == SQLITE_OK) then
+ if(stmt%step() == SQLITE_ROW) then
+ call stmt%column_text(0,str)
+ end if
+ end if
+ end if
+ call stmt%finalize()
+
+ end subroutine get_player_name
+
+ function get_player_id(name)
+ implicit none
+
+ integer::get_player_id
+ character(*), intent(in)::name
+
+ type(sqlite3_stmt)::stmt
+
+ get_player_id = -1
+
+ if(stmt%prepare(db, "SELECT id FROM players WHERE name=?") == SQLITE_OK) then
+ if(stmt%bind_text(1, name) == SQLITE_OK) then
+ if(stmt%step() == SQLITE_ROW) then
+ get_player_id = stmt%column_int(0)
+ end if
+ end if
+ end if
+ call stmt%finalize()
+
+ end function get_player_id
+
+ subroutine get_instruction_name(id, str)
+ implicit none
+
+ integer, intent(in)::id
+ character(*), intent(out)::str
+
+ type(sqlite3_stmt)::stmt
+
+ str = " "
+
+ if(stmt%prepare(db, "SELECT name FROM instructions WHERE id=?") == SQLITE_OK) then
+ if(stmt%bind_int(1, id) == SQLITE_OK) then
+ if(stmt%step() == SQLITE_ROW) then
+ call stmt%column_text(0,str)
+ end if
+ end if
+ end if
+ call stmt%finalize()
+
+ end subroutine get_instruction_name
+
+ function get_instruction_id(name)
+ implicit none
+
+ integer::get_instruction_id
+ character(*), intent(in)::name
+
+ type(sqlite3_stmt)::stmt
+
+ get_instruction_id = -1
+
+ if(stmt%prepare(db, "SELECT id FROM instructions WHERE name=?") == SQLITE_OK) then
+ if(stmt%bind_text(1, name) == SQLITE_OK) then
+ if(stmt%step() == SQLITE_ROW) then
+ get_instruction_id = stmt%column_int(0)
+ end if
+ end if
+ end if
+ call stmt%finalize()
+
+ end function get_instruction_id
+
+ function get_jobs() result(jobs)
+ implicit none
+
+ type(job), dimension(:), pointer::jobs
+ type(sqlite3_stmt)::stmt
+ integer::i, n
+
+ n = get_jobs_count()
+ jobs => null()
+
+ if(n > 0) then
+ allocate(jobs(n))
+ if(stmt%prepare(db, "SELECT id,instruction,player,status,time FROM jobs ORDER BY id DESC") == SQLITE_OK) then
+ i = 1
+ do while(stmt%step() == SQLITE_ROW .and. i <= n)
+ jobs(i)%id = stmt%column_int(0)
+ jobs(i)%instruction = stmt%column_int(1)
+ jobs(i)%player = stmt%column_int(2)
+ jobs(i)%status = stmt%column_int(3)
+ call stmt%column_text(4, jobs(i)%time)
+ i = i + 1
+ end do
+ end if
+ call stmt%finalize()
+ end if
+
+ end function get_jobs
+
+ function get_jobs_for_instruction(id) result(jobs)
+ implicit none
+
+ type(job), dimension(:), pointer::jobs
+ integer, intent(in)::id
+ type(sqlite3_stmt)::stmt
+
+ integer::n, i
+
+ jobs => null()
+ n = 0
+ if(stmt%prepare(db, "SELECT COUNT(*) FROM jobs WHERE instruction=?") == SQLITE_OK) then
+ if(stmt%bind_int(1, id) == SQLITE_OK) then
+ if(stmt%step() == SQLITE_ROW) then
+ n = stmt%column_int(0)
+ end if
+ end if
+ end if
+ call stmt%finalize()
+
+ if(n > 0) then
+ allocate(jobs(n))
+ if(stmt%prepare(db, &
+ "SELECT id,player,status,time FROM jobs WHERE instruction=? ORDER BY id DESC") &
+ == SQLITE_OK) then
+
+ if(stmt%bind_int(1, id) == SQLITE_OK) then
+ i = 1
+ do while(stmt%step() == SQLITE_ROW .and. i <= n)
+ jobs(i)%id = stmt%column_int(0)
+ jobs(i)%instruction = id
+ jobs(i)%player = stmt%column_int(1)
+ jobs(i)%status = stmt%column_int(2)
+ call stmt%column_text(3, jobs(i)%time)
+ i = i + 1
+ end do
+ end if
+
+ end if
+ call stmt%finalize()
+
+ end if
+
+ end function get_jobs_for_instruction
+
+ function is_player_busy_by_id(id)
+ implicit none
+
+ logical::is_player_busy_by_id
+ integer, intent(in)::id
+ type(sqlite3_stmt)::stmt
+
+ is_player_busy_by_id = .false.
+
+ if(stmt%prepare(db, "SELECT COUNT(*) FROM jobs WHERE player=? AND status=?") == SQLITE_OK) then
+ if(stmt%bind_int(1, id) == SQLITE_OK .and. stmt%bind_int(2, JOB_STATUS_WORKING) == SQLITE_OK) then
+ if(stmt%step() == SQLITE_ROW) then
+ is_player_busy_by_id = (stmt%column_int(0) > 0)
+ end if
+ end if
+ end if
+ call stmt%finalize()
+
+ end function is_player_busy_by_id
+
+ function is_player_busy_by_name(name)
+ implicit none
+
+ logical::is_player_busy_by_name
+ character(*), intent(in)::name
+ integer::id
+
+ is_player_busy_by_name = .false.
+ id = get_player_id(name)
+ is_player_busy_by_name = is_player_busy_by_id(id)
+
+ end function is_player_busy_by_name
+
+ function get_instruction_players(id) result(res)
+ implicit none
+
+ integer, dimension(:), pointer::res
+ integer, intent(in)::id
+ type(sqlite3_stmt)::stmt
+ integer::n, i
+
+ res => null()
+ if(stmt%prepare(db, "SELECT COUNT(*) FROM available WHERE instruction=?") == SQLITE_OK) then
+ if(stmt%bind_int(1, id) == SQLITE_OK) then
+ if(stmt%step() == SQLITE_ROW) then
+ n = stmt%column_int(0)
+ end if
+ end if
+ end if
+ call stmt%finalize()
+
+ if(n > 0) then
+ allocate(res(n))
+ res = -1
+ if(stmt%prepare(db, "SELECT player FROM available WHERE instruction=?") == SQLITE_OK) then
+ if(stmt%bind_int(1, id) == SQLITE_OK) then
+ i = 1
+ do while(stmt%step() == SQLITE_ROW .and. i <= n)
+ res(i) = stmt%column_int(0)
+ i = i + 1
+ end do
+ end if
+ end if
+ call stmt%finalize()
+ end if
+
+ end function get_instruction_players
+
+ subroutine add_player_for_instruction(instruction, player)
+ implicit none
+
+ integer, intent(in)::instruction, player
+ type(sqlite3_stmt)::stmt
+
+ if(stmt%prepare(db, "INSERT OR IGNORE INTO available(instruction, player) VALUES(?,?)") == SQLITE_OK) then
+ if(stmt%bind_int(1, instruction) == SQLITE_OK .and. stmt%bind_int(2, player) == SQLITE_OK) then
+ call stmt%step_now()
+ end if
+ end if
+ call stmt%finalize()
+
+ end subroutine add_player_for_instruction
+
+ subroutine remove_player_for_instruction(instruction, player)
+ implicit none
+
+ integer, intent(in)::instruction, player
+ type(sqlite3_stmt)::stmt
+
+ if(stmt%prepare(db, "DELETE FROM available WHERE instruction=? AND player=?") == SQLITE_OK) then
+ if(stmt%bind_int(1, instruction) == SQLITE_OK .and. stmt%bind_int(2, player) == SQLITE_OK) then
+ call stmt%step_now()
+ end if
+ end if
+ call stmt%finalize()
+
+ end subroutine remove_player_for_instruction
+
subroutine scan_instructions_for_db()
use config
use utilities
diff --git a/captain/external.f90 b/captain/external.f90
index 1a27be0..ee4d2e6 100644
--- a/captain/external.f90
+++ b/captain/external.f90
@@ -3,15 +3,112 @@ implicit none
contains
- function generate_jobs_gemini() result(res)
+ pure function get_status_utf8(status) result(res)
use captain_db
implicit none
+ integer, intent(in)::status
+ character(4)::res
+
+ select case(status)
+
+ case(JOB_STATUS_SUCCESS)
+ ! Check mark 0x2714
+ res = char(226)//char(156)//char(148)//" "
+
+ case(JOB_STATUS_FAILURE)
+ ! Ballot x 0x2717
+ res = char(226)//char(156)//char(151)//" "
+
+ case(JOB_STATUS_WORKING)
+ ! Runner 0x1f3c3
+ res = char(240)//char(159)//char(143)//char(131)
+
+ case(PLAYER_STATUS_IDLE)
+ ! Sleeping 0x1f4a4
+ res = char(240)//char(159)//char(146)//char(164)
+
+ end select
+
+ end function get_status_utf8
+
+ function render_jobs_links(jobs, startindex, stopindex) result(res)
+ use captain_db
+ implicit none
+
+ type(job), dimension(:), pointer, intent(in)::jobs
+ integer, intent(in), optional::startindex, stopindex
+ character(len=:), pointer::res
+
+ integer::nsize, i, first, last
+ character(len=16)::int_text
+ character(len=(2*PLAYER_NAME_LENGTH + 64))::link
+ character(PLAYER_NAME_LENGTH)::player, instruction
+ character(1)::nl = new_line(' ')
+
+ if(.not. associated(jobs)) then
+ allocate(character(len=32)::res)
+ res = "None Yet"
+
+ else
+ first = 1
+ if(present(startindex)) then
+ first = startindex
+ end if
+
+ last = size(jobs)
+ if(present(stopindex)) then
+ last = min(size(jobs), stopindex)
+ end if
+
+ if(first < last .and. first < size(jobs)) then
+ nsize = (last-first+1)*(2*PLAYER_NAME_LENGTH + 64)
+ allocate(character(len=nsize) :: res)
+ res = " "
+
+ do i = first, last
+ call get_instruction_name(jobs(i)%instruction, instruction)
+ call get_player_name(jobs(i)%player, player)
+
+ write(int_text, '(I8)') jobs(i)%id
+ link = "=> jobs/"//trim(adjustl(int_text))//".gmi"// &
+ trim(get_status_utf8(jobs(i)%status))//" Job "// &
+ trim(adjustl(int_text))//" - "//trim(instruction)
+
+ res = trim(res)//nl//nl//link
+
+ res = trim(res)//nl//"Running on "//trim(player)// &
+ " - Last Update: "//trim(jobs(i)%time)
+ end do
+
+ else
+
+ ! Indices exceed array
+ allocate(character(len=64)::res)
+ write(int_text, '(I8)') first
+ res = "None at index "//trim(adjustl(int_text))//" or beyond"
+
+ end if
+ end if
+
+ end function render_jobs_links
+
+ function generate_jobs_gemini(req) result(res)
+ use captain_db
+ use server_response
+ implicit none
+
+ type(request)::req
character(len=:), pointer::res
type(job), dimension(:), pointer::jobs
integer::n, i, nsize
+ integer::i_start_jobs, ierr
- n = get_player_count()
+ character(len=:), pointer::linklist
+ character(16)::int_text, int_text2
+
+
+ n = get_jobs_count()
if(n == 0) then
allocate(character(len=1024) :: res)
@@ -19,6 +116,27 @@ contains
res = "None Yet"
else
+ if(associated(req%query_string)) then
+ read(req%query_string, *, iostat=ierr) i_start_jobs
+ else
+ ierr = -1
+ end if
+
+ if(ierr /= 0) then
+ i_start_jobs = 1
+ end if
+
+ jobs => get_jobs()
+
+ ! 15 per page
+ nsize = 15*(2*PLAYER_NAME_LENGTH + 64) + 1024
+ allocate(character(len=nsize) :: res)
+
+ res = "## Jobs "
+
+ linklist => render_jobs_links(jobs, i_start_jobs, min(i_start_jobs+14, n))
+ res = res//trim(linklist)
+ deallocate(linklist)
end if
@@ -70,6 +188,150 @@ contains
end function generate_players_gemini
+ function generate_one_instuction_gemini(req) result(res)
+ use captain_db
+ use server_response
+ implicit none
+
+ type(request)::req
+ character(len=:), pointer::res
+ integer::id_from_req
+
+ character(128)::instruction_name
+ type(job), dimension(:), pointer::jobs
+ integer, dimension(:), pointer::players
+ character(len=PLAYER_NAME_LENGTH), dimension(:), pointer::all_players
+ integer::i, j, n_jobs, n_players, nsize
+
+ character(len=:), pointer::job_link_text, player_link_text
+ character(1)::nl = new_line(' ')
+ character(PLAYER_NAME_LENGTH)::player_name
+ character(4)::player_status
+
+ i = index(req%location, "/", back=.true.)
+ j = index(req%location, ".", back=.true.)
+
+ instruction_name = req%location(i+1:j-1)
+ id_from_req = get_instruction_id(trim(instruction_name))
+ jobs => get_jobs_for_instruction(id_from_req)
+ if(associated(jobs)) then
+ n_jobs = size(jobs)
+ job_link_text => render_jobs_links(jobs)
+ else
+ n_jobs = 0
+ job_link_text => null()
+ end if
+
+ players => get_instruction_players(id_from_req)
+ if(associated(players)) then
+ n_players = size(players)
+ else
+ n_players = 0
+ end if
+
+ nsize = 1024
+ if(n_jobs > 0) then
+ nsize = nsize + len_trim(job_link_text)
+ end if
+ if(n_players > 0) then
+ nsize = nsize + n_players*256
+ end if
+
+ nsize = nsize + get_player_count()*(PLAYER_NAME_LENGTH+32)
+
+ allocate(character(len=nsize) :: res)
+
+ res = nl//"## "//trim(instruction_name)
+ if(n_players == 0) then
+ res = trim(res)//nl//nl//"No players currently can run these instructions"
+ else
+ res = trim(res)//nl//nl//"## Launch Now"
+ do i = 1, n_players
+ call get_player_name(players(i), player_name)
+ if(is_player_busy(players(i))) then
+ player_status = get_status_utf8(PLAYER_STATUS_BUSY)
+ else
+ player_status = get_status_utf8(PLAYER_STATUS_IDLE)
+ end if
+
+ res = trim(res)//nl//"=> "//trim(req%location)//"?launch="//trim(player_name)// &
+ " "//trim(player_status)//trim(player_name)
+ end do
+ end if
+
+ res = trim(res)//nl//nl//"### Jobs"
+ if(n_jobs == 0) then
+ res = trim(res)//nl//"None Yet"
+ else
+ res = trim(res)//nl//job_link_text
+ deallocate(job_link_text)
+ end if
+
+ all_players => get_player_names()
+ if(associated(all_players)) then
+ res = trim(res)//nl//nl//"### Assign"//nl//"Assign a player to these instructions"
+ do i = 1, size(all_players)
+ if(n_players > 0) then
+ j = get_player_id(all_players(i))
+ if(any(j == players)) then
+ cycle
+ end if
+ end if
+ res = trim(res)//nl//"=> "//trim(req%location)//"?assign="//trim(all_players(i))// &
+ " "//trim(all_players(i))
+ end do
+ deallocate(all_players)
+ end if
+
+ if(n_players > 0) then
+ res = trim(res)//nl//nl//"### Remove"//nl//"Remove a player from these instructions"
+ do i = 1, n_players
+ call get_player_name(players(i), player_name)
+ res = trim(res)//nl//"=> "//trim(req%location)//"?remove="//trim(player_name)// &
+ " "//trim(player_name)
+ end do
+ end if
+
+ end function generate_one_instuction_gemini
+
+ subroutine handle_instruction_command(req)
+ use captain_db
+ use server_response
+ use remote_launch
+ implicit none
+
+ type(request), intent(in)::req
+ character(32)::command
+ character(PLAYER_NAME_LENGTH)::argument, instruction_name
+
+ integer::i, j
+
+ i = index(req%location, "/", back=.true.)
+ j = index(req%location, ".", back=.true.)
+
+ instruction_name = req%location(i+1:j-1)
+
+ i = index(req%query_string, "=")
+ command = req%query_string(1:i-1)
+ argument = req%query_string(i+1:len_trim(req%query_string))
+
+ if(trim(command) == "launch") then
+ call launch_instructions_on_player(instruction_name, argument)
+
+ else if(trim(command) == "assign") then
+ i = get_instruction_id(trim(instruction_name))
+ j = get_player_id(trim(argument))
+ call add_player_for_instruction(i, j)
+
+ else if(trim(command) == "remove") then
+ i = get_instruction_id(trim(instruction_name))
+ j = get_player_id(trim(argument))
+ call remove_player_for_instruction(i, j)
+
+ end if
+
+ end subroutine handle_instruction_command
+
function generate_instructions_gemini() result(res)
use captain_db
implicit none
@@ -163,12 +425,27 @@ contains
class(request), intent(in)::req
type(response)::resp
+ character(64)::first
+
+ call req%path_component(1, first)
if(req%location == "/players/add.gmi") then
call add_player_db(req%query_string)
resp%code = GEMINI_CODE_REDIRECT
call resp%set_url("/players.gmi")
+
+ else if(req%location == "/jobs.gmi") then
+ ! Used for paging - send it back
+ resp = external_request_templated(req)
+
+ else if(trim(first) == "instructions") then
+ ! Instruction command
+ call handle_instruction_command(req)
+ ! Go back to the same location
+ resp%code = GEMINI_CODE_REDIRECT
+ call resp%set_url(req%location)
+
end if
end function external_input_request_gemini
@@ -180,9 +457,20 @@ contains
class(request), intent(in)::req
logical::is_request_static
- character(64)::first
+ character(64)::first, last
+
+ character(4)::ext
+ integer::j
call req%path_component(1, first)
+ call req%last_component(last)
+
+ j = index(last, ".", back=.true.)
+ if(j > 0) then
+ ext = last(j+1:len_trim(last))
+ else
+ ext = " "
+ end if
call write_log("Static check: "//trim(first))
@@ -191,7 +479,7 @@ contains
(trim(first) == "results") .or. &
(trim(first) == "static") .or. &
(trim(first) == "favicon.txt") .or. &
- (trim(first) == "instructions"))
+ (trim(first) == "instructions" .and. trim(ext) == "json"))
end function is_request_static
@@ -301,6 +589,7 @@ contains
character(1024)::template_file
type(template)::page
character(len=:), pointer::contents
+ character(64)::first
! Open the base template
call template_filepath("index.gmi", template_file)
@@ -308,6 +597,8 @@ contains
call write_log("Processing request")
+ call req%path_component(1, first)
+
if(trim(req%location) == "/" .or. trim(req%location) == "/index.gmi") then
call write_log("Assign")
@@ -321,7 +612,7 @@ contains
else if(trim(req%location) == "/jobs.gmi") then
call page%assign('title', 'Jobs')
- contents => generate_jobs_gemini()
+ contents => generate_jobs_gemini(req)
call page%assign('contents', contents)
else if(trim(req%location) == "/players.gmi") then
@@ -342,6 +633,12 @@ contains
contents => generate_instructions_gemini()
call page%assign('contents', contents)
+ else if(trim(first) == "instructions") then
+
+ call page%assign('title', 'Build Instructions')
+ contents => generate_one_instuction_gemini(req)
+ call page%assign('contents', contents)
+
else
call page%assign('title', 'Not Found')
diff --git a/captain/launch.f90 b/captain/launch.f90
new file mode 100644
index 0000000..97e0a59
--- /dev/null
+++ b/captain/launch.f90
@@ -0,0 +1,14 @@
+module remote_launch
+implicit none
+
+contains
+
+ subroutine launch_instructions_on_player(instruction, player)
+ implicit none
+
+ character(*), intent(in)::instruction, player
+
+ end subroutine launch_instructions_on_player
+
+
+end module remote_launch \ No newline at end of file
diff --git a/captain/levitating-captain.prj b/captain/levitating-captain.prj
index 31841f9..0122d1f 100644
--- a/captain/levitating-captain.prj
+++ b/captain/levitating-captain.prj
@@ -4,99 +4,80 @@
"Folders":[],
"Name":"+common",
"Files":[{
- "filename":"../common/jessl.f90",
+ "filename":"..\\common\\jessl.f90",
"enabled":"1"
},{
- "filename":"../common/network.F90",
+ "filename":"..\\common\\network.F90",
"enabled":"1"
},{
- "filename":"../common/protocol.f90",
+ "filename":"..\\common\\protocol.f90",
"enabled":"1"
},{
- "filename":"../common/request.f90",
+ "filename":"..\\common\\request.f90",
"enabled":"1"
},{
- "filename":"../common/utilities.F90",
- "enabled":"1",
- "panel":1,
- "open":"1"
+ "filename":"..\\common\\utilities.F90",
+ "enabled":"1"
},{
- "filename":"../common/wsa.f90",
+ "filename":"..\\common\\wsa.f90",
"enabled":"0"
}]
},{
"Folders":[],
"Name":"+example",
"Files":[{
- "filename":"example/levitating.conf",
- "enabled":"1",
- "panel":1,
- "open":"1"
+ "filename":".\\example\\levitating.conf",
+ "enabled":"1"
}]
},{
"Folders":[],
"Name":"+sql",
"Files":[{
- "filename":"sql/create.sql",
+ "filename":".\\sql\\create.sql",
"enabled":"1"
},{
- "filename":"sql/scan_instructions.sh",
+ "filename":".\\sql\\scan_instructions.sh",
"enabled":"1"
}]
},{
"Folders":[],
"Name":"+templates",
"Files":[{
- "filename":"templates/index.gmi",
- "enabled":"1",
- "panel":1,
- "open":"1"
+ "filename":".\\templates\\index.gmi",
+ "enabled":"1"
}]
}],
"Name":"+levitating-captain (levitating-captain)",
"Files":[{
- "filename":"captian.f90",
- "enabled":"1",
- "panel":1,
- "open":"1"
+ "filename":".\\captian.f90",
+ "enabled":"1"
},{
- "filename":"config.f90",
- "enabled":"1",
- "panel":1,
- "open":"1"
+ "filename":".\\config.f90",
+ "enabled":"1"
},{
- "filename":"db.f90",
- "enabled":"1",
- "panel":1,
- "open":"1"
+ "filename":".\\db.f90",
+ "enabled":"1"
},{
- "filename":"external.f90",
- "enabled":"1",
- "panel":1,
- "open":"1"
+ "filename":".\\external.f90",
+ "enabled":"1"
},{
- "filename":"gemini.f90",
- "enabled":"1",
- "panel":1,
- "open":"1"
+ "filename":".\\gemini.f90",
+ "enabled":"1"
+ },{
+ "filename":".\\launch.f90",
+ "enabled":"1"
},{
- "filename":"log.f90",
- "enabled":"1",
- "panel":1,
- "open":"1"
+ "filename":".\\log.f90",
+ "enabled":"1"
},{
- "filename":"response.f90",
- "enabled":"1",
- "panel":1,
- "open":"1"
+ "filename":".\\response.f90",
+ "enabled":"1"
},{
- "filename":"sqlite.f90",
+ "filename":".\\sqlite.f90",
"enabled":"1"
},{
- "filename":"template.f90",
- "enabled":"1",
- "panel":1,
- "open":"1"
+ "filename":".\\template.f90",
+ "enabled":"1"
}]
},
"Name":"levitating-captain (levitating-captain)",
diff --git a/captain/sql/create.sql b/captain/sql/create.sql
index 745c536..edb21c7 100644
--- a/captain/sql/create.sql
+++ b/captain/sql/create.sql
@@ -7,4 +7,5 @@ CREATE TABLE tasks(job INTEGER, task INTEGER, status INTEGER, FOREIGN KEY(job) R
CREATE TABLE instructions(id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL);
-CREATE TABLE available(instruction INTEGER, player INTEGER, FOREIGN KEY(instruction) REFERENCES instructions(id), FOREIGN KEY(player) REFERENCES players(id));
+CREATE TABLE available(instruction INTEGER, player INTEGER, FOREIGN KEY(instruction) REFERENCES instructions(id), FOREIGN KEY(player) REFERENCES players(id), UNIQUE(instructions, player));
+